首页
论坛
课程
招聘
[原创]KCTF 2021秋季赛 第九题 万事俱备
2021-12-9 12:41 17125

[原创]KCTF 2021秋季赛 第九题 万事俱备

2021-12-9 12:41
17125

初步分析

  • 易语言?
    拿到程序,发现是易语言写的,还有点奇怪,直接x64dbg调试先看,跑起来发现易语言只是套了个外壳:
    执行py脚本

  • Py脚本
    从temp目录找到check.py和对应的CPython2.7解释器,不用想解释器肯定是改过的,先定位到程序版本:
    图片描述
    程序版本是2.7.18,从官网下载源码编译,https://www.python.org/ftp/python/2.7.18/Python-2.7.18.tgz

    • Windows:解压源码,进入PCBuild目录,打开pcbuild.sln,选择Debug模式编译python和pythoncore
    • 其他的模块,如socket等按需编译即可(如果import失败则编译)
    • 编译报错:
  • Python执行原理
    温顾一遍解释器原理也是有必要的,首先定位到几个关键点:

    • PyObject基类
    • PyEval_EvalFrameEx函数 解释器dispatcher
    • run_pyc_file函数 执行pyc
    • opcode.h里面定义各种opcode宏,大于90是带参数的
    • 直接各种调试下断点,栈回溯即可摸清执行流程

详细分析

  • 处理Opcode
    目前还没找到很简单的方法,目前用了两种方式:
    1、编写测试代码生成,用两个解释器生成pyc,再用dis模块反汇编对比,这里可写个脚本提高下效率,这种方式能找到到绝大部分。
    2、对于方式1不好生成的指令,通过IDA对比PyEval_EvalFrameEx函数,手动识别特征代码(字符串、关系调用等)

  • 重新编译python
    替换了源码中的Include/opcode.h和Lib/opcode.py,最终生成python可以执行check.py,开始以为作者在解释器上做了大量工作,事实上并没有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
//恢复的opcode如下
#define NOP        65
#define STOP_CODE 61
#define POP_TOP 30
#define ROT_TWO 52
#define ROT_THREE 56
#define DUP_TOP 13
#define ROT_FOUR 16
#define UNARY_POSITIVE 32
#define UNARY_NEGATIVE 89
#define UNARY_NOT 57
#define UNARY_CONVERT 87
#define UNARY_INVERT 25
#define BINARY_POWER 77
#define BINARY_MULTIPLY 69
#define BINARY_DIVIDE 71
#define BINARY_MODULO 14
#define BINARY_ADD 81
#define BINARY_SUBTRACT 53
#define BINARY_SUBSCR 40
#define BINARY_FLOOR_DIVIDE 76
#define BINARY_TRUE_DIVIDE 48
#define INPLACE_FLOOR_DIVIDE 26
#define INPLACE_TRUE_DIVIDE 63
#define SLICE 0
#define SLICE_1 1
#define SLICE_2 2
#define SLICE_3 3
#define STORE_SLICE 4
#define STORE_SLICE_1 5
#define STORE_SLICE_2 6
#define STORE_SLICE_3 7
#define DELETE_SLICE 8
#define DELETE_SLICE_1 9
#define DELETE_SLICE_2 10
#define DELETE_SLICE_3 11
#define STORE_MAP 64
#define INPLACE_ADD 17
#define INPLACE_SUBTRACT 86
#define INPLACE_MULTIPLY 20
#define INPLACE_DIVIDE 74
#define INPLACE_MODULO 67
#define STORE_SUBSCR 73
#define DELETE_SUBSCR 23
#define BINARY_LSHIFT 66
#define BINARY_RSHIFT 19
#define BINARY_AND 18
#define BINARY_XOR 88
#define BINARY_OR 85
#define INPLACE_POWER 33
#define GET_ITER 70
#define PRINT_EXPR 39
#define PRINT_ITEM 59
#define PRINT_NEWLINE 15
#define PRINT_ITEM_TO 62
#define PRINT_NEWLINE_TO 24
#define INPLACE_LSHIFT 41
#define INPLACE_RSHIFT 72
#define INPLACE_AND 45
#define INPLACE_XOR 37
#define INPLACE_OR 29
#define BREAK_LOOP 50
#define WITH_CLEANUP 42
#define LOAD_LOCALS 83
#define RETURN_VALUE 46
#define IMPORT_STAR 28
#define EXEC_STMT 51
#define YIELD_VALUE 60
#define POP_BLOCK 22
#define END_FINALLY 31
#define BUILD_CLASS 54
#define HAVE_ARGUMENT    90    /* Opcodes from here have an argument: */
#define STORE_NAME 112
#define DELETE_NAME 127
#define UNPACK_SEQUENCE 107
#define FOR_ITER 108
#define LIST_APPEND 141
#define STORE_ATTR 102
#define DELETE_ATTR 137
#define STORE_GLOBAL 98
#define DELETE_GLOBAL 114
#define DUP_TOPX 110
#define LOAD_CONST 131
#define LOAD_NAME 94
#define BUILD_TUPLE 106
#define BUILD_LIST 133
#define BUILD_SET 116
#define BUILD_MAP 139
#define LOAD_ATTR 140
#define COMPARE_OP 95
#define IMPORT_NAME 124
#define IMPORT_FROM 135
#define JUMP_FORWARD 115
#define JUMP_IF_FALSE_OR_POP 123
#define JUMP_IF_TRUE_OR_POP 128
#define JUMP_ABSOLUTE 118
#define POP_JUMP_IF_FALSE 92
#define POP_JUMP_IF_TRUE 120
#define LOAD_GLOBAL 138
#define CONTINUE_LOOP 113
#define SETUP_LOOP 93
#define SETUP_EXCEPT 111
#define SETUP_FINALLY 130
#define LOAD_FAST 109
#define STORE_FAST 119
#define DELETE_FAST 142
#define RAISE_VARARGS 134
#define CALL_FUNCTION 90
#define MAKE_FUNCTION 126
#define BUILD_SLICE 105
#define MAKE_CLOSURE 136
#define LOAD_CLOSURE 132
#define LOAD_DEREF 125
#define STORE_DEREF 122
#define CALL_FUNCTION_VAR 99
#define CALL_FUNCTION_KW 100
#define CALL_FUNCTION_VAR_KW 101
#define SETUP_WITH 143
#define EXTENDED_ARG 145
#define SET_ADD 146
#define MAP_ADD 147
  • 反汇编pyc
    使用uncompyle6,发现反编译都失败了,最后选择了Decompyle++
    Decompyle++仓库:https://github.com/zrax/pycdc
    替换opcode后,WSL里cmake编译,执行pycdas,反编译虽然失败,至少能够反汇编了,最后py代码做了些保护如下:

名称混淆:
图片描述

 

指令switch平坦 + 花指令:
switch:每一条指令通过XOR跳往switch判断下一条指令
花指令:这里的opcode当NOP处理
图片描述

 

字符串加密(这里是后面Trace发现的),通过base64、zlib、xor等解密

1
eNpVlq+LImEYx/Xu9g6GxbDBIGLYsCwiGwz+AQO3YRCDwXAHEwwHLmIweN1gEDEYDCIGg0HEYNg/wGAYlglymA+DYVgmHGI+n1nm8/KUL+/M+7zPj+/zfZ+ZnzeJROLx0xUav5PXVVJWt7y7+XxdfZPVq0BPoCLgC6R5V+LE9ys+fpGVG280xgLPmOzF6df43YeDyN9CoClQwPhOjP8m4902GTwIvAh4AlMBSyCMS2h0BXYE2hHIFvghcBLY4OWAyUWgzmMeiFzNKatHoKjefwIjCNvyLvKcEThismJjRn6OTrKPscfjEA76cBDwuKWiJRtDYtzH7H7kXKCOPjHudJJbTCLCcpz1qWihV1mBMv2IWraHtS05/8JkxtkWG+90dUqmAxrfxPMU7s9U3kQHDr2M+pEitXt6OSGrOYqw2XV1e0LOjiBiTi6vqMRChANChtpzl7hF7eqNaGeOFWnABNVddFY2mUa9XLMRciwi9g92BWCCvxPdr+ir0aPyPMdGNL6N5z4hPYpZQ8mFUgdcjYB2b5FADTpPxB2wWsJGR6DKCRu1L7BbINY62jhiZ+E5RPzmuhjGl2yMGX0W9/cJpweq3CABF65KUOLQPI9ALfLbUH4bmjxGhqNvtwNNAYkbjZvrZ0b4kHQXekp1YdLXQtpQWxoh5aBuSZVFcikTsstQCCh6j0kRDdXhdEWgHYS1Ma6itR3EHvHiIgtbT8IenktsbLhlVYKfaWiHcZPHX4r7G+rm1eGqS1ZZiC1rO4vJ9cyqxjUYE2iNF5tdn9aaqdKmHwd9IqWH1pGmmA9Lh5AuSQ4he4zCamykqXyNBExDz5x9oSlmmhlKmoj1nSQrWu1rzYFHkj7+WggkpL97/T2q6q+puY0jGpCjSgsTcw1M0Q/MiCqdDnjMkOkFDZ2p0kyGgv6gTTQRTzA5o/wVArZgKMsJc89LqCmjf6gc7swEwm4JJKn9B4X61ks=', 866: '0', 715: 'marshal', 341: 'zlib', 203: 'base64'}, 866)

动态Trace

因为指令switch那块比较固定,静态还原代码也不是很难,考虑效率问题,最后还是找到了不错的动态跟踪的工具:
x-python仓库:https://github.com/rocky/x-python
做了如下改动:

  • 仓库依赖xdis,修改其opcode_2x.py等
  • 简单修改PyVM的eval_frame和format_instruction过滤掉干扰的指令
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #eval_frame函数
             if (bytecode_name not in ['COMPARE_OP','POP_JUMP_IF_FALSE','JUMP_ABSOLUTE','NOP','LOAD_FAST','STORE_FAST']):
                 if len(arguments) > 0:
                     if isinstance(arguments[0],long):
                         if arguments[0] < 111111183538799472:
                             self.log(bytecode_name, int_arg, arguments, offset, line_number)
                     else:
                         self.log(bytecode_name, int_arg, arguments, offset, line_number)                                           
                 else:
                     self.log(bytecode_name, int_arg, arguments, offset, line_number)
    1
    2
    3
    4
    5
    6
    7
    8
    #format_instruction函数
     if vm and bytecode_name in vm.byteop.stack_fmt:
         stack_args = vm.byteop.stack_fmt[bytecode_name](vm, int_arg, repr)
         if (bytecode_name == 'INPLACE_XOR'):
                 pos = stack_args.find('L')
                 if pos != -1:
                     if long(stack_args[stack_args.find('(')+1:pos])>111111111111111111111:
                         return ""

