pyd逆向基础
业务上遇到一个pyinstaller打包的样本需要逆向分析,分析的过程中遇到了很多问题,开篇文章记录一下
pyinstaller
pyinstaller可以理解为把pyc,pyd,相关的lib依赖,python运行时全部打包进一个exe,然后运行时释放在内存中,分析的第一步是解包,推荐使用pyinstxtractor-ng,对多版本python兼容性较好
main
解包完后应该会出现一个项目文件夹,这个时候一般在根目录找pyc文件,比如说app.pyc这样的命名看着就很像程序入口,使用pylingual将其还原成python代码,一般都能成功还原,如果出错了就手撕还原字节码,pyc的字节码才用的是基于栈运行的虚拟机,并不难手撕,这里不展开赘述,只要去查一下相关对应的字节码对应的指令即可
比如我遇到的app.pyc反编译结果就如下
1 | # Decompiled with PyLingual (https://pylingual.io) |
可以看到主函数里啥都没有,python逆向对抗的主要区域是在pyd,稍微注重一点加固的软件都不会把重要代码放在pyc里
这个阶段还有一个重要的工作是收集信息,根据目录下的dll确定python版本(非常重要,后续的运行和调试基本要基于相同的版本),如果可以的话也可以尝试确认依赖库的具体版本,不过这个相对没有那么严格
然后就是尝试运行程序python app.pyc
,因为如果运行打包好的程序所有东西都是混在一起的,没法调试,一般是运行python主文件,然后用调试器attach到python进程上,这样每个pyd文件都会作为独立的dll存在
如果是比较复杂的程序这个时候会缺各种依赖,我们最好使用一个虚拟环境python -m venv .venv
,然后根据报错去下载对应的依赖,同时可能会出现只有特定版本区间的依赖库才能正常运行的情况,这种情况就要根据报错去查资料然后试着换不同版本号的依赖库
如果程序能正常运行了,那么前期的准备工作基本就完毕了,可以进入pyd的分析了
pyd
恢复符号
pyd里的符号是全抹去的,因为pyd只导出pyInit_module这一个符号用来将c实现的python方法导出给python进程,我们首先要做的是尽可能恢复一些和python数据结构操作相关的符号以及python特有的结构体,这里一般采取的方法是编译一份带符号和调试信息的pyd文件,然后在ida中导出头文件来提供相关类型定义,然后再用bindiff和样本pyd比对恢复一部分符号
bindiff在这个repo下载,解压了直接拖进ida的插件目录即可,好像9.1的有个bug,他会去找ida64.exe这个文件执行导出任务,但是ida9.0以及把ida64和ida合并成了一个文件,解决办法就是复制一份ida.exe并重命名成ida64.exe放在ida根目录下
1 | # setup.py |
1 | # MyPyd.py |
原则上样本文件越复杂越好,包括越多的操作生成的符号就越多,能恢复的也越多
运行python setup.py build_ext --inplace
即可编译,这里最好用相同版本的python进行编译
然后运行Parse C header file
导出头文件即可
然后退出ida并把分析结果保存为i64/idb数据库
打开要分析的样本,并plugins
中选择bindiff
,再选择diff database
然后选中刚刚的保存的数据库,就会自动开始匹配符号,一般看着选一部分,然后import symbols
即可导入符号,把绿色的差不多都导入了就行,反正置信度这种就看个乐
定位方法
正常情况下方法名都会直接直接以裸字符串储存在rdata里(用于在报错时添加traceback),通过字符串可以迅速定位到指定方法的具体实现
动调分析
PyObject默认都是未初始化的,具体来说就是静态的时候看到的一大堆空的qword
网上有说法是有一个Pyinit函数统一处理这些所有对象的初始化,可惜我并没有定位到对应的实现,不过像这种PyASCIIObject跳过去基本能看到是什么,对应的初始值就和PyObject指针放在一起
这里再介绍一种动调的方法,attach到python进程后在想看的PyObject附近下断点,然后跳到对应内存,需要跳两次,因为是结构体指针,然后可以看到明显的结构体特征
这里前两个值直接转化成qword,所有PyObject这两个都是固定的,分别是引用次数和PyType
知道了type后就好办了,我们之前准备的头文件已经搞定了这些PyObject类型的定义,像这里其实这就是一个PyASCIIObject
紧跟在PyASCIIObject结构体后的就是这个python对象的具体值
另外在分析的过程中可能会看到有结构体没还原的情况(ida识别为qword,然后相关操作都带偏移),这种情况可以先试试重定义为PyObject*,大部分情况下其实都是PyObject*
另外如果目标程序里有复杂的加密计算流程,可以考虑trace相关的数值操作,这里使用frida或条件断点都是不错的选择
不过位操作(左右移,and,or,xor)似乎不使用导入的函数,所以还要去具体的加密部分找函数hook
替换返回值
如果要替换数值对象或字符串返回值最好用frida,主动调用PyXXX_fromXXX构造PyObject对象后返回,如果是替换true/false倒是可以直接patch,注意到python中ture和false分别被储存到两个全局单例中,所以如果想让某个函数永远返回true或false,只要将该单例的指针储存到返回寄存器(rax),然后增加引用数并return即可