应用安全系列之三十五:格式化字符串

在使用格式化字符串函数时,通常会以printf([格式化字符串],参数)的形式进行调用,如果攻击者可以控制格式化字符串的值,或者不正确的以printf(参数)的形式进行调用,并且参数的值攻击者可控,攻击者可以通过使用特定的格式化字符获取系统内存信息或者修改系统的内存数据。

格式化字符串漏洞所造成的危害包括:

  1. 泄露系统内存信息,造成系统的信息泄露
  2. 可用于修改内存数据,当修改内存中的函数返回地址时,攻击者就可以控制代码执行逻辑,注入Shellcode,造成远程代码执行。
  3. 发起DOS攻击

格式化字符串一般C语言使用的比较多,出问题的情况也很多。如下示例代码:

#include <stdio.h>



int main () {

   char ch='a';



   printf("ASCII value = %d, Character = %c\n", ch , ch );



   return(0);

}

正常情况下,格式化字符串中的%+一个类型的字符和后面参数的个数是一样,这样保证打印出来的内容符合预期。但是,如果格式化字符串可以被控制,%+类型字符少了一个,示例如下:

#include <stdio.h>



int main () {

   char ch='a';



   printf("ASCII value = %c, Character = %lx %lx %lx\n", ch );



   return(0);

}

这种情况会怎么样呢?

编译代码之后,执行的结果如下:

通过gdb debug如下:

可以看出通过篡改格式化字符串,可以将其他寄存器的内容读出来。

再来看一下经典的堆栈的结构:

上图,参数内容之后便是当前函数的栈帧,EBP固定执行旧的EBP起始地址,而旧的EBP存储着上一个函数的执行地址,这样等到末尾出栈之后就能按层级返回上一级函数了,而ESP总是执行栈顶,会随着函数的调用或这些不断变化。

在运行的时候,当执行到printf时,就会根据其中的%+类型的个数去栈上找对应的参数,当实际传递的参数不够时,也依然在栈上继续向上找,这样就会把上一栈帧的EBP当做第二个参数打印出来。这只是简单打印,当使用snprintf将参数组合成一个字符串而且这个字符串会回返回到客户端显示,就会导致栈上的内容泄露。

当使用sprintf函数根据输入的内容格式化一个字符串时,如果攻击者可以控制字符串参数的内容,也可以造成缓冲区溢出的问题。示例代码如下:

#include <stdio.h>



int main (int argc, char* argv[]) {

   char str[80];



   sprintf(str, "Value of 2nd argument is %s", argv[1]);

   puts(str);

   

   return(0);

}

这里,当argv[1]的长度加上格式化字符串的长度,如果大于80就会导致缓冲区溢出。

由于这种错误在编译时,是无法被发现的,甚至如果格式化字符串本身也是变量时,在代码审查的时候,可能也很难发现。

C主要的Sink如下:

头文件

方法

stdlib.h

int fprintf(FILE *stream, const char *format, ...)

int printf(const char *format, ...)

int sprintf(char *str, const char *format, ...)

int vfprintf(FILE *stream, const char *format, va_list arg)

int vprintf(const char *format, va_list arg)

int vsprintf(char *str, const char *format, va_list arg)

wchar.h

int fwprintf (FILE* stream, const wchar_t* format, ...);

int swprintf (wchar_t* ws, size_t len, const wchar_t* format, ...);

int vfwprintf (FILE* stream, const wchar_t* format, va_list arg);

int vswprintf (wchar_t * ws, size_t len, const wchar_t * format, va_list arg );

int vwprintf (const wchar_t* format, va_list arg);

int wprintf (const wchar_t* format, ...);

Java语言也有格式化字符串的方法String.format,示例代码如下:

public static void testFormatString() {
    int first = 88000;
    int second= 99000;
    String s = String.format("第一个参数:%,d 第二个参数:%,d", first);
    System.out.println(s);
}

当只传递一个参数时,会报异常如下:

由于所抛的异常MissingFormatArgumentException是RuntimeException。根据RuntimeException与Exception的区别:

异常类型

说明

Exception

非运行时异常,如果代码里不try-catch或者不通过throws传递,IDE就会显示错误,编译也不能通过。

RuntimeException

运行时异常,在项目运行之后出错则直接中止运行,异常由JVM虚拟机处理。

由于RuntimeException在编码时即使不适用try-catch语句,也不会提醒这里会发生异常,所以,可能在编程的时候,就会忽略这个异常。JVM一般处理这个异常的时候,就是终止程序,所以,当格式化字符串可以被攻击者控制时,攻击者就可以发起DOS攻击。

Java主要的Sink如下:

类型

方法

PrintStream

PrintStream format(String format, Object ... args)

PrintStream format(Locale l, String format, Object ... args)

PrintStream printf(Locale l, String format, Object ... args)

PrintStream printf(String format, Object ... args)

String

String format(String format, Object... args)

String format(Locale l, String format, Object... args)

关于格式字符串问题的预防,主要是尽量避免格式化字符串可以被攻击者控制,在编程时能够细心,保证格式化字符串的个数和参数的个数一致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值