pyd文件逆向分析(二)
摘要:关于对.pyd文件的逆向分析
引言
最近研究破解某软件发现.pyd破解只能看脸。按平台分为以下两种结果
- 如果.pyd文件是在linux上编译的(后缀是.so)很可能是带调试信息的,函数的名称什么的是不会删去,很容易根据函数名字猜到功能。
- 如果.pyd文件是在windows上编译的默认是不带调试信息的,函数的名称什么的会删去,破解很难定位到相关函数。
准备工作
# 文件名 setup.py
from setuptools import setup
from Cython.Build import cythonize
setup(
name='test',
ext_modules=cythonize('util1.py', gdb_debug=True)
)
# 文件名 util1.py
def fun_hello(s):
if s == "didi":
return 'hello world'
elif s == "emm":
return '222222222'
# 文件名 test.py
import util1
if __name__ == '__main__':
while 1:
a=util1.fun_hello("hahaha")
print(a)
input("任意字符")
运行下面命令编译util1.py生成.pyd文件
python setup.py build_ext --inplace
然后运行下面命令将test.py打包成位于dist目录下的test.exe文件
pyinstaller test.py
关于参数的传递
当向fun_hello函数传递"hahaha"参数时会发现s变量是一个pyobject类型的指针。为此我去翻了翻关于pyobject结构的python源代码,但是发现对不上,所以不在这说了,下面直接说结果。s指针指的地址的值加上0x30字节就是字符串所在的地址。
如下图以pyx_n_s_didi举例:
当向fun_hello函数传递数字2参数时会发现s变量是一个pyobject类型的指针。为此我去翻了翻关于pyobject结构的python源代码,但是发现对不上,所以不在这说了,下面直接说结果。s指针指的地址的值加上0x18字节就是数字2所在的地址。
如下图以pyx_int_2举例:
关于函数的调用
# 文件名 util2.py
import util1
a=util1.fun_hello(2)
print(a)
util2.pyd中调用util1.pyd中的函数的机制。
要调用的函数名为fun_hello,在util2.pyd搜索字符串
转到对fun_hello的引用
转到__pyx_n_s_fun_hello
转到对__pyx_n_s_fun_hello的引用
转到函数伪代码
关键函数为_Pyx_PyObject_GetAttrStr和PyObject_Call,详情看图片中的IDA注释。PyDict_SetItem将函数返回值赋值给_pyx_n_s_a变量。 __Pyx_GetModuleGlobalName将_pyx_n_s_a的值赋值给变量v30
对应的C代码:
/* "util2.py":2
* import util1
* a=util1.fun_hello(2) # <<<<<<<<<<<<<<
* print(a)
*/
__Pyx_GetModuleGlobalName(__pyx_t_1, __pyx_n_s_util1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 2, __pyx_L1_error)
__Pyx_GOTREF(__pyx_t_1);
__pyx_t_2 = __Pyx_PyObject_GetAttrStr(__pyx_t_1, __pyx_n_s_fun_hello); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 2, __pyx_L1_error)
__Pyx_GOTREF(__pyx_t_2);
__Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
__pyx_t_1 = __Pyx_PyObject_Call(__pyx_t_2, __pyx_tuple_, NULL); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 2, __pyx_L1_error)
__Pyx_GOTREF(__pyx_t_1);
__Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
if (PyDict_SetItem(__pyx_d, __pyx_n_s_a, __pyx_t_1) < 0) __PYX_ERR(0, 2, __pyx_L1_error)
__Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
/* "util2.py":3
* import util1
* a=util1.fun_hello(2)
* print(a) # <<<<<<<<<<<<<<
*/
__Pyx_GetModuleGlobalName(__pyx_t_1, __pyx_n_s_a); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 3, __pyx_L1_error)
__Pyx_GOTREF(__pyx_t_1);
if (__Pyx_PrintOne(0, __pyx_t_1) < 0) __PYX_ERR(0, 3, __pyx_L1_error)
__Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
关于判断两个字符串或者两个数字是否相等
无论以何种形式判断字符串或者数字相等,python编译的.pyd文件都会在判断代码之后返回一个Py_FalseStruct或者Py_TrueStruct的结构体
然后调用__Pyx_PyObject_IsTrue或者PyObject_IsTrue判断,如果返回值是Py_FalseStruct返回0,如果是Py_TrueStruct返回1。汇编通过1和0跳转。这样如果能定位到大致代码段,可直接暴力破解。
下面是已经探明的cpython生成的.c文件和.pyd文件的对照关系。
判断数字相等的相关函数
__Pyx_PyObject_IsTrue
__Pyx_PyObject_IsTrue 是对 PyObject_IsTrue的二次封装,返回值为1或者0。
对应的c代码:
__Pyx_PyObject_IsTrue
static CYTHON_INLINE int __Pyx_PyObject_IsTrue(PyObject* x) {
int is_true = x == Py_True;
if (is_true | (x == Py_False) | (x == Py_None)) return is_true;
else return PyObject_IsTrue(x);
}
对应的IDA伪代码:
调用举例:
/* "util1.py":3
* def fun_hello(s):
* print("emm")
* if s == 1: # <<<<<<<<<<<<<<
* print("emm")
* return 'hello world'
*/
__pyx_t_1 = __Pyx_PyInt_EqObjC(__pyx_v_s, __pyx_int_1, 1, 0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 3, __pyx_L1_error)
__Pyx_GOTREF(__pyx_t_1);
__pyx_t_2 = __Pyx_PyObject_IsTrue(__pyx_t_1); if (unlikely(__pyx_t_2 < 0)) __PYX_ERR(0, 3, __pyx_L1_error)
__Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
//其中 __pyx_int_1是一个pyobject类型,对应数字1。__pyx_v_s与__pyx_int_1进行比较。
PyObject_IsTrue
PyObject_IsTrue,这个应该是python里面的内置函数,找不到对应的c代码,返回值为1或者0
对应的IDA伪代码:
调用举例:
_Pyx_PyInt_EqObjC
_Pyx_PyInt_EqObjC 判断数字相等,_Pyx_PyInt_EqObjC就算对PyObject_RichCompare的二次封装,返回值为Py_FalseStruct或者Py_TrueStruct的结构体。
对应的c代码:
static CYTHON_INLINE PyObject* __Pyx_PyInt_EqObjC(PyObject *op1, PyObject *op2, CYTHON_UNUSED long intval, CYTHON_UNUSED long inplace) {
if (op1 == op2) {
Py_RETURN_TRUE;
}
#if PY_MAJOR_VERSION < 3
if (likely(PyInt_CheckExact(op1))) {
const long b = intval;
long a = PyInt_AS_LONG(op1);
if (a == b) Py_RETURN_TRUE; else Py_RETURN_FALSE;
}
#endif
#if CYTHON_USE_PYLONG_INTERNALS
if (likely(PyLong_CheckExact(op1))) {
int unequal;
unsigned long uintval;
Py_ssize_t size = Py_SIZE(op1);
const digit* digits = ((PyLongObject*)op1)->ob_digit;
if (intval == 0) {
if (size == 0) Py_RETURN_TRUE; else Py_RETURN_FALSE;
} else if (intval < 0) {
if (size >= 0)
Py_RETURN_FALSE;
intval = -intval;
size = -size;
} else {
if (size <= 0)
Py_RETURN_FALSE;
}
uintval = (unsigned long) intval;
#if PyLong_SHIFT * 4 < SIZEOF_LONG*8
if (uintval >> (PyLong_SHIFT * 4)) {
unequal = (size != 5) || (digits[0] != (uintval & (unsigned long) PyLong_MASK))
| (digits[1] != ((uintval >> (1 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)) | (digits[2] != ((uintval >> (2 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)) | (digits[3] != ((uintval >> (3 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)) | (digits[4] != ((uintval >> (4 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK));
} else
#endif
#if PyLong_SHIFT * 3 < SIZEOF_LONG*8
if (uintval >> (PyLong_SHIFT * 3)) {
unequal = (size != 4) || (digits[0] != (uintval & (unsigned long) PyLong_MASK))
| (digits[1] != ((uintval >> (1 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)) | (digits[2] != ((uintval >> (2 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)) | (digits[3] != ((uintval >> (3 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK));
} else
#endif
#if PyLong_SHIFT * 2 < SIZEOF_LONG*8
if (uintval >> (PyLong_SHIFT * 2)) {
unequal = (size != 3) || (digits[0] != (uintval & (unsigned long) PyLong_MASK))
| (digits[1] != ((uintval >> (1 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK)) | (digits[2] != ((uintval >> (2 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK));
} else
#endif
#if PyLong_SHIFT * 1 < SIZEOF_LONG*8
if (uintval >> (PyLong_SHIFT * 1)) {
unequal = (size != 2) || (digits[0] != (uintval & (unsigned long) PyLong_MASK))
| (digits[1] != ((uintval >> (1 * PyLong_SHIFT)) & (unsigned long) PyLong_MASK));
} else
#endif
unequal = (size != 1) || (((unsigned long) digits[0]) != (uintval & (unsigned long) PyLong_MASK));
if (unequal == 0) Py_RETURN_TRUE; else Py_RETURN_FALSE;
}
#endif
if (PyFloat_CheckExact(op1)) {
const long b = intval;
double a = PyFloat_AS_DOUBLE(op1);
if ((double)a == (double)b) Py_RETURN_TRUE; else Py_RETURN_FALSE;
}
return (
PyObject_RichCompare(op1, op2, Py_EQ));
}
对应的IDA伪代码:
调用举例:
/* "util1.py":3
* def fun_hello(s):
* print("emm")
* if s == 1: # <<<<<<<<<<<<<<
* print("emm")
* return 'hello world'
*/
__pyx_t_1 = __Pyx_PyInt_EqObjC(__pyx_v_s, __pyx_int_1, 1, 0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 3, __pyx_L1_error)
__Pyx_GOTREF(__pyx_t_1);
__pyx_t_2 = __Pyx_PyObject_IsTrue(__pyx_t_1); if (unlikely(__pyx_t_2 < 0)) __PYX_ERR(0, 3, __pyx_L1_error)
__Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
//其中 __pyx_int_1是一个pyobject类型,对应数字1。__pyx_v_s与__pyx_int_1进行比较。
PyObject_RichCompare
PyObject_RichCompare应该是python里面的内置函数,可以在反汇编中找到,_Pyx_PyInt_EqObjC就是对PyObject_RichCompare的二次封装。
无名函数
这个函数找不到对应的名称。返回值为Py_FalseStruct或者Py_TrueStruct的结构体。
对应的IDA伪代码:
调用举例:
判断字符串相等的相关函数
__Pyx_PyString_Equals
__Pyx_PyString_Equals在python3中等于__Pyx_PyUnicode_Equals,在python2中等于__Pyx_PyBytes_Equals。
符合python3中的字符串就是python2中的字节序列。
对应的IDA伪代码:
调用举例:
/* "util1.py":2
* def fun_hello(s):
* if s == "didi": # <<<<<<<<<<<<<<
* return 'hello world'
* elif s == "emm":
*/
__pyx_t_1 = (__Pyx_PyString_Equals(__pyx_v_s, __pyx_n_s_didi, Py_EQ)); if (unlikely(__pyx_t_1 < 0)) __PYX_ERR(0, 4, __pyx_L1_error)
//其中 __pyx_n_s_didi是一个pyobject类型,按照上文的方法对应"didi"字符串。
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。