下面从一些python的基本操作开始进行分析。总体流程准备跟着菜鸟教程上教的顺序走。
Cython程序分析_2
样例代码
1 | # test2.py |
第一个函数是基本数据类型的操作
第二个是字符串操作
第三个是导入模块进行使用
同时函数被调用了,脚本可以直接运行
1 | from setuptools import setup |
分析
这次C文件到达了惊人的5388行。离了个大谱。还是配合自动生成的html文件来分析比较好。
__pyx_pymod_create()
此函数毫无变化。以后可以不用去考虑这个函数了
__pyx_pymod_exec_test2()
这个函数自然是重头戏
1 | // 相关联的片段 |
在此Exec函数中,会将大致的模块流程给展现出来。
声明
会用一个__pyx_t_n
的指针来获取,使用__Pyx_CyFunction_New()
并向其中传入相应的PyModuleDef
来构建。上次博客已经讲过。
补充
值得注意的锚点是PyDict_SetItem()
,这个函数是外部API,不会内联;它会将__pyx_t_1
函数指针存入到一个全局字典__pyx_d
中去,紧接着__Pyx_DECREF(__pyx_t_1)
指针后退一个单位,为承接下一个函数做准备。
1 | __pyx_d = PyModule_GetDict(__pyx_m); if (unlikely(!__pyx_d)) __PYX_ERR(0, 1, __pyx_L1_error) |
调用
首先会__Pyx_GetModuleGlobalName(__pyx_t_1, __pyx_n_s_func_DataType)
,__pyx_n_s_func_DataType
是函数名,那么大概__pyx_t_1
就将作为承接函数的指针。这个函数大概就是根据字符串返回对象索引(函数也是一种PyObject
对象)
__Pyx_GetModuleGlobalName(var, name)宏
1 |
通过了一些判断,然后调用了__Pyx__GetModuleGlobalName()
函数和__Pyx_GetBuiltinName()
函数。前者是模块内自定义的函数,用全局字典维护;而后者Builtin函数则是内置的。
__Pyx__GetModuleGlobalName()
1 | /* GetModuleGlobalName */ |
这块函数是极有可能要被内联的。根据宏定义判别,我选取了最有可能的情况,即
1 |
|
总结一下,核心就是通过 _PyDict_GetItem_KnownHash()
来获取。
__Pyx_GetBuiltin()
函数
1 | /* GetBuiltinName */ |
核心就是个__Pyx_PyObject_GetAttrStr()
宏
1 |
还好这个是个外部API。
总结一下,就是对PyObject_GetAttr(o,n)
的调用。
然后就是调用了。
1 | __pyx_t_2 = __Pyx_PyObject_CallNoArg(__pyx_t_1); |
无参调用方式
真离谱啥都是内联的
1 | /* PyObjectCallNoArg */ |
二进制
1 | __int64 __fastcall _pyx_pymod_exec_test2(_QWORD *a1) |
可以看出编译成二进制之后变得混乱了不少。关于如何辨识出函数声明和调用,在后面的经验总结出谈了一下。这里直接把代码块贴出来。
声明部分
1 | f_datatype = _Pyx_CyFunction_New((unsigned int)&moddef, v26, strp_func_DataType, v27, v47, v50, qword_18000CC60);// |
调用部分
1 | __pyx_t1 = (_QWORD *)PyDict_GetItem_KnownHash( |
各函数解析
此部分将配合C文件和二进制文件一起来分析函数。
C文件中的逻辑其实应该是挺清晰的,但是编译后出现各种各样的内联操作,就会变复杂。
func_DataType()
1 | def func_DataType(): |
C文件
1 | /* Python wrapper */ |
int_1 = 114514
1 | PyObject *__pyx_v_int_1 = NULL; |
这个特征其实不是很明显,因为并没有直接调用API进行操作。不过还好这里没有出现内联问题。
而__pyx_int_114514
在__Pyx_InitGlobals()
函数中被引用。此函数则在__pyx_pymod_exec_test2()
函数的前部分被调用,用于初始化一些全局常量。
1 | static CYTHON_SMALL_CODE int __Pyx_InitGlobals(void) { |
由于CYTHON_SMALL_CODE
的声明,导致此函数会被内联。在IDA的exec函数中,我们会找到这个样子的代码:
1 | } |
不错的是PyLong_FromLong()
等相似基本类型构造函数都是外部API,所以还是很明显的。
对着qword_18000CD58
(后面重命名为int_114514
了)交叉引用,便能够找到在func_DataType()
中的相关代码。
1 | v2 = (_QWORD *)int_114514; |
float_1 = 191981.0
C文件中:
1 | double __pyx_v_float_1; |
被直接变成C风格的浮点类了。这种最容易被优化,所以基本没有直接找到的希望。
bool_1 = True
1 | int __pyx_v_bool_1; |
变成了C的整型。但是打印的时候还是得打印出True
字符串而不是数字1,所以接着往后看。
complex_1 = 1 + 2j
1 | __pyx_t_1 = __Pyx_c_sum_double(__pyx_t_double_complex_from_parts(1, 0), __pyx_t_double_complex_from_parts(0, 2.0)); |
三个函数,一个结构体
__pyx_t_double_complex_from_parts()
1 | /* Declarations */ |
会被内联。暂时也不知道到底选择的是哪种模式。
__pyx_t_double_complex
复数结构体
1 | /* Declarations.proto */ |
结构体其实挺麻烦的,因为IDA基本上是识别不出来的,必然会被拆分开来。
__Pyx_c_sum_double()
1 | /* Arithmetic.proto */ |
也是被内联了。
__pyx_PyComplex_FromComplex()
1 | /* RealImag.proto */ |
总结一下,暂时没有什么好定位的特征点,这些函数基本由于内联,以及常量传递等方法已经被优化掉了。
print(int_1 * 2)
1 | __pyx_t_2 = PyNumber_Multiply(__pyx_v_int_1, __pyx_int_2); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 15, __pyx_L1_error) |
特征很明显,PyNumber_Multiply()
,后面调用的那个函数很明显也是__Pyx_PyObject_CallOneArg()
。
PyNumber_Multiply()
1 | int_114514_mult2 = PyNumber_Multiply(int_114514_, int_2); |
print(float_1 - 1)
1 | __pyx_t_3 = PyFloat_FromDouble((__pyx_v_float_1 - 1.0)); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 16, __pyx_L1_error) |
关注两个点,
PyFloat_FromDouble()
1 | v10 = PyFloat_FromDouble(); |
没传参?应该是反汇编界面错误。
1 | movsd xmm0, cs:qword_1800086A8 |
把qword_1800086A8
或者函数传参设置类型为浮点型。再次反编译后得到
1 | v12 = (_QWORD *)PyFloat_FromDouble(191980.0); |
后面就是打印
1 | v11 = (_QWORD *)__Pyx_PyObject_CallOneArg((_QWORD *)glob_func_print, float_191980); |
回想上一个博客的内容,上一次调用的是__Pyx_PrintOne()
函数打印字符串的,且在二进制文件中被内联了,所以只能根据一些特征,和方法调用来观察。而这个函数在这次并没有被内联。
print(bool_1)
1 | __pyx_t_2 = __Pyx_PyBool_FromLong(__pyx_v_bool_1); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 17, __pyx_L1_error) |
__Pyx_PyBool_FromLong()
1 |
|
又是个屑内联。
且__Pyx_NewRef()
也是个指针操作,所以不算是什么好的锚点。
Py_True
这个倒是个有意思的元素。
在python/include/boolobject.h
中定义。
1 | /* Don't use these directly */ |
这一个结构体是有够复杂的。不过看上去比较好的是,由于实际上这是一个extern
对象(由PyAPI_DATA()
宏定义),所以是保留了符号的。
1 | v7 = (_QWORD *)++Py_TrueStruct[0]; |
直接就这样看吧。
print(complex_1 + 3)
1 | __pyx_t_3 = __Pyx_PyInt_AddObjC(__pyx_v_complex_1, __pyx_int_3, 3, 0, 0); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 18, __pyx_L1_error) |
__Pyx_PyInt_AddObjC
1 | /* PyIntBinop */ |
其特征就是有个switch-case
和多个if-else
判断。此函数会被内联。
IDA中
1 | if ( v9 ) |
后半部分的+3倒是挺清晰的,但是初始化部分还是很乱。
注意到v1
的使用,且伴有偏移,极有可能是结构体。
往上翻相关引用。
1 | v1 = 0i64; |
这里调用了
PyComplex_FromDoubles()
函数
根据python/include/complexobject.h
定义
1 | PyAPI_FUNC(PyObject *) PyComplex_FromDoubles(double real, double imag); |
在IDA里面调整类型,得到
1 | v3 = PyComplex_FromDoubles(1.0, 2.0); |
汇编
1 | .text:0000000180007548 movsd xmm1, cs:qword_180008698 ; double |
func_Str()
str_1 = "teststr1"
等
1 | /* "test2.py":21 |
后面几个初始化都是一样的。上个博客已经写过了。
IDA中
1 | ++*(_QWORD *)::str_1; |
print(str_1[0:4])
1 | __pyx_t_1 = __Pyx_PyUnicode_Substring(__pyx_v_str_1, 0, 4); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 25, __pyx_L1_error) |
__Pyx_PyUnicode_Substring()
会被内联
1 | /* PyUnicode_Substring */ |
PyUnicode_FromUnicode()
或PyUnicode_FromKindAndData()
是特征。
PyUnicode_Ready()
外部API也有可能是特征。
1 | #define __Pyx_PyUnicode_READY(op) (likely(PyUnicode_IS_READY(op)) ?\ |
意思就是从Unicode字符串中根据长度和偏移再返回一个Unicode字符串。
二进制下
1 | if ( *(char *)(str_1 + 32) >= 0 && (unsigned int)PyUnicode_Ready(str_1) == -1 ) |
__Pyx_PyObject_CallOneArg()
1 | /* PyObjectCallOneArg */ |
有意思的是虽然明确了要内联,但这里却没有。可能是因为我的测试样本中print()
函数调用了太多次,导致编译器认为内联了也没有优化作用。
不过要是内联的话,关注__Pyx_PyObject_Call()
里面调用的函数和参数是啥就好了。
print(str_1[-4:-1])
1 | __pyx_t_2 = __Pyx_PyUnicode_Substring(__pyx_v_str_1, -4L, -1L); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 26, __pyx_L1_error) |
没有本质上的区别,不过在长度判断上,即if
块那边反编译器整理出来的结果要稍微混乱一点。
1 | v15 = (*substr)-- == 1i64; |
print(str_1[0])
1 | __pyx_t_3 = __Pyx_GetItemInt_Unicode(__pyx_v_str_1, 0, long, 1, __Pyx_PyInt_From_long, 0, 0, 1); if (unlikely(__pyx_t_3 == (Py_UCS4)-1)) __PYX_ERR(0, 27, __pyx_L1_error) |
单独取这一个值,结果调用的函数又不一样了。
表达的意思跟
1 | print(chr(ord(str_1[0]))) |
一样。
__Pyx_GetItemInt_Unicode()
应该是获得Unicode字符的数字形式,相当于ord()
。
1 | /* GetItemIntUnicode.proto */ |
PyUnicode_FromOrdinal()
输入一个数字,返回其表示的Unicode字符,相当于chr()
函数。
是外部API。
IDA中
1 | v15 = (*substr)-- == 1i64; |
通过这个没啥特征的玩意来判别到底取了哪个值确实很麻烦。于是我又编译了一个例子来比对。
1 | def func_Str2(): |
1 | // write access to const memory has been detected, the output may be wrong! |
可以清楚的看到
1 | t = (_QWORD *)*(unsigned __int8 *)(str_123456 + v4 + 3);// str[3] |
有个+3的偏移。这样就能够知道取值了。
print(str_1[6:])
1 | __pyx_t_2 = __Pyx_PyUnicode_Substring(__pyx_v_str_1, 6, PY_SSIZE_T_MAX); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 28, __pyx_L1_error) |
1 | v15 = (*substr)-- == 1i64; |
这里又不内联了。
函数右侧设置了一个很大的值。
print(str_3[0:7:2])
1 | __pyx_t_1 = __Pyx_PyObject_GetItem(__pyx_v_str_3, __pyx_slice__2); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 29, __pyx_L1_error) |
这次使用的是__Pyx_PyObject_GetItem
函数配合__pyx_slice__2
来获取。
__pyx_slice__2
的初始化在__Pyx_InitCachedConstants()
中
__Pyx_InitCachedConstants
1 | static CYTHON_SMALL_CODE int __Pyx_InitCachedConstants(void) { |
__Pyx_PyObject_GetItem
1 | static PyObject *__Pyx_PyObject_GetItem(PyObject *obj, PyObject* key) { |
IDA中
1 | v15 = (*substr)-- == 1i64; |
qword_18000CCA8
能发现被另一个函数引用
1 | // InitCachedConstants |
而
1 | // PyObject_GetItem |
不管这个多层内联的函数了。知道了slice
,这里我们直接合理推测是GetItem()
函数。
print(str_3 * 2)
1 | __pyx_t_2 = PyNumber_Multiply(__pyx_v_str_3, __pyx_int_2); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 30, __pyx_L1_error) |
直接调用PyNumber_Multiply()
我是真没想到的。这个是个外部API,很好辨识。
1 | v15 = (*substr)-- == 1i64; |
print(str_2 + str_1)
1 | __pyx_t_1 = __Pyx_PyUnicode_Concat(__pyx_v_str_2, __pyx_v_str_1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 31, __pyx_L1_error) |
__Pyx_PyUnicode_Concat()
1 | #if CYTHON_COMPILING_IN_PYPY |
所以是PyUnicode_Concat()
,直接外部API。
1 | v15 = (*substr)-- == 1i64; |
print(str_2.index("str2"))
1 | __pyx_t_2 = __Pyx_CallUnboundCMethod1(&__pyx_umethod_PyUnicode_Type_index, __pyx_v_str_2, __pyx_n_u_str2); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 32, __pyx_L1_error) |
__Pyx_CallUnboundCMethod1()
这个函数被狠狠的内联了。
1 | /* CallUnboundCMethod1 */ |
特征:
函数指针调用
PyTuple_Pack(2, self, arg);
__Pyx_PyObject_Call(cfunc->method, args, NULL);
C文件
1 | /* UnpackUnboundCMethod.proto */ |
如何在IDA中恢复
先把结构体在IDA中简单申明一下。指针用QWORD*
和QWORD**
,QWORD *(__stdcall *PyCFunction)(QWORD *, QWORD *)
即可。
- 然后,可以尝试在exec函数的
InitGlobals()
部分找一下被PyUnicode_Type
等类似对象赋值的qword_xxxxxx
,这个就是结构体的type
元素。就可以顺藤摸瓜恢复结构体。 - 或者,查找字符串,寻找与方法有关的字符串,比如”index”,然后交叉引用,找到字符串的
PyObject*
对象指针,然后再对这个对象指针进行交叉引用,便能找到另外的被引用的地方,若下面一个QWORD
正好被注释为一个函数指针,那么基本就对到所在结构体了(交叉引用会发现不少垃圾代码,具体的在其他部分介绍了)
恢复结构体确实是个很正确的举措,现在看看代码
1 | v15 = (*substr)-- == 1i64; |
可读性已经十分强了,再配合存留的垃圾函数,就能直接确定是str_2.index("str2")
了。
print(str_fmt.format(str_1, str_2))
1 | __pyx_t_1 = __Pyx_CallUnboundCMethod2(&__pyx_umethod_PyUnicode_Type_format, __pyx_v_str_fmt, __pyx_v_str_1, __pyx_v_str_2); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 33, __pyx_L1_error) |
和上面的基本一样操作,不过是CallUnboundCMethod2()
因为传入了两个参数。
1 | v15 = (*substr)-- == 1i64; |
基本一样的操作,不过是PyTuple_Pack(2)
了,三个思路一致的函数调用操作,也能证实。
print("No endl", end="")
1 | // __Pyx_PyDict_NewPresized(n) |
总结一下,打印字符串打包成元组,传入参数变成了键值对,打包成元组然后传入。
IDA的还原应该也不难,稍微对着几个QWORD
溯源查一下字符串即可。
1 | v15 = (*substr)-- == 1i64; |
print('')
1 | // print(("", )) |
也是直接PyTuple_Pack()
,然后传进去。
一个小区别
我在思考,是不是
1 | def func_a(): |
和
1 | def func_b(): |
编译出来不一样
上面那种将会直接打包成元组,且字符串初始化,元组打包在InitCachedConstants()
函数中进行;而下面一种的元组打包则在其他函数调用中实现了。
编译了一下,结果如下:
1 | +1: def func_a(): |
区别确实是有的,但不是很大
1 | // write access to const memory has been detected, the output may be wrong! |
1 | // write access to const memory has been detected, the output may be wrong! |
1 | __int64 InitCachedConstants() //本函数已被内联,这是垃圾代码副本 |
func_ImportModule()
这个博客写的有点多了,留到下面一个,配合出现的一些疑问一起解答。
经验总结
在exec函数中分析声明和调用流程
这一部分其实是先写的,是IDA分析后的总结。
全局变量初始化部分
在__pyx_pymod_exec_test2()
函数的前半段都是检查和初始化。这里可以搜索到初始化的Python int
型变量(PyLong_FromLong
)。
声明部分
从导出界面快速定位初始化函数,然后通过其传入的结构体来定位__pyx_pymod_create()
和__pyx_pymod_exec()
函数。
在exec函数中寻找函数定义(def
),从而找到函数位置:有两种可能性,小代码情况下可能会内敛__Pyx_CyFunction_New()
函数,此时就是上次blog所讲的,查找鲜明的初始化特征;没有内敛也一样,点开来函数看看。
或者从函数对象结构体分析也行。
调用部分
内联情况下,定位_PyDict_GetItem_KnownHash()
外部API,因为这是__Pyx_GetModuleGlobalName(var, name)
宏内联出现的。
然后其下面应该又有一个函数,通过其中的逻辑可以判定是_Pyx_GetBuiltinName()
函数
1 | __int64 __fastcall _Pyx_GetBuiltinName(__int64 a1) |
逻辑就是它会尝试从全局的builtin模块中根据传参获取相应对象。
然后往下就有可能会有一个函数调用了其返回值,这个函数就是Call函数。这就是未内敛情况,不过这个调用函数比较复杂,通常应该不会被内联,没什么意义。
1 | __int64 __fastcall _Pyx_PyObject_CallNoArg(__int64 a1) |
其中的子函数
1 | __int64 __fastcall PyObject_Call_0(__int64 a1, __int64 a2) |
1 | __int64 __fastcall _Pyx_PyObject_CallMethO(__int64 a1, __int64 a2, __int64 a3) |
后两个函数都是后来重命名的。可以通过错误抛出来很轻松的识别出所在函数。这种函数大抵不会被内联,否则错误抛出都没意义了。
待办
- 小浮点数会被转义成C浮点类型,那么只能由python承载的大浮点数呢
- 尝试大整数
其他
有意思的是,即使有的函数被内联了,但似乎其原本形式还是会以一个副本形式保留下来,比如下面的函数,很明显就是InitGlobals()
函数,然而这个函数并未被调用,exec函数中已经有了其内联版本,此函数完全是垃圾代码。
1 | __int64 InitGlobals() |
还有这一个,应该是CallUnboundCMethod1()
函数的副本
1 | __int64 __fastcall sub_180004150(__int64 a1, _QWORD *a2) |
以上函数都是在对_pyx_umethod_PyUnicode_Type_index
结构体交叉引用时遇到的。
- 本文作者: Taardis
- 本文链接: https://taardisaa.github.io/2022/01/27/Cython Reverse 2/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!