PWN-基础-格式化字符串
格式化字符串基础
前置知识
- C语言中关于转义序列
- C语言中的格式化字符
例题1
- 源码
1 |
|
- %s:输出字符串,以空字符串,ascii码0x00,为结束标志,可以打出指针所指向内存里面的字符串
1 | char buf[0x8]={}; |
- %p,打印出变量的地址
- 变量地址在栈上就打出栈的地址
- 变量地址在堆位置、在非栈非堆的位置都会打出来相应的地址
1 |
|
- %x,显示无符号16进制整数
1 |
|
-
%d,显示有符号十进制数,不多说
-
%c,显示字符类型
-
在格式化字符串%加数字再加相应字母,表示对齐,例如:%5d
- 输出的则至少占5位数,输出10:口口口10
- 输出10000:10000
- 输出1000000:1000000
- 总之就是不够位数用空格占位,位数足够就不占位,位数超了就让他超
-
%n记录一个printf在%n之前打印了多少个字符,然后写入printf参数传递的值a,该值a作为地址,将统计的字符写入地址a内存里面。
动调查看程序内部
格式化字符串漏洞
- 在格式化字符串过程中,会导致一些内存的地址泄露,进而可以得到获取权限的机会
- 在没有开保护下还可以使用
%10$p
可以将第10个地址打出来
1 | 例如: |
- 在printf函数调用时,会传递参数。一开始字符串传递给rdi,%p对应的参数传递给rsi,%s对应的参数传递给rdx…
例题1 x64
- 程序源码
1 |
|
- 查看保护
gdb动调
地址的泄露
- 将执行步骤调到dofunc中输入阶段,进行输入,查看输入的数据存储在哪里,发现输入的a最终会存储到栈上
- 再动调到read函数执行后首先执行的printf函数,查看源码,是
printf(buf1)
- rsp所指的位置,是printf函数传递的第6个参数,下一个高地址就是第7个参数,以此类推
- 再来查看栈,会发现第十四个参数到了rbp所指的栈了,该栈中存储的是下一个rbp指向的位置,第十五个参数就是就是返回地址了。
- 如果能把返回地址打印出来,再减去偏移量,就能知道整个程序的基地址了,就能进行后续操作了SQQQ
- 尝试格式化字符串漏洞泄露地址。先回到call read指令,ni一步,然后输入%15$p,对第15个参数(rbp下一位的参数)进行泄露。
- 再跳转到printf函数查看输出结果,会发现把返回地址泄露出来了
- 还可以泄露libc的地址
数据的写入
-
写入原理
- %s是将传递的形参的值a作为地址,去寻找地址a,读取并输出a里面存储的字符串
- %n是将传递的形参a作为地址,去寻找地址a,在a中写入数据
-
尝试写入,先用%9$p打印出地址的值,发现打印的是栈中存放的数据
- 接下来尝试写入,回到call read阶段,写入aaaaaaaaa%9$n
- 打印出来后查看栈,发现地址被写入了0x7fff00000009
- %n是int类型的写入,所以才写入了4字节,000000009,写入的9是前面输入a的个数9
- %hn、%hhn,会写入更小的字节
如何获得例题1的shell
- 查看源码和对应的汇编
- 发现[rbp-8]要与0x64比较,没有直接指向[rbp-8]的地址,要修改改地址里面的值就不太方便修改里面的值了
- 栈里面没有存储[rbp-8]这个地址,但是在栈中发现有一个栈,栈中存储着另外一个栈的地址(尤其是rbp所指的位置)这时就要用%n进行写入操作。也可以利用read写入地址。(前提栈上的地址要泄露出来)
- 或者利用%n将输入的数据进行修改操作
- 先将栈的地址给泄露出来,在read写入%14p,使得printf打印出地址
- 再查看read函数写入栈的起始位置0x7fffffffdfc0,这时就需要通过泄露地址
0x7fffffffdff0 - 0x30
得到起始地址
-
进行下一次的循环之后,用read写入,要比较的地址[rbp-8]即:0x7fffffffdfd8,再通过%n修改指定栈中值所指内存地址里面的值,即可取得shell。这里为了方便就不用exp实现,直接修改栈中的值。
-
利用read函数输入,%100c%8$hhnaaaa
- 设置0x7fffffffdfb0的值为[rbp-8]即:0x7fffffffdfd8
- 这样运行一下就可以修改[rbp-8]即:0x7fffffffdfd8里面的值。这里在运行printf函数时出现段错误,就用set模拟了一下。用脚本这样打肯定是不成功的。
栈对齐
- 在用脚本打时还要注意栈对齐和其他一些小细节问题
- 先放exp:这里用的是别人的exp
1 | from pwn import * |
- 注意点1:stack_1 = int(io.recv()[2:14],16)
1 | 用%p用printf打出来是16进制的,带有0x前缀(0x7fffffff1231),这是不需要接收。 |
- 注意点2:栈对齐,payload = b’%100c%12$hhnaaaa’ + p64(test3_addr)
1 | 0x7fffffffdfc0 ◂— 'aksmdaasdas\nUU' |
- 注意点3:是**%100c%12$hhnaaaa还是%92c%12$hhnaaaa**
1 | %n写入的是前面所统计一共输入多少字长。 |
总结
- 用格式化字符串泄露
栈上格式化字符串
- 找到偏移地址
- 任意地址的写入
- 修改什么地方的值
- 修改栈上的值
- 修改栈上特定值,以便可以得到shell
- 修改返回地址到system,修改返回地址的后三个,做到模拟调用system(‘/bin/sh’)
- 修改got表项
- 修改one_gadget
- 修改malloc_hook:printf参数超过 %43$p(大概这个数),printf就会调用malloc
- 修改iofile
- 修改栈上的值
- 寻找system的地址
- 泄露got表项地址
- 在开启pie无法泄露的情况下,还需要理由内存中有的地址来间接计算(如返回地址等)
x86栈上格式化字符串
- 源码,有一个通用的特点,一般都有一个while的循环
- 由于字符串漏洞可以泄露任意地址,这就导致gcc编译对于字符串格式化的保护就很全,导致没办法打
1 |
|
gdb动调
- si步入dofunc函数里面进去
- ni到这read这一步,继续ni,然后输入aaaa
- ni走到dofunc函数的printf函数
- 之后在输出点看清楚print函数的对应的传参
- esp所指的下面一个栈,才是32为printf函数传递的第一个参数
- 在看清楚对应的传参后,就可以重新走一遍,到上方的read函数,尝试着利用格式化字符串漏洞,输入4个a+10个%p
- 查看print函数输出的内容,对应栈的地址
格式化字符串漏洞利用
修改
- 利用read写入所需要的地址,在利用%n去间接修改地址里面的值
- 先进行尝试,利用read函数写入
aaaa%7$n
,ni到call printf时查看栈的情况
- 将栈地址0xffffd02c里面的内容0x61616161修改为,0xffffd028(也就是0xffffd02c上一个栈的地址),模拟用Python脚本打的时候写入地址。
泄露
在没有开启PIE的情况下
- 可以通过输入got表的地址,再利用格式化字符串泄露出libc地址
- 在调用read函数时写入
aaaa%7$s
,再将aaaa的值修改成为read的got表的地址,利用%s打印出libc的地址
- 会打出来红框数据,但是打出来的是不可见字符
在开启PIE的情况下
- 泄露返回地址
- 查看存储返回地址的栈地址
- 走到call printf时,查看esp指向的栈位置
- 计算返回地址所在的栈的位置是printf函数的第几个参数
- 输入%75%p后打出来的值为
- 这样就可以计算出main的起始地址
1 | main_addr_30 = 0x5655638e |
实行
- 用%hhn+循环修改地址,如果直接用%0xffff5a4c%7$n修改地址,那么%0xffff5a4c太大会导致程序崩溃
- 所以需要一个字节一个字节的修改
1 | 在0x56559010利用%hhn修改一个字节 |
- 这样就可以修改got表里面的地址
-
之后再发送
/bin/sh\0x00
-
可以利用pwntools工具:fmtstr_payload()
X64栈上格式化字符串
- 注意栈对齐和空字符串
非栈上的格式化字符串
-
前面的栈上的格式化字符串,声明的变量是在栈上的。
-
利用read函数写入的数据也存储在栈里面,这样就可以输入目标地址(该目标地址存储在栈上),利用%n对目标地址进行修改,这样就可以获取shell
-
而非栈上格式化字符串,无法通过read函数将目标地址存储在栈上,在利用%n获取目标地址。这时只能利用现有的栈里面的数据,自由度相对较低。
-
四马分肥:
- 内存一定要有和addr(比如printf_got表的地址)除最低2个字节外,其余部分要一样的地址,x64的要3个地址,x86的要2个地址。
- 修改完地址后,利用三连指针和格式化字符串
%hn
一键修改printf的got表,将其改为system的地址,然后得到shell - full relro(整个GOT表进行了保护,got表不可写)
- partial relro (与plt对应的got表仍然可以被修改,但其他与plt无直接关联的got表项是只读的。)
- 在开缓冲区的情况下该一键改两个字节,缓冲区可能会满,导致修改错误;关缓冲区的时候是以换行符为参考的,所以需要
sendline
-
诸葛连弩:
- 在一个四连的地址上重复修改地址,一直修改到目标地址,再返回该目标地址,得到shell
四马分肥
-
该题介绍的是四马分肥
-
源码
1 |
|
前提准备
- 查看一些可执行文件的保护
- 再运行一下程序
- 该程序会原样输出你所输出的内容,并且遇到字符串quit会退出程序
gdb动调查看
- 运行程序进入到play函数里面去
- 设置断点到
do_fmt
这个位置中去,再按c
进入到do_fmt
里面去
- 使用
ni
指令单步执行程序到call read
,在该处设置一个断点,记下read写入buf的地址0x5555555580a0 (buf)
,设置后ni
步过call read
,再输入8个a
- 继续步入到打印
call printf
,栈上是没有输入的aaaaaaaa
,就不能像栈上的格式化字符串一样操作了。
方法
-
利用栈里面现存的数据
栈地址a->地址b->地址c
,利用%$n修改的是地址c的值 -
前面gdb动调按
c
走到call read
断点处,ni
后输入aaaaaaaa
,在走到call printf
,走到该位置后stack 40
查看40个栈空间
- 利用栈上现有的数据进行格式化字符串漏洞的操作
- 查看一下got表,得知printf_got表
0x555555558028
- 使用IDA查看printf_got的偏移量为
0x4028
,由于内存的分页机制和段机制,可以得到地址的最后三位是一定不变的,所以printf_got表地址最后三位为0x028
。 - 这就给%$n修改地址内容的机会了
攻击思路
- printf_got
0x555555558028
——0x55555555802f
一共八个字节 - printf_got表里面存储的值为
0x7ffff7de66f0
即libc中printf函数的地址,将该数据存入内存地址里面。
1 | 一共内存里面存1个字节 |
- 在gdb动调中查看内存是不是这样存储
- 查看栈,看到
0x5555555552a1 (play+30)
,将0x5555555552a1
的后四位52a1
修改为8028
,这样就是printf_got的地址,去修改 - 将
0x5555555552c0 (main+28)
改为0x55555555802a
- 将
0x5555555552a4 (main)
改为0x55555555802c
- 将
0x5555555552a4 (main)
改为0x55555555802e
- 将这些地址改为printf_got的地址,分别俩个字节俩个字节的去改got表的值
- 这里又出现一个问题,这些紫色框内的数据是不可直接被修改的,那么就需要这种形式去间接修改紫色框内的数据
栈地址a->栈地址b->地址c
,rbp地址就成为很好的利用对象了。
总结
1 | 1.泄露栈地址 |
攻击
-
修改got表的地址
-
exp:
1 | from pwn import * |
诸葛连弩
-
本例题介绍的是诸葛连弩的方法
-
源码:
1 |
|
方法
- 寻找四连地址:如果没有现成的四连地址,那就先利用%hhn修改栈上数据,从而构造出一个栈上数据。在
call printf
这个汇编指令这边找,得到的参数会比较准确一点 - 下图中只寻找到了三连地址,没有找到四连地址,那就主动给改成四连地址。
- 利用
%hhn
将三连地址改成四连地址,此题甚至改成了六连地址,下图修改这样就有了四连地址- 这里需要注意的是如果找rbp的地址这个地址是随机的,这个要利用
%p
泄露栈上的地址,再去计算之后的偏移 - 如果找这种
0x7fffffffdfd8 —▸ 0x7fffffffe0c8 —▸ 0x7fffffffe2d3 ◂— '/home/myheart/pwn_learn/chapter_3/fmt_test3/fmt_str_level_2_buf_x64'
,这个地址就不会随机 - 还可以这样
0x7fffffffdf90 -> 0x7fffffffdfa0 -> 0x7fffffffdfb0 -> 0x1
,一步一步改为0x7fffffffdf90 -> 0x7fffffffdfa0 -> 0x7fffffffdfb0 -> got表
- 这里需要注意的是如果找rbp的地址这个地址是随机的,这个要利用
- 具体诸葛连弩的操作
- 具体的目标地址可以是got表(不能修改printf@got表)、返回地址、栈地址、逐个修改栈上数据构造ROP链等等
1 | 目标地址:printf@got:0x555555558028 |
不能修改 的内存
- 循环中要使用的内存,比如printf@got表
- 4链中所需要使用到的内存
格式化字符串星号的作用
- 一般会去掉符号表,稍微提升一点难度
示例程序
1 |
|
gdb动调
- 先步入到如图所示的位置
- 再查看栈,发现read函数会将随机的俩个字节的数读取到栈上
- 如图所示,read将
0xc2f1
读入栈中
- 然后再使用read读2个字节到下一个栈中
- 如下图,read将
0x6e33
读入栈中
- 然后步入到如下图所示的地方
- 根据程序源码得到,如果想要得到shell,那么两个随机读入的2字节数必须相等
- 这是需要用户输入一些值,这些值将存储在buf2中,并且使用printf打印出来,但是读入的值是在栈上的高地址,无法进行覆盖
-
这时就需要使用
%hhn
去修改 -
例如输入:
%100c%7$hhn
,%7$hhn
前面有100个字符,所以%7hhn
就会将100存入stack1 ->stack2 ->data
中的data里面去 -
但是这题的需求是俩个字符串相等,这时就需要使用
%*10$c%7hhn
,这样就会读取printf函数传入的第10个参数(栈)里面的值(假设是142),这样%*10$c%7$hhn
的效果就与%142c%7$hhn
一样了 -
printf传参如下
- 要像使得buf1和buf3的里面的值相等,那么就要输入
%*9$c%7$hn
,将栈0x7fffffffdf40
里面的值通过0x7fffffffdf38 —▸ 0x7fffffffdf40 ◂— 0xc2f1
三连进行修改这俩个字节,然后取得shell
格式化字符串开启FULL-RELRO保护
- 修改栈上的返回地址,构造ROP链