2019看雪CTF-Q4-南充茶馆
2019看雪CTF-Q4-南充茶馆
解压exe后出现了两个文件,CM.exe和一个dll。调试后发现会释放一些文件到Temp目录下,包含一些编译成PE文件的Python库(pyd)、态链接库(dll)、压缩包(zip),然后调用NtCreateUserProcess启动一个子进程。子进程中会创建Python运行环境,核心逻辑就是通过Python代码实现的。
尝试直接用x64dbg调试,只能定位到一段sha256的代码,之后的代码难以定位。于是尝试更换方法。
虽然是Python环境,但是库文件全部被编译成了pyd,难以分析。
于是尝试从环境入手,尝试Hook python37.dll 导出的PyEval_EvalCode。
首先需要Patch dll文件,将PyEval_EvalCode的前五个字节修改成死循环,在父进程执行NtCreateUserProcess的时候断下,将修改后的dll文件放到Temp目录下。然后附加到子进程,还原PyEval_EvalCode的代码。再使用x64dbg注入Hook的dll。
PyEval_EvalCode函数原型:
1 | |
这个函数的第一个参数的类型是PyCodeObject,即Python虚拟机将要执行的代码对象。其中包含了代码的字节码、参数个数等等信息。
通过查阅文档,发现了另一个函数:
1 | |
PyMarshal_WriteObjectToFile可以将一个PyCodeObject写入到一个文件中。
于是我在Hook PyEval_EvalCode 的代码中调用PyMarshal_WriteObjectToFile,将Python虚拟机执行的PyCodeObject dump了出来。补上文件头,这些文件就是一个个标准的pyc文件。
尝试使用pyc反编译工具进行反编译,但有些pyc不能正常反编译,不过关键的代码已经能够看到一部分了。
在查看PyMarshal_WriteObjectToFile的文档时,还发现另一个函数:
1 | |
这两个函数的功能相反,一个是将python对象序列化成文件,一个是通过文件反序列化成Python对象,于是将以下Python代码编译成pyc:
1 | |
去掉生成的pyc文件的头,在Hook PyEval_EvalCode 的代码中调用PyMarshal_ReadObjectFromFile,将返回的对象传给PyEval_EvalCode进行调用,就获得了一个Python Shell。通过这个shell就可以实现调用函数、反编译函数等。


在shell中执行以下代码可以将函数序列化到磁盘,再在自己的代码中反序列化后进行调用,方便调试:
1 | |

接下来说一说这个CM的主要流程:
将Username进行sha256后进行一系列操作得到一个seq列表(值的范围[0-9])。再调用bytes_to_long将Username转成一个数字。
将输入的Serial转成数字:
serial = int(Serial, 16)。调用
check(serial, seq)。
check中会根据seq中的值调用enc0或pow。
pow(now,e,n)实际上是RSA加密。已知n和e,需要将n进行分解成两个质数p,q(我是通过factordb查到了结果),求出d。再调用pow就可以进行解密。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16def check(serial, seq):
now = serial
for j in range(len(seq)):
i = seq[j]
n = pub_n_list[i]
e = pub_e_list[i]
if now > pub_n_list[i]:
return False
if i > 0:
now = pow(now, e, n)
continue
now = enc0(now, e, n)
if 0 == now:
return False
return nowenc0中也是RSA加密(将数字分成4段进行加密),通过字节码还原后的函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14def enc0(m,e,n):
nbit = 192
def xxx(x,t):
return (x>>(t*nbit))&((1 << nbit)-1)
T = (n.bit_length()-1)//nbit +1
ans = 0
for i in range(T-1,-1,-1):
now_n = xxx(n,i)
now_e = xxx(e,i)
now_m = xxx(m,i)
if(now_m >= now_n):
return 0
ans = (ans << nbit) + pow(now_m,now_e,now_n)
return ans判断第1步bytes_to_long(Username) 的结果与第3步check(serial, seq)的结果是否一致。
