摘要工具

nm

将源文件编译成目标文件时,编译器必须嵌入一些全局(外部)符号的位置信息,以便链接器在组合目标文件以创建可执行文件时,能够解析对这些符号的引用。除非被告知要去除最终的可执行文件中的符号,否则,链接器通常会将目标文件中的符号带入最终的可执行文件中。根据nm手册的描述,这一实用工具的目的是“列举目标文件中的符号”。使用rm检查中间目标文件(扩展名为.0的文件,而非可执行文件)时,默认输出结果是在这个文件中声明的任何函数和全局变量的名称。nm实用工具的样本输出如下所示。
20240328143925.png
从中可以看到,m列出了每一个符号以及与符号有关的一些信息。其中的字母表示所列举的
符号的类型。前面的例子中出现了以下字母,下面逐一解释。
U,未定义符号,通常为外部符号引用。
T,在文本部分定义的符号,通常为函数名称。
t,在文本部分定义的局部符号。在C程序中,这个符号通常等同于一个静态函数。
D,已初始化的数据值。
C,未初始化的数据值。

大写字母表示全局符号,小写字母则表示局部符号。请参阅nm手册了解有关字母代码的详细解释。

如果使用nm列举可执行文件中的符号,将会有更多信息显示出来。在链接过程中,符号被解析成虚拟地址(如有可能)。因此,这时运行nm,将可获得更多信息。
20240328143556.png

ldd

创建可执行文件时,必须解析该文件引用的任何库函数的地址。链接器通过两种方法解析对库函数的调用:静态链接(static linking)和动态链接(dynamic linking)。链接器的命令行参数决定具体使用哪一种方法。一个可执行文件可能为静态链接、动态链接,或二者兼而有之。
20240328144501.png
此时查看两种方式编译的可执行文件大小:
20240328144929.png
使用ldd查看:
20240328145043.png

objdump

objdump可以显示大量目标文件有关的信息,超过30个命令行选项分别查看。

  • 节头部,程序文件每节的摘要信息。
  • 专用头部,程序内存分布信息,还有运行时加载器所需的其他信息,包括由ldd等工具生成的库列表。
  • 调试信息,提取出程序文件中的任何调试信息。
  • 符号信息,以类似nm的方式转储符号表信息。
  • 反汇编代码清单,objdump对文件中标记为代码的部分执行线性扫描反汇编。反汇编x86代码时,objdump可以生成AT&T或Intel语法,并可以将反汇编代码保存在文本文件中。这样的文本文件叫做反汇编死代码清单(dead listing),尽管这些文件可用于实施逆向工程,但它们很难有效导航,也无法以一致且无错的方式被修改。20240328145445.png

readelf

和objdump类似,但不依赖libbfd
20240328145617.png

深度检测工具

到目前为止,我们已经讨论了一些工具,利用这些工具,可以在对文件的内部结构知之甚少的情况下对文件进行粗略分析,也可以在深入了解文件的结构之后,从文件中提取出特定的信息。在这一节中,我们将介绍一些专用于从任何格式的文件中提取出特定信息的工具。

strings

有时候,提出一些与文件内容有关的常规性问题,即那些不需要了解文件结构即可回答的问题,对我们会有一定帮助。例如:“这个文件包含字符串吗?”当然,在回答这个问题之前,必须先回答这个问题:“到底什么是字符串?”我们将字符串简单定义为由可打印字符组成的连续字符序列。通常,在这一定义的基础上,还需要指定一个最小长度和一个特定的字符集。因此,可以搜索至少包含4个连续可打印ASCII字符的字符串,并将结果在控制台打印出来。通常,搜索这类字符串不会受到文件结构的限制。在ELF二进制文件中搜索字符串就像在微软Word文档中搜索字符串一样简单。
20240328145810.png

  • 需要牢记的是,使用strings处理可执行文件时,默认情况下,strings仅仅扫描文件中可加载的、经初始化的部分。使用命令行参数-a可迫使strings扫描整个文件。
  • strings不会指出字符串在文件中的位置。使用命令行参数-t可令strings显示所发现的每一个字符串的文件偏移量信息。
  • 许多文件使用了其他字符集。使用命令行参数-e可使strings搜索更广泛的字符,如16位Unicode字符。

反汇编器

如前所述,有很多工具都可以生成二进制目标文件的死列表形式的反汇编代码。PE、ELF和MACH-0文件可分别使用dupbin、objdump和otoo1进行反汇编。但是,它们中的任何一个都无法处理任意格式的二进制文件。有时候,你会遇到一些并不采用常用文件格式的二进制文件,在这种情况下,就需要一些能够从用户指定的偏移量开始反汇编过程的工具。

ndisasm

ndisasm是一款用于x86指令集的流式反汇编器,ndisasm属于NASM工具集。

安装方法:sudo apt-get install nasm

