前置知识
printf
中的格式控制符
格式控制符告诉函数如何解析和处理传递给它们的参数。如果printf
中的格式控制符没有对应的参数,那么将会泄漏内存中的数据。一个%
可以解析一个参数,也就是对应一个参数。
%c
:以字符形式输出%d
:以十进制整数形式输出%x
:以十六进制形式输出,%7$x
表示输出参数列表中的第七个参数%p
:以十六进制形式输出,并加上前缀0x
%n
:将%n
之前printf
已经打印的字符个数赋值给偏移处指针所指向的地址位置。%n
:写入的地址空间为4
个字节%hn
:写入的地址空间为2
个字节%hhn
:写入的地址空间为1
个字节%lln
:写入的地址空间为8
个字节
pwntools
中的fmtstr_paylod
fmtstr_payload
是pwntools
中的一个函数,其函数原型如下:
from pwn import * |
非栈上的格式化字符串漏洞
我们通常所说的在栈上的格式化字符串漏洞的意思就是我们可以直接把数据写到栈上,然后对栈上的数据进行一系列的攻击。如A
是栈上的一个地址,B
是我们要写到A
里的printf
的got
表值,那么现在就是A -> B -> C
,C
是printf
函数的plt
表值。然后我们可以修改B
这个地址中的内容即C
,改成后门函数的地址。而非栈上的格式化字符串漏洞就是我们不能直接把B
写到栈上,但是依然可以泄漏和修改栈中的地址等数据。如果我们不能直接把B
写到栈上,我们可以先在栈上找到D -> E -> F
把F
修改成B
就变成了D -> E -> B
,但是我们最后的目的是修改B
指向的地址,所以说这里的E
必须是与A
和D
一样的栈地址,那么E -> F
就变成了E -> B -> C
,又回到了栈上。
例题
jarvisoj_fm
先看主函数,直接给我们system("/bin/sh")
,当x == 4
的时候才能执行这个函数。我们看到第10
行有个printf(buf)
再结合第9
行,不难知道这是一个格式化字符串漏洞。那我们就可以利用这个漏洞把x
写成4
,就可以拿到shell
。
先计算偏移量,偏移量为11
,然后构造payload
。我们要写入的内容是4
,巧的是我们要写入的地址也是4
个字节,所以我们直接发送地址就可以写了,payload = p32(value_addr)+b'%11$n'
。
exp
from pwn import * |
axb_2019_fmt32
我们看到read
函数,可以读入0x100(256)
个字节,而s
的长度为257
,因此没有办法溢出。继续往下看,printf(format)
这一行明显存在格式化字符串漏洞,程序中也没有system
函数和/bin/sh
字符串。那大概思路就是利用格式化字符串漏洞泄漏printf
的地址(本来我想修改read
的got
表值的,但是后来才相等修改后就相当于程序中没有read
函数了,这样肯定是不行的),算出libc
基地址以及system
函数地址。
怎么泄漏printf
的地址呢?本题中我们通过read
函数将输入的内容写到s
中,而s
又是printf
函数的参数,因此我们可以利用格式化字符串漏洞去泄漏栈上的内容,因此可以先把printf
在got
表中值写到栈上,再去泄漏程序运行起来后printf
在got
表中的地址即printf
的真实地址。这就需要计算我们写入的内容在栈上的位置的偏移量,输入aaaa %p %p %p %p %p %p %p %p %p %p %p
得到输出的内容数一下偏移量为8
。
不过我们也发现了0x20616161
中只有3
个a
,是因为format
中还有Repeater:
这些字符的原因,所以我们尝试着再输入Aaaaa %p %p %p %p %p %p %p %p
,发现0x61616161
刚好就是aaaa
。
得到偏移量后可以开始构造第一个payload
泄漏printf
地址了,如下:
p.recvuntil(b'me:') |
得到printf
真实地址后,可以计算libc
基地址以及system
函数的地址,并把printf
的got
表值改成system
函数的地址,这样再次传入参数 '/bin/sh'
再执行printf
时,由于将got
表给修改了,就相当于执行了system
函数 即:执行system('/bin/sh')
。这也解释了为什么刚开始我改puts
函数的got
表不行,因为就算把puts
的got
表的值改成system
函数的got
表值,我们后面也不会再执行puts
函数,也就执行不了system
函数。
法一
题目如果是32
位的程序,可以用pwntools
中的fmtstr_payload
函数直接修改地址,如下:
base_addr = printf_addr-libc.sym['printf'] |
exp
from pwn import * |
拿到flag
法二
一个字节一个字节的修改,这个方法对32
位和64
位的都适用。
先把两个地址打印出来,发现四个字节中只有最高字节f7
是一样的,我们们需要修改后3
个字节,也就是需要这样修改e3 --> e2
,10 --> 29
,20 --> 40
(当然地址是不确定的,肯定不能直接改数字)。
print('printf_addr =>',hex(printf_addr)) |
虽然每次地址都会变化,但是我们可以把每次的地址表示出来。sys_addr&0xff
将sys_addr
与0xff
进行按位与运算,得到的结果把sys_addr
的低8
位(8
位1
个字节)保留下来了,高位被全部置零。sys_addr&0xff00
将sys_addr
二进制形式的第9
到16
位保存下来了,其他位全部置零举个例子就是11111111 11111111 11111111
变成了00000000 11111111 00000000
。>>8
是再将结果右移8
位即00000000 11111111 00000000
变成了00000000 11111111
。(sys_addr&0xff00)>>8
就是把sys_addr
的倒数第二个字节保留下来了。
sys_addr1 = sys_addr&0xff |

