Site Overlay

缓冲区溢出实验详解(上,热身练习)

在一切的一切开始之前,我们必须了解的是程序的内存布局、栈帧的结构。希望你仔细阅读那篇笔记

首先登录到服务器:

$ ssh xxx@hostname
...
$ ls
act123.tar  a.out  bomb346  bomb346.tar  hello.c  target346.tar  test.txt

热身部分

$  tar -xvf act123.tar
act123/
act123/act1.c
act123/support.s
act123/Makefile
act123/act3.c
act123/act2.c
$ cd act123/
$ ls
act1.c  act2.c  act3.c  Makefile  support.s

编译:

$ make
gcc --std=c99 -g -O3 -static act1.c support.s -o act1
gcc --std=c99 -g -O3 -static act2.c support.s -o act2
gcc --std=c99 -g -O3 -static act3.c support.s -o act3

act1.c

首先看看它是干嘛的。查看 act1.c

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <alloca.h>
  4
  5 void clobber(char*, int);
  6
  7 void printHi()
  8 {
  9     printf("Hi!\n");
 10 }
 11
 12 char* buf;
 13
 14 int main(int argc, char** argv)
 15 {
 16     char* x = alloca(8);
 17     buf = malloc(16);
 18     *(long*) buf = (long)&printHi;
 19     *(long*) (buf + 8) = 0x0000000000400642;
 20     clobber(buf, 16);
 21     clobber(x, 8);
 22     return 0;
 23 }

它做的事情就是分配内存到 x(指针) 和 buf,然后把 printHi 函数的地址存在 buf 处;把 0x0000000000400642 值存在 buf+8 处。

然后调用了两次 clobber,第二个参数应该是表示大小,单位是字节。

clobber 的定义如下(在 support.s 中):

  1     .file "support.s"
  2     .text
  3     .global clobber
  4     .type clobber, @function
  5 clobber:
  6     .cfi_startproc
  7     mov %rsi, %rax              # rax = rsi (第二个参数,表示内存地址大小)
  8     mov %rsp, %rcx              # rcx = rsp (栈顶。保存栈顶的位置。)
  9 .loop:                          # do {
 10     movq (%rdi), %rdx           #   
 11     movq %rdx, (%rcx)           #   (rcx) = (rdi) // rdi 存放第一个参数(指针),
                                    #                    这里是读取指针指向的值,放到
                                    #                    rcx 所指的内存处
 12     add $8, %rdi                #   rdi += 8 (指针往后移动一个字节)
 13     add $8, %rcx                #   rcx += 8 // rcx 所指的内存往高处移动
 14     sub $8, %rax                #   rax -= 8 // 大小减小
 15     jnz .loop                   # } while(rax); // 当内存还有未读取内容时继续循环
 16     ret                         # return rax;
 17     call *%rsi
 18     .size clobber, .-clobber
 19     .cfi_endproc
 20

这里看第 15 行可能会有困难。jnz(Jump when Not Zero)表示 Z 标志位不为零时跳转。需要提一下 sub(减法)指令对标志位的影响,在此处是指:rax -= 8 后,rax != 0 成立时跳转。

我如此理解这个函数的功能:函数有两个参数,内存地址(rdi)和大小(rsi)。该函数能够读取 rdi + N 处的内存,并复制到 rsp + N 指向的内存处。由于 rsp 指向栈顶,+ N 的方向是栈缩小的方向。而回忆刚才提到的栈帧构成,栈缩小的方向就是返回地址所在的方向——也就是说,我们正在把申请到的内存复制到栈中,而栈的空间是有限的,如果复制过来,在当前栈帧放不下会怎么样?

没错,会覆盖后面的栈帧,首当其中被覆盖的就是返回地址。缓冲区溢出攻击,就是通过这样的覆盖,实现修改返回地址的。

下面开始使用 GDB 进行调试。

