从一个问题出发的关于printf中处理fmtstr部分源码分析
0x00
在第六届强网拟态线下赛的一道格式化字符串漏洞题目中,遇到了一个情况没有想通,找了个时间分析了一下 printf
的源码(glibc2.31),分析清楚原因了
0x01 issue
奇怪合理的现象
1 | payload = '%{}c'.format(printf_ret).encode() + b"%11$hn" + \ |
最初我是这样写的,调试发现仅仅成功修改了跳板,而目标,也就是printf
的返回地址却没有修改到
1 | payload = b'%p'*9 + '%{}c'.format(printf_ret - 90).encode() + b"%hn" + \ |
而换个偏移方式,这样却能同时修改成功
0x02 why?
探究一下多个 % 偏移参数和 $ 偏移参数在格式化字符串中的差异
我们找到源码中处理格式化字符串的部分(glibc2.31)
1 | /* Process whole format string. */ |
其中涉及到的几个跳转表结构都在宏STEP0_3_TABLE
以及STEP4_TABLE
里,定义如下
1 |
|
省略部分基本都长一个样,就是按照顺序,先处理宽度精度等等,在根据特定参数进行操作
来分析一下处理流程,以%n
为例,在宏process_arg(fspec)
中
1 | LABEL (form_number): \ |
看到这里判断 fspec
的状态,为NULL
则直接从va_arg(ap, type)
来逐个顺序取参,也就是线性的先取寄存器再取栈上参数,并且实时修改完成(**printf
函数利用格式化字符串减去的应该是后五个寄存器,rdi
本身是一个字符串地址,%
占位符解析的是从 rsi
开始的,五个寄存器再加栈内存单元**)
如果不为 NULL
则从args_value[index]
取参数,这里的index
索引是提前确定的。哪里确定呢?当使用 $ 来确定参数时候,会进入一个特殊的处理函数
1 | if (*f == L_('$')) |
1 | do_positional: |
会调用 printf_positional
函数,这个函数长的要命,不全贴,展示一下关键流程。
先是对参数存储区域的初始化,不赘述;然后依次将参数从va_arg
放入 args_value[]
1 | /* Fill in the types of all the arguments. */ |
这相当于建立了一个“参数快照”,也就是保留了此时的全部参数情况,然后重新处理整个fmtstr
1 | /* Now walk through all format specifiers and process them. */ |
这里也是使用了一样的处理的宏,见此前处理流程
1 | do |
但是printf_positional
函数传入的 fspec
不再是NULL
,而是(&specs[nspecs_done])
,此时就会根据 args_value[]
来取参数,并且这时候即使对参数做了更改,也不会影响到建立的“参数快照”,这也就解释了
1 | payload = b'%p'*9 + '%{}c'.format(printf_ret - 90).encode() + b"%hn" + \ |
这样可以一次性改成,而
1 | payload = '%{}c'.format(printf_ret).encode() + b"%11$hn" + \ |
这样却不行的情况了