本文为基于 CTF WIKI 的 PWN 学习
先附一张经典的图, 如下
其栈上布局如下:
some value |
3.14 |
123456 |
addr of "red" |
addr of format string : "Color %s, Number %d, Float %4.2f" |
如果程序写成了:
printf("Color %s, Number %d, Float %4.2f");
分别将栈上的三个变量分别解析为:
解析其地址对应的字符串
解析其内容对应的整形值
解析其内容对应的浮点值
我们编写程序验证以下:
- #include <stdio.h>
- int main() {
- printf("Color %s, Number %d, Float %4.2f", "red", 123456, 3.14);
- printf("Color %s, Number %d, Float %4.2f");
- return 0;
- }
输入以下命令编译(记得安装 libc6-dev-i386 库):
gcc -m32 -fno-stack-protector -no-pie -o leakmemory 1.c
运行结果
gdb 调试一下:
b printf 后输入 r 运行
可以看到在第一个 printf 的运行中, stack 的参数正如上文所说, 先是格式化字符串, 再是 123456(的 16 进制), 最后是 3.14
继续调试至第二个 printf(一直按 n)
stack 中我们发现, 首先入栈的依旧是格式化字符串, 但是上面三个参数不再是之前的那几个了. 按照原理, 第二次输出的应该是 0xffffcfb4 及之后的两个内存对应的内容. 下面我们来细致讨论.
0x01 漏洞利用
利用格式化字符串漏洞, 我们还可以获取我们所想要输出的内容. 一般会有如下几种操作
泄露栈内存
获取某个变量的值 (%s)
获取某个变量对应地址的内存 (%p)
泄露任意地址内存
利用 GOT 表得到 libc 函数地址, 进而获取 libc, 进而获取其它 libc 函数地址 (addr%n$s)
盲打, dump 整个程序, 获取有用信息.
一, 泄露内存
(1)获取栈变量数值
这里使用 ctf wiki 上面的例子:
- #include <stdio.h>
- int main() {
- char s[100];
- int a = 1, b = 0x22222222, c = -1;
- scanf("%s", s);
- printf("%08x.%08x.%08x.%s\n", a, b, c, s);
- printf(s);
- return 0;
- }
编译运行调试
调试:
直接转载 (copy) 了: 可以看出, 此时此时已经进入了 printf 函数中, 栈中第一个变量为返回地址, 第二个变量为格式化字符串的地址, 第三个变量为 a 的值, 第四个变量为 b 的值, 第五个变量为 c 的值, 第六个变量为我们输入的格式化字符串对应的地址. 继续运行程序, 按 c
将会把上图中 0xffffcf44 及其后面两个地址包含的内容输出输出:
并不是每次得到的结果都一样 , 栈上的数据会因为每次分配的内存页不同而有所不同, 这是因为栈是不对内存页做初始化的. 这可以从我上面的几个截图结果看出来.
(2)获取栈指定变量值
可以使用 %n$x 获得栈上第 n+1 个参数, 格式化字符串是第一个参数, 那么如果想获得 printf 的第 n 个参数, 就需要加 1.
如, 我想获得第三个参数值 f7e946bb, 那么我就输入 %3$x
(3)获取对应字符串:%s
(4)获取数据:%p
二, 获取任意地址内存
上面的泄露并不强力, 比赛中经常需要泄露某一个 libc 函数的 got 表内容, 从而得到其地址, 进而获取 libc 版本以及其他函数的地址, 这时候, 能够完全控制泄露某个指定地址的内存就显得很重要了.
这里我们再看一遍源程序代码:
- #include <stdio.h>
- int main() {
- char s[100];
- int a = 1, b = 0x22222222, c = -1;
- scanf("%s", s);
- printf("%08x.%08x.%08x.%s\n", a, b, c, s);
- printf(s);
- return 0;
- }
scanf 接收入 s 的值, 然后两个 printf. 这里我们输入 %s, 如下调试, 打印出 0xff007325, 就是 %s 对应的字符串值, 所以, 输出函数的栈分布, 栈上的第一个参数就是格式化字符串的地址.
这就意味着格式化字符串内容可控, 同时, 还需要注意的是, 第一个参数虽然放置的是格式化字符串的地址, 但是, 输出函数并没有在这里开始调用, 你也可以从上图中看到, 在 0xffffcf50 处, 又有一个 %s, 这里才是调用格式化字符串的时候, 输出格式化字符串表达的内容时刻. 这就意味着, 因为格式化字符串我们可以自己控制, 那么, 如果我格式化字符串里面包含了 %s, 它会输出 %s 对应地址 (0xff007325) 所包含的内容, 如果包含 scanf@got, 它会输出 scanf@got 对应地址包含的内容, 也就是 scanf 的真实地址.
总结: 1, 格式化字符串可以按照自己的意愿输入. 2, 格式化字符串的地址为栈上的第一个参数, 顺序之后的某个位置会调用这个格式化字符串, 以格式化字符串的内容输出内容.
所以, 我们只要知道, 调用这个格式化字符串的位置就可以了.
根据 CTF WIKI 上的说明方案, 我们可以使用下面的字符来确定格式化字符串在哪调用:
[tag]%p%p%p%p%p%p...
如果输出栈的内容与我们前面的 tag 重复了, 那么我们就可以有很大把握说明该地址就是格式化字符串的地址, 之所以说是有很大把握, 这是因为不排除栈上有一些临时变量也是该数值(0x41414141). 如:
AAAA 0XFFD2RC30 0XC2 0XF7E596BB 0X41414141 0X702570250
我们调试看一下:
我输入的是 AAAA 加上 8 个 %p
你会看到, AAAA 后面依次输出 8 个内容,
第一个输出 AAAA, 这本来就是字符, 作为一个标志显示出来罢了. 然后往后,%p 开始作用, 依次是 0xffffcfa0(可以看到格式化字符串为第一个参数,%p 从格式字符串下一个开始),0xc2, 0xf7e946bb 这些都是跟着格式化字符串后面的参数, 之后, 便打印出来 0xffffcfa0 地址对应的内容, 即字符串. 也就是说, 其相对 printf 函数, 为第 5 个参数(第五行), 但是相对格式化字符串(第一行), 是第四个参数. 那么既然是第四个参数, 我们使用 %4$s 看看测试一下.
然后你会发现 core dump:
为啥? 调试.
首先,%4$s 对应的存放地址为 0xffffcfa0, 我们查看内存发现存着的是 0x73243425, 再看看 0x73243425 放着什么, 啥都没有, 那肯定崩溃.
我们输入 %4$s 是 0x73243425, 我们输入 %5$s 是 0x732434525,................
那就是说, 我们确定了参数为第几个后, 在 tag 处输入想要获得的内容的地址, 那么, 输出的将是输入的地址对应的内容.
然后使用 CTF wiki 上 payload 改改就可以实现获取 scanf 的地址:
- from pwn import *
- sh = process('./leakmemory')
- leakmemory = ELF('./leakmemory')
- __isoc99_scanf_got = leakmemory.got['__isoc99_scanf'] #获取 got 地址
- print hex(__isoc99_scanf_got)
- payload = p32(__isoc99_scanf_got) + '%4$s' #想要输出的地址加上确定好的参数位置
- print payload
- sh.sendline(payload)
- sh.recvuntil('%4$s\n')
- print hex(u32(sh.recv()[4:8])) # 去掉 __isoc99_scanf@got 的地址
- sh.interactive()
来源: https://www.cnblogs.com/pwn2web/p/12077965.html