-
-
[原创]Pyd原理以及逆向实战 (二)变量 与 实战
-
发表于: 2天前 557
-
一、前言
最近在分析一个国外某个体育游戏,热度最高的外挂样本。发现使用的是Python脚本并且Pyd编译后的,中间存在部分名称混淆以及反调试,且Pyd的编译后巨大,故而研究了一下Pyd逆向用来高效逆向这类样本。顺便记录一下方便后面查阅。这一期主要是我如何定位分析这个样本爆破它的登陆,并且简单讲一下Pyd成员的原理。一些对于函数处理和Pyd函数是怎么实现的可以看我上一篇。(感谢版主支持hh)
二、变量
先说一个前提我认为:对于变量和成员变量来说,静态分析不是一个很好的办法,原因是:RTTI类的这种信息比较散,需要一点点处理太麻烦了,不如运行起来再解释器层进行处理。
这里主要抛出几个问题:
1.Python成员和变量如何转换成pyd
2.解释器如何感知变量和成员变量
3.解释器以及native层如何访问这些变量
问题1.Python成员和变量如何转换成pyd
全局变量:
这里我编译了一个Pyd,通过编译选项让他产生中间文件.c,通过来窥探一下全局变量是如何实现的(pyd怎么生成中间文件就不展示以了):
demo代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | # 全局变量g_count = 100g_name = "global_test"def calculate(a, b): """局部变量示例""" x = a + 10 y = b * 2 result = x + y return resultclass 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:

1 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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行:

实际上生成的字符串也是存在字符串缓存表里面

1 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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解释器的创建:

1 2 3 4 5 6 7 8 | /* "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中,也就完成了注册。

总结整理一下就是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | ① 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位置还是一样的:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | /* "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 元组里面去就像是这样
1 2 3 4 5 6 7 8 9 10 | 模块字典 __pyx_d├── "Player" → 类对象 (PyTypeObject)│ └── tp_dict (类的属性字典)│ ├── "max_level" → 99│ ├── "game_name" → "RPG"│ ├── "__init__" → <function> ← 函数在这里│ ├── "take_damage" → <function>│ └── "level_up" → <function>│└── "p" → 实例对象 |
最后上面这个类会在这里进行注册(代码比较多我单拎出来):
1 | 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。
讲一下使用的时候如何创建的:

1 2 3 4 5 6 7 8 9 10 11 12 13 | /* "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; |
实际上创建流程我们不关心,核心就是这个对象怎么被解释器感知:
1 | 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成员变量的访问:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /* "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。我这里就举例几个演示一下:
源码位置:
1 2 3 4 5 | x:\x\cython\Cython\Utility\├── ObjectHandling.c # 对象操作(迭代、属性访问、调用)├── Optimize.c # 内置方法优化(append、pop)├── Builtins.c # 内置函数(globals、exec、getattr)└── TypeConversion.c # 类型转换 |
迭代器:
for x in obj
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | /////////////// 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: IterNextPlainstatic 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]
1 2 3 4 5 6 7 8 9 10 11 | /////////////// 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脚本
1 2 3 4 5 6 7 8 9 10 11 | 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)); }}); |
外挂文件如下:

程序的启动入口:

之后我打印出这个目录下所有类的导出成员信息:

脚本代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | // 打印 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的方法信息:
结果如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | // 打印 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 看看到底在干什么(代码放附件里)?

部分代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | // 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文件:

里面存放大量的函数来触发这些内部函数,构造我们想要的。然后配置一下编译脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | from setuptools import setup, Extensionfrom Cython.Build import cythonize# 保持优化,只加调试符号extra_compile_args = ['/Zi'] # 生成PDB,保持默认/O2优化extra_link_args = ['/DEBUG:FULL'] # 生成完整PDBext_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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 分析结果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('2a1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2N6i4c8Z5i4K6u0W2K9h3&6H3N6i4c8K6k6h3&6K6k6g2)9J5k6h3y4G2L8g2)9J5c8X3c8S2N6r3q4T1j5i4y4W2i4K6u0r3x3K6V1I4x3g2)9J5k6i4m8Z5M7q4)9J5y4H3`.`., 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中日志可以发现一个函数在收包和发包说明就是核心的加密算法直接改返回值就行了:

贴一个最后的成品吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | 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,能加快我们的进度。
赞赏
- [原创]Pyd原理以及逆向实战 (二)变量 与 实战 558
- [原创]Pyd原理以及逆向实战 (一)函数 2495
- [原创]基于Apatch模块的安卓Linux内核硬件断点 2489
- [原创]MFC框架攻防深入探讨 7464
- [原创]Gtuner软件分析(一) 3664
向大佬学习