首页
论坛
课程
招聘
[讨论] 看雪.安恒2020 KCTF春季赛 第三题 寻踪觅源 Writeup
2020-4-19 11:34 2304

[讨论] 看雪.安恒2020 KCTF春季赛 第三题 寻踪觅源 Writeup

2020-4-19 11:34
2304

看雪.安恒2020 KCTF春季赛 > 第三题 寻踪觅源

说明:本文分析的是修正多解 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.exeqjsc 本身生成的逻辑上做了一点改动,加入了读入用户名、序列号的部分,读入的用户名和序列号会直接 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 对应的 ml

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())

接下来去提交多解即可。


看雪论坛2020激励机制:能力值、活跃值和雪币体系!会员积分、权限和会员发帖、回帖活跃程度关联!

最后于 2020-4-19 11:34 被Riatre编辑 ,原因:
上传的附件:
收藏
点赞1
打赏
分享
最新回复 (2)
雪    币: 4398
活跃值: 活跃值 (138)
能力值: ( LV15,RANK:1295 )
在线值:
发帖
回帖
粉丝
lelfei 活跃值 22 2020-4-20 07:48
2
0
太强大了
雪    币: 2978
活跃值: 活跃值 (111)
能力值: ( LV6,RANK:88 )
在线值:
发帖
回帖
粉丝
Mirge 活跃值 2020-4-20 09:16
3
0
太强大了 
游客
登录 | 注册 方可回帖
返回