- 献词
- 致谢
- 前言
- 第一部分 IDA 简介
- 第 1 章 反汇编简介
- 第 2 章 逆向与反汇编工具
- 第 3 章 IDA Pro 背景知识
- 第二部分 IDA 基本用法
- 第 4 章 IDA 入门
- 第 5 章 IDA 数据显示窗口
- 第 6 章 反汇编导航
- 第 7 章 反汇编操作
- 第 8 章 数据类型与数据结构
- 第 9 章 交叉引用与绘图功能
- 第 10 章 IDA 的多种面孔
- 第三部分 IDA 高级应用
- 第 11 章 定制 IDA
- 第 12 章 使用 FLIRT 签名来识别库
- 第 13 章 扩展 IDA 的知识
- 第 14 章 修补二进制文件及其他 IDA 限制
- 第四部分 扩展 IDA 的功能
- 第 15 章 编写 IDA 脚本
- 第 16 章 IDA 软件开发工具包
- 第 17 章 IDA 插件体系结构
- 第 18 章 二进制文件与 IDA 加载器模块
- 第 19 章 IDA 处理器模块
- 第五部分 实际应用
- 第 20 章 编译器变体
- 第 21 章 模糊代码分析
- 第 22 章 漏洞分析
- 第 23 章 实用 IDA 插件
- 第六部分 IDA 调试器
- 第 24 章 IDA 调试器
- 第 25 章 反汇编器/ 调试器集成
- 第 26 章 其他调试功能
- 附录 A 使用 IDA 免费版本 5.0
- 附录 B IDC/SDK 交叉引用
22.3 IDA 与破解程序开发过程
假设你确定了一个可被利用的漏洞的位置,那么,IDA 如何为你开发破解程序提供帮助呢?要回答这个问题,你需要了解你需要什么类型的帮助,以便利用 IDA 的功能。
在下面几个方面,IDA 的功能非常强大。在开发破解程序时,这些功能可以为你节省大量反复试验的时间。
在确定控制流路径方面,IDA 图形非常有用,可以帮助你了解如何到达一个易受攻击的函数。对于大型二进制文件,你可能需要仔细选择生成图形的参数,以最大限度地减少所生成图形的复杂程度。请参阅第 9 章了解有关 IDA 图形的更多信息。
IDA 对栈帧进行非常详细的分解。如果你正覆写栈中的信息,IDA 将帮助你了解覆写了什么内容,缓冲区的哪些部分覆写了这些内容。IDA 栈显示在确定格式化字符串的内存布局时,也易受到攻击。
IDA 提供优良的搜索工具。如果你需要在一个二进制文件中搜索某个特定的指令(如
jmp esp
)或指令序列(如pop/pop/ret
),IDA 能够迅速告诉你该指令/指令序列是否存在于二进制文件中,如果存在,则指出该指令/指令序列所在位置的虚拟地址。IDA 映射二进制文件就好像它们被加载到内存中,根据这一点,你可以更加轻松地确定成功加载破解程序所需的虚拟地址。当你拥有“写 4”1 (write four)能力时,利用 IDA 的反汇编代码清单可以轻易确定任何全局分配的缓冲区的虚拟地址以及有用的目标地址(如
GOT
条目)。
1. “写 4”能力使攻击者有机会在他选择的内存位置写入他选定的 4 个字节。
在下面几节中,我们将讨论其中一些功能,以及如何利用这些功能。
22.3.1 栈帧细目
虽然栈保护机制正迅速成为现代操作系统的标准功能,但许多计算机的操作系统仍然允许在栈中运行代码,基于栈的普通缓冲区溢出攻击就是一个例子。即使操作系统设置了栈保护,攻击者仍然可以利用缓冲区溢出破坏基于栈的指针变量,进而完成一次攻击。
在发现一个基于栈的缓冲区溢出后,无论你计划做什么,一定要了解:当你的数据从易受攻击的栈缓冲区溢出时,哪些栈内容将被覆写。你可能还需要了解:你到底需要在缓冲区中写入多少个字节,才能控制其中保存的各种变量,包括函数返回地址。只要你做一些算术运算,IDA 的默认栈帧显示窗口将为你回答所有这些问题。用一个变量的偏移量减去另一个变量的偏移量,即可计算出栈中任何两个变量之间的距离。下面的栈帧包含一个缓冲区,如果仔细控制相应函数的输入,可以使这个缓冲区溢出:
-0000009C result dd ? -00000098 buffer_132 db 132 dup(?) ; this can be overflowed -00000014 p_buf dd ? ; pointer into buffer_132 -00000010 num_bytes dd ? ; bytes read per loop -0000000C total_read dd ? ; total bytes read -00000008 db ? ; undefined -00000007 db ? ; undefined -00000006 db ? ; undefined -00000005 db ? ; undefined -00000004 db ? ; undefined -00000003 db ? ; undefined -00000002 db ? ; undefined -00000001 db ? ; undefined +00000000 s db 4 dup(?) +00000004 r db 4 dup(?) ; save return address +00000008 filedes dd ? ; socket descriptor
易受攻击的缓冲区( buffer_132
)的开始部分到所保存的返回地址之间的距离为 156 个字节( 4- -98h
或 4- -152
)。我们还可以看到,在 132 个字节( -14h - -98h
)后, p_buf
的内容将开始被覆写,这可能会造成问题。在触发破解程序之前,为了防止目标应用程序崩溃,你必须清楚地知道,覆写缓冲区之后的变量,将会造成什么样的后果。在这个例子中, filedes
(一个套接字描述符)可能是另一个存在问题的变量。如果在我们溢出缓冲区之后,易受攻击的函数需要使用该套接字描述符,那么,这时我们需要小心应付,确保覆写 filedes
不会使该函数因为出现无法预料的错误而中断。处理将要被覆写的变量的一种策略,是在这些变量中写入对程序有意义的值,从而使程序能够继续正常运行,直到破解程序被触发。
为了获得一个更具可读性的栈帧细目,我们可以修改代码清单 22-3 中的栈帧扫描代码,以枚举一个栈帧的所有成员,计算它们的表面大小,并显示每个成员与所保存的返回地址之间的距离。最终的脚本如代码清单 22-4 所示。
代码清单 22-4 使用 Python 枚举一个栈帧
func = ScreenEA() #process function at cursor location frame = GetFrame(func) if frame != -1: Message("Enumerating stack for %s\n" % GetFunctionName(func)) ➊ eip_loc = GetFrameLvarSize(func) + GetFrameRegsSize(func) prev_idx = -1 idx = 0 while idx GetStrucSize(frame): member = GetMemberName(frame, idx) if member is not None: if prev_idx != -1: #compute distance from previous field to current field delta = idx - prev_idx Message("%15s: %4d bytes (%4d bytes to eip)\n" % \ (prev, delta, eip_loc - prev_idx)) prev_idx = idx prev = member idx = idx + GetMemberSize(frame, idx) else: idx = idx + 1 if prev_idx != -1: #make sure we print the last field in the frame delta = GetStrucSize(frame) - prev_idx Message("%15s: %4d bytes (%4d bytes to eip)\n" % \ (prev, delta, eip_loc - prev_idx))
这个脚本引入了 GetFrameLvarSize
和 GetFrameRegsSize
函数(也可用在 IDC 中),分别用于获取一个栈帧的局部变量和所保存的寄存器区域的大小。保存的返回地址正好在这两个区域的下面,保存的返回地址的偏移量为这两个值的总和(➊)。如果对示例函数执行这个脚本,将生成以下输出:
Enumerating stack for handleSocket result: 4 bytes ( 160 bytes to eip) buffer_132: 132 bytes ( 156 bytes to eip) p_buf: 4 bytes ( 24 bytes to eip) num_bytes: 4 bytes ( 20 bytes to eip) total_read: 12 bytes ( 16 bytes to eip) s: 4 bytes ( 4 bytes to eip) r: 4 bytes ( 0 bytes to eip) fildes: 4 bytes ( -4 bytes to eip)
这些输出对函数的栈帧进行了简要的总结,其中的注释提供了其他可能对破解程序开发者有用的信息。
事实证明,在开发针对格式化字符串漏洞的入侵程序时,IDA 的栈帧显示也非常有用。下面的代码片段提供了一个示例,这段代码使用用户提供的缓冲区(作为格式化字符串提供)调用 fprintf
函数。
.text:080488CA lea eax, [ebp+format] ➋ .text:080488D0 mov [esp+4], eax ; format .text:080488D4 mov eax, [ebp+stream] ➊ .text:080488DA mov [esp], eax ; stream .text:080488DD call _fprintf
这个示例仅向 fprintf
函数传递两个参数:一个文件指针(➊)和作为格式化字符串的用户缓冲区的地址(➋)。这些参数占用栈顶部的两个位置,以及在函数的“序言”阶段已由实施调用的函数分配的内存。这个易受攻击的函数的栈帧如代码清单 22-5 所示。
代码清单 22-5 格式化字符串示例的栈帧
➊ -00000128 db ? ; undefined -00000127 db ? ; undefined -00000126 db ? ; undefined -00000125 db ? ; undefined ➋ -00000124 db ? ; undefined -00000123 db ? ; undefined -00000122 db ? ; undefined -00000121 db ? ; undefined -00000120 db ? ; undefined -0000011F db ? ; undefined -0000011E db ? ; undefined -0000011D db ? ; undefined -0000011C db ? ; undefined -0000011B db ? ; undefined -0000011A db ? ; undefined -00000119 db ? ; undefined -00000118 s1 dd ? ; offset -00000114 stream dd ? ; offset -00000110 format db 264 dup(?)
帧偏移量 128h
到 119h
中的 16 个未定义的字节表示编译器(此例中为 gcc)为传递给将由该易受攻击的函数调用的函数的参数预分配的内存块。 fprintf
的 stream
参数将位于栈的顶部(➊),格式化字符串指针则紧跟在 stream
参数的后面(➋)。
在格式化字符串入侵程序中,攻击者通常对从格式化字符串指针到保存攻击者的输入的缓冲区开头位置之间的距离感兴趣。在上一个栈帧中,有 16 个字节将格式化字符串参数与具体的格式化字符串缓冲区分隔开来。为进行深入讨论,我们假设攻击者已输入以下格式化字符串。
"%x %x %x %x %x"
这时, fprintf
期待在格式化字符串参数后紧跟 5 个参数。这些参数中的前四个参数将占用格式化字符串参数与格式化字符串缓冲区之间的空间,第五个参数(也就是最后一个参数)将覆盖格式化缓冲区的前四个字节。熟悉格式化字符串入侵程序2 的读者知道,格式化字符串中的参数可以按照索引号显式命名。下面的格式化字符串说明了如何访问上述格式化字符串之后的第五个参数,以将其格式化为十六进制值。
2. 希望了解格式化字符串入侵程序的详细信息的读者,可以再次参阅 Jon Erickson 的Hacking:The Art of Exploitation Second Edition 版。
"%5$x"
再回到上一个示例,这个格式化字符串会将格式化字符串缓冲区的前四个字节[ 前面我们提到,这些字节将占用传递给格式化字符串(如果需要一个格式化字符串)的第五个参数的空间]读取为一个整数,将该整数格式化为十六进制值,然后将结果输出到指定的文件流。传递给该格式化字符串的其他参数(第六个、第七个等)将覆盖格式化字符串缓冲区中的后续四字节代码块。
创建一个能够正常运行的格式化字符串,以对易受攻击的二进制文件加以利用,可能非常麻烦,并且这通常依赖于是否准确指定格式化字符串中的参数。前面的讨论说明,在许多时候,IDA 可用于快速准确地计算格式化字符串缓冲区中所需的偏移量。将这些信息与 IDA 在反汇编各种程序节(如全局偏移量表.got 或解构器表.dtor )时显示的信息相结合,可以快速获得仅使用调试器开发入侵程序时所需的格式化字符串,而且不需要进行试用,其中也不包含任何错误。
22.3.2 定位指令序列
为了可靠地加载破解程序,你通常可以使用一种特殊的控制权转交机制,这种机制不需要你了解你的 shellcode 的具体内存地址。如果你的 shellcode 位于堆或栈中,其地址无法预测,则更需要采用这种机制。在这种情况下,如果找到一个在你的破解程序被触发时指向 shellcode 的寄存器,则更加理想。例如,如果你在接管指令指针时,已知 ESI 寄存器指向你的 shellcode ,那么如果该指令指针恰巧指向一条 jmp esi
或 call esi
指令,这将会为你提供极大的帮助。因为这些指令不需要你了解你的 shellcode 的确切地址,就可以执行 shellcode 。同样,使用 mp esp
指令也可以非常方便地将控制权转交给你插入栈中的 shellcode 。因为如果一个函数包含易受攻击的缓冲区,当该函数返回时,栈指针将指向你刚刚覆写的被保存的返回地址下面。如果你继续覆写被保存的返回地址上面的栈,那么,栈指针将指向你的数据(应该是代码)。将指向 shellcode 的寄存器与通过跳转到或调用该寄存器指向的位置来重定向执行的指令序列相结合的过程称为“trampoline”。
搜索这类指令序列并不是一个新的概念。在论文“Variations in Exploit Methods between Linux and Windows ”3 的附录 D 中,David Litchfield 介绍了一个名为 getopcode.c 的程序,这个程序用于在 Linux ELF 二进制文件中搜索有用的指令。Metasploit4 项目提供了 msfpescan
工具,该工具可用于在 Windows PE 二进制文件中扫描有用的指令序列。和这些工具一样,IDA 也能够定位有用的指令序列。
3. 参见 http://www.nccgroup.com/Libraries/Document_Downloads/Variations_in_Exploit_methods_between_Linux_and_Windows.sflb.ashx 。
4. 参见 http://www.metasploit.com/ 。
比方说,假设你希望确定一条 jmp esp
指令在某个 x86 二进制文件中的位置。这时,你可以使用 IDA 的文本搜索功能来寻找 jmp esp
这个字符串。如果你知道 jmp
与 esp
之间空格的确切数量,你将能够找到这个字符串。但是,任何时候你都不可能找到该字符串,因为编译器很少使用跳转到栈的命令。那么,为什么还要在第一个位置进行搜索呢?因为你真正感兴趣的并不是反汇编后的文本 jmp esp
,而是字节序列 FF E4
,不论它位于何处。例如,下面的指令包含一个内嵌的 jmp esp
:
.text:080486CD B8 FF FF E4 34 mov eax, 34E4FFFFh
如果想要一个 jmp esp
,可以使用虚拟地址 080486CFh
。IDA 的二进制搜索(Search▶Sequence of Bytes )功能可以迅速定位这样的字节序列。如果对一个已知的字节序列执行完全匹配的二进制搜索,请记得区分大小写,否则,字节序列 50 C3
( push eax/ret
)将与字节序列 70 C3
相匹配(因为 50h 代表大写的 P ,而 70h 代表小写的 p ), 70 C3
是一个对使用-61 字节的相对偏移量溢出的跳转。在 IDC 中,使用 FindBinary
函数,可以通过编程执行二进制搜索,如下所示:
ea = FindBinary(MinEA(), SEARCH_DOWN | SEARCH_CASE, "FF E4");
这个函数以区分大小写的方式,从数据库中最低的虚拟地址向下(朝较高的地址)搜索 jmp esp(FF E4)
。如果发现 jmp esp
,则返回该字节序列起始位置的虚拟地址。如果没有找到该字节序列,则返回 BADADDR(-1) 。读者可以在本书的网站上下载一个脚本,它能够自动搜索大量指令。使用这个脚本,我们可以搜索将控制权转交给 EDX 寄存器所指位置的指令,并收到类似于下面的结果:
Searching... Found jmp edx (FF E2) at 0x80816e6 Found call edx (FF D2) at 0x8048138 Found 2 occurrences
在数据库中搜索指令时,这样的便捷脚本不但可以为我们节省大量时间,还可以确保我们不会忘记考虑到所有可能的情况。
22.3.3 查找有用的虚拟地址
我们下面将要简要提到的最后一项是 IDA 在它的反汇编代码清单中显示的虚拟地址。知道我们的 shellcode 最终将进入一个静态缓冲区(例如,在 .data
或 .bss
节中),总比知道 shellcode 将加载到堆或栈中要强,因为我们最后可以得到一个已知的固定地址,并可以将控制权转交给这个地址。因此,我们不必使用“NOP 滑道”(NOP slide)或查找特殊的指令序列。
NOP 滑道
“NOP 滑道”是一个长长的连续无操作(什么也不做)指令序列,当知道我们的 shellcode 的地址可变时,它为我们提供了更广泛的目标,用以触发我们的 shellcode 。现在,我们的目标不是 shellcode 的第一条有用指令,而是“NOP 滑道”的中间位置。如果“NOP 滑道”(以及有效负载的剩余部分)在内存中的位置稍微向上或向下移动,我们仍然有很大机会进入“滑道”的某个位置,并成功触发 shellcode 。例如,如果我们有用于 500 个 NOP 作为我们 shellcode 的“前缀”的空间,那么,只要我们猜测的滑道中间的地址与真实地址之间的差距在 250 个字节以内,我们仍然能够以“滑道”的中间位置为目标,并触发我们的 shellcode 。
攻击者可以在他们选择的任何位置写入任何数据,一些破解程序则利用了这一点。许多时候,这种写入仅限于 4 字节覆写,但通常这个空间已经足够了。如果可以进行 4 字节覆写,我们可以用我们的 shellcode 的地址覆写一个函数指针。许多 ELF 二进制文件使用的动态链接过程利用一个叫做 全局偏移量表 (global offset table)的函数指针表存储动态链接的二进制函数的地址。如果攻击者能够覆写这个表中的一个条目,那么,他们就可以“劫持”一个函数调用,并将该调用重定向到他们选择的位置。通常,在这类情况下,攻击者会首先将 shellcode 放置在一个已知的位置,然后覆写将要被破解程序调用的下一个库函数的 GOT 条目。当这个库函数被调用时,控制权将被转交给攻击者的 shellcode 。
在 IDA 中,通过滚动到 got
区块,并搜索我们希望覆写其条目的函数,即可轻易确定 GOT 条目的地址。以尽可能自动化的方式,下面的 Python 脚本能够迅速报告将要被给定的函数调用使用的 GOT 条目的地址:
ea = ScreenEA() dref = ea for xref in XrefsFrom(ea, 0): ➊ if xref.type == fl_CN and SegName(xref.to) == ".plt": ➋ for dref in DataRefsFrom(xref.to): Message("GOT entry for %s is at 0x%08x\n" % (GetFunctionName(xref.to), dref)) break if ea == dref: Message("Sorry this does not appear to be a library function call\n")
将光标放置在任何库函数调用上,如下所示,这个脚本将会运行:
.text:080513A8 call _memset
这个脚本首先遍历交叉引用,直到到达 GOT。测试获取的第一个交叉引用(➊),以确保它是一个调用交叉引用,并且引用的是 ELF 过程链接表( .plt
)。PLT 条目中的代码用于读取一个 GOT 条目,并将控制权转交给该 GOT 条目指定的地址。脚本获取的第二个交叉引用(➋)获得从 PLT 中读取的位置的地址,这也是相关 GOT 条目的地址。如果对前面的_memset 函数调用执行这个脚本,将生成以下输出:
GOT entry for .memset is at 0x080618d8
在我们通过“劫持”一个 memset
函数调用来控制相关程序时,这个输出为我们提供了所需的信息,即我们需要用我们的 shellcode 的地址覆写地址 0x080618d8
处的内容。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论