说明:本文分析的是修正多解 bug 前的 binary,但相关方法应当是通用的。
题目给出了一个 Win32 控制台应用程序 lelfei.exe
,简单看下会发现它是由 GCC: (i686-posix-dwarf-rev2, Built by MinGW-W64 project) 6.3.0
构建的,且带有调试符号,IDA Pro 可正常识别。
这些像是 JS_NewObjectFromShape
的符号非常扎眼,搜索一下会发现它是 QuickJS 中的。下载 QuickJS,构建出来(直接打 make 就可以)之后试一试,可以发现它有一个 qjsc
,可以将一段 JavaScript 脚本编译为字节码后,和一个生成出的 stub 及 QuickJS 本身一起链接成一个可执行程序。使用 qjsc -e
可以让它输出嵌入字节码后,编译链接前的 C 代码。随便写一个 Hello World,这么做一下,观察得到的程序结构:
/* File generated automatically by the QuickJS compiler. */ #include "quickjs-libc.h" const uint32_t qjsc_hello_size = 78; const uint8_t qjsc_hello[78] = { 0x02, 0x04, 0x0e, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x06, 0x6c, 0x6f, 0x67, 0x16, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x10, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x6a, 0x73, 0x0e, 0x00, 0x06, 0x00, 0x9e, 0x01, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x14, 0x01, 0xa0, 0x01, 0x00, 0x00, 0x00, 0x39, 0xdf, 0x00, 0x00, 0x00, 0x43, 0xe0, 0x00, 0x00, 0x00, 0x04, 0xe1, 0x00, 0x00, 0x00, 0x24, 0x01, 0x00, 0xcf, 0x28, 0xc4, 0x03, 0x01, 0x00, }; int main(int argc, char **argv) { JSRuntime *rt; JSContext *ctx; rt = JS_NewRuntime(); ctx = JS_NewContextRaw(rt); JS_SetModuleLoaderFunc(rt, NULL, js_module_loader, NULL); JS_AddIntrinsicBaseObjects(ctx); JS_AddIntrinsicDate(ctx); JS_AddIntrinsicEval(ctx); JS_AddIntrinsicStringNormalize(ctx); JS_AddIntrinsicRegExp(ctx); JS_AddIntrinsicJSON(ctx); JS_AddIntrinsicProxy(ctx); JS_AddIntrinsicMapSet(ctx); JS_AddIntrinsicTypedArrays(ctx); JS_AddIntrinsicPromise(ctx); JS_AddIntrinsicBigInt(ctx); js_std_add_helpers(ctx, argc, argv); js_std_eval_binary(ctx, qjsc_hello, qjsc_hello_size, 0); js_std_loop(ctx); JS_FreeContext(ctx); JS_FreeRuntime(rt); return 0; }
跟 lelfei.exe
基本是一样的。lelfei.exe
的 main 函数显著大,是因为构建时开了 LTO,自动 inline 了很多函数进来。
简单阅读一下可以发现,lelfei.exe
在 qjsc
本身生成的逻辑上做了一点改动,加入了读入用户名、序列号的部分,读入的用户名和序列号会直接 patch 进程序内嵌的字节码。
简单看下代码可以发现程序中嵌入的字节码在 0x458040 处,长度为 946 字节。
我们将它提取出来,照猫画虎换进一个像是上面那样的 .c
中,然后编译并和 QuickJS 链接起来,看看能不能执行:
$ gcc -ggdb hello.c libquickjs.a -lm -ldl $ ./a.out [1] 2164345 segmentation fault ./a.out
呃……好像不可以。挂上调试器调一下(因为是源码构建的,可以源码调试 QuickJS),会发现看起来读入的字节码从中间某块往后就不太正确了。考虑到并不是全都不对,先不考虑是作者将它打乱了,而是考虑是否是版本不匹配。下载 QuickJS 的历史版本逐一尝试,尝试到 2020-01-19 版的时候:
$ gcc -ggdb hello.c libquickjs.a -lm -ldl $ ./a.out Error...
执行成功了。得到了在 Windows 下直接执行这个程序并输入错误序列号一样的反应。
我们手动将作者给出的用户名和序列号 patch 进字节码里重新编译,可以发现确实能输出 Success!
。至此,原始的 lelfei.exe
已经没有了任何作用,可以抛在一边了。
事情进展到这种地步之后,基本的思路自然是在 QuickJS 中加入代码,将读取回来的字节码输出出来,然后逆向字节码。但稍微读一下 QuickJS 的代码就会发现,它本身就带有这个功能,修改相关宏定义启用即可;另外,当函数对象是读入而非编译出来的时,并没有打印字节码,修改 JS_ReadObjectRec
函数在 BC_TAG_FUNCTION_BYTECODE
这个 case 最后加一行输出即可。总之,对 quickjs.c
文件应用下面这个 patch:
85c85 < //#define DUMP_BYTECODE (1) --- > #define DUMP_BYTECODE (1) 99c99 < //#define DUMP_READ_OBJECT --- > #define DUMP_READ_OBJECT 33899a33900,33902 > #if DUMP_BYTECODE > js_dump_function_bytecode(ctx, b); > #endif
再照上文编译并运行程序,即可得到一段比较漂亮的输出(编辑掉了无关的部分,完整文件见附件):
0000: 02 0e 14 atom indexes { 0002: 04 75 6e string: 1"un" 0005: 04 73 6e string: 1"sn" 0008: 02 73 string: 1"s" 000a: 02 69 string: 1"i" 000c: 02 6a string: 1"j" 000e: 02 6b string: 1"k" 0010: 02 6c string: 1"l" 0012: 02 6d string: 1"m" 0014: 02 6e string: 1"n" 0016: 20 4b 43 54 46 32 30 32 30 51 31 6c 65 6c 66 65 69 string: 1"KCTF2020Q1lelfei" 0027: 40 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a string: 1"********************************" 0048: 14 63 68 61 72 43 6f 64 65 41 74 string: 1"charCodeAt" 0053: 18 66 72 6f 6d 43 68 61 72 43 6f 64 65 string: 1"fromCharCode" 0060: 0a 70 72 69 6e 74 string: 1"print" } 0066: 0e function { 0067: 00 06 00 9e 01 00 01 00 06 00 0a d6 05 01 name: "<eval>" args=0 vars=1 defargs=0 closures=0 cpool=10 stack=6 bclen=726 locals=1 vars { 0075: a0 01 00 00 00 name: "<ret>" } bytecode { 007a: 40 df 00 00 00 00 40 e0 <... 略 ...> 00 00 00 f1 cf 28 at 1, fixup atom: un <... 略 ...> at 719, fixup atom: s } debug { <... 略 ...> } cpool { <... 略 ...> } } s:1: function: <eval> locals: 0: var <ret> stack_size: 6 opcodes: check_define_var un,0 check_define_var sn,0 check_define_var s,0 check_define_var i,0 check_define_var j,0 check_define_var k,0 check_define_var l,0 check_define_var m,0 check_define_var n,0 check_define_var i,0 define_var un,0 define_var sn,0 define_var s,0 define_var i,0 define_var j,0 define_var k,0 define_var l,0 define_var m,0 define_var n,0 define_var i,0 push_atom_value KCTF2020Q1lelfei dup put_var un put_loc0 0: "<ret>" push_atom_value "********************************" dup put_var sn put_loc0 0: "<ret>" push_const8 0: 0n dup put_var m put_loc0 0: "<ret>" undefined put_loc0 0: "<ret>" push_0 0 dup put_var i drop 163: get_var i get_var un get_length lt if_false8 243 get_var m push_const8 1: 43n mul dup put_var m put_loc0 0: "<ret>" get_var m get_var BigInt get_var un get_field2 charCodeAt get_var i call_method 1 call1 1 add dup put_var m put_loc0 0: "<ret>" get_var i post_inc put_var i drop goto8 163 243: get_var Number get_var m push_const8 2: 127n mod call1 1 dup put_var l put_loc0 0: "<ret>" push_const8 3: 0n dup put_var n put_loc0 0: "<ret>" push_0 0 dup put_var s put_loc0 0: "<ret>" push_0 0 dup put_var k put_loc0 0: "<ret>" undefined put_loc0 0: "<ret>" push_0 0 dup put_var i drop 299: get_var i get_var sn get_length lt if_false 601 get_var sn get_field2 charCodeAt get_var i call_method 1 dup put_var j put_loc0 0: "<ret>" undefined put_loc0 0: "<ret>" get_var j push_i8 48 gte dup if_false8 363 drop get_var j push_i8 57 lte 363: dup if_true8 388 drop get_var j push_i8 97 gte if_false 601 get_var j push_i8 102 lte 388: if_false 601 get_var k post_inc put_var k put_loc0 0: "<ret>" get_var j push_i8 48 sub dup put_var j put_loc0 0: "<ret>" undefined put_loc0 0: "<ret>" get_var j push_i8 9 gt if_false8 447 get_var j push_i8 39 sub dup put_var j put_loc0 0: "<ret>" 447: get_var s push_i8 16 mul dup put_var s put_loc0 0: "<ret>" get_var s get_var j add dup put_var s put_loc0 0: "<ret>" undefined put_loc0 0: "<ret>" get_var k push_2 2 mod push_0 0 eq if_false8 586 get_var s get_var l xor dup put_var s put_loc0 0: "<ret>" get_var s push_4 4 sar push_i8 10 mul get_var s push_i8 16 mod add dup put_var s put_loc0 0: "<ret>" get_var n push_const8 4: 100n mul dup put_var n put_loc0 0: "<ret>" get_var n get_var BigInt get_var s call1 1 add dup put_var n put_loc0 0: "<ret>" push_0 0 dup put_var s put_loc0 0: "<ret>" goto8 586 586: get_var i post_inc put_var i drop goto16 299 601: undefined put_loc0 0: "<ret>" get_var m get_var n eq if_false8 627 push_const8 5: 18071254662143010n dup put_var n put_loc0 0: "<ret>" goto8 636 627: push_const8 6: 24706849372394394n dup put_var n put_loc0 0: "<ret>" 636: push_empty_string dup put_var s put_loc0 0: "<ret>" undefined put_loc0 0: "<ret>" 646: get_var n push_const8 7: 0n gt if_false8 713 get_var s get_var String get_field2 fromCharCode get_var Number get_var n push_const8 8: 127n mod call1 1 call_method 1 add dup put_var s put_loc0 0: "<ret>" get_var n push_const8 9: 127n div dup put_var n put_loc0 0: "<ret>" goto8 646 713: get_var print get_var s call1 1 set_loc0 0: "<ret>" return Error...
可以看到并不长,也很容易理解,对照着将验证逻辑还原回 JavaScript:
let un, sn, s, i, j, k, l, m, n; un = "KCTF2020Q1lelfei" sn = "********************************" m = 0n for (i = 0; i < un.length; i++) { m = m * 43n + BigInt(un.charCodeAt(i)) } l = Number(m % 127n) n = 0n s = 0 k = 0 for (i = 0; i < 256; i++) { z = i; z = (z >> 4) * 10 + (z % 16); } for (i = 0; i < sn.length; i++) { j = sn.charCodeAt(i) if ((j >= 48 && j <= 57) || (j >= 97 && j <= 102)) { k++ j -= 48 if (j > 9) { j -= 39 } } else break; s = s * 16 + j if (k % 2 == 0) { s = s ^ l; s = (s >> 4) * 10 + (s % 16); n = n * 100n n = n + BigInt(s) s = 0 } } if (m == n) { n = 18071254662143010n // Success } else { n = 24706849372394394n // Error.. } // 略去输出提示信息的部分
验证逻辑比较简单,解起来应该也相对容易,但比较无语的是,下面这一行做的变换显然不是双射(怀疑作者是想打 0x10,手滑打错了):
s = (s >> 4) * 10 + (s % 16)
因此存在多解。
用上面逆向得出的 js 代码算出 KCTFKCTFKCTFKCTF
对应的 m
和 l
:
let un, sn, s, i, j, k, l, m, n; un = "KCTFKCTFKCTFKCTF" m = 0n for (i = 0; i < un.length; i++) { m = m * 43n + BigInt(un.charCodeAt(i)) } l = Number(m % 127n) console.log(m, l)
得到 243377798925556026477314360n 66
。
再算出序列号:
f = lambda x: (x >> 4) * 10 + (x % 16) m = 243377798925556026477314360 x = m p = [] while x: p.append(x%100) x //= 100 p = p[::-1] ans = [] for w in p: ans.append([x for x in range(256) if f(x) == w]) for z in itertools.product(*ans): print(xor(z, 66).hex())
接下来去提交多解即可。
最后于 7小时前 被Riatre编辑 ,原因: