KCTF 2021秋季赛 第九题 万事俱备
文章发布至看雪《KCTF 2021秋季赛 第九题 万事俱备》
初步分析
- 易语言? 拿到程序,发现是易语言写的,还有点奇怪,直接x64dbg调试先看,跑起来发现易语言只是套了个外壳:
- 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,开始以为作者在解释器上做了大量工作,事实上并没有。
//恢复的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等解密
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过滤掉干扰的指令
#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)
#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输入,看了下之前代码如下:
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、接着进行解密运算,拿到结果和用户名比对,具体代码如下:
#两次迭代过程,直接把表提取出来
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
#最后解密运算
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:
KCTF@021GoodLuck
AD0A1F8179ABE48ED3B073F840DA52A7
#求解过程片段
#计算差值
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)