$ gdb act1
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
...
Reading symbols from act1...done.
(gdb) break clobber
Breakpoint 1 at 0x400a41: file support.s, line 7.
(gdb) run
Starting program: /home/students/xxx/act123/act1

Breakpoint 1, clobber () at support.s:7
7           mov %rsi, %rax

使用 x (examine)命令查看栈顶指针($rsp)指向的内存地址中的值:

(gdb) x $rsp
0x7fffffffe2b8: 0x00400642

使用 backtrace(或 bt) 查看函数调用栈的信息:

(gdb) backtrace
#0  clobber () at support.s:7
#1  0x0000000000400642 in main (argc=<optimized out>, argv=<optimized out>) at act1.c:20

我们注意观察数字,(rsp) 的值刚好就是 main 的值(0x0000000000400642),这同时也就是 clobber 函数的返回地址。

查看 rdi 中的值:

x /2gx $rdi
0x6cf970:       0x0000000000400a30      0x0000000000400642

这里 /2gx 表示查看两个(2) 8 byte(g)的值,以十六进制(x)显示。也就是查看 16 个字节的值。前面说过 rdi 是第一个参数,其内容是指针所存的地址。

同样注意到了相同的值 0x0000000000400642

(gdb) stepi
8           mov %rsp, %rcx
(gdb) bt
#0  clobber () at support.s:8
#1  0x0000000000400642 in main (argc=<optimized out>, argv=<optimized out>) at act1.c:20
(gdb) si
10          movq (%rdi), %rdx
(gdb) si
11          movq %rdx, (%rcx)
(gdb) si
12          add $8, %rdi
(gdb) bt
#0  clobber () at support.s:12
#1  0x0000000000400a30 in frame_dummy ()
#2  0x0000000000000001 in ?? ()
#3  0x00007fffffffe438 in ?? ()
#4  0x0000000000401787 in __libc_csu_init ()
#5  0x0000000000400ca6 in generic_start_main ()
#6  0x0000000000401295 in __libc_start_main ()
#7  0x0000000000400939 in _start ()

我们发现,重新执行 bt(backtrace)之后,返回地址变了!从 0x0000000000400642 变成了 0x0000000000400a30。而 0x0000000000400a30 恰好是我们打算复制过去的内容(即第一个参数 rdi 中的内存地址)。

这样会发生什么后果呢?

(gdb) finish
Run till exit from #0  clobber () at support.s:12
Hi!

hhh,看到了一个 Hi,和我们当初对程序的分析并不一样——回头去看看查看 act1.c 的内容吧,它并没有直接调用 printHi(),但是通过缓冲区溢出,覆盖返回地址,相当于调用了 printHi()

acg2.c

我们来看一看它的内容:

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <alloca.h>
  4 #include <sys/mman.h>
  5 #include <stdint.h>
  6
  7 void clobber(char*, int);
  8 const char hiStr[] = "Hi\n";
  9
 10 int main(int argc, char** argv)
 11 {
 12     char* x = alloca(32);
 13     unsigned char* m = malloc(128);
 14
 15     puts("Activity 2!");
 16     if (m == NULL)
 17     {
 18         fprintf(stderr, "Allocation failure\n");
 19         return -1;
 20     }
 21     if (mprotect((void*)(((uint64_t)x) & (~0xfff)), 4096, PROT_READ | PROT_WRITE | PROT_EXEC) == -1)
 22     {
 23         perror("MPROTECT");
 24         free(m);
 25         return -1;
 26     }
 27     *(uint64_t*) m = (uint64_t)(x);
 28     m[8] = 0xbf;
 29     *(uint32_t*) (m + 9) = (unsigned int)(uint64_t) hiStr;
 30     *(uint32_t*) (m + 13) = 0x410100be;
 31     *(uint32_t*) (m + 18) = 0xd6ff;
 32     *(uint32_t*) (m + 20) = 0x40ec40be;
 33     *(uint32_t*) (m + 25) = 0xd6ff;
 34     clobber(m, 32);
 35
 36     return 0;
 37 }

