Learn learn Cython Reverse

非系统学习pyd逆向

pyd生成

  • pyd文件生成:

    • 编写pyx文件

        #test.pyx
        def say_hello_world(name):
        print("Hello world" % name)
      
    • 编写setup

        #setup.py
        from distutils.core import setup
        from Cython.Build import cythonize
        setup(name='Hello world app', ext_modules=cythonize("test.pyx"))
      
    • 生成pyd文件终端执行(也可以直接在pycharm中含参运行)

      python .\setup.py build_ext --inplace

  • 终端执行报错

      error: Microsoft Visual C++ 14.0 or greater is required. Get it with "Microsoft C++ Build Tools": https://visualstudio.microsoft.com/visual-cpp-build-tools/
    
    • 分析目的:目的就是安装一个Visual Studio Build Tools,他可以在不需要完整Visual Studio IDE的情况下获得编译和构建环境.
      这时候就有一个问题,我又不是没有python或者c的编译环境,为什么会这样?
    • 原因: 笔者安装的是Mingw环境,而报错安装所需要的环境是MSVC.而安装MSVC的最好方法就是使用Visual Studio Build Tools.,并且在Windows上,Python 官方发行版是使用 MSVC 编译的,所以setup.py编译拓展模块(c)一般需要MSVC.
  • 最后生成文件test.cp311-win_amd64.pyd

  • 上面的方法好像会丢失符号表,直接用py文件生成pyd文件,并且生成pdb表,运行方法同上

      from setuptools import setup, Extension
      from Cython.Build import cythonize
    
      ext_module = [
          Extension(
              name="www",
              sources=["www.py"],
              extra_compile_args=["/Zi"],
              extra_link_args=["/DEBUG"]
          )
      ]
    
      setup(
          name = "www",
          ext_modules = cythonize(ext_module,annotate=True)
      )
    

    其中/Zi是MSVC(Microsoft Visual C++ 编译器)的编译选项,用于生成调试符号(程序数据库文件,PDB).

  • 注意在使用自己生成的pyd修复原程序符号表的时候需要版本对应.

  • python3 .\setup.py build_ext --inplace

  • pyd实例:

    def check(flag):
        mm = 22827847752346
        ww = 25639186504356
        aa = 2282784771256
        bb = 256391865265
        cc = 2282784773234
        dd = 256391865465
        ee = 228278477523
        ff = 256391865623465
        gg = 22827847772365
        hh = 256391865823456
        ii = 25639186592356
        jj = 2282784772015432
        kk = 25639186522
        ll = 22827847723412
        tt = 2563918652324235
        a = 1*1
        b = 2*2
        c = 2^3
        a = [0]*6
        b = 0
        while b:
            print("11")
            b -= 1
        rand1m(flag)
        tt = mm>>2
        aa = mm>>b
        bb = mm<<a
        dd = mm<<2
        return a+b+c+flag
    def rand1m(x):
        return x^123
    __text__ = {}
    

简单分析pyd

  • 将.pyd文件放在当前工作目录便可以import,或者将.pyd文件放在lib/site-packages.如果无法运行,可能是缺少库导致的,或者版本不匹配(检测库的时候会显示python版本,更改版本即可)

      import pefile
    
      pe = pefile.PE("your_file.pyd")
      for entry in pe.DIRECTORY_ENTRY_IMPORT:
      print(entry.dll.decode("utf-8"))
    
  • 代码(IDE),而终端不需要加print(),因为解释器,按步骤解释会将内容输出

      import cy
    
      help(cy)
      a = cy.QOOQOOQOOQOOOQ() //cy.QOOQOOQOOQOOOQ()表示生成一个instance,而cy.QOOQOOQOOQOOOQ则是一个将a当作引用
      print(dir(a))
      print(a.get_key())
    
  • 当dir(cy.QOOQOOQOOQOOOQ)时会有输出,函数名都存在,但是好像缺少了变量
    img

通过注入class,获取运算过程

import cy