我们通过VIPER生成一段shellcode,然后进行反编译:
20240328155349.png
通过ndisasm进行反编译:
20240328160107.png
由于流式反汇编非常灵活,因此它的用途相当广泛。例如,在分析网络数据包中可能包含shellcode的计算机网络攻击时,就可以采用流式反汇编器来反汇编数据包中包含shellcode的部分以分析恶意负载的行为。另外一种情况是分析那些格式未知的ROM镜像。ROM中有些部分是数据,其他部分则为代码,可以使用流式反汇编器来反汇编镜像中的代码。

IDA

主要数据显示窗口

Names窗口

20240329101313.png ### 反汇编窗口(IDA-View) 反汇编窗口有两种显示格式: 面向文本的列表视图和自IDA5.0开始的引入的基于图形的视图。多数IDA用户会有所偏好,具体使用哪一种视图,要由用户的喜好决定。首次启动IDA时,用户会发现,默认情况下,反汇编窗口以图形视图显示。要更改这个默认设置,可以取消选中Options》General对话框的Graph选项卡下的Use graph viewbydefault(默认使用图形视图)选项。 ## 次要IDA显示窗口 ### 导出/导入窗口 导出窗口列出文件的入口点。这包括程序的执行入口点(在程序的文件头部分指定),以及任何由文件导出给其他文件使用的函数和变量。通常,用户可在共享库(如WindowsDLL文件)中找到导出的函数。导出的项目按名称、虚拟地址和序数(如果可用)排列。对于可执行文件,导出窗口中至少包含一个项目:程序的执行入口点,IDA将这个入口点取名为start。 20240329100121.png 导入窗口列出二进制文件导入的所有函数,只有在二进制文件使用共享库时,IDA才需要用到导入窗口。静态链接的二进制文件不存在外部依赖关系,因此不需要导入其他内容。导入窗口中的每个条目列出一个导入项目(函数或数据)的名称,以及包含该项目的库的名称。由于被导入的函数的代码位于共享库中,窗口中每个条目列出的地址为相关导入表条目"的虚拟地址。 20240329100136.png ### 结构体窗口 结构体窗口用于显示IDA决定在一个二进制文件中使用的任何复杂的数据结构(如C结构体和联合)的布局。在分析阶段,IDA会查询它的函数类型签名扩展库,设法将函数的参数类型与程序使用的内存匹配起来。 20240329100924.png ### 枚举窗口 枚举窗口有点类似于结构体窗口。如果IDA检测到标准枚举数据类型(Cenum),它将在举窗口中列出该数据类型。你可以使用枚举来代替整数常量,提高反汇编代码的可读性。像结构体窗口一样,在枚举窗口中也可以定义自己的枚举类型,并将其用在经过反汇编的二进制代码中。 ## 其他IDA显示窗口 ### 段窗口 段窗口显示的是在二进制文件中出现的段的简要列表。需要注意的在,在讨论二进制文件的结构时,IDA术语“段”(segment)常称为“节”(section)。请不要将这里的术语“段”与实施分段内存体系结构的CPU中的内存段混淆。该窗口中显示的信息包括段名称、起始和结束地址以及许可标志。起始和结束地址代表程序段在运行时对应的虚拟地址范围。 20240329101537.png ### 签名窗口Signatures IDA利用一个庞大的签名库来识别已知的代码块。签名用于识别由编译器生成的常用启动顺 序,以确定可能已被用来构建给定二进制文件的编译器。签名还可用于将函数划归为由编译器插 入的已知库函数,或者因为静态链接而添加到二进制文件中的函数。 # 反汇编导航 ## 基本IDA导航 ### 双击导航 反汇编一个程序时,程序的每个位置都分配到了一个虚拟地址。因此,只要提供希望访问的位置的虚拟地址,就可以导航到程序的任何地方。遗憾的是,对我们而言,记住大量地址并非易事。这促使早期程序员给他们希望引用的程序位置分配符号名称,这大大简化了他们的工作。给程序地址分配符号名称,与给程序操作码分配助记指令名称并无不同。由于程序更易于记忆,读取和写入程序也更加方便。 ### 跳转到地址 使用Jump》Jump to Address命令或在反汇编窗口中按下热键G,均可以打开Jump to Address对话框。如果把这个对话框看成Go对话框,可能有助于你记住相关的热键。要想导航到二进制文件中的某个位置,只需指定一个地址(名称或十六进制值),然后单击OK,IDA会立即显示你指定的位置。IDA会记住你在这个对话框中输入的值,并通过一个下拉列表显示,以方便你随后使用。使用这项历史记录功能,你可以迅速返回你之前访问过的位置。 20240329103100.png ## 栈帧 **栈帧**(**stackfame**)一种低级概念。栈帧是在程序的运行时栈中分配的内存块,专门用于特定的函数调用。程序员通常会将可执行语句分组,划分成叫做函数(也称过程、子例程或方法)的单元。有时候,这样做是遵照所使用的语言的要求。多数情况下,以这些函数单元为基础构建程序是一种良好的编程实践。 如果一个函数并未执行,通常它并不需要内存。但是,当函数被调用时,它就可能因为某种原因需要用到内存。这源于几方面的原因。其一,函数的调用方可能希望以参数的方式向该函数传递信息,这些参数需要存储在某个地方,以方便函数查找它们。其二,在执行任务的过程中,函数可能需要临时的存储空间。程序员通常会通过声明局部变量来分配这类临时空间,这些变量将在函数执行过程中使用,但一旦完成函数调用,就无法再访问它们。 ### 局部变量布局 #### 栈帧示例 以下面32位编译的函数为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void bar(int j, int k){
};

