出题思路

本来是想基于MazeLang出个逆向的题目,找了一圈没有找到好用的C/C++解释器 (出题人非常懒,不想自己写),于是就复用了一下之前在THUCTF2022上出的题Darkness Pro中实现的MazeLang的Python解析器 (没有想到被人发现了)

本题没有完全依赖PyInstaller进行打包。 如果用PyInstaller打包的话,大家就能直接获得源码了。 使用Cython将Python库文件转换为C代码后编译为动态链接库maze.so,通过在Python中调用动态链接库的方式实现函数调用。

在本题中,出题人并没有strip掉DWARF调试信息及符号表。因为出题人在出题的过程中发现,strip掉之后的题目难度会直线上升,为了保证大家的比赛体验,为大家保留了DWARF信息和符号表,只对类名及变量名进行了简单的base64混淆,以保证大家的做题体验。

解题思路

PyInstaller解包

通过字符串分析就可以发现,题目下发的可执行文件为PyInstaller打包,使用例如PyInstaller Extractor
pyinstxtractor-ng等对可执行文件进行解包,发现python版本为3.8,其中入口文件为chal.pyc

pyc文件是由py_compile解释器生成的字节码文件,便于python虚拟机更快地加载和执行,目前针对旧版本pyc文件的反编译有成熟的工作,例如Decompyle++

1
2
3
4
5
6
> ./pycdc chal.pyc
# Source Generated with Decompyle++
# File: chal.pyc (Python 3.8)

from maze import run
run()

通常来说,PyInstaller会将库文件生成对应的pyc文件,并打包成PYZ-00.pyz,pyinstxtractor-ng在解包的过程中会自动解包PYZ-00.pyz,但是仔细观察会发现,在chal_extracted/PYZ-00.pyz_extracted/文件夹中并没有maze.pyc这个文件。

通过Python Document会发现可以通过C/C++扩展Python,可以编写特殊的so库文件使Python可以直接import,观察chal_extracted/下的文件会发现一个特别的so文件maze.so。通过关键字符串可以辨认出该so文件由cython 3.0.5编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
> strings maze.so | grep cython
if _cython_generator_type is not None:
else: Generator.register(_cython_generator_type)
if _cython_coroutine_type is not None:
else: Coroutine.register(_cython_coroutine_type)
_cython_3_0_5.cython_function_or_method
_cython_3_0_5
_cython_coroutine_type
_cython_generator_type
cython_runtime
_cython_3_0_5.generator
__pyx_cython_runtime
cython_runtime_dict

分析maze.so初始化过程

使用IDA分析maze.so,通过查看导出表,可以看到函数PyInit_maze,根据相应的Python Document,这是maze对应的初始化函数。函数PyInit_maze会调用Python的对应函数初始化_pyx_moduledef_pyx_moduledef中包含两个对应的PyModuleDef_SlotPy_mod_create: __pyx_pymod_createPy_mod_exec: __pyx_pymod_exec_maze

根据文档中的说明,Py_mod_exec会绑定对应的函数,向模块中添加类和常量。可以在__pyx_pymod_exec_maze函数中看到非常多的常量定义,同时其中调用了_Pyx_CreateStringTabAndInitStrings函数。

Py_mod_exec: 指定一个供调用以执行模块的函数。 这造价于执行一个 Python 模块的代码:通常,此函数会向模块添加类和常量。

_Pyx_CreateStringTabAndInitStrings函数将所有Python代码使用到的字符串插入到__pyx_string_tab中,并于__pyx_mstate_global_static中对应的变量绑定。

__pyx_pymod_exec_maze中会将对应的字符串于变量绑定,同时还会初始化数组变量及绑定函数。

部分选手发现通过利用Python自带的help函数可以直接获得库中包含的所有类名和全局变量名。然后输出对应变量即可获得对应变量的值。

分析核心逻辑

通过符号表可以定位到_pyx_pw_4maze_5run函数,在chal.py中调用的便是这个函数。_pyx_pw_4maze_5run函数中首先print了欢迎语,然后input读取输入的flag,并调用了_pyx_mstate_global_static.__pyx_n_s_c29sdmU,通过查找Py_mod_exec对应绑定或者利用符号表可以找到函数_pyx_pw_4maze_3c29sdmU