class Symbol:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return self.name

    def __rshift__(self, other):
        if isinstance(other, Symbol):
            expression = Symbol(f"({self.name} >> {other.name})")
        else:
            expression = Symbol(f"({self.name} >> {other})")
        return expression

    def __lshift__(self, other):
        if isinstance(other, Symbol):
            expression = Symbol(f"({self.name} << {other.name})")
        else:
            expression = Symbol(f"({self.name} << {other})")
        return expression

    def __rxor__(self, other):
        if isinstance(other, Symbol):
            expression = Symbol(f"({self.name} ^ {other.name})")
        else:
            expression = Symbol(f"({self.name} ^ {other})")
        return expression

    def __xor__(self, other):
        if isinstance(other, Symbol):
            expression = Symbol(f"({self.name} ^ {other.name})")
        else:
            expression = Symbol(f"({self.name} ^ {other})")
        return expression

    def __add__(self, other):
        if isinstance(other, Symbol):
            expression = Symbol(f"({self.name} + {other.name})")
        else:
            expression = Symbol(f"({self.name} + {other})")
        return expression

    def __and__(self, other):
        if isinstance(other, Symbol):
            expression = Symbol(f"({self.name} & {other.name})")
        else:
            expression = Symbol(f"({self.name} & {other})")
        return expression

class AList:
    def __init__(self, nums):
        self.nums = [Symbol(str(num)) for num in nums]

    def __getitem__(self, key):
        return self.nums[key]

    def copy(self):
        return AList(self.nums)

    def __len__(self):
        return len(self.nums)

    def __setitem__(self, key, value):
        print(f"new_{self.nums[key]} = {value}")
        self.nums[key] = Symbol(f"new_{self.nums[key].name}")

    def __eq__(self, other):
        print(f"{self.nums} == {other}")
        return self.nums == other

inp = AList([f"a[{i}]" for i in range(32)])
res = cy.sub14514(inp)

if __name__ == '__main__':
    print(res)
  • 分析(........)

补一下python的内容

  • python的函数,类方法,静态方法,实例方法

    • 函数在类中,类外都可以实现只要加def关键字就行
    • 类方法,静态方法,实例方法都需要在类中

    类方法: @classmethod

    静态方法: @staticmethod

    实例方法: def fouth_func(self):

    • 类方法、实例方法其实都是method的对象,而函数和静态方法则都是funcation的对象,可以用type()检测一下
  • 为了能较全面的还原pyd的符号表,我想用dir检测method的方法,把所有的对应起来,发现一个很神奇的东西,一个函数有return和没有return会少用很多method,也就是说return包含了很多很多method,此题(ciscn2024 rand0m)中所有的method,用一个return就能实现.

修复符号表,导入结构体

  • __pyx_mstate_global 是一个__pyx_mstate类型的全局变量指针

  • 找到很多字符串的地方(_Pyx_CreateStringTabAndInitStrings()),其中off代表的是__pyx_mstate_global,而指向它的则是__pyx_StringTabEntry的p变量,以此将很多变量y成__pyx_StringTabEntry结构体(可以对比第二张图进行set type)
    img
    下面是一个标准的形式
    img

  • 对于off需要修改为__pyx_mstate *类型

  • 在转换的时候会收到这样的提示,convert to pointer表示转化为指针,而我们应选择set the type覆盖下面的内存组合成这个结构体.

    img

  • ida中编辑结构体local type界面(shift f1),可以在这个界面查看结构体,刚刚导入的头文件中的结构体可以在这里找到

  • diaphora后也会导入结构体,__pyx_mstate *这个结构体我们就可以直接用,哪里不合适改哪里就好,这样就不用自己导入头文件了,右键edit type或者ctrl+E进行编辑(选中C syntax)

    img

  • 因为diaphora导入的莫名的结构体也可能会影响别的函数,可以注意一下,有这种split的,就是导入的结构体,可以进入,将这个结构体删了,再看看

    img

  • f3功能

    img

  • 初始化整数_Pyx_InitConstants()所有常数都在这里面初始化,初始化后的变量会以结构体的形式取值,例如

    img
    这里把pyx_mstate_global[1].__pyx_n_s_i赋值2654435769,在使用的时候就这么使用

    img

  • 如何找python函数对应的c代码函数: 有这个字符串的就是pyx_name.method_name,一般会有两个函数,一个是加载这个方法,一个是方法的实现

  • 看运算的时候只看用到运算符号的,或者一个未知的函数(例如自己实现的左移右移而不是直接调用api)

  • 返回值是什么!

    img

  • 动态调试,如果创建了虚拟环境,需要选择c盘中的python程序(不知道为什么选虚拟环境中的python为什么不行.....)

