(一)函数调用约定
这里只介绍x86-64,即最常见的指令架构(64位)
参数 寄存器
arg0 rdi
arg1 rsi
arg2 rdx
arg3 rcx或r10,具体情况具体分析
arg4 r8
arg5 r9
arg6 栈中寻找
(二)格式化字符串
这里只对64位的格式化字符串进行分析,32位下的情况及更详细的原理可以参照其他博客:好好说话之格式化字符串漏洞利用-CSDN博客
1. 概述
C语言的printf()中规定了参数以何种形式输出的字符串
e.g.
%c, %s, %d, %f, %p, %n
当遇到形如printf("hello, %s", name);的代码时,name字符串的内容就会填充并代替%s,然后输出这个字符串
所以,格式化字符串(printf()函数的第一个参数)是决定printf()函数输出的关键
而程序猿的偷懒,会导致一些严重问题:
# include <stdio.h>
# include <stdlib.h>
int main()
{
char fmt[] = "hello, world!\n";
printf(fmt);
return 0;
}
这里的程序编译后运行会正常输出"hello, world!\n",但是,当fmt被用户控制时,就会产生意想不到的后果
2. 泄露
这里先不讨论fmt怎么被用户控制,我们直接看看其可能产生的后果:
# include <stdio.h>
# include <stdlib.h>
int main()
{
char fmt[] = "%p\n";
printf(fmt);
return 0;
}
编译后运行,产生的输出如下:

这里输出了一串奇怪的地址(这里是16进制数字,是“指针”,也即“地址”)
通过调试,我们能够清晰的看到第二个参数(arg1, rsi)对应的地址是 0x7fffffffd7a8

继续运行后验证输出:

经分析可以知道,这个%p会输出参数的“地址”
3. 修改
%n这个参数的作用是:将printf()函数已经输出的字节数(函数会自己统计)作为int型变量(4个字节)写入该参数作为地址的内容处
这里有点绕。我们以上面printf函数的参数为例来解释:
format: 0x7fffffffd684 ◂— 0x67724400000a7025 /* '%p\n' */
vararg: 0x7fffffffd7a8 —▸ 0x7fffffffdb26 ◂— 0x65722f656d6f682f ('/home/re')
第二个参数(arg1)的值是0x7fffffffd7a8, 其地址是rsi寄存器,而%n会修改的值是0x7fffffffdb26。
我们尝试一下:
# include <stdio.h>
# include <stdlib.h>
int main()
{
char fmt[] = "aa%n\n";
printf(fmt);
return 0;
}
调试:

这里提前printf了两个'a'字符,所以如果覆盖应该会把0x7fffffffd7a8覆盖成0x7fff00000002
继续执行,验证:

(三)栈溢出
这一题的栈溢出非常简单,不作过多赘述
只需要知道,函数在层层调用过程中,是需要把返回地址(return 0;等C语言语句执行后从被调用函数退出时需要跳转到调用函数的地址)存放入栈中的,所以如果栈可控,攻击者在函数返回前将返回地址修改,就可以让程序执行到其他位置去,从而实现控制程序执行流
原理图:

(四)栈上格式化字符串
1. 参数序号选择
如何利用可操作的栈上数据实现格式化字符串泄露/修改?
之前已经学过了如何利用格式化字符串进行简单的泄露和修改,但是细心的同学会发现我们泄露和修改的都只是在非常有限的范围内进行的,比如只泄露和修改了rsi(第2个参数, arg1)的值,如何扩大范围,实现"任意地址"读、写呢?
聪明的同学可能会说:%p%p%p%p%p%p……一直重复,就能泄露出好多内容
是的,但是太麻烦了,我们有更简单的方法:%k$p
k是一个具体数字,通过此决定是第k+1个参数,例如,%6$p,表示第arg[6],是第7个参数,栈顶的数据。
由此还可以通过%k$n向arg[k](第k+1个参数)写入数值,在这之前可以通过%kc提前输出k个字符
2. 栈上实现任意地址写
但是写的地址如果只是栈上的还是太有限了,有没有办法实现任意地址读写?
有的兄弟有的。
利用条件:
可控栈区
明确的地址
利用address + '%k$n'拼接的形式(详细可上面的参考文章),这里提一个64位程序区别于32位程序的点:
64位的地址有8个字节,高位的4个字节全都是\x00空字节,因此在32位系统下可行的address + '%k$n'方案会因为64位系统下address里含有\x00而被截断。所以64位下应该将格式化字符放在address之前:address + '%k$n'
下面是一个实例

图中绿色框住的部分是我的输入:
%43947c 决定了我的输入值为0xabab
%13$n 决定了我要吸入的是第13个参数(arg[12]),是7+6 = 13得出的
0x404040是我们的目标,再次分析一遍:0x7ffd8550ad78 —▸ 0x404040 (data_start) ◂— 0,这里0x7ffd8550ad78 是参数的地址;0x404040 是参数值,它将作为地址;0x404040 中的值0 是我们的目标,也就是即将被0xabab所覆盖的值
由此,可以实现任意地址写
二、题解
1. 分析
上述前置知识交代清楚之后,这道题就相对简单了
首先我们使用IDA查看C语言伪代码(这题的难度还不到需要看汇编的程度)
查看main函数里的vuln函数,这是漏洞点所在:

发现printf(buf)的存在(后续跟了很多参数,这是 大概 是因为buf不是静态的全局变量,反汇编器没能根据buf确定后续参数个数,所以把可以作为后续参数的值全部列举出来了),说明这里有格式化字符串的漏洞。
由于需要getshell,我们要找到system函数的位置,定位到backdoor函数(0x00000000004011B6),发现其调用了system(cmd),cmd是可写的全局变量

cmd的值为"ls",表示列出当前目录下所有文件名,
考点:既然cmd字符串可写,能否将其覆盖成我们想要执行的"/bin/sh"字符串(或者简短的写法,"sh"字符串也可以,效果是一样的)
因此,我们的思路如下:
首先利用栈溢出,将返回地址覆盖成backdoor里的system
在执行到返回地址之前将cmd的值覆盖成"sh",实现system调用的是"sh"
2. exp
from pwn import *
io = remote("121.36.4.0", 1002)
cmd = 0x404050
backdoor = 0x4011BE
payload = f'%{???}c%{?}$hn'.encode().ljust(???, b'\x00') + p64(cmd)
payload = payload.ljust(???, b'\x00') + p64(backdoor)
io.send(payload)
io.interactive()
这里隐去了关键数值,请同学们学习明白之后再补充完整。
思考题:backdoor的地址明明是0x00000000004011B6,为什么exp里要写0x4011BE?