粗略一看,是分配了 32 字节内存到指针 x,分配 128 字节到 m

分配 x 时用的是 alloca,查询手册:

       The alloca() function allocates size bytes of space in the stack
       frame of the caller.  This temporary space is automatically freed
       when the function that called alloca() returns to its caller.

可知,是在当前栈帧上分配 32 字节。也就是让 rsp -= 32

两个 if 应该是错误处理,注意到调用了 mprotect(),查询手册可知:

int mprotect(void *addr, size_t len, int prot);

其中 prot 标志可以取如下值:

       PROT_NONE
              The memory cannot be accessed at all.

       PROT_READ
              The memory can be read.

       PROT_WRITE
              The memory can be modified.

       PROT_EXEC
              The memory can be executed.

而具体到本程序则是(我拆成两行了):

addr = ((uint64_t)x) & (~0xfff)
mprotect((void*)addr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC)    

看第一行:64位与上 0xfff 的非,而0xfff(展开成六十四位:0x0000000000000fff) 表示后3x4 = 12 bit,取反可知是前 52 位是 1,后 12 位是 0。x 与运算该数,相当于取前 52 位,舍弃了后 12 位。

第二行则是说:把 x & (~0xfff) 及其后面 4096 字节的内存下改为 可读,可写,可执行

再之后的代码,是对 m 指向的内存进行赋值。最后调用 clobber,clobber 前面我们说过了,是复制内存到栈上。上命令!

$ gdb act2
(gdb) break clobber
Breakpoint 1 at 0x400aae: file support.s, line 7.
(gdb) run
Starting program: /home/students/xxx/act123/act2
Activity 2!

Breakpoint 1, clobber () at support.s:7
7           mov %rsi, %rax
(gdb) x $rsp
0x7fffffffe298: 0x0040068d
(gdb) bt
#0  clobber () at support.s:7
#1  0x000000000040068d in main (argc=<optimized out>, argv=<optimized out>) at act2.c:34

上面说明栈顶指针的内容是 0x0040068d,恰好是返回地址(对照 bt 的执行结果)。

(gdb) x /4gx $rdi
0x6cfa30:       0x00007fffffffe2a0      0x0100be004a186dbf
0x6cfa40:       0x40ec40bed6ff0041      0x0000000000d6ff00

注意,我们的断点是打在 clobber() 上的,所以其参数 rdi 的值已经准备好了。 /4gx 表明查看四个 8bytes 的值。

我们有必要关注一下 rdi里的这些值是哪儿来的,自然它是第一个参数,对应的变量是 m

 27     *(uint64_t*) m = (uint64_t)(x);
 28     m[8] = 0xbf;
 29     *(uint32_t*) (m + 9) = (unsigned int)(uint64_t) hiStr;
 30     *(uint32_t*) (m + 13) = 0x410100be;
 31     *(uint32_t*) (m + 18) = 0xd6ff;
 32     *(uint32_t*) (m + 20) = 0x40ec40be;
 33     *(uint32_t*) (m + 25) = 0xd6ff;

可以看到是从第八个字节开始赋值。而且这块内存在前面改成了 可执行,这暗示这些数字是 指令。我们按照指令格式打印出来看看:

(gdb) x /5i $rdi+8
   0x6cfa38:    mov    $0x4a186d,%edi
   0x6cfa3d:    mov    $0x410100,%esi
   0x6cfa42:    callq  *%rsi
   0x6cfa44:    mov    $0x40ec40,%esi
   0x6cfa49:    callq  *%rsi

里面有三个地址,里面是什么呢?

(gdb) x /s 0x4a186d
0x4a186d <hiStr>:       "Hi\n"
(gdb) x /s 0x410100
0x410100 <puts>:        "ATUI14S04?
(gdb) x /s 0x4a186d
0x4a186d <hiStr>:       "Hi\n"
(gdb) x /s 0x410100
0x410100 <puts>:        "ATUI\211\374S\350\064?\001"
(gdb) x /d 0x40ec40
0x40ec40 <exit>:        72
1" (gdb) x /d 0x40ec40 0x40ec40 <exit>: 72

