TPCTF2023 Maze WP
出题思路
本来是想基于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_Slot,Py_mod_create: __pyx_pymod_create
和Py_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
的逻辑:
- 解析传入的参数,并赋给
__pyx_n_s_SvL6VEBRwx
。 - 调用base64.b64decode解密
__pyx_n_s_UJ9mxXxeoS
。 - 使用解密后的值初始化了一个
__pyx_n_s_TWF6ZUxhbmc
类。 - 调用了
__pyx_n_s_aW5pdF9zZWNyZXQ
函数。 - 将传入参数第i位取
ord
后与__pyx_n_s_TWF6ZUxhbmc
类__pyx_n_s_cnVuX3RpbGxfb3V0cHV0
函数的结果异或,然后与__pyx_n_s_c2VjcmV0
的第i位比较。
分析__pyx_n_s_aW5pdF9zZWNyZXQ
对应的_pyx_pw_4maze_1aW5pdF9zZWNyZXQ
逻辑:
for i in range(33)
- 取出
__pyx_n_s_c2VjcmV0[__pyx_n_s_EqdU3uQNCi[i]]
- 取出
__pyx_n_s_c2VjcmV0[i]
- 将
__pyx_n_s_c2VjcmV0[__pyx_n_s_EqdU3uQNCi[i]]
赋给__pyx_n_s_c2VjcmV0[i]
- 将
__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 | ## ## ## ## ## ## ## |
这是一个MazeLang的代码,通过Maze Interpreter v2或者直接使用maze.so
中的__pyx_n_s_TWF6ZUxhbmc
类运行获得输出结果 (或者直接学习MazeLang的语法),这段代码会循环输出HITPCTF
,然后只需要实现一个循环异或解密即可。
EXP
1 | 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] |
总结
原本是希望大家能够理解Cython生成的链接库so文件的原理,从中分析出数组初始化方法、函数及字符串组织结构及代码逻辑分析,然后可以解出此题,所以在MazeLang代码设计和解释器类内部的逻辑上没有设置考察点。
各队的逆向师傅们还是太强了,help函数学习到了,给各位磕一个 orz