最后执行python_d.exe xpy.py -v check.pyc,几分钟后到了raw_input输入,看了下之前代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
INFO:xpython.vm:       @7266: LOAD_NAME dict
INFO:xpython.vm:       @7637: BUILD_LIST 0
INFO:xpython.vm:       @5069: LOAD_NAME __import__
INFO:xpython.vm:       @6232: LOAD_CONST marshal
INFO:xpython.vm:       @4648: CALL_FUNCTION (__import__) 1 positional, 0 named
INFO:xpython.vm:       @8028: LOAD_ATTR loads
INFO:xpython.vm:       @6832: LOAD_CONST eNpdmD2LKkkUhmf37geIGPSCgQwGBjKIdGDgDzAwkMFggg5c6MBAuCIGBv4AAwNxO5igA5EODAwGMTAwWDYyMJCLLCL9Aww6EOlAlonX8uJz9mxyqLKqznnPez6q2t9/fnp6evnxJr7++eU2+sGMkvz2202+/GpGayOGRtSN2BuR5reynDCjX8yoyup989yIjhFFNv9xM/fykxm5Dy1f/cfZ71sKZvT3l4eCnhEpI9qc8Iw4GlEy4h24MyMmRmSN+MeIjBFLI6ZGRKA6se8OPGdEBX0n1E/waGNEDOYTUxdAAyNGRlyN+IaCGgpyHMtwwgJ9jLBAsAZV/KR4GePRDM3CQQ6T4lH0CPd/TE7QF8FazIkBW8psuY8CoA1wP8fmNpvXWDuB5b65jwt99j2D4GDEBxwcWVgxvcB9wghH77svtIxosNDAmguTb4TnirUsNGWBcc/srhELTGZwtY2qsxE2+AJGR07cleY1vgRaPIxXWPgkliWI3ZDKNVC1dWnMyPuALD5iN0VQ2oxeiYK4H6EvQaCukNgEhnhU1Mk/JzcCcm2KC+JbmeqJsZbGmsNvz8AooioJ+tOjjXzX8k7cBugrMi2TUneyd4DcsLBDc1pHJoODM73Fwm4T4xGrWxRIfyni5RloAal3RoGL5yuI9THkwu6BjLUB9MmCw9mWpsnB8x7AA5B2UbDF6QJu+WTYmlpwyM4+WXLlWB19Cc5uQeVgY0AJdTR/0kqXqDqDRQrb1r3JhisLzTIKCe0BNq66yViYzGPojZukoGt1A+YUdsfUucBNU4hjSKwQ6S4Z9s6xOkG2cGtHii7ohM/gC6lpm+mZmjnqYsqTf55u9VmMt5gmQPoBk3v0fdAPQkrcIih1fJPeniL6PnAzjCSRdqCv89sEzDGaM5rdOV6WdH/xod0nlj4mu2he6Us9DTk9/ZutA1+GnDRVewGQh4Ihbs3B1yRafaxVONEmZHk2T3B/ra+xKZSUtDNb/JV3jrybpuirgGWD6OpkyBOUI6rq0D5kX6DTJ4/nPfB1mC5gsgiJCVxNcZuO9DXRxP0IG1tiPsGjV4p4wbEBeVrARgJX5VgMJXvq7QSxHbRIV6nRRsa4ekFplWLvAbKDNUf3XXnizEBfA6ncGkuOOeArozQgCicSJIlbUxrKFgdT+lXqUecbnaLySFjox528uUbAfcfagNUWoypnLfKgioMOkTnhqqcvNBvMPYS8ckP0ORDbJXcbUFLFeAiWEazNWUgR3wmpV2LfSPeIEWRnubHTsDaEziWtr0HtN3X9hmTsHmFjY6O7VAd/k5ytkbF54KaZzvS7xMLBHf6uwNzRfbxGlsw422XkkwIRJ+RefWZ1TicM2bdncwm4ZRiakHotoMn9a8GfvMctPC+SAhd9X9ZJPblYTmSdsNFA+HA/1vdWESLeqIAtdemRFg4V8L/Hp08+N0kam5D1UTpEeKwe9LdazMIUxuXBYoFeviHW+pEw1VFYsCC5O9J9fM7UhaEhSgMi7REU+ZqoQESWtHWxmwXQJ8bLGrOkihDxCdnylSrA67Cx1LF0iUwTrkJUyaegVE+ku0UJuBdaboEge1SUp19LS+wmdYNPQaeDCxHlUiDNYl26SVyV727JXcmcHOQ47AtJ6hAiclTAK6ik3QT6P4ouXkaALACoxtQlrxaETP7BaLMq3TsiyCt9WW/pL5K7Hv7K7dLS0Lb6vRvrv3LkK1W+Eg66GitsyaB5DC93pX/dxL//a4kn
INFO:xpython.vm:       @4908: LOAD_ATTR decode
INFO:xpython.vm:       @4959: LOAD_CONST base64
INFO:xpython.vm:       @6483: CALL_FUNCTION (decode) 1 positional, 0 named
INFO:xpython.vm:       @7383: LOAD_ATTR decode
INFO:xpython.vm:       @8232: LOAD_CONST zlib
INFO:xpython.vm:       @4318: CALL_FUNCTION (decode) 1 positional, 0 named
INFO:xpython.vm:       @5568: CALL_FUNCTION (loads) 1 positional, 0 named
INFO:xpython.vm:       @4207: FOR_ITER 7304
INFO:xpython.vm:       @5658: STORE_NAME ((954, (12,))) ooo00o
INFO:xpython.vm:       @5122: LOAD_NAME ooo00o
INFO:xpython.vm:       @8417: LOAD_CONST 0
INFO:xpython.vm:       @5897: BINARY_SUBSCR ((954, (12,)), 0)
INFO:xpython.vm:       @8584: LOAD_CONST 216
INFO:xpython.vm:       @8639: BINARY_XOR (954, 216)