void demo_stackframe(int a, int b, int c){
int x = a;
char buffer[64];
int y = b;
int z = c;

bar(z,y);
}

void main(){
demo_stackframe(1,2,3);
}
我们可以计算到,局部变量最少需要76个字节的栈空间(4\*3+64=76) 这个函数可能使用stdcall或者cdecl调用约定,我们分为两种情况来分析 未使用帧指针寄存器/基址指针寄存器EBP,因此使用ESP作为帧指针,通过以下命令编译:
1
gcc -m32 -O0 -fno-stack-protector -fno-pie -no-pie -fcf-protection=none -fomit-frame-pointer stack_frame.c -o b.out    #-fomit-frame-pointer指定不使用EBP
使用帧指针寄存器/基址指针寄存器EBP:
1
gcc -m32 -O0 -fno-stack-protector -fno-pie -no-pie -fcf-protection=none stack_frame.c -o a.out
20240329173300.png 分别反编译分析一下 ##### 未使用EBP 20240329173435.png 可以看到进入demo函数后栈顶指针向低地址申请了80个字节的空间。 > Q:为什么是80个字节而不是76个字节? > A:在大多数现代操作系统和编译器中,局部变量通常存储在栈(stack)上。关于栈空间的分配和对齐,可能受到一些特定规则的影响。 > **有些硬件架构可能要求数据对齐到特定的边界以提高访问效率。例如,某些处理器可能要求4字节对齐或8字节对齐的数据访问。在这种情况下,编译器可能会自动调整局部变量的对齐,以确保它们满足硬件的要求。** > 此外,编译器优化也可能影响栈空间的分配和对齐。编译器可能会进行内联优化、寄存器分配等优化操作,这些操作可能会影响局部变量在栈上的布局和大小。

此时分析一下栈的情况:
20240329175450.png
我们可以清楚的看出此时的栈帧

变量 偏移量
z [esp]
y [esp+4]
buffer [esp+8]
x [esp+72]
padding(可能存在) [esp+76]
saved eip [esp+80] —–
a [esp+84]
b [esp+88]
c [esp+92]
最后收尾部分返回调用函数的时候需要从从栈顶弹出所需返回地址,也就是保存的eip指令:
20240329181320.png
计算一下正好是88个字节,回收了这些地址:
20240329181155.png
使用EBP

我们同样跟进到demo_stackframe函数:
20240329181641.png
同样是申请了80字节的空间,不过前面多了两步操作栈底指针和栈顶指针的步骤。
此时我们来分析一下栈帧的情况:
20240329181906.png
可以看到大致上是和之前差不多的,只不过这里多了4个字节的保存的ebp的地址
这里使用一个专用的帧指针,所有变量相对于帧指针寄存器的偏移量得以计算出来。许多时候(尽管并无要求),正偏移量用于访问函数参数,而负偏移量则用于访问局部变量。 使用专用的指针,我们可以自由更改栈指针,而不至影响帧内其他变量的偏移量。
调用bar函数后来到收尾部分,在使用EBP的程序中返回原函数地址的步骤有所不同,一般的步骤有以下两条指令:

1
2
3
mov    ebp, esp
pop ebp
ret

因为非常常用,所以x86体系提供了一个新的指令leave来直接完成这个操作
20240329184027.png

IDA栈视图

很明显,栈帧是一个运行时概念,没有栈和运行中的程序,栈帧就不可能存在。话虽如此,但这并不意味着你在使用IDA之类的工具进行静态分析时,就可以忽略栈帧的概念。二进制文件中包含配置每个函数的栈帧所需的全部代码。通过仔细分析这段代码,我们可以深入了解任何承数的栈帧的结构,即使这个函数并未运行。
我们以gcc编译的以下函数为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void bar(int x, int y){}

void demo_stackframe(int a, int b, int c){
int x = c;
char buffer[64];
int y = b;
int z = 10;
buffer[0] = 'A';
bar(z, y);
}

void main(){
demo_stackframe(1, 2, 3);
}

我们可以提前预估一下需要的栈空间(4+64+4+4=76)