可以看到,应该是调用了 puts,exit。第一个地址是 Hi 字符串,第二个和第三个都是函数的地址。打上断点:

(gdb) break puts
Breakpoint 2 at 0x410100
(gdb) break exit
Breakpoint 3 at 0x40ec40

可以看到果然是函数的地址。

act3.c

代码:

  1 #include <string.h>
  2 #include <stdlib.h>
  3 #include <stdio.h>
  4 #include <alloca.h>
  5 #include <stdint.h>
  6
  7 void clobber(char*, int);
  8 const char hiStr[] = "Hi\n";
  9
 10 void printAndExit(char* s)
 11 {
 12     puts(s);
 13     exit(0);
 14 }
 15
 16 int main(int argc, char** argv)
 17 {
 18     char* x = alloca(48);
 19     unsigned char* m = malloc(128);
 20
 21     puts("Activity 3!");
 22     if (m == NULL)
 23     {
 24         fprintf(stderr, "Allocation failure\n");
 25         return -1;
 26     }
 27
 28     *(uint64_t*) m = (uint64_t)(0x40374b);
 29     *(uint64_t*) (m + 8) = (uint64_t)(hiStr);
 30     *(uint64_t*) (m + 16) = (uint64_t)(0x400643);
 31     *(uint64_t*) (m + 24) = (uint64_t)(&printAndExit);
 32     *(uint64_t*) (m + 32) = (uint64_t)(0x402dcd);
 33     clobber(m, 40);
 34
 35     return 0;
 36 }

和前面很相似的代码。

$ gdb act3
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
...
Reading symbols from act3...done.
(gdb) break clobber
Breakpoint 1 at 0x400a40: file support.s, line 7.
(gdb) run
Starting program: /home/students/xxx/act123/act3
Activity 3!

Breakpoint 1, clobber () at support.s:7
7           mov %rsi, %rax
(gdb) x /5gx $rdi
0x6cfa30:       0x000000000040374b      0x00000000004a13a4
0x6cfa40:       0x0000000000400643      0x0000000000400a30
0x6cfa50:       0x0000000000402dcd

上面打出了 m (rdi)的内容。这些将会被复制到栈上。前面已经分析过clobber 函数,rdi += 8,指向被复制内容的指针,往高地址移动;rcx 所指的内存同样往高处移动。回忆栈帧结构rdi 指向的是堆内存,往高地址增长,rcx 指向栈内存,往低地址增长。所以首先被复制到栈的是 rdi 低处的 0x000000000040374b

方便起见,我们添加两个实时显示:

(gdb) display /12gx $rsp # 这儿表示监视栈顶附近的十二个8bytes
(gdb) display /2i $rip   # 这儿监视最近两条指令

同时反复执行 si,观察变化:

1: x/12xg $rsp
0x7fffffffe2f8: 0x0000000000400641      0x00000000004002c8
0x7fffffffe308: 0x0000000000400ca6      0x0000000000000000
0x7fffffffe318: 0x0000000100000000      0x00007fffffffe438
0x7fffffffe328: 0x00000000004005f0      0x00000000004002c8
0x7fffffffe338: 0xd32545837a649968      0x0000000000401710
0x7fffffffe348: 0x00000000004017a0      0x0000000000000000
2: x/2i $rip
=> 0x400a49 <clobber+9>:        mov    %rdx,(%rcx)
   0x400a4c <clobber+12>:       add    $0x8,%rdi