分析__pyx_n_s_c29sdmU对应的_pyx_pw_4maze_3c29sdmU的逻辑:

  1. 解析传入的参数,并赋给__pyx_n_s_SvL6VEBRwx
  2. 调用base64.b64decode解密__pyx_n_s_UJ9mxXxeoS
  3. 使用解密后的值初始化了一个__pyx_n_s_TWF6ZUxhbmc类。
  4. 调用了__pyx_n_s_aW5pdF9zZWNyZXQ函数。
  5. 将传入参数第i位取ord后与__pyx_n_s_TWF6ZUxhbmc__pyx_n_s_cnVuX3RpbGxfb3V0cHV0函数的结果异或,然后与__pyx_n_s_c2VjcmV0的第i位比较。

分析__pyx_n_s_aW5pdF9zZWNyZXQ对应的_pyx_pw_4maze_1aW5pdF9zZWNyZXQ逻辑:

  1. for i in range(33)
  2. 取出__pyx_n_s_c2VjcmV0[__pyx_n_s_EqdU3uQNCi[i]]
  3. 取出__pyx_n_s_c2VjcmV0[i]
  4. __pyx_n_s_c2VjcmV0[__pyx_n_s_EqdU3uQNCi[i]]赋给__pyx_n_s_c2VjcmV0[i]
  5. __pyx_n_s_c2VjcmV0[i]赋给__pyx_n_s_c2VjcmV0[__pyx_n_s_EqdU3uQNCi[i]]

即实现了一个swap的功能,进行相同的操作可以获得真正的__pyx_n_s_c2VjcmV0

__pyx_n_s_UJ9mxXxeoS__pyx_kp_u_IyMgIyMgIyMgIyMgIyMgIyMgIyMKIyMg进行base64解密之后获得:

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
## ## ## ## ## ## ##
## ## ## ^^ ## ^^ ##
## ## ## .. ## IZ ## ## ## ##
## %R .. %D ## %D .. .. %L ##
## >> ## .. ## EA ** PP %U ##
## %U IA TA ## EB ** PP %U ##
## %U IB TB ## EC ** PP %U ##
## %U IC TC ## ED ** PP %U ##
## %U ID TD ## EE ** PP %U ##
## %U IE TE ## EF ** PP %U ##
## %U IF TF ## %R ** IZ %U ##
## %U IG %L ## ## ## ## ## ##
## ## ## ## ## ##

PP -> +=1
MM -> -=1
IZ -> =0
EA -> IF ==0 THEN %R ELSE %D
EB -> IF ==1 THEN %R ELSE %D
EC -> IF ==2 THEN %R ELSE %D
ED -> IF ==3 THEN %R ELSE %D
EE -> IF ==4 THEN %R ELSE %D
EF -> IF ==5 THEN %R ELSE %D
TA -> IF ** THEN %L ELSE %D
IA -> =72
TB -> IF ** THEN %L ELSE %D
IB -> =73
TC -> IF ** THEN %L ELSE %D
IC -> =84
TD -> IF ** THEN %L ELSE %D
ID -> =80
TE -> IF ** THEN %L ELSE %D
IE -> =67
TF -> IF ** THEN %L ELSE %D
IF -> =84
IG -> =70
LT -> IF ==6 THEN %D ELSE %L

这是一个MazeLang的代码,通过Maze Interpreter v2或者直接使用maze.so中的__pyx_n_s_TWF6ZUxhbmc类运行获得输出结果 (或者直接学习MazeLang的语法),这段代码会循环输出HITPCTF,然后只需要实现一个循环异或解密即可。

EXP

1
2
3
4
5
6
7
8
9
secret = [7, 47, 60, 28, 39, 11, 23, 5, 49, 49, 26, 11, 63, 4, 9, 2, 25, 61, 36, 112, 25, 15, 62, 25, 3, 16, 102, 38, 14, 7, 37, 4, 40]
pos = [18, 17, 15, 0, 27, 31, 10, 19, 14, 21, 25, 22, 6, 3, 30, 8, 24, 5, 7, 4, 13, 29, 9, 26, 1, 2, 28, 16, 20, 32, 12, 23, 11]
key = b"HITPCTF"

for i in range(33):
secret[i], secret[pos[i]] = secret[pos[i]], secret[i]

flag = "".join([chr(secret[i] ^ key[i % len(key)]) for i in range(len(secret))])
print(flag)

总结

原本是希望大家能够理解Cython生成的链接库so文件的原理,从中分析出数组初始化方法、函数及字符串组织结构及代码逻辑分析,然后可以解出此题,所以在MazeLang代码设计和解释器类内部的逻辑上没有设置考察点。

各队的逆向师傅们还是太强了,help函数学习到了,给各位磕一个 orz