执行效果还不错,看到了具体代码,以后有空再改造下把栈的信息拿出来,方便分析。

验证过程

  • 1、KCTF补位16字节,KCTF@021GoodLuck
  • 2、helloctf_pediy_Archaia进行md5计算,然后初始化表
  • 3、迭代两次,flag和表里元素xor,拿到中间结果
  • 4、接着进行解密运算,拿到结果和用户名比对,具体代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
#两次迭代过程,直接把表提取出来
t1 = [(145,161),(125,161),(11,161),(202,161),(7,161),(12,161),(126,161),(130,161),(114,161),(69,161),(43,161),(110,161),(48,161),(225,161),(43,161),(6,161)]
t2 = [(22,61),(174,61),(99,61),(135,61),(98,61),(99,61),(186,61),(139,61),(90,61),(211,61),(91,61),(87,61),(255,61),(225,61),(13,61),(144,61)]
 
import binascii
def build_flag(x):
    flag=''
    for i,v in enumerate(x):
        c = ord(v)^t1[i][0]^t1[i][1]^t2[i][0]^t2[i][1]
        flag += chr(c)
    flag = binascii.b2a_hex(flag.encode('latin-1')).decode('latin-1').upper()
    return flag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#最后解密运算
def WORD(v):
    return v&0xffff
def BYTE_LO(v):
    return v&0xff
def BYTE_HI(v):
    return v>>8
def MAKE_BYTE(lo,hi):
    return lo|(hi<<8)
def ROR(v, num):
    return (v>>(16-num)) | (v<<num)
def ROL(v, num):
    return (v<<(16-num)) | (v>>num)
def check_sn(a, b, x):
    flag = []
    for i in range(8):
        pos1 = (b ^ (a + i*2)) % 16
        pos2 = (b ^ (a + i*2 + 1)) % 16
        flag.append((x[pos1]<<8)|x[pos2])
 
    m = (flag[0]^flag[1])
    n = (flag[2]+flag[3])
    j = (flag[4]-flag[5])
    k = (flag[6]^flag[7])
 
    k = (k&0x55555555) + (k>>1&0x55555555)
    k = (k&0x33333333) + ((k>>2)&0x33333333)
    k = (k&0x0F0F0F0F) + ((k>>4)&0x0F0F0F0F)
    k = (k&0xff00ff) + ((k>>8)&0xff00ff)
    k = WORD(k) + ((k>>16)&0xff00ff)
 
    xx = (m&n)|((~m)&j)
    yy = WORD(WORD((m*xx)>>k)+24)
    zz = yy^j
    qq = (yy&xx)|(zz&xx)|(zz&yy)
 
    flag[0] = WORD(flag[0]^qq)
    flag[1] = WORD(flag[1]^qq)
    flag[2] = WORD(flag[2]+zz)
    flag[3] = WORD(flag[3]-zz)
    flag[4] = WORD(flag[4]+yy)
    flag[5] = WORD(flag[5]+yy)
    flag[6] = WORD(ROR(flag[6]^xx, k))
    flag[7] = WORD(ROR(flag[7]^xx, k))
 
    user = []
    for v in flag:
        lo = BYTE_LO(v)
        hi = BYTE_HI(v)
        #print('%c%c'%(lo,hi),end='')
        user.append(lo)
        user.append(hi)
    return user

