C代码审计之格式化字符串
格式化输出函数
printf() 写入标准输出
fprintf() 写入流
sprintf() 写入数组
snprintf() 等同于sprintf(),但是指定了可写入字符的最大值
格式化字符串函数的原理
例:printf("test%d%s", 20, "tom");
函数运行时部分栈:
低地址 &"test%d%s"
0x14
高地址 &"tom"
函数执行时,低地址往高地址依次出栈,直到出栈的数据把格式化字符串的占位符填满为止
缓冲区溢出
当格式化输出对一个数据结构进行越界写时就可能导致缓冲区溢出
//脆弱性代码
int main(){
char outbuf[512];
char buffer[512];
sprintf(buffer, "ERR Wrong command: %.400s", input);
sprintf(outbuf, buffer);
}
//假设input构造成格式化字符串"%497d\x3c\xd3\xff\xbf"
//执行第一个sprintf时,只是简单地把buffer赋值为"ERR Wrong command: %497d\x3c\xd3\xff\xbf"
//执行第二个sprintf时,识别到buffer中的%497d,就会向outbuf写入497个字符(从程序栈中取),加上前面的普通字符在内,刚好能够占满outbuf和ebp
//剩下的\x3c\xd3\xff\xbf就会覆盖返回地址,去执行攻击者在该地址写的恶意代码
修复方案:
第二个sprintf在这里只是起到简单的复制作用,但是其识别格式化字符串的特性让攻击者有机可乘。
第一个sprintf已经限制了输入的字符数,因此可以把第二个sprintf()替换成strcpy()
被恶意利用后main函数的栈帧
查看内存
占位符+字母对栈的操作
%x 输出栈的数据
%s 把栈中的数据当做地址,获取这个地址的数据。
//脆弱性代码
int main(){
scanf("%s", s); printf(s);
}
假设输入"\xdc\xf5\x42\x01%x%x%x%x%s",程序运行时
printf函数的部分栈帧(低地址->高地址)
...
&"\xdc\xf5\x42\x01%x%x%x%x%s"
...
main函数的部分栈帧(低地址->高地址)
...
0142f5dc //小端存储
&"%x%x"
&"%x%x"
&"%s\0"
...
因为在程序栈中,main函数(主调函数)的栈帧比printf函数(被调函数)的栈帧地址高
而格式化字符串从低地址往高地址按顺序取出数据
假设此时0142f5dc在栈中比格式化字符串&"\xdc\xf5\x42\x01%x%x%x%x%s"高5个栈,那么:
%x%x%x%x依次取出4个数据
%s根据0142f5dc这个地址,读取内存中该地址的数据
在上述过程中,"\xdc\xf5\x42\x01"是攻击者想要读取的内存地址,%x移动参数指针,%s获取内存地址的数据
缓解策略
1、限制输入的字节数 2、排除用户输入的格式化字符串
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异