PWN中格式化字符串漏洞(fmt)的利用
格式化字符串漏洞
C语言中最常见的输出函数
C++ |
其中的 %d 其实就是所谓的“格式化字符串”
wiki定义: 格式化字符串(英语:format string),是一些程序设计语言在格式化输出API函数中用于指定输出参数的格式与相对位置的字符串参数,例如C、C++等程序设计语言的printf类函数,其中的转换说明(conversion specification)用于把随后对应的0个或多个函数参数转换为相应的格式输出;格式化字符串中转换说明以外的其它字符原样输出。 |
简单说,格式化字符串就是用于控制输出函数输出内容时的方式与位置
例如上面的代码的作用就是让系统按照整形格式输出变量a
以下是常见的格式化字符串:
点击图片可查看完整电子表格
对于其中%n的解释: |
在这段代码中变量s被赋值为18,这是怎么做到的呢?
%n的作用是将字符个数赋值给指定地址,也就是说在涉及%n的情况下,某种程度上可以
将其视为赋值语句。
但问题是,这个赋值可不一般,它是赋值给一个地址,而且这个地址还可以是一个变量形
式的,也就是说我们可以通过这个漏洞控制本来不能控制的内存地址。
格式化字符串的标准格式:%[parameter][flags][field width][.precision][length]type
而d,s,x,p,n就是其中的type(类型)
其中parampreter也比较重要
它可以忽略,写出后可以是:n$ (输出栈中第n个参数,这一点后面可以拿来泄露canary.libc等)
printf()函数支持可变参数,即可以给它传递无限个参数(但在VS或者win系统中有一定的保护机制)
而根据规定,在调用函数时,参数以从右向左的顺序压入栈中。如:
C++ |
Bash |
(汇编形式)
那么在执行函数时,printf()会在栈中向前找三个参数,然后以十进制形式打印出来。 |
既然说printf()可以接受更多的参数,那么在传参时如果传入5个参数:
C++ |
那么汇编代码就会呈现这种状态: |
不难看出,这次printf()函数听话的传入了5个参数,但它只读出了3个参数。
参数比占位符多的情况貌似并没有什么实际利用价值。
接下来看参数少的情况:
C++ |
这次我们只传入了两个参数,但我们却要求printf()函数输出三个参数
printf:我说小伙子你不讲武德。
运行结果如下:
可以发现,它还是打印了三个参数,但第三个参数显然有点奇怪,那它是哪来的呢:
这个值其实就是栈上在1和2之后的第三个值,但这个值确实不是由我们传入的。
在前面的代码中,我们告诉printf():“我传入了三个值,你自己找吧”
这时它会进行一个又聪明又笨的操作:它不怀疑我们有没有骗它,但它很“聪明”的自己去找了第三个值。所以程序就被它给出卖了:有坏比通过这个函数打印了不该被打印的值,而结合前面学到的格式化字符串中n$这个可选参数,这个值实际上往往可以操作为一个敏感的参数。
引用: 这种操作在泄露canary等情境下会有大用 |
某种程度上,这个漏洞的造成可以说是由于程序员的偷懒:
如按标准来做的话:
C++ |
这段代码是没有任何问题的,但在实际中,可能会由于省事把第三行写作这样
C++ |
行不行?彳亍。有没有问题?问题很大!
对于以上的三段代码,我们跑起来,输入三个a,结果是一样的:
但是由于没有占位符,函数并不知道它接受了多少个参数,那么如果我们输入的奇怪一点会怎样呢?
如: |
很显然,出问题了
这时printf输出的是个什么?——答:"aaa.%x"
是的,虽然没在函数调用给出占位符,但我们却在输入时传入了占位符,即使这样,printf也会把这个占位符执行掉,那么就会输出一个十六进制的值。
依然是对于上面的程序,此时如果我们输入多个“%x”,那么函数就会读取多个内存中的内容并把它打印出来。那么在实际运用中,我们就可以通过控制偏移量输出特定地址的内容,也就是利用前文提到的n$。
例如我们需要读取第七个内存地址中的内容,那么我们就可以构造为:aaa%6$x
因为这里是直接访问了栈,那么就是访问了第六个内存地址。
开了NX和canary,但这题存在格式化字符串漏洞,所以canary有没有其实都一样 |
这道题考察的是利用格式化字符串漏洞改写栈上变量内容
从主函数中可以看出这题只要把变量x改成4就可以直接获取shell了
因为%n这个控制符是要把数写到地址处,所以还要用到目标变量的地址
先找找x的地址 |
然后要利用格式化字符串漏洞还有一个非常重要的步骤:计算偏移量。通过前面的理论基础可以想到,要想接触到栈中某个指定位置的元素就得利用格式化字符串中的n$这个参数,那么n代表的数字就要根据字符串输入位置的偏移来决定。
存在格式化字符串漏洞时计算偏移非常简单,只需要输入4个字符加上数个格式化字符串就可以找到偏移量:
可以看到第二个红框处的内容是41414141即4个A,而前面我们用read函数写入的第一部分内容就是4个A,也就是说从这里开始就是写入位置了,所以就能得出偏移量是11。
构造payload: |
这里第一块的地址其实就相当于前面测试偏移时输入的AAAA,在将这个地址也就是变量x的位置写入栈上后,%11$n这个格式化字符串就会把前面输出的字符个数赋值给偏移为11的位置也就是变量x。而32位中一个地址是4字节,所以这样构造正好实现了将4复制给x,也就完成了改写。
完整脚本: |
一、"%Nc"(N指整数)
我们知道%n这个fmt可以将已打印的字符个数作为整形赋值给目标地址,在刚了解到格式化字符串漏洞时一般都会用aaaa%x$n这种方式去赋值,这种方式当然是效率极低的一种利用方式,实际上有一种更好的方式:"%c":
%c这个fmt是用来解析字符的,如果出现了[width]参数即%nc这种情况则表示printf应该打印出7个字符,如果参数不足7个那么就会用空格进行填充,所以在有限的读入长度下就可以用这种写法去赋值,比如%777c%n$n就会将第n个栈里的地址赋值为777
那么这种方式最大可以赋值多少呢,答案是一个int型的最大值65535=0xffff(内存里的两字节)
二、"%hn"
在与%Nc搭配使用时往往不会直接用%n,而是再加一个h参数,这个h参数的作用是规定修改目标地址的哪几位,64位下%n会修改4字节的内容,而加一个h %hn会修改2字节的内容,而前面也说到了%Nc的N最大是0xffff,正好就是两字节内容的最大值,所以一般情况下使用这种手法都是用%hn的
三、%n赋值到底是赋值给哪里的?
答:赋值给这块栈上的地址里
画个图:
是以栈上的地址为目标而不是以栈为目标,如果定位到的栈里没有合法地址,程序就会趋势
来到题目中:
先拿栈顶这里试试水:
输入一个%64c%6$n
可以成功将该处赋值为64
接下来就是考虑利用这个漏洞将某处篡改为shell地址进行劫持了,这个题开了partial RELRO,got表不可写的,所以不能劫持got表;我这题是篡改的return地址,接下来就写一下这个思路的分析。
———————————————————————————————————————————————————————
非栈上的格式化字符串漏洞:
如果格式化字符串是在栈上的,我们就可以先将目标地址写到栈上再对目标地址赋值,但是这里是在bss段上的,没办法直接任意写
对于这种情况可以使用一个连环劫持的手法
我们可以利用一个这样的结构:
如图,这两块栈的构造是
A: 栈地址1-->栈地址2-->栈地址3(其他地址)
B: 栈地址2-->栈地址3(其他地址)
(实际利用中找到 栈地址1-->栈地址2-->栈地址3 这样的三连环就能用了)
根据我画的图里的两个白色箭头,通过这个结构可以用%n$n直接修改到两个位置,定位栈A就可以修改address,而图中的a又是在address里的,通过,通过这样一个连环控制就可以实现任意地址写了,这里的逻辑关系稍微有些绕,需要好好理清楚,本质上是很简单的。
接下来实操看看,我们试着利用这个三连环将迭代变量篡改成一个奇怪的值
在这里栈地址3的后四位我们都可以利用格式化字符串漏洞去篡改,
篡改的目标后四位是0xde48,写一个%56904c%11$hn就ok了
可以看到通过定位栈A成功把栈B的指向篡改到了de48的位置
同时可以看到,在df38这里的内容也是成功的同步为de48
那么此时定位栈B(就是截图这里)会发生什么?篡改的会是de48处的内容,我们定位它篡改一下试试:
%1911c%37$hn
成功将de48处的内容改为了0x777(当然,是低位)
一个非栈上的格式化字符串漏洞,主要是用到了格式化字符串给目标地址赋值的特性
题目程序非常简单,就是一个重复触发了五次格式化字符串漏洞的循环
由于唯一的读入是朝bss段上读的,靠溢出做题就别想了,这题很明显是想让我们利用格式化字符串(fmt)漏洞
通过前面的一段解释,应该(maybe?)能明白利用三连环结构构造任意地址写的手法了,接下来就考虑题目应该怎么打了,思路很简单:先在栈A处篡改内容为返回地址,然后再定位栈B去篡改返回地址的内容
就像这样
选择这块栈即可
我们要先定位这个位置然后将"栈地址3"篡改为返回地址,由于栈地址的偏移地址会随机化,所以这里要知道返回地址的后四位就得先拿到一个栈地址,这里正好泄露这个栈A就得了,拿到的是0xdf38
减去0xe0就是返回地址了,然后可以用 address & 0xffff 这个运算取出一个地址的后四位
然后就是用前面说过的方法填到栈上了
成功篡改栈地址3为返回地址
这时再定位这里篡改的就是返回地址里的内容了
那么现在有一个新的问题,这题我选择打onegadget,而这个地址在libc里的偏移是有五位的,比如这里libc_main是0x719e14c20840,而onegadget地址应该是0x719e14cxxxxx,也就是说篡改应该改五位,但是开头也说了用%c最大也只能改到后四位(低2字节),所以这里是要改两次的
改最后的四位和常识相似正常填地址正常篡改就得了,那改倒数第二个四位呢?其实我们可以先把地址+2放上去再改,画图好理解一点:
在内存里大概就是这样的感觉(按小端序画的)
我们用de48这个地址改的是蓝色部分(地址的最低两字节),+2后改的是绿色部分(地址的倒数第3到4字节)
所以我们就需要两次篡改去把onegadget的后八位字节分两次写进去
可以用这种方法将地址切成三段
Python |
先将中间四位地址写到对应位置
同理再把后四位写到对应位置
这样就成功在return地址填入我们的onegadget了
完整exp:
Python |