我想了解有关 unix/linux 的更多信息,这个问题突然出现在我的脑海中——假设我制作了一个静态/动态库(.a 或 .so)并且丢失了 c/c++ 源代码和头文件。默认的 nm 输出为我提供了符号的名称,但我需要知道返回类型和参数计数/类型才能制作标题。是否有可能以某种方式获取这些额外信息来对给定库的标头进行逆向工程?
2 回答
您标记了 C 和 C++,答案在两者之间略有不同。
对于 C++,类的方法名称在符号名称中嵌入了类型信息。您只需要弄清楚编译库的编译器是如何处理的名称。
对于 C,没有真正干净的方法可以做到这一点。您可以拆开程序集并分析读取了哪些寄存器和堆栈区域,而无需编写以确定函数需要多少参数。这需要了解编译库的任何编译器使用的调用约定。
同样,您可以查看程序集中如何使用每个参数。如果您看到它在加载指令中使用,它很可能是某种指针,而如果您看到它在算术中使用,它可能是某种整数。
对于返回类型,您可以检查在返回指令之前是否将任何看似有意义的内容放入返回寄存器中。同样,这需要了解您平台的调用约定。
这是我将如何在 ARM 汇编中执行操作的示例。
我知道ARM中的参数在寄存器r0到r3中传递,返回值存储在寄存器r0中。考虑到这一点,我们可以开始逆向工程。让我们看一下两个函数的程序集,并尝试找出函数原型是什么。
00000000 <func1>:
0: e3510000 cmp r1, #0
4: 0a000007 beq 28 <func1+0x28>
8: e0801001 add r1, r0, r1
c: e1a03000 mov r3, r0
10: e3a00000 mov r0, #0
14: e4d32001 ldrb r2, [r3], #1
18: e1530001 cmp r3, r1
1c: e0800002 add r0, r0, r2
20: 1afffffb bne 14 <func1+0x14>
24: e12fff1e bx lr
28: e1a00001 mov r0, r1
2c: e12fff1e bx lr
如果我们看一下这里,r0 和 r1 在写入任何内容之前都会被读取。我们还可以看到 r2 和 r3 在被读取之前被写入。因此,我们可以推断出func1
最多有两个参数。
我们还意识到 r0 被移动到 r3 然后用作指向 的地址ldrb
,这是一条从内存中加载字节的指令。因此,我们推断第一个参数是一个指针。因为指令只加载一个字节,我们也可以判断它可能是指向某种单字节数据类型的指针。
r1 中的第二个参数似乎从未使用过,除非在比较和添加指令中使用,因此它可能是一个整数。
在每个bx lr
(返回调用者指令)之前,都会在 r0 中放置一些东西,因此我们推断该函数返回某种值。
如果把这个函数呈现给我,我猜函数原型应该是这样的:
int func1(unsigned char *, int);
原来的:
unsigned int func1(void *, unsigned int);
这是另一个功能
00000030 <func2>:
30: e0822001 add r2, r2, r1
34: e5c02000 strb r2, [r0]
38: e12fff1e bx lr
这很容易。
我们看到 r0、r1 和 r2 都是在写入之前读取的,因此我们可以猜测该函数采用三个参数。r0 用作strb
指令的地址(存储字节),因此它可能是一个指针。同样,它只存储一个字节,因此它可能是一个指向字节大小的数据类型的指针。
另外两个仅在加法指令中使用,因此可能是整数。
最后似乎没有放入 r0 ,因此该函数要么返回第一个参数,要么不返回值。
我猜原型将是以下之一
void func2(unsigned char *, int, int);
unsigned char *func2(unsigned char *, int, int);
原来的:
void func2(char *, char, char);
请记住,调用者/被调用者的约定因不同的处理器指令集而异,并且您已经知道在一起使用 c 和 c++ 库时会发生名称修改,您可以尝试以下方式:
gdb <executable>
....
disas <function name>
....
Here you can make a wild guess about the type of return value and parameters using the bit size of those values written on stack making use of assembly code.