Logo 逆向知识库

Cython

一些资料#

概述#

Cython可以将Python代码转换成c,再编译成so,以便提高效率。

目前自动化分析Cython elf的工具极少,遇到Cython的逆向题目基本只能手撕。

本篇文章主要是探索的过程,还在寻找Cython的规律,或许需要研究下Cython的编译器。

 

编译#

pip install cython

helloworld.pyx

print("HelloWorld!")

main.py

import helloworld

setup.py

from setuptools import setup, Extension

module = Extension ('helloworld', sources=['helloworld.pyx'])

setup(
    name='cythonTest',
    version='1.0',
    author='jetbrains',
    ext_modules=[module]
)
python3 setup.py build_ext --inplace
python3 main.py
HelloWorld!

同目录下就会生成 helloworld.chelloworld.cpython-310-x86_64-linux-gnu.so 用vsc打开helloworld.c,用ida打开so,开始寻找对应关系

注:"Python.h" 一开始没导入,在vsc里点“Quick Fix”里有个自动导入的,点一下

开发人员选项#

在main.py的开头使用 import pyximport; pyximport.install() 可以避免每次都要进行编译

可视化C与Python的交互#

编辑 setup.py 加上 annotate=True

from setuptools import setup, Extension
from Cython.Build import cythonize

module = Extension ('ok', sources=['ok.pyx'])

setup(
    name='cythonTest',
    version='1.0',
    author='jetbrains',
    ext_modules=cythonize([module], annotate=True)
)

同目录下就会生成一个 html,打开

Article Image

黄色的条目就是 C 与 Python 的交互生成的代码,可以点开

如何分析#

Cython翻译Python成C的过程中,会附加一大堆“胶水代码”,比如

def add(a, b):
    return a + b

用IDA看对应的函数,有180行,其实对逆向有帮助的只有最后几行

Article Image

所以需要找到逆向的关键点

Argument unpacking done#

# file ok.pyx
def enc(a,b):
    return a + b

带参数的函数,源码和so中会出现 argument_unpacking_done 标签

// file ok.c

__pyx_pw_2ok_1enc(...) {
    // 此处省略一大坨代码
    goto __pyx_L4_argument_unpacking_done;
    // 此处再省略另一坨代码
    __pyx_L4_argument_unpacking_done:;
    __pyx_r = __pyx_pf_2ok_enc(__pyx_self, __pyx_v_a, __pyx_v_b);
    // ...
    return __pyx_r;
}

static PyObject *__pyx_pf_2ok_enc(CYTHON_UNUSED PyObject *__pyx_self, PyObject *__pyx_v_a, PyObject *__pyx_v_b) {
  // ...
  __pyx_r = PyNumber_Add(__pyx_v_a, __pyx_v_b);
  // ...
  return __pyx_r;
}

可知, __pyx_pf_2ok_enc 才是真正的函数逻辑,前面都是在 Argument unpacking

IDA里

只有 _pyx_pw_2ok_1enc 函数,没有 __pyx_pf_2ok_enc 所以应该是内联了

// file ok.cpython-310-x86_64-linux-gnu.so with IDA

PyObject *__fastcall _pyx_pw_2ok_1enc(
        PyObject *__pyx_self,
        PyObject *const *__pyx_args,
        Py_ssize_t __pyx_nargs,
        PyObject *__pyx_kwds)
{
    // 此处省略380行
    LABEL_14:
      v10 = values;
      v11 = __pyx_v_b;
__pyx_L4_argument_unpacking_done:
    result = (PyObject *)PyNumber_Add(v10, v11);
    if ( !result )
    {
        _Pyx_AddTraceback("ok.enc", 2303, 2, "ok.pyx");
        return 0LL;
    }
    return result;
}

注:有些gcc时提高了优化等级,可能没有 unpacking done

PyObject结构体#

// file /usr/include/python3.10/object.h

#define _PyObject_HEAD_EXTRA            \
    struct _object *_ob_next;           \
    struct _object *_ob_prev;

/* Nothing is actually declared to be a PyObject, but every pointer to
 * a Python object can be cast to a PyObject*.  This is inheritance built
 * by hand.  Similarly every pointer to a variable-size Python object can,
 * in addition, be cast to PyVarObject*.
 */
typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    PyTypeObject *ob_type;
} PyObject;

所以 pyObject[2] 就是 ob_refcnt ,特别是 Py_Listob_refcnt 的值就是 Py_List 的大小,Cython会以此作为优化,省略一个 len(list)

// file /usr/include/python3.10/listobject.h
PyAPI_FUNC(Py_ssize_t) PyList_Size(PyObject *);

实例#

# file ok.pyx
def enc():
    r = [1] * 8
    result = 0
    for i in range(len(r)):
        result += r[i]
    return result
// file ok.so with IDA
    v2 = PyList_New(8LL, unused);
    // ...
    v3 = (_QWORD *)v2;
    // ...
    v8 = v3[2];
    // ...
while (1) {
    // ...
    ++v10;
    _Py_Dealloc(pyx_int_0);
    if ( v8 == v10 )
      break;
    // ...
}

经测试,这是基于已知长度的 Py_List ,即调用了 PyList_New 的,对于未知长度,还是会调用 PyList_SizePyObject_Size

文档#

/usr/include/python3.10/abstract.h

里面是所有Python代码和C函数的对应文档

比如

/* Returns the result of adding o1 and o2, or NULL on failure.

   This is the equivalent of the Python expression: o1 + o2. */
PyAPI_FUNC(PyObject *) PyNumber_Add(PyObject *o1, PyObject *o2);

可能考虑把注释导入IDA

工具#

/usr/include/python3.10/abstract.h 里的函数应该是生成的 .c 中包含的所有函数,其他函数都是内部调用。

如果以这个思路的话,其实只需要跟踪:

  1. 调用 abstract.h 中函数的代码行
  2. 相关变量

如何debug#

在调用函数之前,加个 input() ,运行python文件,然后 IDA attach,再回车,就可以debug了

多试几下#

1. for 循环#

def enc():
    r = 0
    for i in range(5):
        r += 1
    return r
Article Image
Article Image

2. Traceback#

Article Image

在汇编的最后会 __Pyx_AddTraceback ,参数分别是 ok.enc c_line py_line ok.pyx

Article Image

看看版本#

Article Image