1 预备知识
在c语言中的printf
,fprintf
,sprintf
,snprintf
等print
函数经常会用到类似%
形式的一个或者多个说明符。比如printf("my name is %s",panghu);
中的%s
。
说明符如下
说明符 | 注释 |
---|---|
%d | 以十进制整数的格式输出 |
%s | 以字符串的的格式输出 |
%x | 以十六进制数的格式输出 |
%c | 以字符的格式输出 |
%p | 以指针的格式输出 |
%n | 到目前为止所输出的字符数(把一个int值写到指定的地址去) |
%n
是一个不常见的格式化字符串,可以将一个int
型的值(4字节)写入指定的地址中,这将可以实现栈空间的随意改写。
除了%n
,还有%hn
,%hhn
,%lln
,分别为写入目标空间2字节,1字节,8字节,这里的字节对应的是参数的地址开始的几字节。
以如下程序举例:
#include <stdio.h>
int main() {
int c = 0;
printf("the usage of %n", &c);
printf("c = %d\n", c);
printf("%p", c);
return 0;
}
执行结果如下
the usage of
长度为13,而0xd为13 指针形式16进制表示。而想打印c
的地址还得用 printf("%p", &c);
来。
2 漏洞产生机理
printf
并不是一般的函数,它是C语言中少有的支持可变参数的库函数,所以,在被调用之前,被调用者无法知道函数调用之前有多少个参数被压入栈中。所以printf
函数要求传入一个format
参数以指定参数的数量和类型,然后printf
函数就会严格的按照format
参数所规定的格式逐个从栈中取出并输出参数。
比如在 printf("%s %c %d","num",'a',2);
中,format
参数有3个格式串。
#include "stdio.h"
#include "string.h"
#include <stdio.h>
int main()
{
printf("%s %c %d %d %d %d %d %d %d\n","num",'a',2, 1, 2, 3, 4, 5, 6);
return 0;
}
用gcc -o no_format no_format.c -g
命令编译后
查看main
函数汇编代码,发现printf
的前6个参数从左到右分别用rdi, rsi, edx, ecx, r8d, r9d保存,直到第7个才开始压栈。
再看一个有问题的
#include "stdio.h"
#include "string.h"
#include <stdio.h>
int main()
{
printf("%s %c %d %d %d %d %d %d %d %x %p\n","num",'a',2, 1, 2, 3, 4, 5, 6);
return 0;
}
栈中的数据如下
其中,CF0
为rsp,D10
为rep。
执行结果
当然,仅仅有问题还不够,还得知道如何利用,用CTF题目改版后的代码来看看
#include "stdio.h"
#include "string.h"
#include <stdio.h>
int main()
{
int pwnme = 0x11111111;
char v8[100];
puts("leave your message please:");
gets(v8);
puts("your message is:");
printf(v8);
printf("\n");
if ( pwnme == 8 )
{
puts("you pwned me, here is your flag:\n");
}
else
{
puts("Thank you!\n");
}
return 0;
}
用gcc -m32 -fno-stack-protector -o middle_format middle_format.c
编译,这里用32位编译,不然64位都是8字节有点难搞。
假设能够让pwnme
的值为8就算胜利,这里可以利用printf
的%n
参数修改pwnme
的值。
用IDA查看堆栈信息,发现v8
在ebp - 0x70
的位置, pwnme
为 ebp - 0xC
。
输入v8
为abcdefgh-%p-%p-%p-%p-%p-%p-%p-%p-%p
测试v8
在printf
参数第几个位置
可以看到从第6和第7个参数的位置分别均为0x64636261
和0x68676665
(linux小段存储),即v8从第6个参数开始(4字节一个)。
(代码执行过程中的很多信息都保存在栈上),比如调用printf
时堆栈保存的数据为(这里不考虑ebp,esp指向位置,只考虑堆栈内容)
栈 |
---|
printf 函数局部变量等 |
main 函数的 ebp |
返回地址 |
printf 参数1(aaaa-%x)等等 |
main 函数局部变量 |
执行printf
时,main
函数ebp和printf
函数局部变量等位于printf
的栈帧中,剩下3个位于main
函数的栈帧(不考虑现在用寄存器保存调用参数)。
而需要利用printf
泄漏或者修改的为 main
函数的一些局部变量(比如局部变量v8,它相对于printf
的参数相对位置为6个int
变量,而根据IDA信息推测,v8在-0x70,而pwnme
在-0xC,中间隔了112-12=100个字节,间隔相对位置为25个,所以修改的为25 + 6 = 31)。
那么怎么修改呢?肯定是用%n
和$
操作符。这里%6$n
表示把之前输出的长度赋值为第6个参数,不包括%6$n
的长度。而这里修改pwnme肯定是%31$n
。payload为aaaaaaaa%31$n
。
假如输入abcdefgh-%6$p
可以看到参数区的和main局部变量区的abcd
都输出了
现在看看第31个参数位置是什么,输出0x11111111(即pwnme
)。现在要修改pwnme
的值为8,那么输入即abcdefgh%31$n
(随便8个可打印字符都可以)。
可以触发了保护机制,不知道怎么关闭,希望大神可以指教指教。