- 发布于
二进制漏洞利用简介
- 作者
- Name
- CuB3y0nd
- GitHub
- @CuB3y0nd
二进制利用 是指发现程序中的漏洞并利用它们来执行你想要的操作。有时,这可能会导致绕过身份验证或机密信息泄露,但偶尔(如果幸运的话)它也可能导致远程代码执行 (RCE)。二进制利用的最基本形式发生在 栈 上,栈是存储代码中函数创建的临时变量的内存区域。
当调用一个新函数时,被调用函数的内存地址被压入栈——这样,程序就知道被调用函数执行完成后应该返回到哪里。让我们看一个基本的二进制文件来展示这一点。
introduction.zip
Binary
0x01 分析
该压缩包有两个文件 —— source.c
和 vuln
;后者是 ELF
文件,Linux 的可执行文件格式。
这里将使用 pwndbg
来分析调用函数时二进制文件的行为。
$ pwndbg vuln
我们可以反汇编 main
函数。
$ disass main
disassemble (disass)
表示反汇编。
0x080491ab <+0>: push ebp
0x080491ac <+1>: mov ebp,esp
0x080491ae <+3>: and esp,0xfffffff0
0x080491b1 <+6>: call 0x80491c3 <__x86.get_pc_thunk.ax>
0x080491b6 <+11>: add eax,0x2e4a
0x080491bb <+16>: call 0x8049172 <unsafe>
0x080491c0 <+21>: nop
0x080491c1 <+22>: leave
0x080491c2 <+23>: ret
对 unsafe
的调用位于 0x080491bb
,我们可以在那里设一个断点:
$ b *0x080491bb
b
表示 breakpoint,设置断点。断点的作用是在到达时暂停程序的执行以便运行其它命令。现在我们使用 r
,run 运行程序。这将在遇到我们设置的断点时暂停。
Note
c
代表 continue。作用是继续运行程序,直到遇到断点时暂停。
注意区分 c
和 r
指令的区别,r
指令本身是用来 运行/重启 整个程序的。但由于有断点存在,所以才会在遇到断点时暂停程序的执行。
它应该在调用 unsafe
之前暂停;现在我们来分析一下栈顶:
$ x/20wx $esp
0xffffd740: 0x00000000
[...]
Note
ESP 寄存器
是栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
x
指令可以查看内存,具体用法可以自行 Google。
前面的 0xffffd740
表示栈内的位置;0x00000000
是在该位置存储的值。让我们用 s
,step,步入指令,并再次检查栈顶。
$ x/20wx $esp
0xffffd740: 0x080491c0
[...]
可以发现值 0x080491c0
被压入栈顶,它应该出现在二进制文件中:
[...]
0x080491b6 <+11>: add eax,0x2e4a
0x080491bb <+16>: call 0x8049172 <unsafe>
0x080491c0 <+21>: nop
[...]
不难发现,这里其实是调用了 unsafe
之后的指令。这说明了程序是如何知道 unsafe()
执行完成后应该返回到哪里的。
0x02 漏洞
现在让我们看看如何破解这个程序。首先,我们反汇编 unsafe
并在 ret
指令处进行中断;ret
相当于 pop eip
,它将把我们刚才分析的栈上保存的返回地址 0x080491c0
压入 eip 寄存器
中。
Note
EIP 寄存器
用来存储 CPU 要读取的下一条指令的地址,CPU 通过 EIP 寄存器
读取即将要执行的指令。
每次 CPU 执行完相应的汇编指令后,EIP 寄存器
的值就会增加。
现在让我们继续将一堆字符发送到输入中,看看这会对它有什么影响。
$ b *0x080491aa
$ r
Overflow me
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
现在让我们读取返回地址之前所在位置的值:
$ x/20wx 0xffffd73c
0xffffd740: 0x41414141 0x41414141 0x41414141 0x41414141
[...]
我们发现栈上内容全部变成了 0x41414141
,覆盖了原先的返回地址 0x080491c0
。
原理很简单:由于我们输入的数据比程序预期的要多,这导致我们覆盖的栈也比程序预期的要多。因为保存的返回地址也在栈上,这意味着我们设法覆盖它。结果,ret
指令本来应该压入 eip
中的值被覆盖,不会在前面的函数中执行,而是执行了 0x41414141
。
我们可以用 regs
确认:
*EIP 0x41414141 ('AAAA')
下一条将要执行的指令变成了:0x41414141
。
我们也可以运行 regs eip
以确保 0x41414141
是 eip
中的值:
*EIP 0x41414141 ('AAAA')
我们已经成功劫持了程序的执行流程!让我们试试当我们用 c
继续运行时,它是否会崩溃。
$ c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
正如我们所料,程序果然崩溃了。这说明我们成功找到了这个程序的漏洞,并且利用漏洞破坏了程序的执行流程。
pwndbg
非常有用,它会打印出导致程序崩溃的地址。如果程序崩溃,它通常会显示「Segmentation fault」。这可能意味着多种情况,但通常是你已经覆盖了 EIP
。
Note
当然,你可以防止人们在使用程序时输入比预期更多的字符,通常使用其他 C 函数即可解决问题。例如 fgets()
;gets()
本质上是不安全的,因为它不检查输入的长度。你始终应该确保程序中没有使用诸如 gets()
这样危险的函数。你也可能给 fgets()
提供错误的参数,导致它仍然接受太多字符。
0x03 总结
当一个函数调用另一个函数时:
- 将返回指针压入栈,以便被调用的函数知道返回到哪里
- 当被调用函数执行完成时,它再次将其从栈中弹出
因为这个值保存在栈上,就像我们的局部变量一样,如果我们写入的字符比程序预期的多,我们可以覆盖该值并将代码执行重定向到我们希望的任何地方。fgets()
等函数可以防止这种简单的溢出,但你始终应该检查程序实际读取了多少内容。