结构体

__pyx_StringTabEntry结构体如下,至于__pyx_mstate可以直接用diaphora导入的,或者可以用下面系统学习pyd逆向中的方法

#ifndef SINGEL_FILE_CPYTHON_H
#define SINGEL_FILE_CPYTHON_H
struct __pyx_StringTabEntry{
    __int64 p,s,n,encoding;
    __int8 is_unicode,is_str,intern;
    __int8 b1,b2,b3,b4,b5;
};

如何找函数对应c代码的函数(如图)

含有这个字符串的函数

img

系统学习pyd逆向

参考文章(太详细了,我哭死): https://blog.csdn.net/qq_36791177/article/details/144567790

逆向思路

  • 注:我的博客具有一定的笔记性质,所以就直接搬了
  • 分析pyd进行信息搜集,确认python版本,函数名称等
  • linux编译一份相同python版本的so文件,ida载入,File->Produce File->Create C Header File导出结构体
  • 加载需要逆向的pyd,File->Load File->Parse C Header File,导入so文件导出xxx.h(有错误就修复),导入so文件导出的xxx.h是因为错误比较少,windows带调试符号导出的header错误较多比较难修复
  • windows编译一份带调试信息的pyd,ida导出idb,bindiff载入
  • 定位__Pyx_CreateStringTabAndInitStrings(),还原__pyx_mstate结构体(python变量名)
  • 定位_Pyx_InitConstants(),还原__pyx_mstate结构中的整数成员
  • 定位到核心函数,动态调试
  • 根据调用Cython api的库函数,重新定义变量类型为导入的结构体,来高效还原python代码

PyObject

参考文章: https://www.cnblogs.com/zhangxian/articles/4587770.html

python的所有对象都会被Python解释器表示为PyObject,PyObject结构包含Python对象的所有成员指针,并且对Python对象的类型信息和引用计数进行维护。所以要用c或者c++对python对象进行处理,就意味着要维护好一个PyObject结构.

引用计数

Py_INCREF(PyObject* obj) //新增引用计数
Py_DECREF(PyObject* obj) //减少引用计数

数据类型类型

文章写的超级详细,都有点不忍心copy了(侵权必删),早点看到能拿超级多的分数.

基础类型
//整数类型
PyObject* pInt = Py_BuildValue("i", 2003);
assert(PyInt_Check(pInt));
int i = PyInt_AsLong(pInt);
Py_DECREF(pInt);

// 浮点类型
PyObject* pFloat = Py_BuildValue("f", 3.14f);
assert(PyFloat_Check(pFloat));
float f = PyFloat_AsDouble(pFloat);
Py_DECREF(pFloat);

