什么是格式化字符串(仅从ctf解题方面理解,以printf函数为例)
int printf("格式化字符串",参量...)
参数的返回值是正确输出的字符的个数,如果输出失败,返回负值
参量表中的参数的个数是不定的(参数的个数可以是一个,两个,三个...)
格式化字符串函数有很多,例如输入:scanf;
输出:
printf | 输出到 stdout |
fprintf | 输出到指定 FILE 流 |
vprintf | 根据参数列表格式化输出到 stdout |
vfprintf | 根据参数列表格式化输出到指定 FILE 流 |
sprintf | 输出到字符串 |
snprintf | 输出指定字节数到字符串 |
vsprintf | 根据参数列表格式化输出到字符串 |
vsnprintf | 根据参数列表格式化输出指定字节到字符串 |
setproctitle | 设置 argv |
syslog | 输出日志 |
格式化字符串漏洞点
格式化字符串漏洞的点在于printf函数,当函数没有规定输出格式的时候我们就可以利用这个漏洞来泄露我们想要的东西
printf的参数类型:
%d,数字
%s,字符串
%c,字符
%x,十六进制不带0x
%p,十六进制带0x(0x。。。)
%f,浮点型
%n(printf写入的一个参数),将%n之前打印出来的字符个数存入到参数中,唯一一个能写入参数的printf函数
eg.printf("%n",buf)
printf("aaaaa%n",buf)
这样buf就等于5,因为%n前面有五个字符a
eg.printf函数中,对方不限制打印的格式,可以尝试存入一些字符+%p来看字符存到了偏移多少的地方(eg.aaaaaaa%p,%p,%p,%p,%p,%p),这样看到打印出来输入的字符存到了第六个位置,那就是相对偏移为6,那么就用%6$p来输入一些我们想要的东西,如下
64位0x7f开头的地址一般是libc 或栈地址,32位是0xf7
调试界面看到的地址是十六进制,%$p中的偏移数字是十进制
我们可以利用已知的我们写入地址的偏移计算出我们想要泄露的地址的偏移,或者把地址改为我们想要的地址,再把它泄露出来,然后接收,%p和%s泄露出来的地址的接收方式是不同的
1.通过%s
根据偏移覆盖掉偏移位置函数,然后就可以泄露出所覆盖的函数地址
2.通过%$p
%p打印1位置的地址,%s打印2位置的地址,用的方式不同接收地址方式和泄露地址方式不同
%p接收方式(p.recv(,)):
libc_base=int(p.recv(14),16)-libc.sym['__libc_start_main']-245
接收14个字节,转化为16进制
%s接收方式(p.recvuntil("\0x"[- :])):
addr=u32(u64)(p.recvuntil("\xf7")[-4:])函数地址=libc基址+函数偏移
当使用格式化字符串漏洞泄露出我们需要的Libc地址之后我们就可以算出libc基址了,下一步就可以接着利用想要利用的函数或者寄存器,再结合题目的某些限制,选出最佳方法解题了
非栈上格式化字符串漏洞
首先还是要先泄露出libc基址,然后又有大概两种情况:
1.当我们可溢出字节较多时,我们可以使用自动化payload改写地址:
fmtstr_payload(8(修改长度),{stack1(要改的地址):one_gadget(目标地址)})
这样我们就可以把某个我们知道的地址直接改为one_gadget地址,然后让程序返回到这个地址,这样就可以直接获取权限
2.当我们可溢出字节较少但是没有少到只能用栈迁移写时,就得手搓payload大概长这样:
(b'%'+str().encode()+b'c%-$hn').ljust(,b'\x00')+p64()
这里可以根据题目决定是否要打包补齐
ep.
这里可能会用到&和>>
ep.print(hex(one_gadget))
print(hex(one_gadget&0xff))
print(hex(one_gadget&0xffff))
打印结果:
ep.print(hex(one_gadget))
print(hex(one_gadget>>16))
print(hex(one_gadget>>32))
打印结果:
见到这种情况时,一般就是找栈上地址偏移,利用手搓的payload一点一点的把原栈地址改为你想要的地址,最后让程序返回到这里来执行你想要的东西
这里我们手搓的payload修改的地址只能是a→b→c中的c地址
也就是例如上图中的2位置的地址
有时如果没有合适的a→b→c地址,我们可以选c→d→e和a→b→c,通过改c→d→e达到修改a→b→c