2019看雪CTF-Q4-南充茶馆

2019看雪CTF-Q4-南充茶馆

解压exe后出现了两个文件,CM.exe和一个dll。调试后发现会释放一些文件到Temp目录下,包含一些编译成PE文件的Python库(pyd)、态链接库(dll)、压缩包(zip),然后调用NtCreateUserProcess启动一个子进程。子进程中会创建Python运行环境,核心逻辑就是通过Python代码实现的。

尝试直接用x64dbg调试,只能定位到一段sha256的代码,之后的代码难以定位。于是尝试更换方法。
sha256

虽然是Python环境,但是库文件全部被编译成了pyd,难以分析。

于是尝试从环境入手,尝试Hook python37.dll 导出的PyEval_EvalCode
首先需要Patch dll文件,将PyEval_EvalCode的前五个字节修改成死循环,在父进程执行NtCreateUserProcess的时候断下,将修改后的dll文件放到Temp目录下。然后附加到子进程,还原PyEval_EvalCode的代码。再使用x64dbg注入Hook的dll。

PyEval_EvalCode函数原型:

1
PyAPI_FUNC(PyObject *) PyEval_EvalCode(PyObject *, PyObject *, PyObject *);

这个函数的第一个参数的类型是PyCodeObject,即Python虚拟机将要执行的代码对象。其中包含了代码的字节码、参数个数等等信息。

通过查阅文档,发现了另一个函数:

1
PyAPI_FUNC(void) PyMarshal_WriteObjectToFile(PyObject *, FILE *, int);

PyMarshal_WriteObjectToFile可以将一个PyCodeObject写入到一个文件中。

于是我在Hook PyEval_EvalCode 的代码中调用PyMarshal_WriteObjectToFile,将Python虚拟机执行的PyCodeObject dump了出来。补上文件头,这些文件就是一个个标准的pyc文件。
尝试使用pyc反编译工具进行反编译,但有些pyc不能正常反编译,不过关键的代码已经能够看到一部分了。

在查看PyMarshal_WriteObjectToFile的文档时,还发现另一个函数:

1
PyAPI_FUNC(PyObject *) PyMarshal_ReadObjectFromFile(FILE *);

这两个函数的功能相反,一个是将python对象序列化成文件,一个是通过文件反序列化成Python对象,于是将以下Python代码编译成pyc:

1
2
3
4
5
while True:
try:
eval(input())
except Exception as e:
print e

去掉生成的pyc文件的头,在Hook PyEval_EvalCode 的代码中调用PyMarshal_ReadObjectFromFile,将返回的对象传给PyEval_EvalCode进行调用,就获得了一个Python Shell。通过这个shell就可以实现调用函数、反编译函数等。

获得Python Shell

dis check

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

1
2
3
4
5
6
7
8
# 序列化
exec('import marshal')
marshal.dump(general.enc0.__code__, open('/path/enc0.data','wb'))

# 反序列化
code = marshal.load(open('/path/enc0.data','rb'))
enc0 = types.FunctionType(code, globals(), "")
print(enc0(1,2,3))

本地反编译反序列化后的enc0函数

接下来说一说这个CM的主要流程:

  1. 将Username进行sha256后进行一系列操作得到一个seq列表(值的范围[0-9])。再调用bytes_to_long将Username转成一个数字。

  2. 将输入的Serial转成数字: serial = int(Serial, 16)

  3. 调用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
    16
    def 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 now

    enc0中也是RSA加密(将数字分成4段进行加密),通过字节码还原后的函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    def 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
  4. 判断第1步bytes_to_long(Username) 的结果与第3步check(serial, seq)的结果是否一致。
    Correct