几年前,我参加了40小时的逆向工程基础课程。在教我们使用IDAPro的过程中,讲师相当迅速且没有过多说明,展示了如何将ASM中的某些变量标记为结构的成员,基本上等同于C / C ++中的老式struct,并对其进行处理。无论在其余代码中什么地方看到它们。这对我来说似乎很有用。

但是,他没有讲的是如何识别结构。您怎么知道一组变量实际上是一个结构而不只是一组相关变量?您如何确定作者在那里使用了struct(或类似的东西)?

评论

出于好奇,那是我的(Rolf Rolles)课吗?

不,不是。

我相信我已经讲授了您所指课程的这一部分(尽管您上这门课时可能没有)。现在,我们在此问题上的花费通常比以前更多。抱歉造成混乱!

这是我公司内部的一门私人课程,因此除非您碰巧被同一个人雇用并积极从事该领域的工作,否则这不太可能。我想周围会有很多。

#1 楼

在代码中可以找到表示结构用法的非常常见的模式。

取消引用偏移量:

如果您有一个在某个非零偏移量处被取消引用的指针,您可能正在处理结构。查找类似以下内容的模式:在本示例中,我们有一个包含指针的变量,但是我们关心内存内容在指针之前的特定偏移处。指针。这正是使用结构的方式:我们获得一个指向该结构的指针,然后取消对该指针的引用以及一些偏移量以访问特定成员。在C中,其语法为:pMyStruct->member_at_offset_8

旁注:不要将在某些变量的偏移量处进行解引用与在堆栈指针或帧指针(espebp)的偏移量处进行解引用混淆。当然,您可以将局部变量和函数参数视为一个大型结构,但是在C中,它们并未明确定义为此类。

更多细微的指针偏移量:

实际上,您不需要取消引用任何内容即可检测结构成员。例如:

mov eax, [ebp-8]  ; Load a local variable into eax
mov ecx, [eax+8]  ; **Dereference a dword at eax+8**


在此示例中,我们将最多0x30个字符从某个源字符串复制到eax + 0xC(请参阅strncpy)。这说明eax可能指向偏移量为0xC的字符串缓冲区(至少为0x30字节)的结构。例如,该结构可能类似于:

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


在这种情况下,示例代码应类似于: >
旁注:有可能(尽管不太可能)我们可以将文件复制到偏移量为+ 0xC的大字符串缓冲区中,但是您可以通过上下文确定这一点。例如,如果offset + 0x8是一个整数,那么它绝对是一个结构。但是,如果我们将固定长度为0xC的字符串复制到地址eax,然后将另一个字符串复制到地址eax+0xC,则可能是一个巨型字符串。

所有读取/全部写入:

假设您有一个结构(而不​​是指向结构的指针)作为堆栈的局部变量。大多数时候,IDA不知道堆栈上的结构或一堆单独的局部变量之间的区别。但是,要处理结构的一个巨大提示是,如果您仅从变量中读取而不写入数据,或者(如果不是这样,则)仅从变量中读取而不读取数据。以下是每个示例的示例:

struct _MYSTRUCT
{
    DWORD a;      // +0x0
    DWORD b;      // +0x4
    DWORD c;      // +0x8
    CHAR d[0x30]; // +0xC
    ...
}


在此示例中,我们从var_54读取数据,而没有对其进行任何写入(在此函数内)。这可能意味着它是从其他函数调用访问的结构的成员。在此示例中,暗示var_58可能是该结构的开始,因为其地址被推入some_function的参数中。您可以通过遵循some_function的逻辑并检查其参数是否已在偏移量+ 0x4处取消引用(并修改)来验证这一点。当然,不一定要在some_function中发生这种情况,它可能发生在其子功能之一或子功能之一等中。

存在类似的示例编写:

strncpy(&pMyStruct->d, "Sample String", sizeof(pMyStruct->d));