最后求解

直接逆推不出来,注意到m、n、j、k的初始值和最后运算方式一致,因此可以把mnjk推导出来,至于k的值(最多是0-15),因此跑个16次计算,再把结果校验,最后得到KCTF的flag:

1
2
KCTF@021GoodLuck
AD0A1F8179ABE48ED3B073F840DA52A7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#求解过程片段
 
#计算差值
for i in range(int(len(x)/2)):
    v = x[i*2]|(x[i*2+1]<<8)
    brutes.append(v)
    print(brutes)
    m0 = brutes[0]^brutes[1]
    m1 = brutes[2]+brutes[3]
    m2 = brutes[4]-brutes[5]
    m3 = brutes[6]^brutes[7]
 
def reverse(flag):
    cc = flag.copy()
    for i in range(16):
        m,n,j,k = 1311,24946,2776,i  #m0,m1,m2
        xx = (m&n)|((~m)&j)
        yy = WORD(WORD((m*xx)>>k)+24)
        zz = yy^j
        qq = (yy&xx)|(zz&xx)|(zz&yy)
        #print(xx,yy,zz,qq)
        flag = cc.copy()
        flag[0] = WORD(flag[0]^qq)
        flag[1] = WORD(flag[1]^qq)
        flag[2] = WORD(flag[2]-zz)
        flag[3] = WORD(flag[3]+zz)
        flag[4] = WORD(flag[4]-yy)
        flag[5] = WORD(flag[5]-yy)
        flag[6] = WORD(ROL(flag[6], k)^xx)
        flag[7] = WORD(ROL(flag[7], k)^xx)
 
        x=[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
        a,b=14, 5
        for i in range(8):
            pos1 = (b ^ (a + i*2)) % 16
            pos2 = (b ^ (a + i*2 + 1)) % 16
            x[pos1] = flag[i]>>8
            x[pos2] = flag[i]&0xff
 
        import binascii
        t1 = [(145,161),(125,161),(11,161),(202,161),(7,161),(12,161),(126,161),(130,161),(114,161),(69,161),(43,161),(110,161),(48,161),(225,161),(43,161),(6,161)]
        t2 = [(22,61),(174,61),(99,61),(135,61),(98,61),(99,61),(186,61),(139,61),(90,61),(211,61),(91,61),(87,61),(255,61),(225,61),(13,61),(144,61)]
        gogo = ''
        for i,v in enumerate(x):
            c = v^t1[i][0]^t1[i][1]^t2[i][0]^t2[i][1]
            gogo += chr(c)
        xxx = x
        user = user_encrypt(14,5,xxx)
        if ('KCTF' not in user):
            continue
        print(user)
        gogo = binascii.b2a_hex(gogo.encode('latin-1')).decode('latin-1').upper()
        print(gogo)

【公告】 [2022大礼包]《看雪论坛精华22期》发布!收录近1000余篇精华优秀文章!

最后于 2021-12-9 12:45 被深山修行之人编辑 ,原因:
收藏
点赞0
打赏
分享
最新回复 (1)
雪    币: 6017
活跃值: 活跃值 (3512)
能力值: ( LV12,RANK:211 )
在线值:
发帖
回帖
粉丝
sunfishi 活跃值 3 2021-12-9 18:01
2
0
opcode的还原是个体力活……
游客
登录 | 注册 方可回帖
返回