fmt专题
知识点
简单来说,格式化字符串漏洞就是printf()函数中参数不对应时,利用printf()来泄露栈上地址的内存或者写入数据覆盖内存,%p用来读,%n用来写。
32位(x86)
泄露地址内存:首先,先给printf中的参数输入AAAA%p%p%p%p%p%p%p%p…,找到0x41414141的位置,假设该位置为n(即第n个,这里要注意和后面x64有所不同,x64在gdb查看栈空间的时候n只算到0x41414141的前面一个,而x86要把0x41414141包括在n中),则n就是我们可控的第一个参数,此时我们用AAAA%4$p则可以直接输出0x41414141。
现在假设我们想泄露某个函数的真实地址,把AAAA替换为该函数的got地址就可以了,got地址的获取用pwntools的
elf=ELF('./文件名')
函数名_got=elf.got['函数名']
构造payload=p32(函数名_got)+'%k$s'即可,也可以写成payload='%(k+1)$s'+p32(函数名_got),因为此时占据第k个参数的是’%(k+1)$s’,函数名_got占据了第k+1个参数。
覆盖内存:这里用到了%n来向内存中写入数据,如果要修改参数a的数据为16,第一步像泄露内存一样先给printf的参数输入AAAA%p%p%p%p%p%p%p%p…,然后在ida中找到参数a的地址,构造payload=p32(a_addr)+'a'*12+'%k$n',现在来到最重要的地方:我们写入参数的数据是在他之前的字符个数,这里的’a’*12是因为前面a的地址已经占了4个字节,再补充12个就可以将a修改为16。
而写入数据的时候面临着一些问题,当想要修改的数据小于4的时候,上面的方法就行不通了,因为仅仅是一个参数地址就占用了4个字节,这时候用到一个巧妙的方法,假如我要将参数a修改为2,如下构造payload='aa%(k+2)$naa'+p32(a_addr)换一种方式看payload就是’aa%k’+’$naa’,前面每四个字节分成一个参数,那么a_addr就成了往后推2个的第k+2个参数,又因为%k$n前面是2个a字节,所以就可以实现写入小于4字节的数据,同理,如果想把a修改为1,构造payload='a%(k+2)$naaa'+p32(a_addr)即可。
数据过小的情况说完了,那自然也有数据过大的情况,这一部分还没有太理解,为了赶进度我先挂一下cyberangle博客的截图,以后遇到这种情况了再回来看,,,
64位(x64)
博客上说跟32位很相似,但是64位前6个参数是先存在寄存器中的,我看的博客举的例子是用gdb调试,在printf设断点,输入123456,然后stack 20查看栈上第一个出现目标的位置n,偏移值就是没有用到的寄存器数+n,但现在仍然不太清楚原理,先来做几道fmt的题。
例题
1.[HNCTF 2022 Week1]fmtstrre(fmt泄露栈上的flag)
https://www.nssctf.cn/problem/2932
checksec后发现是64位,拖进ida里f5
看伪代码发现是把flag存到fd中,然后再读到name中,双击name
得到name的地址是0x4040c0,用gdb调试一下,在printf函数设断点b printf,然后run,输入aaaaaaaa,然后ni到printf函数执行完,stack 50查看栈空间
发现name的地址,到rsp的距离为33,再加上5个寄存器,则用%38$s读取内存空间即可得到flag
2.[MoeCTF 2022]babyfmt (got劫持)
https://www.nssctf.cn/problem/3332
checksec发现是32位,拖进ida看伪代码,发现后门函数backdoor,并发现main函数中的格式化字符串漏洞,思路是利用格式化字符串将printf的got地址替换为后门函数的地址,即got表劫持
先运行程序,利用之前提到的方法找偏移值,偏移值为11
got劫持需要用到一个pwntools的函数,fmtstr_payload,具体用法就是payload=fmtstr_payload(offset,{被覆盖的地址: 覆盖的地址}),构造exp如下,这里的gift地址可以在ida中找到,也可以使用pwntools的elf.symbols[‘函数名’],也就是下面注释的那一行
运行脚本,得到flag
**但是!**这道题我还有两个疑惑的地方,这道题没有开启pie保护,明明直接用ida中的backdoor地址就可以了,为什么还要这么麻烦的给我s的地址,简单来说,我要的gift应该是s的内容,而不是s的地址,但是我不太会利用s的地址找到他的内容,而且也不太会用pwntools和python接收数据,之前一直只会r.recvline(),,,python字典不太了解,改天需要专门学习一下接收数据这块内容。
还有一个问题就是为什么运行脚本后输入ls指令没有输出,但是cat flag指令就可以输出呢。。
3.[深育杯 2021]find_flag(fmt泄露canary及ret的地址来绕过canary和pie)
https://www.nssctf.cn/problem/774
checksec发现是64位且保护全开,拖到ida里看伪代码
发现后门函数:
主要函数为第二张图片,发现格式化字符串漏洞和gets栈溢出漏洞,思路是通过格式化字符串漏洞泄露canary和ret的真实地址,再通过将ret真实地址-ret在ida中的偏移地址得到ret的pie的基地址,即可利用后面的栈溢出漏洞调用后门函数。
那么如何泄露canary和ret真实地址呢,看下面的图就很清晰了
接下来要通过gdb找到格式化字符串的偏移 ,由于这题开了pie保护,所以不能用b *地址来设断点,我先b printf然后一直执行到第三个printf也就是格式化字符串漏洞,再查看栈空间,计算rsp到rbp的距离,再加上五个寄存器即可,也可以说成栈的第一个位置是从6开始计算的。
计算偏移的第二种方法:
format 到 rbp 的距离为 0x60
canary = rbp - 0x8
找到格式化字符串的偏移为6
每个地址是 8 Byte
所以最后的 canary_offest = (0x60 - 0x8) / 8 + 6 = 17;
ret_addr_offest = canary_addr + 2 = 19;
则通过%17$p和%19$p即可泄露canary和ret的地址,输入%17$paaaa%19$p,中间4个a方便接收后续信息。
得到ret的真实地址要减去ida里的偏移地址,要注意ida中偏移地址对应的汇编不是retn,而是上面gdb界面对应的mov eax,0,即下图
脚本如下
其中buf = p.recvuntil("!").decode()是为了将接收到的字节数据解码成字符串,
tmp=buf.split(" ")[4].split("aaaa")[0] 的split(“ “)是将字符串以空格划分为[“Nice”, “to”, “meet”, “you,”, “0x79542ecb97501c00aaaa0x557a4ed7646f!\n”],以0下标为第一个取各部分字符串,buf.split(“ “)[4]就是取到第5个小字符串”0x79542ecb97501c00aaaa0x557a4ed7646f!\n”,后面的split(“aaaa”)[0]和split(“!”)[0]也同理。
canary=int(tmp,16)
tmp=buf.split("aaaa")[1].split("!")[0]
re_addr=int(tmp,16)
拿到canary和ret地址后,计算基地址,之后的payload构造就很简单了,先覆盖到canary然后输入canary值,再覆盖canary到rbp再写入后门函数地址即可拿到flag。














