C/C++格式化输出(printf、fprintf、sprintf、syslog等)的不安全用法
遇到问题
前段时间gameserver突然挂掉,查看coredump发现挂掉的位置是调用syslog,代码如下:
syslog(LOG_INFO, pszLog);
pszLog是日志的内容,当时的那局游戏中恰好有一个玩家叫做fcc%nn,而%n在C/C++格式化输出中代表的是将输出字符串到%n为止的所有字符byte数存放到一处内存中。例如:
int a;
printf("abc%ndef\n", &a);
printf("%d\n", a);
Output:
abcdef
3
刚才的那个syslog语句在输出这次对局信息时包含fcc%nn玩家的名字,等价于下面的代码:
syslog(LOG_INFO, "xxxxx fcc%nn xxxxx");
这个syslog语句并没有在之后跟着任何参数,那么这个%n会输出到某处内存上,服务器挂掉的时候收到的信号是SIGSEGV,说明访问了非法的地址,但是并不是打这条日志服务器都会挂掉,说明很多时候这个%n被输出到了一个进程合法的地址上,导致某处合法内存被修改。这种情况比进程挂掉还要可怕,因为被篡改的内存可能是和打日志完全不想干的功能,导致运行到那个功能的时候出现宕机或者是其他逻辑错误,那时根本不知道是由于这个syslog造成的。
深入研究
我想探究一下如果format字符串中含有%n,而后面没有跟任何参数时,%n的输出究竟会被输出到哪块内存上。
printf("abc%ndef\n")
syslog(LOG_INFO, "fcc%nn");
首先得先研究可变参数的传递方式,我写了如下的示例代码:
int foo(const char* a,...)
{
return 0;
}
int main(int argc, const char** argv)
{
foo("abc",1,2,3,4,5,6,7,8,9,10);
return 0;
}
foo所接受的参数和printf一样,生成相应的汇编代码:
foo:
...
movq %rsi, -168(%rbp)
movq %rdx, -160(%rbp)
movq %rcx, -152(%rbp)
movq %r8, -144(%rbp)
movq %r9, -136(%rbp)
...
ret
main:
...
pushq $10
pushq $9
pushq $8
pushq $7
pushq $6
movl $5, %r9d
movl $4, %r8d
movl $3, %ecx
movl $2, %edx
movl $1, %esi
movl $.LC2, %edi ;"abc"
movl $0, %eax
call foo
...
从main的汇编代码中可以知道对于可变参数的前5个在x64下是直接通过5个寄存器传递的,其中rsi中存放着第一个参数,从第6个参数起通过栈来传递参数。从foo函数的汇编代码中也能看出foo会将前5个可变参数从寄存器中保存到自己栈上的临时变量区。回到之前printf的例子:
int a;
printf("abc%ndef\n", &a);
printf("a%n\n");
第一个printf的第一个可变参数是整形a的地址,通过rsi寄存器传递,3会被写入到a的地址中。第二个printf没有显式的传递一个地址,printf函数依然会读取rsi寄存器里面的值,然后把1写入到rsi里面的值所代表的内存位置,在上面这个例子中由于printf的实现里面会改变rsi的值,所以第二个printf的1并不是写入到变量a中。我们利用上面的foo来构造一个例子:
int foo(const char* a,...)
{
return 0;
}
int main(int argc, const char** argv)
{
int a = 100;
printf("%d\n", a);
foo("abc", &a);
printf("a%n\n");
printf("%d\n", a);
return 0;
}
Output:
100
a
1
给foo传递a的地址时是用rsi寄存器,foo函数里面什么也没做,所以在执行接下来的printf的时候rsi里记录的任然是变量a的地址,printf的%n前只有一个“a”字符,所以把1写入到变量a中。此时成功通过printf改变了指定位置的内存。
总结
对于C/C++中格式化输出的函数直接在format参数输出字符串是很危险的。运气好只遇到%d之类的特殊字符串只会使得输出结果不是预期想要的。如果遇到了类似%s、%n那么就有可能污染内存。
printf("abc%ndef\n");
syslog(LOG_INFO, "fcc%nn");
sprintf(buf, "abc%def");
安全的用法如下:
printf("%s", "abc%ndef\n");
syslog(LOG_INFO, "%s", "fcc%nn");
sprintf(buf, "%s", "abc%def");
此时待输出的字符串中的特殊格式字符就不会被解析。