格式化字符串(Format String)
第五空间2019 决赛
一.先check一下,32位小端序,除了PIE保护没开都开了,我们要想办法绕过金丝雀,或获得它随机生成的值,NX保护开启说明栈不可执行,不能往栈上写入shellcode,但可以构造rop链
二.32位IDA打开文件,找到main函数查看源代码,有两个数组nptr和buf,有readgsdword说明有Canary(readgsdword是Canary的实现),v1 = time(0);srand(v1);fd = open("/dev/urandom", 0)这一块是指随机生成一个数,read函数往buf中读取一定的字节然后用printf函数将buf上的内容打印出来。之后又往nptr中读取一定数量的字节,然后是一个if函数,意思是如果使输入的nptr与随机数相等,就可以执行system("/bin/sh"),否则就失败跳出if语句,跳出之后将result赋值为0,紧接着又是一个if语句,如果Canary的值不等于v6就要进入下面的那个函数结束进程。
三.根据分析我们可以考虑用格式化字符串将dword_804C044的值覆盖为一定数值使其等于nptr。那么格式化字符串要如何运用呢,在这里写出我对格式化字符串的见解。
1.定义:格式化字符串是在编程过程中,允许编码人员通过特殊的占位符,将相关对应的信息整合或提取的规则字符串。(格式化字符串漏洞利用:通过一些格式化字符串函数,eg: printf, scanf等并结合一些占位符%s, %x等将一些参数或一些地址或一些地址上的内容泄露出来)
2.常见的格式化字符串函数的介绍见原理介绍 - CTF Wiki (ctf-wiki.org)
3.占位符
%d:十进制有符号整数
%f:浮点数
%x:十六进制无符号整数
%s:解析地址把地址下的东西取出来
%p:输出对应变量的值。printf("%p",a) 用地址的格式打印变量 a 的值(小端序地址是倒着的,用地址的格式打印的意思是正着打印出来),printf("%p", &a) 打印变量 a 所在的地址。
4.格式化字符串的两个利用手段
a.使程序崩溃,因为 %s 对应的参数地址不合法的概率比较大。因为如下二图所示无论是32位还是64位的内存空间都存在一些不可访问地址,例如没有内核权限的内核空间,待分配区域,不可访问区域和空洞等,用%s去取这些地址就会使程序崩溃。
(堆栈之上是内核空间,之下是用户态空间)
b.查看进程内容,根据 %d,%f 输出了栈上的内容。
1)泄露栈内存利用%p来获取对应栈的内存,可以不用考虑位数的区别利用%s来解析地址内容(正过来),有零截断利用 %order$x 来获取指定参数的值,利用 %order$s 来获取指定参数对应地址的内容。(order:是数字=函数第几个参数减一,表示除去字符串的第几个参数)
2)泄露任意地址内存一般我们会重复机器字长来作记号(32位机器字长为4,64位位8),因为A在ASCLL表中的值为65,也就是0x41,所以当某个%p对应的值是0x41414141时,该地址就是格式化字符串的地址
eg:AAAA%P%P%P%P%P%P%P%P%P%P%P%P%PAAAA 0xffaab160 0xc2 0xf76146bb 0x41414141 0x70257025 0x70257025
可以发现第四个%P对应的地址就是格式化字符串的地址,但是为什么AAAA之后还会有个格式化字符串地址将它打印出来呢,举个例子,下面这段代码所对应的栈空间是这样的
#include <stdio.h> void x(){ int a,b; char c[30]; read(0,c,30); printf(c); return 0; }
因此能找到格式化字符串的地址
5.格式化字符串利用不仅可以泄露栈上和任意地址内存,还可以将栈上和任意地址内存的值进行修改,也就是本题我们需要做的-----将dword_804C044的值覆盖为一定数值使其等于nptr。
1)覆盖栈上的变量
a.找到覆盖地址:想要覆盖一个地方的变量首先要知道覆盖的地址,以这道题为例,在IDA上查看dword_804C044这个变量的地址就是0x804C044
b.寻找相对偏移:(利用上面我们所说的泄露栈内存的方法,也就是用pwndbg调试,后面大概会写一篇这个工具的使用的文章吧因为我自己调试也不熟练),这个参数的位置即相对偏移。
在这里使用构造payload为AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p来寻找偏移
F5运行一下脚本
发现0x41414141也就是AAAA在第十个%p下,因此偏移量为10
c.进行覆盖:这里涉及到了一个%number$n
number:将地址写入到的指定参数的位置,number=1,2,3,4...........
$n:将之前输出的字符数量作为一个整数写入到 number 所指定的地址中。例如,如果 number=1,将会把之前输出的字符数量写入到栈上的第一个参数地址处。
(要考虑写入的%number$n们所占的字节数来确定number的数值)
据此我们编写覆盖的脚本
运行
payload发送过去后,会出现一个问题:your passwd:,因为0x804C044地址下被%10$n赋值为4(P32(0x804C044)是四个字节)所以输入的值为4(nptr的值),回车就可以执行system("/bin/sh")了。远程cat flag就可以拿到flag了
2)覆盖任意地址内存
a.覆盖小数字如果使0x804C044的值=2才能执行system("/bin/sh"),我们要如何进行赋值呢?如上所见,32位地址写在前头最后该地址的值打底为4,64位的打底为8,是绝对不可能为2的,但其实地址不仅仅可以写在前面,也可以写在中间或后面,还是上面的那道题,如果我们想要该地址的值覆盖为2,可以如此构造
payload=b'AA%12$nA"+p32(0x804C044)
在这里要解释一下为什么偏移是10,而这里出现的却是12,首先AA%12$nA这一串本身是8个字节,%n后面的A是为了对其用的,AA%1占四个字节是一个参数,2$nA占四个字节又是一个参数,所以是两个参数,因此10+2=12,该位置的数字应为12.
b.覆盖大数字
32位:用一个地址覆盖另一个地址
举个例子
将0x12345678写到0xffffcd28处
用AAAABBBBCCCCDDDD来寻找跳转地址
写入地址 跳转地址 /x78----0xffffcd28----0xffffcd54 /x56----0xffffcd29----0xffffcd58 /x34----0xffffcd2a----0xffffcd5c /x12----0xffffcd2b----0xffffcd60 payload=b'\x28\xcd\xff\xff'+b'\x29\xcd\xff\xff'+b'\x2a\xcd\xff\xff'+b'\x2b\xcd\xff\xff'+b'%104c%13$hhn'+b'%222c%14$hhn'+b'%222c%15$hhn'+b'%222c%16$hhn'
104=0x78-4x4
222=0x156-0x78
222=0x234-0x156
222=0x312-0x234
而0x156->0x56
0x234->0x34
0x312->0x12
64位:64位是寄存器传参,这六个传参的寄存器分别是RDI,RSI,RDX.RCX,R8,R9,其中RDI寄存器用来传参了,其余与32位是一样的,先泄露获取地址,然后再覆盖。
参考:《CTF权威指南(pwn篇)》原理介绍 - CTF Wiki (ctf-wiki.org)
#格式化字符串#