几个夏天前,我参加了 40 小时的基本逆向工程课程。在教我们使用 IDAPro 的同时,讲师演示了如何将 ASM 中的某些变量标记为结构的成员,这基本上相当于struct
C/C++ 中的老式,并在任何地方都如此对待它们它们在代码的其余部分中可见。这对我来说似乎很有用。
然而,他没有涉及如何识别结构。您如何知道一组变量何时实际上构成了一个结构,而不仅仅是一组相关变量?你怎么能确定作者在struct
那里使用了(或类似的东西)?
几个夏天前,我参加了 40 小时的基本逆向工程课程。在教我们使用 IDAPro 的同时,讲师演示了如何将 ASM 中的某些变量标记为结构的成员,这基本上相当于struct
C/C++ 中的老式,并在任何地方都如此对待它们它们在代码的其余部分中可见。这对我来说似乎很有用。
然而,他没有涉及如何识别结构。您如何知道一组变量何时实际上构成了一个结构,而不仅仅是一组相关变量?你怎么能确定作者在struct
那里使用了(或类似的东西)?
您会在代码中找到表示结构用法的非常常见的模式。
如果您有一个在某个非零偏移处取消引用的指针,则您可能正在处理一个结构。寻找像这样的模式:
mov eax, [ebp-8] ; Load a local variable into eax
mov ecx, [eax+8] ; **Dereference a dword at eax+8**
在这个例子中,我们有一个包含指针的变量,但我们关心指针前面某个特定偏移量处的内存内容。这正是结构的使用方式:我们获得一个指向该结构的指针,然后取消引用该指针加上一些偏移量以访问特定成员。在 C 中,它的语法是:pMyStruct->member_at_offset_8
.
旁注:不要将在某些变量的偏移处取消引用与在堆栈指针或帧指针(esp
或ebp
)的偏移处取消引用混淆。当然,您可以将局部变量和函数参数视为一个大结构,但在 C 中,它们并未明确定义为这样。
您实际上不需要取消引用任何东西来检测结构成员。例如:
mov eax, [ebp-8] ; Load a local variable into eax
push 30h ; num = 30h
push aSampleString ; src = "Sample String"
add eax, 0Ch
push eax ; dst = eax + 0xC
call strncpy
在此示例中,我们将最多 0x30 个字符从某个源字符串复制到eax + 0xC
(请参阅strncpy)。这告诉我们它eax
可能指向一个在偏移量 0xC 处具有字符串缓冲区(至少 0x30 字节)的结构。例如,结构可能类似于:
struct _MYSTRUCT
{
DWORD a; // +0x0
DWORD b; // +0x4
DWORD c; // +0x8
CHAR d[0x30]; // +0xC
...
}
在这种情况下,示例代码将如下所示:
strncpy(&pMyStruct->d, "Sample String", sizeof(pMyStruct->d));
旁注:我们有可能(虽然不太可能)复制到偏移量 +0xC 处的大字符串缓冲区,但您可以通过上下文来确定这一点。例如,如果 offset +0x8 是一个整数,那么它肯定是一个结构体。但是如果我们将一个固定长度 0xC 的eax
字符串复制到 address eax+0xC
,然后将另一个字符串复制到 address ,它可能是一个巨大的字符串。
假设您有一个结构体(不是指向结构体的指针)作为堆栈的局部变量。大多数时候,IDA 不知道堆栈上的结构或一堆单独的局部变量之间的区别。但是,您正在处理结构的一个重要提示是,如果您只读取变量而不写入它,或者(较少)如果您只写入变量而不读取它。以下是每个示例:
lea eax, [ebp+var_58] ; Load THE ADDRESS of a local variable into eax
push eax
call some_function
mov eax, [ebp+var_54] ; Let's say we've never touched var_54 before...
test eax, eax ; ...But we're checking its value!
jz somewhere
...
在这个例子中,我们var_54
从不向它写入任何内容(在这个函数中)。这可能意味着它是从某个其他函数调用访问的结构的成员。在这个例子中,它暗示var_58
可能是那个结构的开始,因为它的地址被作为参数推送到some_function
。您可以通过遵循 的逻辑some_function
并检查其参数是否在偏移量 +0x4 处取消引用(和修改)来验证这一点。当然,这并不一定在发生some_function
-它可以在它的子功能中的一个,或一个发生的子功能,等等。
一个类似的例子存在于写作中:
xor eax, eax
mov [ebp+var_28], eax ; Let's say this is the *only* time var_28 is touched
lea eax, [ebp+var_30]
push eax
call some_other_function
...
当您看到局部变量被设置然后不再被引用时,您不能只是忘记它们,因为它们很可能是传递给另一个函数的结构的成员。这个例子意味着一个结构(从 开始var_30
)在该结构的地址被传递到之前被写入偏移量 +0x8 some_other_function
。
这两个 C 中的示例可能如下所示:
some_function(&myStruct);
if (myStruct.member_at_offset_4) ...
和
myStruct.member_at_offset_8 = 0;
some_other_function(&myStruct);
旁注:尽管这些示例中的每一个都使用了局部变量,但相同的逻辑适用于全局变量。
这可能是显而易见的,IDA 几乎所有时间都会为您处理这个问题,但是了解代码中何时具有结构的一种简单方法是调用需要特定结构的文档化函数。例如,CreateProcessW
需要一个指向STARTUPINFOW
结构的指针。这个不应该需要一个例子。
我想说的最后一点是,在所有这些情况下,是的,从技术上讲,程序的作者可以在不使用结构的情况下编写他们的代码。他们也可以通过将每个函数定义为__declspec(naked)
一个大__asm
内联来编写他们的代码。你永远无法分辨。但可以说,这无关紧要。如果存在连续存储在内存中并从函数传递到函数的逻辑值组,将它们注释为结构仍然有意义。几乎所有的时间,作者都是这样编写代码的。
如果您需要我详细说明任何事情,请告诉我。
你不能。在 C 中,结构是为 C 程序的读者提供的,它们在程序映像中的使用是可选的。完全有可能在原始程序中,某个疯狂的混蛋决定使用完美大小的 char* 缓冲区做所有事情,并进行适当的转换和添加,而您永远不会知道其中的区别。
'struct' 标签完全是为了您作为代码查看者的利益。很可能您应用到程序的结构标签实际上是两个始终并排存储的变量。这并不重要,只要它不会导致您对程序所做的事情做出错误的结论。
查找结构体很棘手,但对理解代码有很大帮助。正如安德鲁所说,结构只是一个 C 抽象,在汇编中,它只是一块内存,没有识别结构的万无一失的方法。但是,对于更简单的程序,一些启发式方法可能会有所帮助。例如,“小”大小的数组比巨型数组更有可能是结构体。例如,从循环中读取的整数会使它看起来是一个数组,而以某个恒定偏移量读取几个整数会使它看起来更像一个结构体。另一种方法是在代码的不同区域看到同一组取消引用。如果两个不同的函数将某个指针作为参数,并且都尝试遵循偏移量 0x10 后跟 0x18 后跟 0x14 或其他东西,则它可能是结构的代码设置字段。还,
了解何时处理结构的最简单方法是代码何时调用您知道(或文档状态)的函数,将结构作为参数。
例如函数的in_addr
结构inet_ntoa
。
鉴于 IDA 一开始并没有弄清楚这一点。