pwn入门-格式化字符串初探
pwn入门-格式化字符串初探
C语言中格式化字符串常用占位符
了解了格式化字符串的工作原理,在利用格式化字符串漏洞之前,我们还需要对占位符有着深刻的理解,我们就来回顾一下C语言格式化字符串中常用的占位符:
占位符 | 含义 |
---|---|
%d | 以十进制形式输出整数 |
%u | 以十进制形式输出无符号整数 |
%x | 以十六进制形式输出整数 (小写字母) |
%X | 以十六进制形式输出整数 (大写字母) |
%o | 以十进制形式输出整数 |
%f | 以浮点数形式输出实数 |
%e | 以指数形式输出实数 |
%g | 自动选择%域者%e输出实数 |
%c | 输出单个字符 |
%s | 输出字符串 |
%p | 输出指针的地址 |
%n | 将已经输出的字符数写入参数 |
%c字符串通常用来输出单个字符,比如printf(“%c”,65),输出即为A,65为A的ascii码,%c将int参数转为unsiened char类型输出:但结合field width这个参数,就可以输出大量字符,比如:printf(“%1088c”):这行代码
以上这些就是常用的占位符了,而在我们格式化字符串漏洞利用中,常用%p来泄露地址,使用%n来实现向指定地址写入数据(4字节),我们还通常会使用%hn(2字节),%hhn(1字节),%lln(8字节)进行写入。
而在我们格式化字符串漏洞的利用中,我们通常还会用到正常开发很少用到的字符:数字+$的形式。
我们可以使用数字+$的形式,直接指定参数相对于格式化字符串的偏移,我们来看看这个程序:
1 | int main() { |
这样,当程序看到%3$s的时候,就不是直接找相对于格式化字符串的第一个参数了,而是去找相对于格式化字符串的第三个参数,这样的话,就会输出cccc,而整个程序输出cccc bbbb aaaa。
1 | int main() { |
任意地址泄露:%数字$s
任意地址写:%数字c%数字$n
假设我们想要将 0x41
(65的十六进制表示)写入到某个特定的内存地址(例如:0x601048
)。
思路:
- 使用
%65c
来确保字符计数器达到65。 - 使用
%n
将这个字符计数器的值(65)写入到指定的内存地址。
1 |
|
开了PIE
寄存器传参顺序:rdi, rsi, rdx, rcx, r8, r9
RDX第二个参数,RCX第三个
到rsp就是第六个,下面6,7,8,9,10(因为超出了寄存器,剩下的放在了栈里)
- 第 7 个 参数位于栈偏移
-038
(0x7fffffffde78
) - 第 8 个 参数位于栈偏移
-030
(0x7fffffffde80
) - 第 9 个 参数位于栈偏移
-028
(0x7fffffffde88
) - 第 10 个 参数位于栈偏移
-020
(0x7fffffffde90
)
055x24f地址-0000124F地址 = 基地址
输入%15$p
输出0x55555555524f
因为从上往下数是第十五个
%s是把指向的地址读出来,%n是把指向的地址去写,攻击者可能利用 %n
改写内存中的关键数据,比如返回地址、全局变量等,导致格式化字符串漏洞。
不是直接读第几个参数,而是把第几个参数当作一个地址去修改
这是什么意思呢?
**aaaaaaaa
**:printf
将会先打印出 8 个字符 a
。字符计数器现在为 8。
%9$n
:printf
会寻找第9个参数。将当前字符计数器的值(8) 写入到第9个参数指向的内存地址。
一些杂知识:
0x7fffffffde88 —▸ 0x7fffffffdeb0 ◂— 0x0
**
0x7fffffffde88
**是栈上的一个地址,表示某个局部变量或栈帧中的一个存储单元。
—▸ 0x7fffffffdeb0
0x7fffffffde88
的内容是一个指针,指向另一个栈地址0x7fffffffdeb0
。- 也就是说,在
0x7fffffffde88
这个位置存储的值是0x7fffffffdeb0
。**
◂— 0x0
**在0x7fffffffdeb0
这个地址处,存储的值是0x0
。
问题:
- 当前没有任何指针指向
0x7fffffffdea8
。 - 但我们可以控制栈上的输入内容(
0x7fffffffde90
)。
第一步:控制指针 0x7fffffffde90
修改为指向目标地址 0x7fffffffdea8
第二步:写入目标值,%9$n
:将计数器的值 100
写入第 9 个参数指向的地址(现在是 0x7fffffffdea8
)。
exp:
1 | from pwn import * |
0X18怎么来的?根据输入地址和rbp算的,为什么要写-0x18而不是0x8呢,因为那里不是我们可控的
test3_addr和后面的能不能对调?可以,因为printf会从栈上取参数,如果要改变test3addr我们只需要改变$后的数字就行
payload = p64(test3_addr) + b’%100c%10$hhn’ 为什么是错的?
注意:
c语言字符串是以00结束的,不以00开始,64位系统,地址只用6个字节,0x00007fff88888888,因为是小端序,那个实际发送的顺序是从右向左发的。因为地址前面有/x00,会导致printf函数截断。所以后发地址,但是这样一改栈的位置会发生改变,因此需要改成%12$hhn。
需要改成payload = b’%100c%12$hhnaaaa’ + p64(test3_addr)
因为是6字节对齐,所以我们原本的那边是12字节也就是
aaaaaaaa
aaaatest3
这样就会出问题,所以我们得补全(对齐)
aaaaaaaa
aaaabbbb
test3_addr
32位:
1 | from pwn import * |
还有个问题,就是如果我们要把test_addr放在前面就需要给100-去前面的输入
因为%n是将已经输出的字符数写入参数
%96c
中的 96
是指 96个字节,而不是字长。
星盟基础
fmtarg 0xffffcf88(快速识别地址是第几个参数)
p/x 0xaaa - 0xbbbb
System和printf的地址有5位不一样,由于格式化字符串最小只能修改到1字节,所以我们至少需要修改printf函数的3字节。%188c%6$hn这一串格式化字符的作用是往printf函数的第7个参数(相对于格式化字符串的第6个)写入188,108占2字节。