当您看到设置了局部变量,然后再也没有引用它时,您不能只是忘记它们,因为它们很可能是结构的成员传递给另一个函数。此示例暗示在将该结构的地址传递给var_30之前,将结构(起始于some_other_function)写入到偏移量+ 0x8处。 />
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
...




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
...


侧面说明:尽管这些示例均使用局部变量,但相同的逻辑适用于全局变量。

需要结构的文档化函数:

这可能很明显,IDA几乎总是在为您处理此问题,但是知道代码中何时具有结构的一种简单方法是,如果调用需要某种结构的文档化函数。例如,CreateProcessW需要一个指向STARTUPINFOW结构的指针。

我怎么知道这些模式是否真正表明了结构的使用?

我要说的最后一点是:情况是的,从技术上讲,该程序的作者可以在不使用结构的情况下编写其代码。他们还可以通过将每个函数定义为带有较大内联__declspec(naked)__asm来编写代码。你永远无法分辨。但是可以说,这并不重要。如果有逻辑值组连续存储在内存中并从一个函数传递到另一个函数,则将它们注释为结构仍然有意义。几乎所有时间都是作者编写代码的方式。

如果您需要我进行详细说明,请告诉我。

评论


哇。这令人印象深刻。

–肯·贝娄
13年3月22日在13:43

“当然,您可以将局部变量和函数参数视为一个大型结构,但是在C语言中,它们并未明确定义为此类。”但是,也值得指出的是,用于在函数中指定堆栈变量的IDA接口与用于定义结构的接口几乎相同。在C中将两者融合起来并没有多大帮助,但是在IDA中,认识到它们的相似性是一件好事。

–约书亚·泰勒(Joshua Taylor)
16年5月18日在12:39

#2 楼

你不能在C语言中,为C程序的读者提​​供了结构,它们在程序映像中的使用是可选的。完全有可能在原始程序中,一个疯狂的混蛋决定使用大小合适的char *缓冲区来做所有事情,并适当地进行强制转换和添加,而您永远不会知道其中的区别。

“结构”标签完全是为了您作为代码查看器的利益。很有可能是您应用于程序的结构标签实际上是两个始终彼此相邻存储的变量。只要这不会导致您对程序执行错误的结论,这将无关紧要。

评论


很有意思。当定义一个结构可能导致错误的结论时,您可以举一个例子吗?

–肯·贝娄
13年3月19日在19:57

@KenB即使程序员确实使用了结构,也可能会做出错误的结论,例如,如果该结构位于联合中,则数据结构可以根据对象生命周期中的代码路径或阶段而具有不同的布局。

–吉尔斯'所以-不再是邪恶的'
13年3月19日在20:29

我能想到的一个(类)人为例子是通过参数类型进行函数识别。有一个调用eax,您知道参数是作为参数传递的指针,并且由于您认为指针指向baz类型的结构,因此您认为'eax'可以包含带有签名'void(* )(struct baz *);'。但是如果您的结构标识不正确,则不必是这种情况。

–安德鲁(Andrew)
13年3月19日在20:40



可惜的是,这个答案被太快地接受了,而现在正因为如此而被接受。尽管并非总是能够可靠地进行操作,但在其他情况下,绝对有可能恢复或至少猜测结构布局,这由其他答案证明。

–伊戈尔·斯科钦斯基♦
13年3月20日在2:51



我认为说不可能是没有建设性的。尽管您不能肯定地说它是否是一个结构,但是正如您所说的那样,它可能具有很大的“代码查看器优势”。您必须对得出的结论持保守态度-首先遵循执行路径! -但这可能是非常有用的工具。

–罗伯特·梅森
13年3月20日在12:38

#3 楼

