摘要:关于对.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举例:

寻找字符串1.jpg

寻找字符串2.jpg

寻找字符串3.jpg

当向fun_hello函数传递数字2参数时会发现s变量是一个pyobject类型的指针。为此我去翻了翻关于pyobject结构的python源代码,但是发现对不上,所以不在这说了,下面直接说结果。s指针指的地址的值加上0x18字节就是数字2所在的地址。

如下图以pyx_int_2举例:

寻找数字.jpg

寻找数字2.jpg

寻找数字3.jpg

关于函数的调用

# 文件名 util2.py

import util1
a=util1.fun_hello(2)
print(a)

util2.pyd中调用util1.pyd中的函数的机制。

要调用的函数名为fun_hello,在util2.pyd搜索字符串

定位函数1.jpg

转到对fun_hello的引用

定位函数2.jpg

转到__pyx_n_s_fun_hello

定位函数3.jpg

转到对__pyx_n_s_fun_hello的引用

定位函数4.jpg

转到函数伪代码

定位函数5.jpg

定位函数6.jpg

关键函数为_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伪代码:

__Pyx_PyObject_IsTrue定义

调用举例:

_Pyx_PyInt_EqObjC调用举例.jpg

  /* "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伪代码:

PyObject_IsTrue定义.jpg

调用举例:

PyObject_IsTrue调用举例.jpg

_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伪代码:

_Pyx_PyInt_EqObjC定义.jpg

调用举例:

_Pyx_PyInt_EqObjC调用举例.jpg

  /* "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伪代码:

无名函数定义.jpg

调用举例:

无名函数调用举例

判断字符串相等的相关函数

__Pyx_PyString_Equals

__Pyx_PyString_Equals在python3中等于__Pyx_PyUnicode_Equals,在python2中等于__Pyx_PyBytes_Equals。
符合python3中的字符串就是python2中的字节序列。

对应的IDA伪代码:

__Pyx_PyUnicode_Equals1.jpg

__Pyx_PyUnicode_Equals2.jpg

调用举例:

__Pyx_PyString_Equals调用举例.jpg

    /* "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"字符串。