pyd文件逆向

​ 具体是看l1nk师傅的博客:https://tttang.com/archive/1641/#toc_pyd_1。**本博客就是按照他的,一步一步来复现。**

​ pyd是python的链接库文件。

0x00 环境准备

​ 首先写test_for_pyd.py

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
def test_hello(a):
test = 2
result = test + 2
t = 4
print(a)
print(test, result, t)

"""
相当于重载了python的int
"""
def int(string):
table = "8ed4bc0123a567f9"
v7 = 0
index = 0
num = 0
string = reversed(string)
for each in string:
num *= 16
index = table.find(each)
num += index


return num

if __name__ == "__main__":
test_hello("hello")
print(int("123"))
""" 输出(2439也就是0x987)
hello
2 4 4
2439
"""

​ 之后写setup.py

1
2
3
4
5
6
7
from setuptools import setup
from Cython.Build import cythonize

setup(
name="test_for_pyd",
ext_modules=cythonize('test_for_pyd.py')
)

​ 输入python .\setup.py build_ext --inplace,即可生成test_for_pyd.cp38-win_amd64.pydtest_for_pyd.ctest_for_pyd.c可以理解为test_for_pyd.py对应的C代码,而test_for_pyd.cp38-win_amd64.pyd是对应的库文件。

0x01 变量分析

​ 变量区分为变量名区变量定义区

​ 以下是变量名区:

image-20221227173222549

​ 可以看到,这个变量名,真鸡儿全。

​ 以下是变量定义区:

image-20221227173951015

​ 可以看到,Python使用PyObject*来描述变量。

注:在python中,即使是数字也是一个对象。如果在我们的代码中显式的写了某个数字,并且这个数字不仅仅用于赋值,那这个数字就会被作为一个对象存储。例如,test_helloint函数中数字一共有0,2,4,16这4个数字,2,16在表达式中使用了,0赋值的num也用在了表达式中,但是4没有,就是单纯的赋值,因此在变量定义区就只有:

1
2
3
static PyObject *__pyx_int_0;
static PyObject *__pyx_int_2;
static PyObject *__pyx_int_16;

​ 而没有static PyObject *__pyx_int_4;

​ 最终,这些对象将在全局表中进行关联,如下所示:

image-20221227182516966

​ 在实际逆向工作中,找到这个全局表的位置非常重要,通过此表可以还原出大部分的符号信息。当然,可作为对象的数字也会被初始化,如下:

image-20221227182946555

0x02 函数分析

​ 在pyd中,不同类型的函数有不同的实现形式。所有以__pyx_pf开头的变量都是python中函数的定义。

​ 以test_hello为例,首先,此函数会被声明为:

image-20221227183328185

​ 可以看到,test_hello函数并不会被直接调用,而是会先定义一个proto(原型)。此原型声明了函数的调用模式,其中第一个参数是__pyx_self,其实就是描述python这个运行的对象(相当于C++中的self指针)。然后第二个参数表述了要引用的参数。

​ 接着,又用PyMethodDef定义了函数的其他信息,PyMethodDef是一个结构体,在逆向过程中,可以通过这个PyMethodDef所在的表找到当前代码中实现的相关函数

​ 最后,实现了此函数的wrapper形式(啥是wrapper不必多言)。

​ 下面进入test_hello的函数体部分:

image-20221227185131150

(1)增加__pyx_int_2对象的引用计数,表示此处用了这个数字。

(2)将这个对象赋值给__pyx_v_test,也就是python中定义的test对象。

image-20221227185357163

(3)通过使用方法__Pyx_PyInt_AddObjC进行整数对象的相加。

(4)完成加法后,其增加了一个临时的引用对象__pyx_t_1的引用计数。

(5)将答案赋值给了__pyx_v_result,之后将这个值重新值为了0,相当于完成了result = test + 2这个流程。

(6)对__pyx_v_t变量进行赋值。

(7)将__pyx_v_a对象传入__Pyx_PrintOne这个函数中,实现print的调用。

​ 再来看int函数的调用流程,与test_hello类型,不同的是在这个函数中存在一些内置的函数(reserverd)以及常见的迭代对象的使用。直接来看不同的部分:

image-20221227185917013

(1)使用__Pyx_PyObject_CallOneArg调用reversed(string),并将结果保存在__pyx_t_1中。

image-20221227190309679

(2)检查一个字符串是否为List或者Tuple。如果是的话,直接将其赋值给__pyx_t_1,之后会以迭代器的方式调用,并且用__pyx_t_2标记此时的循环下标;否则,调用PyObject_GetIter,获取当前对象的可迭代对象,赋值给__pyx_t_1,此时__pyx_t_3会记录当前的带对象的下一个迭代对象。并且__pyx_t_2会被赋值为-1作为标记。

(3)进入一个循环,每次循环的时候首先检查__pyx_t_3是否为空,如果不为空的话进行迭代逻辑处理,并且将迭代对象交给__pyx_t_4,否则进行迭代对象的尝试获取。

(4)假设为空,此时确认__pyx_t_1是否为List或者Tuple,若满足其中一项,则通过PyList_GET_SIZE取出其大小,并且如果大小大于__pyx_t_2,则条数循环。显然,我们这个时候的__pyx_t_2作为下标,小于我们的size,于是调用Py*_GET_ITEM(或者PySequence_ITEM)将其中元素赋值给__pyx_t_4,同时自增作为下标的__pyx_t_2

image-20221227190825722

(5)调用了一个内置函数find。可以看到其首先通过函数__Pyx_PyObject_GetAttrStr,指定需要在__pyx_v_table找到__pyx_n_s_find,并且这个值会临时存放在__pyx_t_5

(6)通过__Pyx_PyObject_Call2Args对函数进行了调用。注意,第一个参数是通过调用__Pyx_PyObject_GetAttrStr找到了方法,然后第二个参数为上一段,找到被迭代对象后,通过__Pyx_XDECREF_SET(__pyx_v_each, __pyx_t_4);赋值得到的,真正的迭代对象的值。

(7)将运算结果得到的值赋值给__pyx_v_index

image-20221227191620093

(8)将__pyx_v_index与前文的__pyx_v_num进行相加,完成最后的运算,并且将最终运算结果__pyx_v_num作为返回值。

补充:函数名会放到一个全局的dict对象中,从而保证可以从module中取出对应对象。

image-20221227192201912

0x03 技巧总结

​ 需要关注的数据结构:

  1. __pyx_string_tab(用于初始化Python符号表)。

  2. __Pyx_InitCachedBuiltins(初始化各类函数)。

  3. __Pyx_InitGlobals(初始化数字)。

留言

2022-12-27

© 2024 wd-z711

⬆︎TOP