// 字符串类型
PyObject* pString = Py_BuildValue("s", "Python");
assert(PyString_Check(pString);
int nLen = PyString_Size(pString);
char* s = PyString_AsString(pString);
Py_DECREF(pString);
元组
//创建元组对象
PyObject* pTuple = PyTuple_New(3);
assert(PyTuple_Check(pTuple));
assert(PyTuple_Size(pTuple) == 3);
// 初始化元组
PyTuple_SetItem(pTuple, 0, Py_BuildValue("i", 2003));
PyTuple_SetItem(pTuple, 1, Py_BuildValue("f", 3.14f));
PyTuple_SetItem(pTuple, 2, Py_BuildValue("s", "Python"));


// 解析元组
int i;
float f;
char *s;
if (!PyArg_ParseTuple(pTuple, "ifs", &i, &f, &s))
    PyErr_SetString(PyExc_TypeError, "invalid parameter");
// cleanup
Py_DECREF(pTuple);
列表
// 创建列表
PyObject* pList = PyList_New(3); // new reference
assert(PyList_Check(pList));
// pList[i] = i
for(int i = 0; i < 3; ++i)
    PyList_SetItem(pList, i, Py_BuildValue("i", i));
// 插入元素
PyList_Insert(pList, 2, Py_BuildValue("s", "inserted"));
// 追加元素
PyList_Append(pList, Py_BuildValue("s", "appended"));
// 排序数组
PyList_Sort(pList);
// 反转数组
PyList_Reverse(pList);
// 数组切片
PyObject* pSlice = PyList_GetSlice(pList, 2, 4); // new reference
for(int j = 0; j < PyList_Size(pSlice); ++j) {
PyObject *pValue = PyList_GetItem(pList, j);
assert(pValue);
}

Py_DECREF(pSlice);
Py_DECREF(pList);
字典
// 创建字典
PyObject* pDict = PyDict_New();
assert(PyDict_Check(pDict));
// pDict["first"] = 2003
PyDict_SetItemString(pDict, "first", Py_BuildValue("i", 2003));
// pDict["second"] = 3.14
PyDict_SetItemString(pDict, "second", Py_BuildValue("f", 3.14f));

// pDicts.Keys();
PyObject* pKeys = PyDict_Keys();
for(int i = 0; i < PyList_Size(pKeys); ++i) {
PyObject *pKey = PyList_GetItem(pKeys, i);
PyObject *pValue = PyDict_GetItem(pDict, pKey);
assert(pValue);
}
Py_DECREF(pKeys);
// 删除pDict["second"]
PyDict_DelItemString(pDict, "second");
Py_DECREF(pDict);

常见宏/函数

引用相关
#define __Pyx_GOTREF(obj)
// obj 引用计数+1,表示占用该对象

#define __Pyx_DECREF(obj)
// obj 引用计数-1

#define __Pyx_DECREF_SET(r, v)
// r引用计数-1,r = v

void _Py_Dealloc(PyObject *op)
//当Python对象op引用计数为0时,释放此对象内存
属性
#define __Pyx_GetModuleGlobalName(var, name)  (var) = __Pyx__GetModuleGlobalName(name)
// 从全局命名空间中获取模块对象,name是模块名称
// var = module对象

PyObject *PyObject_GetAttr(PyObject *o, PyObject *attr_name);
//从对象o检索名为attr_name的属性。成功返回属性值,失败返回Null。
//这相当于 Python 表达式o.attr_name.

PyObject * PyObject_GetAttrString( PyObject  *o , const char  *attr_name ) 
//从对象o中读取一个名为attr_name的属性。成功返回属性值,失败则返回NULL。这相当于Python表达式o.attr_name。

PyObject * PyObject_GenericGetAttr( PyObject  *o , PyObject  *name ) 
//通用的属性获取函数,在对象的__dict__中查找某个属性。
//正如实现描述器所述,数据优先于实例属性,失败触发触发AttributeError。

int PyObject_SetAttr( PyObject  *o , PyObject  *attr_name , PyObject  *v ) 
//这相当于Python语句。o.attr_name = v
//将对象o中名为attr_name的属性值设为v。失败时引发异常并返回-1;成功时返回返回0。
//如果v为NULL,属性将被删除,但此功能已被废弃,应改用PySequence_DelItem()。

int PyObject_SetAttrString( PyObject  *o , const char  *attr_name , PyObject  *v ) 
//这相当于Python语句。o.attr_name = v
//将对象o中名为attr_name的属性值设为v。失败时引发异常并返回-1;成功时返回返回0。
//如果v为NULL,该属性将被删除,但此功能已被废弃,应改用PySequence_DelItem()。

int PyObject_GenericSetAttr( PyObject  *o , PyObject  *name , PyObject  *value ) 
//通用的属性设置和删除函数,用于插入类型对象的tp_setattro槽。它在类的字典中(位于对象的MRO中)查找数据描述器,如果找到,则将比在实例字典中设置或删除属性优先执行。否则,该属性将在对象的__dict__中设置或删除。如果成功将返回0,否则将引发AttributeError并返回-1。

int PyObject_DelAttr( PyObject  *o , PyObject  *attr_name ) 
//删除对象o中名为attr_name的属性。失败时返回-1。这相当于Python语句。del o.attr_name

int PyObject_DelAttrString( PyObject  *o , const char  *attr_name ) 
//删除对象o中名为attr_name的属性。失败时返回-1。这相当于Python语句。del o.attr_name
函数
PyTypeObject PyMethod_Type;
//这个 PyTypeObject 实例代表 Python 方法类型。 它作为 types.MethodType 向 Python 程序公开。

int PyMethod_Check(PyObject *o)
//判断o是否是PyMethod_Type

PyObject *PyMethod_New(PyObject *func, PyObject *self)
//返回一个新的方法对象,func 应为任意可调用对象,self 为该方法应绑定的实例。 在方法被调用时 func 将作为函数被调用。 self 必须不为 NULL。

PyObject *PyMethod_Function(PyObject *meth)
//返回关联到方法 meth 的函数对象,相当于获取this指针

PyObject *PyMethod_GET_FUNCTION(PyObject *meth)
//宏版本的 PyMethod_Function(),略去了错误检测。

PyObject *PyMethod_Self(PyObject *meth)
//返回关联到方法 meth 的实例。

PyObject *PyMethod_GET_SELF(PyObject *meth)
//宏版本的 PyMethod_Self(),略去了错误检测。

PyObject * _PyObject_FastCallDict( PyObject  *callable , PyObject *const  *args , size_t  nargsf , PyObject  *kwdict ) 
//callable => 可调用的函数对象相当于this,可以通过PyMethod_GET_FUNCTION()获取
数字/运算
int PyNumber_Check ( PyObject * o ) 
//判断对象o是否为整数类型

PyObject * PyNumber_Add ( PyObject * o1 , PyObject * o2 )
PyObject * PyNumber_Subtract ( PyObject * o1 , PyObject * o2 )
PyObject * PyNumber_Multiply ( PyObject * o1 , PyObject * o2 )
PyObject * PyNumber_FloorDivide ( PyObject * o1 , PyObject * o2 )
//加减乘除

PyObject * PyNumber_InPlaceAdd ( PyObject * o1 , PyObject * o2 )
//o1 += o2
PyObject * PyNumber_InPlaceSubtract ( PyObject * o1 , PyObject * o2 )
//o1 -= o2
PyObject * PyNumber_InPlaceMultiply ( PyObject * o1 , PyObject * o2 )
//o1 *= o2
PyObject * PyNumber_InPlaceFloorDivide ( PyObject * o1 , PyObject * o2 )
//o1 /= o2


PyObject * PyNumber_Lshift ( PyObject * o1 , PyObject * o2 )
PyObject * PyNumber_Rshift ( PyObject * o1 , PyObject * o2 )
//左移右移

PyObject * PyNumber_And ( PyObject * o1 , PyObject * o2 )
PyObject * PyNumber_Xor ( PyObject * o1 , PyObject * o2 )
PyObject * PyNumber_Or ( PyObject * o1 , PyObject * o2 )
//逻辑运算

PyObject * PyNumber_Long ( PyObject * o )
//int(o)
PyObject * PyNumber_Float ( PyObject * o )
//float(o)
PyObject * PyNumber_Index ( PyObject * o )
//o转换为int,并且失败抛出异常
PyObject *PyNumber_ToBase(PyObject *n, int base)
//将字符串转换成2,8,16进制的字符串
//如果n不是整数,则会先调用PyNumber_Index(n)

type Py_ssize_t;	//有符号整数类型
Py_ssize_t PyNumber_AsSsize_t(PyObject *o, PyObject *exc)
//将o转换成有符号整数类型,如果失败则抛出异常exc
比较
PyObject *PyObject_RichCompare(PyObject *o1, PyObject *o2, int oper)
//比较对象函数相当于:o1 oper o2
//oper:0-5 
//		Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT, Py_GE
//对应:<, <=, ==, !=, >, >=
//成功返回Py_True,失败返回Py_False

int PyObject_RichCompareBool(PyObject *o1, PyObject *o2, int opid)
//和上面一样,错误返回-1,成功1,失败0

cython逆向

可以参考上面的非系统学习pyd逆向,还是很全的.

cython api

网址: https://docs.python.org/3/c-api/object.html#c.PyObject_GetAttr

py混淆(Pyarmor)

使用教程: https://pyarmor.readthedocs.io/zh/stable/tutorial/getting-started.html#pyarmor

概念

pyarmor可以加密py文件,生成一个新的同名py文件与一个文件夹pyarmor_runtime_000000
img
而py文件内容如下(后面一大串是被混淆后的字节码)

# Pyarmor 9.0.7 (trial), 000000, non-profits, 2025-01-23T19:42:36.381023
from pyarmor_runtime_000000 import __pyarmor__
__pyarmor__(__name__, __file__, b'PY000000\x00\x03\n\x00o\r\r\n\x80\x00\x01\x00\x08\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00f\x01\x00\x00\x12\t\x04\x00\x9ai\n"\x10\xf5\x1a\xa3\xb7\x01\x98a\xee%\x83d\x00\x00\x00\x00\x00\x00\x00\x00\x85\x18\x8aV\t\xf4t\xcdF\xe51Y\xbe\xb3x\xc5\x10>\xc98\x8a\x17h\xbb\xd9g m\xb3R\xee\x93\xeb}\xea\x98\xb2_@yu\x7f\xb1v\x99$\xfe\xd0V\x8dX\x14\x8f\x0e\xf2vg\x89\x80\xd3B\x88\x8b\x8b\x8c\x98i\x15\xab\x9a\xa4~8\xd0\xc9\x83\x8a\xcdT\xb7\xab\xf7\x89\xe1\xc8\xb4\xa4\xe6\xc3@\'\x81\xe8\xb5\x91\x87\x9bL1\x8e\xe8\xc3\xa2\xd39\xbc\x16O\xe0\xfb\xb2[\x0eP6\x99A\xa2\x1c\xe6+\x08\x88>\x0e\x07\x16N\xfdFw\xc3\xb7T\x93\\C\xec\xfd\x05s\x0e\x02\x92\xbb#\xe0\xe8\x9aM\x1e\xfc\xdd\xb9\xf4\xb0\x87\x00\xa7\x13-\x9f\xea0\xb8\x1eX)+\xac\xd6\xab\x92Jc\xf2w\x15\xab\x91\x88[\x93J\xe6\x12\xcb\x97y(g\xa3\x01\x1bU\xa8\xd6\xbc\xab\xb12Dc\xc0\x14\n\'P^\xb4\xb2e\x99o\x8c\xcav0(\nl\x08\xb3\n\xe5\xa0\xc6\xf1Q\x08A\x8a\xaa\x81\xa8\xfe\xc3\xf3\x89P\xe9>46n{S\xa3/\xce\x03\x99\xb5\xa0;k\x8fv\x9d\x05\x90=\x08x.6\xa0\x98\x97\xbeoq\x1br\xc7\xab\xc3\xa9H\x01\x1dS\x03\x01\x10\xff>\xf8\xb8\xe5[\x08\xc9\x1b.\x11\x96\x9e\xa9~\x1d;-2u\xe75\\\xfe\x02\xfc\xd2\xa7}\xb5\x82g\x12d\xaf\xe9%\xc6\xe9S\xa1q\xbc\xc7J_+\x1a/\x83\x19\x8a\x04\x86\x9b;\x00\xa1\x19\xe4\x83^a3\xfd\xd9s6\x13@#\x9e\xc8')

pyarmor_runtime_000000文件夹内容
img

加密

pyarmor gen filename.py
pyarmor g filename.py   //gen可简化为g
pyarmor g filename.py   //gen可写为generate

反混淆(Not Support v8+)

PyArmor-Unpacker: https://github.com/Svenskithesource/PyArmor-Unpacker

使用教程分为三种mothod,分析的少,之后遇到题目再研究.

试解

Black Hat MEA Quals CTF 2023有道关于pyarmor的题目(没有附件,遇到题目再研究研究): https://medium.com/@0xMr_Robot/black-hat-mea-quals-ctf-2023-reverse-challenges-662449be9108

i春秋 pyhumor(仿pyarmor自制混淆)

posted @ 2024-12-16 21:02  Un1corn  阅读(201)  评论(0)    收藏  举报