然后下面这一步中sys_addr1-(9+13)
减的是printf
中前面参数字节的总和,程序中的Repeater:
是9
个字节,payload2 = b'a'+p32(strlen_got)+p32(strlen_got+1)+p32(strlen_dot+2)
一共13
个字节。(其实为什么要这样我也不知道)
sys_addr1_value = sys_addr1-(9+13) # 前面已经输出了0xa+0x10个字符,要减去 |
最后构造payload2
,bytes(str(sys_addr1_value),encoding='utf-8')
的意思是把sys_addr1-value
转换为字节串bytes
类型。%hhn
是修改低1
个字节,printf_got+1
会把printf_got
的倒数第二个字节变成倒数第一个,然后就可以修改低2
个字节了。
payload2 = b'a'+p32(printf_got)+p32(printf_got+1)+p32(printf_got+2) |
exp
from pwn import * |
拿到flag
axb_2019_fmt64
源码
思路
第一步,计算偏移量为8
到了与32
位程序不一样的地方了,泄漏地址这里如果是按照32
的写成payload = p64(puts)+b'%8$saaaa'
,然后我们看到发送的数据在puts_got
与%8$s
之间有很多'00'
,字符串中的'00'
就代表结束,所以在printf
到'00'
的时候就以为字符串后面没有内容了,后面的内容也就不会被printf
了。
因此我们现在要换个写法构造payload
,payload = b'%9$saaaa'+p64(puts_got)
这样我们可以看到'00'
就在后面了,有用的字符串也就不会再被截断了。9
是因为现在p64(puts_got)
变成了第2
个参数了。
刚开始我泄漏的是printf
函数的got
表值,但是不知道为什么一直不成功,后来看别的师傅的wp
泄漏的都是puts
函数就可以,后来我用read,strlen
这些函数都可以。然后我发现puts
函数发送的是0x601018
(重定位前),接收的是0x7fd7f1458690
(重定位后)
而printf
函数发送的与接收的一样都是0x601030
,也就是说并没有泄漏出printf
重定位后的got
表值,所以就不能泄漏printf
的got
表值
payload1 = b'%9$saaaa'+p64(puts_got) |
打印出来他们的地址,发现只有后3
个字节不一样,我们只需要修改后3
个字节即可
同32
位,分别表示出这3
个字节
sys_addr1 = system_addr&0xff |
构造第二个payload
的时候同样不能和32
位的题一样了,payload2 = b'%'+bytes(str(sysaddr1_value),encoding='utf-8')+b'c'+b'%13$hhn'
的意思是将sys_addr1_value
的值作为字符写入到第13
个参数所指向的内存地址中,%hhn
是只修改1
个字符的意思。
payload2 = b'%'+bytes(str(sys_addr1_value),encoding='utf-8')+b'c'+b'%13$hhn' |
exp
from pwn import * |
拿到flag
hitcontraining_playfmt
(ebp
链)
保护&源码
查看保护,32
位程序,有RWX
段我们第一时间想到用shellcode
。
源码:
int do_fmt() |
思路
分析代码,printf(buf)
格式化字符串漏洞,buf
在.bss
段。大概思路就是利用格式化字符串漏洞修改函数返回地址为buf
的起始地址+4
(因为执行ret
指令就需要跳出循环,就需要buf
的前4
个字节是quit
),最后发送shellcode
。
下图中ebp
下面的0xffffd07c
这个地址中放的是函数返回地址,我们可以把0xffffd098
改成0xffffd07c
,这样0xffffd088 -> 0xffffd098
就变成了0xffffd088 -> 0xffffd07c -> 0x80485ad(play+77)
,我们也就可以修改0xffffd07c
中的内容即返回地址为shellcode
的首地址了。
exp
from pwn import * |
拿到flag