首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
逆向工程
发新帖
4
8
[原创]Pyd原理以及逆向实战 (二)变量 与 实战
发表于: 2026-1-28 18:47
3613
[原创]Pyd原理以及逆向实战 (二)变量 与 实战
BitWarden
2
2026-1-28 18:47
3613
# 一、前言 最近在分析一个国外某个体育游戏,热度最高的外挂样本。发现使用的是Python脚本并且Pyd编译后的,中间存在部分名称混淆以及反调试,且Pyd的编译后巨大,故而研究了一下Pyd逆向用来高效逆向这类样本。顺便记录一下方便后面查阅。这一期主要是我如何定位分析这个样本爆破它的登陆,并且简单讲一下Pyd成员的原理。一些对于函数处理和Pyd函数是怎么实现的可以看我上一篇。(感谢版主支持hh) # 二、变量 先说一个前提我认为:对于变量和成员变量来说,静态分析不是一个很好的办法,原因是:RTTI类的这种信息比较散,需要一点点处理太麻烦了,不如运行起来再解释器层进行处理。 这里主要抛出几个问题: 1.Python成员和变量如何转换成pyd 2.解释器如何感知变量和成员变量 3.解释器以及native层如何访问这些变量 ## 问题1.Python成员和变量如何转换成pyd 全局变量: 这里我编译了一个Pyd,通过编译选项让他产生中间文件.c,通过来窥探一下全局变量是如何实现的(pyd怎么生成中间文件就不展示以了): demo代码: ```python # 全局变量 g_count = 100 g_name = "global_test" def calculate(a, b): """局部变量示例""" x = a + 10 y = b * 2 result = x + y return result class Player: """类成员示例""" # 类变量 max_level = 99 def __init__(self, name): # 实例成员 self.name = name self.health = 100 self.level = 1 def take_damage(self, amount): self.health -= amount if self.health < 0: self.health = 0 return self.health ``` 这里生成了一个 example.c ,大概有 8630行非常巨大(也就是为什么pyd难以逆向的原因) 这里画了一个图整理了一下:  对照生成出来的 example.c 讲解一下: 1.全局变量的字符串名称如何存储? 首先字符串会存储在一个字符串索引表里面,可以看到这里就有我们需要的g_count:  ```c const char* const bytes = "?example.py\347\261\273\346\210\220\345\221\230\347\244\272\344\276\213PlayerPlayer.__init__Player.take_damage__Pyx_PyDict_NextRefaamountasyncio.coroutinesbcalculatecline_in_traceback__doc__example__func__g_countg_nameglobal_testhealth__init___is_coroutineitemslevel__main__max_level__metaclass____module__name__name__pop__prepare____qualname__resultself__set_name__setdefaulttake_damage__test__valuesxy\200\001\340\004\010\210\002\210\"\210A\330\004\010\210\002\210\"\210A\330\004\r\210R\210r\220\021\330\004\013\2101\200A\340\010\014\210H\220A\330\010\014\210J\220a\330\010\014\210I\220Q\200A\330\010\014\210K\220q\330\010\013\2104\210x\220r\230\021\330\014\020\220\n\230!\330\010\017\210t\2201"; ``` 在文件中定义了一些来表示字符串在字符串数组中的位置:  字符串数组是存储在一个结构体中 __pyx_mstatetype:  当程序运行起来的时候,就会访问,并且动态创建这些全局变量,最后吧这些注入到我们的dict里面去给解释器使用。 对象成员有所不同: 创建一个demo ```python class Player: # 类变量 max_level = 99 game_name = "RPG" def __init__(self, name, hp): # 实例成员 self.name = name self.hp = hp self.level = 1 self.items = [] def take_damage(self, amount): self.hp -= amount return self.hp def level_up(self): if self.level < Player.max_level: self.level += 1 ``` 编译一下,8500行:  实际上生成的字符串也是存在字符串缓存表里面  ``` const char* const cstring = "BZh91AY&SY(\311\237\341\000\000\017\377\343A\\\300\000 \001$\000\245\341p\000\277\357\377\340@@@@\000@@\0000\001\0316\301*\236\247\24514\323\t\215LI\201\001\201\203R\rF\204\3654OQ\2102\031\006M214\323@i\244F\200\232d\320\006\200\0004i\247\251\225/\374\217(\367)\t$\r\363\334\222\241\366=\204=\273\010\023\355\211m\205DC\245\314B^@^\312\235~\261\266\267\\\2567b\211\030e\"9\024`\021\005)1A\003\024$x\017\301\342\014^C7`i\014S\314\362E<j\037\023(\332c\305\020\252\232\363|\326\217C=r\310\013a\"\331uj\306\223 \243\rz\256\013H\231n\230\222P\311\257\247\325-\026\362S \004$N\200jx\020.`:f\271\246i\017\232P\260\240A\036\215k\016ao\r\020\020\352\232\201M\"\263d\301q\210o:f\224SC\002W\206g\n\250\2064\235\314'\224\320\020\260\223Uk\2429D\247\233\237\252^tB \221\204M\201l\251B\205OW\371\376x\365\206\031Y\3337\036\377\\\357oeE\0253\374]\311\024\341B@\243&\177\204"; ``` 这里验证了从静态层面是无法分析处于内存地址与变量关系。所以纯ida极难分析。 ## 问题2.解释器如何感知变量和成员变量 再讲之前我们需要了解一下存储这些成员变量的结构: __pyx_mstatetype ```cpp typedef struct { // ===== 模块核心 ===== PyObject *__pyx_d; // 模块字典 (__dict__) PyObject *__pyx_b; // builtins 模块 PyObject *__pyx_cython_runtime; // cython_runtime 模块 // ===== 空对象缓存 ===== PyObject *__pyx_empty_tuple; // 空元组 () PyObject *__pyx_empty_bytes; // 空字节 b"" PyObject *__pyx_empty_unicode; // 空字符串 "" // ===== 方法缓存 ===== __Pyx_CachedCFunction __pyx_umethod_PyDict_Type_items; // dict.items __Pyx_CachedCFunction __pyx_umethod_PyDict_Type_pop; // dict.pop __Pyx_CachedCFunction __pyx_umethod_PyDict_Type_values; // dict.values // ===== 常量表 ===== PyObject *__pyx_codeobj_tab[3]; // 代码对象表 (函数) PyObject *__pyx_string_tab[45]; // 字符串表 ("g_count", "g_name"...) PyObject *__pyx_number_tab[6]; // 数值表 (0, 1, 2, 10, 99, 100) // ===== 类型对象 ===== PyTypeObject *__pyx_CommonTypesMetaclassType; // 元类 PyTypeObject *__pyx_CyFunctionType; // Cython 函数类型 // ===== 代码缓存 ===== struct __Pyx_CodeObjectCache __pyx_code_cache; } __pyx_mstatetype; ``` 几乎是所有想要的都在这里了,如果我们想对解释层动手就需要找到它。 这里详细讲一下,动态创建的流程: 首先Pyd入口实际上是 __pyx_pymod_exec_example (今天从源码生成角度解析一下) slot的数组的第二个就是入(ida逆向查找看昨天的文章)  __pyx_pymod_exec_example 中就调用了 __Pyx_InitConstants 来初始化整个 全局变量: 我们demo中的g_count 就是这里创建的: 这里创建了一个Long对象,也就是我们全局对象的容器,并且初始化值,这里会创建一个key,value,用来索引这个容器位置。  在__pyx_pymod_exec_example中 容器与字符串进行绑定完成python解释器的创建:  ```python /* "example.py":4 * * # * g_count = 100 # <<<<<<<<<<<<<< * g_name = "global_test" * */ if (PyDict_SetItem(__pyx_mstate_global->__pyx_d, __pyx_mstate_global->__pyx_n_u_g_count, __pyx_mstate_global->__pyx_int_100) < (0)) __PYX_ERR(0, 4, __pyx_L1_error) ``` 这里可能看上去对不上,实际上这里全是宏(有点灾难) 最后这里走完后就会把信息存入Dict中,也就完成了注册。  总结整理一下就是: ```cpp ① import example ↓ ② PyInit_example() → PyModuleDef_Init() ↓ ③ __pyx_pymod_exec_example() │ ├─→ ④ PyModule_Create() // 创建模块对象 │ ├─→ ⑤ __pyx_d = PyModule_GetDict() // 获取模块字典 (line 3275) │ ├─→ ⑥ __Pyx_InitConstants() // 初始化常量 (line 3299) │ │ │ ├─→ ⑦ stringtab[16] = PyUnicode("g_count") │ │ │ └─→ ⑧ numbertab[5] = PyLong(100) │ ├─→ ⑨ __Pyx_InitGlobals() // Cython 内部状态 │ ├─→ ⑩ PyDict_SetItem(__pyx_d, stringtab[16], numbertab[5]) // g_count=100 (line 3333)(这里就是进行注册了,最后) ``` 这里需要注意的是:假设有两个100的全局变量,实际上他们都会指向同一块初始化值为100的内存,如果后续要更改实际上是重新在创建了一个long对象,所以python这里实现可以用指针来代替绑定,最后绑定的也是提前就生成好的值为100的数组。所以他和传统的cpp不一样没有一个变量对应一个内存这个概念。 ### 类如何注册到dict里面 __pyx_pymod_exec_member_demo 这个函数主要负责注册类的信息: 由解释器加载的时候调用,还是存在slots字段中  注册到dict位置还是一样的:  ```c /* "member_demo.py":3 * # member_demo.py - * * class Player: # <<<<<<<<<<<<<< * # * max_level = 99 */ __pyx_t_2 = __Pyx_Py3MetaclassPrepare((PyObject *) NULL, __pyx_mstate_global->__pyx_empty_tuple, __pyx_mstate_global->__pyx_n_u_Player, __pyx_mstate_global->__pyx_n_u_Player, (PyObject *) NULL, __pyx_mstate_global->__pyx_n_u_member_demo, (PyObject *) NULL); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 3, __pyx_L1_error) __Pyx_GOTREF(__pyx_t_2); /* "member_demo.py":5 * class Player: * # * max_level = 99 # <<<<<<<<<<<<<< * game_name = "RPG" * */ if (__Pyx_SetNameInClass(__pyx_t_2, __pyx_mstate_global->__pyx_n_u_max_level, __pyx_mstate_global->__pyx_int_99) < (0)) __PYX_ERR(0, 5, __pyx_L1_error) /* "member_demo.py":6 * # * max_level = 99 * game_name = "RPG" # <<<<<<<<<<<<<< * * def __init__(self, name, hp): */ if (__Pyx_SetNameInClass(__pyx_t_2, __pyx_mstate_global->__pyx_n_u_game_name, __pyx_mstate_global->__pyx_n_u_RPG) < (0)) __PYX_ERR(0, 6, __pyx_L1_error) /* "member_demo.py":8 * game_name = "RPG" * * def __init__(self, name, hp): # <<<<<<<<<<<<<< * # * self.name = name */ __pyx_t_3 = __Pyx_CyFunction_New(&__pyx_mdef_11member_demo_6Player_1__init__, 0, __pyx_mstate_global->__pyx_n_u_Player___init, NULL, __pyx_mstate_global->__pyx_n_u_member_demo, __pyx_mstate_global->__pyx_d, ((PyObject *)__pyx_mstate_global->__pyx_codeobj_tab[0])); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 8, __pyx_L1_error) __Pyx_GOTREF(__pyx_t_3); #if CYTHON_COMPILING_IN_CPYTHON && PY_VERSION_HEX >= 0x030E0000 PyUnstable_Object_EnableDeferredRefcount(__pyx_t_3); #endif if (__Pyx_SetNameInClass(__pyx_t_2, __pyx_mstate_global->__pyx_n_u_init, __pyx_t_3) < (0)) __PYX_ERR(0, 8, __pyx_L1_error) __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0; ``` 因为python一切皆对象的观念。而我们这上面成员和函数信息都会注册到一个tp_dict 元组里面去就像是这样 ```python 模块字典 __pyx_d ├── "Player" → 类对象 (PyTypeObject) │ └── tp_dict (类的属性字典) │ ├── "max_level" → 99 │ ├── "game_name" → "RPG" │ ├── "__init__" → <function> ← 函数在这里 │ ├── "take_damage" → <function> │ └── "level_up" → <function> │ └── "p" → 实例对象 ``` 最后上面这个类会在这里进行注册(代码比较多我单拎出来): ``` if (__Pyx_SetNameInClass(__pyx_t_2, __pyx_mstate_global->__pyx_n_u_init, __pyx_t_3) < (0)) __PYX_ERR(0, 8, __pyx_L1_error) ``` 实际上类对象已经在初始化的时候创建好了,并且注册到解释器层了。主要就是注册函数,初始化成员变量,等各种信息。但是内存的分配实际上是创建对象实例的时候才会使用。所以核心实际上还是这个 __pyx_mstatetype。 讲一下使用的时候如何创建的:  ```cpp /* "member_demo.py":24 * * # * p = Player("Tom", 100) # <<<<<<<<<<<<<< * p.take_damage(20) */ __Pyx_GetModuleGlobalName(__pyx_t_2, __pyx_mstate_global->__pyx_n_u_Player); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 24, __pyx_L1_error) __Pyx_GOTREF(__pyx_t_2); __pyx_t_3 = __Pyx_PyObject_Call(__pyx_t_2, __pyx_mstate_global->__pyx_tuple[0], NULL); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 24, __pyx_L1_error) __Pyx_GOTREF(__pyx_t_3); __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; if (PyDict_SetItem(__pyx_mstate_global->__pyx_d, __pyx_mstate_global->__pyx_n_u_p, __pyx_t_3) < (0)) __PYX_ERR(0, 24, __pyx_L1_error) __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0; ``` 实际上创建流程我们不关心,核心就是这个对象怎么被解释器感知: ``` PyDict_SetItem(__pyx_mstate_global->__pyx_d, __pyx_mstate_global->__pyx_n_u_p, __pyx_t_3) ```  最后还是存到了 __pyx_d,所以实际上存储的位置是一样的。 ## 问题3.解释器以及native层如何访问这些变量 全局变量:  实际上就是查表,因为所有对象都在那个dict,核心api PyObject_GetAttr __Pyx_PyDict_GetItemRef _PyDict_GetItem_KnownHash 区别是: 下面这两个是直接查hash表 __Pyx_PyDict_GetItemRef _PyDict_GetItem_KnownHash 这个从模块地址多一层访问 PyObject_GetAttr self成员变量的访问: ```c /* "member_demo.py":16 * * def take_damage(self, amount): * self.hp -= amount # <<<<<<<<<<<<<< * return self.hp * */ __pyx_t_1 = __Pyx_PyObject_GetAttrStr(__pyx_v_self, __pyx_mstate_global->__pyx_n_u_hp); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 16, __pyx_L1_error) __Pyx_GOTREF(__pyx_t_1); __pyx_t_2 = PyNumber_InPlaceSubtract(__pyx_t_1, __pyx_v_amount); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 16, __pyx_L1_error) __Pyx_GOTREF(__pyx_t_2); __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0; if (__Pyx_PyObject_SetAttrStr(__pyx_v_self, __pyx_mstate_global->__pyx_n_u_hp, __pyx_t_2) < (0)) __PYX_ERR(0, 16, __pyx_L1_error) __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0; ``` 实际上是通过这个进行访问的: __Pyx_PyObject_GetAttrStr 写入则是这个 __Pyx_PyObject_SetAttrStr 要注意的是写入和访问实际上参数是静态的,都是已经提前定义好的宏,所以在程序没有运行起来的时候很难静态分析:  全局变量的写入也比较单一没有读写那复杂: PyDict_SetItem  # 三、函数补充 对函数稍微补充一些内容: 由于Python语法的特性问题,Python自己的一些特性迭代器,或者数组遍历。。。会进行转换,实际上他们也是函数,内部函数,并且有转换模版。 如果我们想要分析的话也需要对这些函数进行hook。我这里就举例几个演示一下: 源码位置: ``` x:\x\cython\Cython\Utility\ ├── ObjectHandling.c # 对象操作(迭代、属性访问、调用) ├── Optimize.c # 内置方法优化(append、pop) ├── Builtins.c # 内置函数(globals、exec、getattr) └── TypeConversion.c # 类型转换 ``` 迭代器: for x in obj ```c /////////////// IterNext.proto /////////////// #define __Pyx_PyIter_Next(obj) __Pyx_PyIter_Next2(obj, NULL) static CYTHON_INLINE PyObject *__Pyx_PyIter_Next2(PyObject *, PyObject *); /*proto*/ /////////////// IterNext /////////////// //@requires: Exceptions.c::PyThreadStateGet //@requires: Exceptions.c::PyErrFetchRestore //@requires: GetBuiltinName //@requires: IterNextPlain static PyObject *__Pyx_PyIter_Next2Default(PyObject* defval) { PyObject* exc_type; __Pyx_PyThreadState_declare __Pyx_PyThreadState_assign exc_type = __Pyx_PyErr_CurrentExceptionType(); if (unlikely(exc_type)) { if (!defval || unlikely(!__Pyx_PyErr_GivenExceptionMatches(exc_type, PyExc_StopIteration))) return NULL; __Pyx_PyErr_Clear(); Py_INCREF(defval); return defval; } if (defval) { Py_INCREF(defval); return defval; } __Pyx_PyErr_SetNone(PyExc_StopIteration); return NULL; } ``` 索引访问: list[i] ```cpp /////////////// GetItemInt /////////////// // 针对 List 和 Tuple 的优化 static CYTHON_INLINE PyObject *__Pyx_GetItemInt_List_Fast(PyObject *o, Py_ssize_t i, ...) { if (wraparound & unlikely(i < 0)) { wrapped_i += PyList_GET_SIZE(o); // 负索引处理 } if ((!boundscheck) || likely(__Pyx_is_valid_index(wrapped_i, PyList_GET_SIZE(o)))) { return __Pyx_NewRef(PyList_GET_ITEM(o, wrapped_i)); // 直接访问内部数组 } ... } ``` 还有很多需要自己耐心收集这些函数。 # 四、实战 样本是通过Gtuner软件启动的(一个游戏机的转换器作弊平台,之前有分析过相关的软件以及协议可以查阅我以往文章) 因为这个软件存在大量的名称混淆,看上去应该是一个pyqt6的程序。(给作者留点面子软件界面我就删除了hh) 验证函数分析手法如下: ## 1. 定位 软件目录:  启动界面  打开任务管理器后发现:  发现软件不是依托于Gtuner启动的,Gtuner平台只是一个幌子,通过网络下载启动并且启动了程序。笔者当时在目录下找了很久也没发现关键的核心模块并且这个外挂作者还是使用一些假模块在目录下误导分析。  这个时候通过frida就可以完美的处理这些问题:  通过打印发现了异常,外挂作者吧真实的本体Pyd藏到了python的运行目录下,这样我们在本地文件无法找到的(混淆名称后这掩耳盗铃一样的hh): 贴一下frida脚本 ```js console.log('已加载的 PYD 模块:'); console.log('='.repeat(60)); Process.enumerateModules().forEach(function(module) { if (module.path.toLowerCase().endsWith('.pyd')) { console.log('名称: ' + module.name); console.log('路径: ' + module.path); console.log('基址: ' + module.base); console.log('大小: ' + module.size); console.log('-'.repeat(60)); } }); ``` 外挂文件如下:  程序的启动入口:  之后我打印出这个目录下所有类的导出成员信息:  脚本代码: ```js // 打印 pyd 模块的成员(变量、常量、类定义等,不包括函数) var targetDir = 'j:\\anaconda3\\envs\\py11ae\\lib\\site-packages\\xisutil'.toLowerCase(); var python = Process.getModuleByName('python311.dll'); var PyGILState_Ensure = new NativeFunction( python.getExportByName('PyGILState_Ensure'), 'int', [] ); var PyGILState_Release = new NativeFunction( python.getExportByName('PyGILState_Release'), 'void', ['int'] ); var PyImport_GetModuleDict = new NativeFunction( python.getExportByName('PyImport_GetModuleDict'), 'pointer', [] ); var PyDict_GetItemString = new NativeFunction( python.getExportByName('PyDict_GetItemString'), 'pointer', ['pointer', 'pointer'] ); var PyObject_Dir = new NativeFunction( python.getExportByName('PyObject_Dir'), 'pointer', ['pointer'] ); var PyObject_GetAttrString = new NativeFunction( python.getExportByName('PyObject_GetAttrString'), 'pointer', ['pointer', 'pointer'] ); var PyObject_Repr = new NativeFunction( python.getExportByName('PyObject_Repr'), 'pointer', ['pointer'] ); var PyUnicode_AsUTF8 = new NativeFunction( python.getExportByName('PyUnicode_AsUTF8'), 'pointer', ['pointer'] ); var PyList_Size = new NativeFunction( python.getExportByName('PyList_Size'), 'int64', ['pointer'] ); var PyList_GetItem = new NativeFunction( python.getExportByName('PyList_GetItem'), 'pointer', ['pointer', 'int64'] ); var PyObject_Type = new NativeFunction( python.getExportByName('PyObject_Type'), 'pointer', ['pointer'] ); function getTypeName(obj) { try { var typeObj = PyObject_Type(obj); if (!typeObj.isNull()) { var typeNamePtr = Memory.allocUtf8String('__name__'); var typeName = PyObject_GetAttrString(typeObj, typeNamePtr); if (!typeName.isNull()) { var nameUtf8 = PyUnicode_AsUTF8(typeName); if (!nameUtf8.isNull()) { return nameUtf8.readUtf8String(); } } } } catch (e) {} return 'unknown'; } function dumpMembers() { var gilState = PyGILState_Ensure(); try { console.log('目标目录: ' + targetDir); console.log('='.repeat(80)); var pydModules = []; Process.enumerateModules().forEach(function(mod) { if (mod.path.toLowerCase().startsWith(targetDir) && mod.path.toLowerCase().endsWith('.pyd')) { var fileName = mod.name.split('.')[0]; var pyModName = 'xisutil.' + fileName; pydModules.push({ pyName: pyModName, shortName: fileName, path: mod.path, base: mod.base, size: mod.size }); } }); var modulesDict = PyImport_GetModuleDict(); pydModules.forEach(function(pydMod) { console.log('\n模块: ' + pydMod.pyName); console.log('路径: ' + pydMod.path); console.log('-'.repeat(60)); var modNamePtr = Memory.allocUtf8String(pydMod.pyName); var module = PyDict_GetItemString(modulesDict, modNamePtr); if (!module.isNull()) { var dirList = PyObject_Dir(module); if (!dirList.isNull()) { var size = PyList_Size(dirList); console.log('\n[常量/变量]'); for (var i = 0; i < size; i++) { var item = PyList_GetItem(dirList, i); var nameStr = PyUnicode_AsUTF8(item); if (!nameStr.isNull()) { var name = nameStr.readUtf8String(); if (name.startsWith('__') && name.endsWith('__')) continue; var attrNamePtr = Memory.allocUtf8String(name); var attr = PyObject_GetAttrString(module, attrNamePtr); if (!attr.isNull()) { var reprObj = PyObject_Repr(attr); if (!reprObj.isNull()) { var reprUtf8 = PyUnicode_AsUTF8(reprObj); if (!reprUtf8.isNull()) { var reprStr = reprUtf8.readUtf8String(); // 跳过函数和类 if (reprStr.indexOf('<built-in function') !== -1) continue; if (reprStr.indexOf('<class ') !== -1) continue; if (reprStr.indexOf('<module ') !== -1) continue; // 截断过长的值 if (reprStr.length > 100) { reprStr = reprStr.substring(0, 100) + '...'; } var typeName = getTypeName(attr); console.log(' ' + name + ' (' + typeName + ') = ' + reprStr); } } } } } console.log('\n[类定义]'); for (var i = 0; i < size; i++) { var item = PyList_GetItem(dirList, i); var nameStr = PyUnicode_AsUTF8(item); if (!nameStr.isNull()) { var name = nameStr.readUtf8String(); if (name.startsWith('__') && name.endsWith('__')) continue; var attrNamePtr = Memory.allocUtf8String(name); var attr = PyObject_GetAttrString(module, attrNamePtr); if (!attr.isNull()) { var reprObj = PyObject_Repr(attr); if (!reprObj.isNull()) { var reprUtf8 = PyUnicode_AsUTF8(reprObj); if (!reprUtf8.isNull()) { var reprStr = reprUtf8.readUtf8String(); // 只显示属于当前 pyd 的类 if (reprStr.indexOf("<class '" + pydMod.shortName + '.') !== -1) { console.log(' ' + name + ' : ' + reprStr); } } } } } } console.log('\n[对象实例]'); for (var i = 0; i < size; i++) { var item = PyList_GetItem(dirList, i); var nameStr = PyUnicode_AsUTF8(item); if (!nameStr.isNull()) { var name = nameStr.readUtf8String(); if (name.startsWith('__') && name.endsWith('__')) continue; var attrNamePtr = Memory.allocUtf8String(name); var attr = PyObject_GetAttrString(module, attrNamePtr); if (!attr.isNull()) { var reprObj = PyObject_Repr(attr); if (!reprObj.isNull()) { var reprUtf8 = PyUnicode_AsUTF8(reprObj); if (!reprUtf8.isNull()) { var reprStr = reprUtf8.readUtf8String(); // 检测对象实例 (格式: <xxx object at 0x...>) if (reprStr.indexOf(' object at 0x') !== -1) { console.log(' ' + name + ' : ' + reprStr); } } } } } } } } console.log('\n' + '='.repeat(80)); }); } finally { PyGILState_Release(gilState); } } dumpMembers(); ``` 在打印一下当前目录下所有Pyd的方法信息: 结果如下:  ```js // 打印 pyd 模块的函数(内置函数 + 类方法) // 只显示地址在 pyd 范围内的自定义函数 var targetDir = 'j:\\anaconda3\\envs\\py11ae\\lib\\site-packages\\xisutil'.toLowerCase(); var python = Process.getModuleByName('python311.dll'); var PyGILState_Ensure = new NativeFunction( python.getExportByName('PyGILState_Ensure'), 'int', [] ); var PyGILState_Release = new NativeFunction( python.getExportByName('PyGILState_Release'), 'void', ['int'] ); var PyImport_GetModuleDict = new NativeFunction( python.getExportByName('PyImport_GetModuleDict'), 'pointer', [] ); var PyDict_GetItemString = new NativeFunction( python.getExportByName('PyDict_GetItemString'), 'pointer', ['pointer', 'pointer'] ); var PyObject_Dir = new NativeFunction( python.getExportByName('PyObject_Dir'), 'pointer', ['pointer'] ); var PyObject_GetAttrString = new NativeFunction( python.getExportByName('PyObject_GetAttrString'), 'pointer', ['pointer', 'pointer'] ); var PyObject_Repr = new NativeFunction( python.getExportByName('PyObject_Repr'), 'pointer', ['pointer'] ); var PyUnicode_AsUTF8 = new NativeFunction( python.getExportByName('PyUnicode_AsUTF8'), 'pointer', ['pointer'] ); var PyList_Size = new NativeFunction( python.getExportByName('PyList_Size'), 'int64', ['pointer'] ); var PyList_GetItem = new NativeFunction( python.getExportByName('PyList_GetItem'), 'pointer', ['pointer', 'int64'] ); function getBuiltinFunctionAddress(funcObj) { try { var m_ml = funcObj.add(0x10).readPointer(); if (!m_ml.isNull()) { var ml_meth = m_ml.add(0x08).readPointer(); return ml_meth; } } catch (e) { return null; } return null; } function isAddressInModule(addr, base, size) { if (!addr) return false; try { return addr.compare(base) >= 0 && addr.compare(base.add(size)) < 0; } catch (e) { return false; } } function dumpFunctions() { var gilState = PyGILState_Ensure(); try { console.log('目标目录: ' + targetDir); console.log('='.repeat(80)); var pydModules = []; Process.enumerateModules().forEach(function(mod) { if (mod.path.toLowerCase().startsWith(targetDir) && mod.path.toLowerCase().endsWith('.pyd')) { var fileName = mod.name.split('.')[0]; var pyModName = 'xisutil.' + fileName; pydModules.push({ pyName: pyModName, shortName: fileName, path: mod.path, base: mod.base, size: mod.size }); } }); var modulesDict = PyImport_GetModuleDict(); pydModules.forEach(function(pydMod) { console.log('\n模块: ' + pydMod.pyName); console.log('路径: ' + pydMod.path); console.log('基址: ' + pydMod.base + ' ~ ' + pydMod.base.add(pydMod.size)); console.log('-'.repeat(60)); var modNamePtr = Memory.allocUtf8String(pydMod.pyName); var module = PyDict_GetItemString(modulesDict, modNamePtr); if (!module.isNull()) { var dirList = PyObject_Dir(module); if (!dirList.isNull()) { var size = PyList_Size(dirList); // 模块级函数 console.log('\n[模块函数]'); for (var i = 0; i < size; i++) { var item = PyList_GetItem(dirList, i); var nameStr = PyUnicode_AsUTF8(item); if (!nameStr.isNull()) { var name = nameStr.readUtf8String(); if (name.startsWith('__') && name.endsWith('__')) continue; var attrNamePtr = Memory.allocUtf8String(name); var attr = PyObject_GetAttrString(module, attrNamePtr); if (!attr.isNull()) { var reprObj = PyObject_Repr(attr); if (!reprObj.isNull()) { var reprUtf8 = PyUnicode_AsUTF8(reprObj); if (!reprUtf8.isNull()) { var reprStr = reprUtf8.readUtf8String(); if (reprStr.indexOf('<built-in function') !== -1) { var funcAddr = getBuiltinFunctionAddress(attr); if (funcAddr && isAddressInModule(funcAddr, pydMod.base, pydMod.size)) { console.log(' ' + name + ' @ ' + funcAddr); } } } } } } } // 类方法 console.log('\n[类方法]'); for (var i = 0; i < size; i++) { var item = PyList_GetItem(dirList, i); var nameStr = PyUnicode_AsUTF8(item); if (!nameStr.isNull()) { var name = nameStr.readUtf8String(); if (name.startsWith('__') && name.endsWith('__')) continue; var attrNamePtr = Memory.allocUtf8String(name); var attr = PyObject_GetAttrString(module, attrNamePtr); if (!attr.isNull()) { var reprObj = PyObject_Repr(attr); if (!reprObj.isNull()) { var reprUtf8 = PyUnicode_AsUTF8(reprObj); if (!reprUtf8.isNull()) { var reprStr = reprUtf8.readUtf8String(); if (reprStr.indexOf("<class '" + pydMod.shortName + '.') !== -1) { console.log('\n ' + name + ':'); var classDir = PyObject_Dir(attr); if (!classDir.isNull()) { var classSize = PyList_Size(classDir); for (var j = 0; j < classSize; j++) { var classItem = PyList_GetItem(classDir, j); var classNameStr = PyUnicode_AsUTF8(classItem); if (!classNameStr.isNull()) { var methodName = classNameStr.readUtf8String(); if (methodName.startsWith('__') && methodName.endsWith('__')) continue; var methodNamePtr = Memory.allocUtf8String(methodName); var method = PyObject_GetAttrString(attr, methodNamePtr); if (!method.isNull()) { var methodAddr = getBuiltinFunctionAddress(method); if (methodAddr && isAddressInModule(methodAddr, pydMod.base, pydMod.size)) { console.log(' .' + methodName + ' @ ' + methodAddr); } } } } } } } } } } } } } console.log('\n' + '='.repeat(80)); }); } finally { PyGILState_Release(gilState); } } dumpFunctions(); ``` 这个时候我们这些模块有一个大概的了解,我们相分析他的登录保护位置,应该要找到类似json或者请求的特征。看一下成员信息,可以发现几个疑点: ## 2. 分析 发现了疑似请求的url链接,所以我对这个bi.pyd模块高度怀疑  通过对python调用的特征,我们选择首先hook这个里面所有的模块函数,以及外部调用函数py_call 看看到底在干什么(代码放附件里)?  部分代码: ```js // Python 调用相关函数 var callFunctions = [ 'PyObject_Call', 'PyObject_CallObject', 'PyObject_CallFunction', 'PyObject_CallMethod', 'PyObject_CallFunctionObjArgs', 'PyObject_CallMethodObjArgs', '_PyObject_FastCall', '_PyObject_Call', 'PyObject_Vectorcall', '_PyObject_Vectorcall', 'PyObject_VectorcallMethod', ]; callFunctions.forEach(function(funcName) { try { var addr = python.getExportByName(funcName); if (addr) { Interceptor.attach(addr, { onEnter: function(args) { // 检查调用是否来自 bi.pyd var retAddr = this.returnAddress; if (isAddressInModule(retAddr, biModule.base, biModule.size)) { this.fromBi = true; this.callFunc = funcName; // args[0] 是被调用的 callable 对象 var callable = args[0]; var callableRepr = pyRepr(callable); // args[1] 通常是参数 tuple 或 args 数组 var argsRepr = ''; if (funcName.indexOf('Vectorcall') !== -1) { // Vectorcall: args[1] 是 PyObject** 数组, args[2] 是 nargsf try { var argsArray = args[1]; var nargsf = args[2].toInt32(); var nargs = nargsf & 0x7fffffff; var argsList = []; for (var i = 0; i < nargs && i < 5; i++) { var argObj = argsArray.add(i * Process.pointerSize).readPointer(); argsList.push(pyRepr(argObj)); } argsRepr = argsList.join(', '); if (nargs > 5) argsRepr += ', ...'; } catch (e) {} } else if (funcName === '_PyObject_FastCall') { // FastCall: args[1] 是 PyObject** 数组, args[2] 是 nargs try { var argsArray = args[1]; var nargs = args[2].toInt32(); var argsList = []; for (var i = 0; i < nargs && i < 5; i++) { var argObj = argsArray.add(i * Process.pointerSize).readPointer(); argsList.push(pyRepr(argObj)); } argsRepr = argsList.join(', '); if (nargs > 5) argsRepr += ', ...'; } catch (e) {} } else { // 普通调用: args[1] 是 tuple if (!args[1].isNull()) { argsRepr = parseArgs(args[1]); } } console.log('\n[EXT CALL] ' + callableRepr); console.log(' via: ' + funcName); console.log(' args: (' + argsRepr + ')'); console.log(' from: ' + retAddr + ' (bi.pyd+' + retAddr.sub(biModule.base) + ')'); } else { this.fromBi = false; } }, onLeave: function(retval) { if (this.fromBi) { var retStr = pyRepr(retval); console.log('[EXT RET] => ' + retStr); } } }); console.log(' Hooked: ' + funcName); } } catch (e) { // 函数不存在,跳过 } }); ``` 发现了关键函数 bi.br.sq(),所以针对这个,把Python.dll里面的导出函数hook一下看看:  发现组包并且发送。重新调整一下hook策略,通过ida找到这个函数地址,并且找一下 内部函数快速调用的接口,以及设置属性和访问属性的函数地址 GetAttr 和 SetAttr,再次之前我们需要做一些sig,这里我还是展示一下手法吧。 这里首先创建一个cython文件:  里面存放大量的函数来触发这些内部函数,构造我们想要的。然后配置一下编译脚本: ```python from setuptools import setup, Extension from Cython.Build import cythonize # 保持优化,只加调试符号 extra_compile_args = ['/Zi'] # 生成PDB,保持默认/O2优化 extra_link_args = ['/DEBUG:FULL'] # 生成完整PDB ext_modules = [ Extension( "test_cython", ["test_cython.pyx"], extra_compile_args=extra_compile_args, extra_link_args=extra_link_args, ) ] setup( ext_modules=cythonize( ext_modules, compiler_directives={ 'language_level': 3, 'boundscheck': False, 'wraparound': False, } ) ) ``` 外挂作者肯定是o2拉满的所以这里直接把o2打开并且开启pdb,编译一手 x:\xxx\envs\xx\python.exe setup.py build_ext --inplace,接下来加载这个pyd然后ida生成就有了,加载后就是这个效果(sig文件生成可以看其他帖子也很简单ida点几下的事情)   sig之后的样子非常好,很多函数都识别出来了,想要更全点,对照源码看看什么条件下回生成这个函数。 接下来就我们使用ida吧这些函数的rva都导出来,然后全部hook掉,几乎就是把这个函数从解释器层面给他trace出来了(实际上到这里就可以了,后面就是一些体力活) 导出的结果:   这里准备hook这些函数,trace出了400多个,实际上这些都是python的调用,几乎是全在这里了,就像是一个解释器层的trace一样,把这个结果拷贝下来,人肉翻译或者使用ai翻译一下都行。(这一部分必须这样做,因为这些函数他是不进解释器的你就算patch 解释器也不行) 发包函数的函数地址:  机器码的id  想爆破他的话只需要吧参数整备一下hook一下就完事了,这简单手法不解释了,想怎么玩就怎么玩,跟源码没区别了hhh。 顺带优化一下看看失败流程到底是怎么走的(ai总结一下) ``` 分析结果 1. 收集硬件信息 [116] me => '11111111111122323' # 用户ID [117] hw => '9BE22CF5-BD03-F879-EC43-E89C257B367D' # 硬件ID [118] bi => 'System Serial Number' # BIOS序列号 [119] bo => '231028078600235' # 主板序列号 [120] t2 => None # T2芯片(Mac) 2. 发送认证请求 [123] requests.post('https://auth.inputsense.com/database/3911.php', data={'id': ..., 'hwid': ..., 'bios': ..., 'board_serial': ...}) => <Response [403]> # 认证失败 3. 解析返回的错误信息 [124-164] 用chr()逐字符构建: {"error_message":"Authentication failed"} [174] bb('{lm', 9) => 'red' # 解密函数,返回颜色"red" 4. 设置失败状态 [292] cc = 6 # 错误代码6 = 认证失败 5. 文件完整性检查(反调试) [300-457] 计算多个pyd文件的hash: - _b.cp311-win_amd64.pyd - _c.cp311-win_amd64.pyd - _h.cp311-win_amd64.pyd - _s.cp311-win_amd64.pyd - _u.cp311-win_amd64.pyd - _v.cp311-win_amd64.pyd - af.cp311-win_amd64.pyd - bi.cp311-win_amd64.pyd 还检查了 .i64 文件(IDA数据库)! ``` 破解思路也讲一下吧,看trace中日志可以发现一个函数在收包和发包说明就是核心的加密算法直接改返回值就行了:  贴一个最后的成品吧: ```js var bi = null; try { bi = Process.getModuleByName("bi.cp311-win_amd64.pyd"); var biInRun = false; Interceptor.attach(bi.base.add(0x7090), { onEnter: function() { biInRun = true; }, onLeave: function() { biInRun = false; } }); Interceptor.attach(bi.base.add(0x28510), { onEnter: function(args) { if (!biInRun) return; try { var nameAttr = Memory.allocUtf8String("__name__"); var attr = PyObject_GetAttrString(args[0], nameAttr); if (!attr.isNull()) { var str = PyUnicode_AsUTF8(attr); if (!str.isNull()) { this.isSq = (str.readUtf8String() === "sq"); } } } catch(e) {} }, onLeave: function(retval) { if (!this.isSq) return; var size = PyTuple_Size(retval); if (size >= 4 && PyTuple_GetItem(retval, 2).equals(Py_False)) { var msg = PyUnicode_FromString(Memory.allocUtf8String("Activated")); Py_IncRef(msg); PyTuple_SetItem(retval, 0, msg); var one = PyLong_FromLong(1); Py_IncRef(one); PyTuple_SetItem(retval, 1, one); Py_IncRef(Py_True); PyTuple_SetItem(retval, 2, Py_True); var token = PyLong_FromLong(999999); Py_IncRef(token); PyTuple_SetItem(retval, 3, token); console.log("[+] Layer 1: bi.pyd sq() bypassed!"); } } }); console.log("[*] Layer 1: bi.pyd hook installed"); } catch(e) { console.log("[!] bi.pyd not found: " + e); } ``` 这个作者通过trace发现有一套还算复杂的加密体系,但是你放在一个函数中做,这么复杂的算法有什么用呢?属实对这些"顶级海外大牛"的手法堪忧。  ### 五、总结 实际上Python逆向就是和Cython打交道,我们要做的是从Cython中吧代码转换成Python,hook所有Cython函数以及解释器层的接口dll,能加快我们的进度。
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于
2026-1-28 19:49 被BitWarden编辑 ,原因:
#调试逆向
上传的附件:
hook_cython_all.js
(11.44kb,18次下载)
hook_python_calls.js
(12.67kb,17次下载)
test_cython.cp311-win_amd64.sig
(9.18kb,17次下载)
收藏
・
4
点赞
・
8
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
wx_晨梦
期待更多优质内容的分享,论坛有你更精彩!
2026-7-16 05:35
AEVE
+2
感谢你分享这么好的资源!
2026-2-3 17:09
咖啡_741298
这个讨论对我很有帮助,谢谢!
2026-1-31 18:33
5m10v3
非常支持你的观点!
2026-1-29 10:06
Gagma
谢谢你的细致分析,受益匪浅!
2026-1-29 08:53
青梧雨梦
非常支持你的观点!
2026-1-29 03:56
我的小拇指啊
感谢你的积极参与,期待更多精彩内容!
2026-1-28 22:49
moshuiD
非常支持你的观点!
2026-1-28 21:00
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
4
)
Imxz
雪 币:
110
活跃值:
(9571)
能力值:
( LV2,RANK:10 )
在线值:
发帖
6
回帖
775
粉丝
9
关注
私信
Imxz
2
楼
tql
2026-1-29 08:46
0
mb_mkxeabat
雪 币:
847
活跃值:
(877)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
4
粉丝
0
关注
私信
mb_mkxeabat
3
楼
向大佬学习
2026-1-29 09:13
0
MsScotch
雪 币:
4269
活跃值:
(5146)
能力值:
( LV2,RANK:10 )
在线值:
发帖
3
回帖
397
粉丝
5
关注
私信
MsScotch
4
楼
非常棒的分析,支持!
2026-2-7 10:50
0
mb_flcmhcvi
雪 币:
531
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
11
粉丝
0
关注
私信
mb_flcmhcvi
5
楼
向大佬学习
2026-5-6 17:35
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
BitWarden
2
11
发帖
54
回帖
120
RANK
关注
私信
他的文章
[原创]AI时代逆向分析:动态分析框架
745
[原创]VMProtect3.96 实战逆向 (一) 外壳研究
1118
[原创] 某安全厂的面试题 Android病毒分析
814
[原创]Intel 酷睿 CPU Management Engine 固件研究与分析逆向 (二) 实战DCI链接与ME解锁尝试
7013
[原创] Intel 酷睿 CPU Management Engine 固件研究与分析逆向 (一) 前置准备与解包
14189
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
huangyalei
ddwwtom
hackroad
AEVE
错过
dnawujun
智童
lzgking
doduhuang
寻影星尘
威兹曼
git_11677Sh4DoVV
mb_ldbucrik
npc0vo
mb_flcmhcvi
ReGenesis
mb_lthgjpwj
谁下载
×
huangyalei
ddwwtom
hackroad
AEVE
dnawujun
智童
lzgking
doduhuang
寻影星尘
威兹曼
git_11677Sh4DoVV
mb_ldbucrik
npc0vo
mb_flcmhcvi
ReGenesis
mb_lthgjpwj
谁下载
×
huangyalei
ddwwtom
hackroad
dnawujun
智童
lzgking
doduhuang
寻影星尘
威兹曼
git_11677Sh4DoVV
fu11p7r
mb_ldbucrik
npc0vo
mb_flcmhcvi
ReGenesis
mb_lthgjpwj
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部