(gdb) si
12          add $8, %rdi
1: x/12xg $rsp
0x7fffffffe2f8: 0x000000000040374b      0x00000000004002c8
0x7fffffffe308: 0x0000000000400ca6      0x0000000000000000
0x7fffffffe318: 0x0000000100000000      0x00007fffffffe438
0x7fffffffe328: 0x00000000004005f0      0x00000000004002c8
0x7fffffffe338: 0xd32545837a649968      0x0000000000401710
0x7fffffffe348: 0x00000000004017a0      0x0000000000000000
2: x/2i $rip
=> 0x400a4c <clobber+12>:       add    $0x8,%rdi
   0x400a50 <clobber+16>:       add    $0x8,%rcx
(

和预料的一样,0x000000000040374b 被首先复制过去了。这个时候执行 bt ,看看返回地址:

(gdb) bt
#0  clobber () at support.s:12
#1  0x000000000040374b in _nl_find_domain ()

果然,返回地址不再是 main 的地址了。继续 si 直到变化:

(gdb) si
12          add $8, %rdi
1: x/12xg $rsp
0x7fffffffe2f8: 0x000000000040374b      0x00000000004a13a4
0x7fffffffe308: 0x0000000000400ca6      0x0000000000000000
0x7fffffffe318: 0x0000000100000000      0x00007fffffffe438
0x7fffffffe328: 0x00000000004005f0      0x00000000004002c8
0x7fffffffe338: 0xd32545837a649968      0x0000000000401710
0x7fffffffe348: 0x00000000004017a0      0x0000000000000000
2: x/2i $rip
=> 0x400a4c <clobber+12>:       add    $0x8,%rdi
   0x400a50 <clobber+16>:       add    $0x8,%rcx

可以看到,0x00000000004a13a4 也被复制过去了。

(gdb) bt
#0  clobber () at support.s:12
#1  0x000000000040374b in _nl_find_domain ()
#2  0x0000000000400ca6 in generic_start_main ()
#3  0x0000000000401295 in __libc_start_main ()
#4  0x0000000000400939 in _start ()

返回地址没有变化。继续 si

(gdb) si
12          add $8, %rdi
1: x/12xg $rsp
0x7fffffffe2f8: 0x000000000040374b      0x00000000004a13a4
0x7fffffffe308: 0x0000000000400643      0x0000000000000000
0x7fffffffe318: 0x0000000100000000      0x00007fffffffe438
0x7fffffffe328: 0x00000000004005f0      0x00000000004002c8
0x7fffffffe338: 0xd32545837a649968      0x0000000000401710
0x7fffffffe348: 0x00000000004017a0      0x0000000000000000
2: x/2i $rip
=> 0x400a4c <clobber+12>:       add    $0x8,%rdi
   0x400a50 <clobber+16>:       add    $0x8,%rcx
(gdb) bt
#0  clobber () at support.s:12
#1  0x000000000040374b in _nl_find_domain ()
#2  0x0000000000400643 in main (argc=<optimized out>, argv=<optimized out>) at act3.c:35
(gdb)

神奇的事情发生了。上上个(#2)返回地址被“掰”回了 main

(gdb) si
12          add $8, %rdi
1: x/12xg $rsp
0x7fffffffe2f8: 0x000000000040374b      0x00000000004a13a4
0x7fffffffe308: 0x0000000000400643      0x0000000000400a30
0x7fffffffe318: 0x0000000100000000      0x00007fffffffe438
0x7fffffffe328: 0x00000000004005f0      0x00000000004002c8
0x7fffffffe338: 0xd32545837a649968      0x0000000000401710
0x7fffffffe348: 0x00000000004017a0      0x0000000000000000
2: x/2i $rip
=> 0x400a4c <clobber+12>:       add    $0x8,%rdi
   0x400a50 <clobber+16>:       add    $0x8,%rcx
(gdb) bt
#0  clobber () at support.s:12
#1  0x000000000040374b in _nl_find_domain ()
#2  0x0000000000400643 in main (argc=<optimized out>, argv=<optimized out>) at act3.c:35
(

上面是第四个值 0x0000000000400a30 被复制到栈上。

(gdb) si
12          add $8, %rdi
1: x/12xg $rsp
0x7fffffffe2f8: 0x000000000040374b      0x00000000004a13a4
0x7fffffffe308: 0x0000000000400643      0x0000000000400a30
0x7fffffffe318: 0x0000000000402dcd      0x00007fffffffe438
0x7fffffffe328: 0x00000000004005f0      0x00000000004002c8
0x7fffffffe338: 0xd32545837a649968      0x0000000000401710
0x7fffffffe348: 0x00000000004017a0      0x0000000000000000
2: x/2i $rip
=> 0x400a4c <clobber+12>:       add    $0x8,%rdi
   0x400a50 <clobber+16>:       add    $0x8,%rcx
(gdb) bt
#0  clobber () at support.s:12
#1  0x000000000040374b in _nl_find_domain ()
#2  0x0000000000400643 in main (argc=<optimized out>, argv=<optimized out>) at act3.c:35

上面是第五个 0x0000000000402dcd

(gdb) si
0x000000000040374b in _nl_find_domain ()
1: x/12xg $rsp
0x7fffffffe300: 0x00000000004a13a4      0x0000000000400643
0x7fffffffe310: 0x0000000000400a30      0x0000000000402dcd
0x7fffffffe320: 0x00007fffffffe438      0x00000000004005f0
0x7fffffffe330: 0x00000000004002c8      0xd32545837a649968
0x7fffffffe340: 0x0000000000401710      0x00000000004017a0
0x7fffffffe350: 0x0000000000000000      0x0000000000000000
2: x/2i $rip
=> 0x40374b <_nl_find_domain+155>:      pop    %rdi
   0x40374c <_nl_find_domain+156>:      retq

上面,第六个。

再走一步,发现退栈了:

(gdb) si
0x000000000040374c in _nl_find_domain ()
1: x/12xg $rsp
0x7fffffffe308: 0x0000000000400643      0x0000000000400a30
0x7fffffffe318: 0x0000000000402dcd      0x00007fffffffe438
0x7fffffffe328: 0x00000000004005f0      0x00000000004002c8
0x7fffffffe338: 0xd32545837a649968      0x0000000000401710
0x7fffffffe348: 0x00000000004017a0      0x0000000000000000
0x7fffffffe358: 0x0000000000000000      0x2cdabaa5fc749968
2: x/2i $rip
=> 0x40374c <_nl_find_domain+156>:      retq
   0x40374d <_nl_find_domain+157>:      nopl   (%rax)

再走一步,回到了 main

(gdb) si
main (argc=<optimized out>, argv=<optimized out>) at act3.c:36
36      }
1: x/12xg $rsp
0x7fffffffe310: 0x0000000000400a30      0x0000000000402dcd
0x7fffffffe320: 0x00007fffffffe438      0x00000000004005f0
0x7fffffffe330: 0x00000000004002c8      0xd32545837a649968
0x7fffffffe340: 0x0000000000401710      0x00000000004017a0
0x7fffffffe350: 0x0000000000000000      0x0000000000000000
0x7fffffffe360: 0x2cdabaa5fc749968      0xd32545da22969968
2: x/2i $rip
=> 0x400643 <main+83>:  pop    %rbx
   0x400644 <main+84>:  retq

由于 main 的返回地址再往前也让我们写入了数据(返回地址),所以 main 函数退出后,进入了 printAndExit()

经过这三个热身程序,我们已经清楚了缓冲区攻击的原理:

  1. 可以把我们的函数地址拷贝到返回地址,从而让其在当前栈帧退出后,执行到我们的函数。
  2. 可以直接把指令写到 malloc 分配的堆内存,然后复制到栈内存,并把相应的内存区域设为可读可写可执行,这样就可以动态执行我们写入的指令。
  3. 还可以多次复制,使得一系列的我们的函数被注入调用栈的返回地址,依次执行。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注