查找结构很棘手,但是可以帮助您更好地理解代码。正如安德鲁所说,结构只是C的抽象,在汇编中,它只是内存的一滴而已,没有可靠的方法来识别结构。但是,对于较简单的程序,一些启发式方法可能会有所帮助。例如,“小”大小的数组比大数组更可能是结构。例如,看到从循环读取的整数将使其看起来像是一个数组,而以恒​​定的偏移量读取几个整数将使其看起来更像一个结构。另一种方式是看到同一组取消引用发生在代码的不同区域。如果两个不同的函数将某个指针用作参数,并且都尝试遵循偏移量0x10、0x18、0x14等,则可能是结构的代码设置字段。同样,从单个输入的指针解引用的对不同大小数据的任何访问都是一个很好的指示。

#4 楼

知道何时处理结构的最简单方法是,代码何时调用您知道的(或文档状态)将结构作为参数的函数。

例如in_addr的结构inet_ntoa函数。

鉴于IDA最初并没有解决这个问题。

评论


很好的一点。

–肯·贝娄
2013年3月20日14:41

#5 楼

我尝试寻找一种情况,其中将指向数据块的指针传递到函数中,然后在该函数中使用它时,与其不同的偏移量被视为不同的数据类型。这向我表明1)它不是单一数据类型的数组,2)它是从基址中引用而不是作为单独的参数传递的。当您看到它被动态分配(malloc或new)然后被各种类型的数据填充时,它也有帮助。

在IDA中,我总是建议我的学生继续为他们怀疑是结构的事物创建IDA结构,并在他们看到被引用的元素时填充它们,即使他们后来转向出了错/误导。这是一个非常反复的过程,随着您对程序的了解以及在使用过程中如何使用这些数据,您将对其进行更详细的填写,因此,重要的是要对所发现的事物进行标记。 >

#6 楼

您可以使用多种方法(大多数方法已由先前的答案提示),但是出于完整性考虑,我将在此处列出一些方法。


查找对期望的库的调用结构。除非可执行文件确实执行了它实际上不应该做的事情,并且将其转换为指针,然后将其提供给库函数并希望它不会崩溃(不太可能),那么您可以在这里很好地了解哪些内存部分是结构。
看看函数如何传递/返回内存的一部分。一些编译器/调用约定/平台传递和返回小的结构(可以容纳几个寄存器)的方式与其他类型的数据不同。例如,如果您看到eaxedx都返回了数据,则可能是在处理一个小的结构。
看看如何寻址内存。如果看起来内存部分正在通过指针修改超过一个寄存器的大小,则很可能是(同样,除非它们做了异常的事情)数组或结构。另外,就像Yifan所说的那样,如果有很多通过常量偏移量进行的访问,则您可能正在查看结构或以类似于结构的方式使用的小数组(例如unsigned char rgb[3])。而且,如果您要查看的是不同大小的数据块,那么它几乎肯定是一个结构。

但是,在任何假设情况下,只要数据的使用与您的模型一致,在原始代码中,它只是一个字节数组,并带有关于行进的约定或完整的结构的关系,这并不重要。使用任何可以帮助您推理代码的模型。

要解决有关在不同代码分支中具有不同内存布局的结构的联合问题,我将介绍联合的两种最常见用法(以我的经验) :


输入punning:在这种情况下,您甚至可能在编译后的代码中看不到它。尽管它在技术上没有定义行为AFAIK,但大多数编译器会将其视为reinterpret_cast<>()
代数类型:通常,它附带一个易于识别的标记。如果您在多个位置看到代码,这些代码检查某个整数,然后根据该值将内存视为不同,那么您可以猜测这是怎么回事。如果某些函数不检查标记,那么这也可以告诉您有关该函数的信息-假设作者不是很懒,并且想破坏他们的代码,他们可能认为该函数只会在分支中被调用是不变的。已经知道它是某种类型。


评论


FWIW,eax:edx返回通常表示64位整数,而不是小结构。

–伊戈尔·斯科钦斯基♦
13年3月20日在13:57

#7 楼

正如其他人所说,我通常会在传递引用的情况下寻找函数调用-但我还会寻找返回malloc的缓冲区的实例(这是您知道/确认结构大小的方式)-以及各种成员'buffer'的设置/初始化。