发布于

二进制漏洞利用简介

作者

二进制利用 是指发现程序中的漏洞并利用它们来执行你想要的操作。有时,这可能会导致绕过身份验证或机密信息泄露,但偶尔(如果幸运的话)它也可能导致远程代码执行 (RCE)。二进制利用的最基本形式发生在 上,栈是存储代码中函数创建的临时变量的内存区域。

当调用一个新函数时,被调用函数的内存地址被压入栈——这样,程序就知道被调用函数执行完成后应该返回到哪里。让我们看一个基本的二进制文件来展示这一点。

introduction.zip

Binary

0x01 分析

该压缩包有两个文件 —— source.cvuln;后者是 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,设置断点。断点的作用是在到达时暂停程序的执行以便运行其它命令。现在我们使用 rrun 运行程序。这将在遇到我们设置的断点时暂停。

Note

c 代表 continue。作用是继续运行程序,直到遇到断点时暂停。

注意区分 cr 指令的区别,r 指令本身是用来 运行/重启 整个程序的。但由于有断点存在,所以才会在遇到断点时暂停程序的执行。

它应该在调用 unsafe 之前暂停;现在我们来分析一下栈顶:

$ x/20wx $esp
0xffffd740:	0x00000000
[...]

Note

ESP 寄存器 是栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。

x 指令可以查看内存,具体用法可以自行 Google。

前面的 0xffffd740 表示栈内的位置;0x00000000 是在该位置存储的值。让我们用 sstep,步入指令,并再次检查栈顶。

$ 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 以确保 0x41414141eip 中的值:

*EIP  0x41414141 ('AAAA')

我们已经成功劫持了程序的执行流程!让我们试试当我们用 c 继续运行时,它是否会崩溃。

$ c
Continuing.

Program received signal SIGSEGV, Segmentation fault.

正如我们所料,程序果然崩溃了。这说明我们成功找到了这个程序的漏洞,并且利用漏洞破坏了程序的执行流程。

pwndbg 非常有用,它会打印出导致程序崩溃的地址。如果程序崩溃,它通常会显示「Segmentation fault」。这可能意味着多种情况,但通常是你已经覆盖了 EIP

Note

当然,你可以防止人们在使用程序时输入比预期更多的字符,通常使用其他 C 函数即可解决问题。例如 fgets()gets() 本质上是不安全的,因为它不检查输入的长度。你始终应该确保程序中没有使用诸如 gets() 这样危险的函数。你也可能给 fgets() 提供错误的参数,导致它仍然接受太多字符。

0x03 总结

当一个函数调用另一个函数时:

  • 将返回指针压入栈,以便被调用的函数知道返回到哪里
  • 当被调用函数执行完成时,它再次将其从栈中弹出

因为这个值保存在栈上,就像我们的局部变量一样,如果我们写入的字符比程序预期的多,我们可以覆盖该值并将代码执行重定向到我们希望的任何地方。fgets() 等函数可以防止这种简单的溢出,但你始终应该检查程序实际读取了多少内容。