来自pwnhub春季赛的一道题目,比赛的时候看了,完全没有思路。赛后复现发现题目确实很复杂。但是也学到了非常多。包括:能够任意地址写的magic,反向shell,alarm设置rax,构造syscall指令的方法。
easyrop
逻辑很简单,只不过关了输入输出流。
当时一开始看到的想法是,这个直接重新打开输出流不就完事了?但是可能没有这种选项。
(这里竟然能被检查出来canary也真是醉了…
同时,题目开了沙箱。也就是允许正规的ORW。但是无法输出
思路
基本想法肯定是ORW。如果不能本地读取,看了wp之后发现还有一种方法,就是自己开一个socket然后在公网服务器上读。这牵涉到很多rop的gardet。因此需要考虑是否有可能把某部分映射成为可执行,也就是调用mprotect。但是怎么控制rax呢?
这道题给了一个新的思路。以前是通过read返回值可以设置rax,这里通过将alarm重新设置为0也就是alarm(0)时,会返回没有结束的秒数到寄存器rax中。利用这个特性我们可以获得一个0xf以下的rax。
因此,可以等待5秒,然后调用alarm(0),得到eax为10,然后利用ret2csu执行mprotect(bss, 0x1000, 7)。
可以看到这里正好有mprotect这个系统调用,也符合我们写汇编代码的需求。于是我们可以把堆映射为可执行的,从而调一个socket,在公网上完成读flag。
但是,进一步发现,我们并没有syscall;ret
的gardet。这似乎就没法调用mprotect了。
答案中给出了一个神奇的办法。注意到下面这个gardet。
add dword ptr [rbp - 0x3d], ebx; nop xxxxx; ret
是一般的binary都有的。他的opcode是015dc3
。
通过以上的gardet,结合ret2csu的gardet。我们就可以实现一个任意地址写的rop链。这是一个神奇的操作。而且在一般的binary里面基本都有!
通过这个rop链,我们可以把alarm的got表改成syscall
。具体来说如下图所示。我们把alarm的got表的内容(下图中0x00007ffff7e9fd90)改成0x00007ffff7e9fd99即可。
只要把这个修改了,我们就能创造出一个syscall ret
的gadget。
然而,也不是这么容易调用mprotect。因为我们没有pop rdx的gardet。不过我们可以用ret2csu来解决。注意到下面的mov rdx,r15可以用来解决这个问题。
最后。(这个算是一个补充知识),使用rep的时候(因为我们需要把输入转移到堆上)各个寄存器放什么。如下图,是把esi移动到edi中。
因此,整理一下思路。
思路的总结
- 等待五秒,调用
alarm(0)
,这里可以直接使用csu来调用。 - 上一步结束之后,应该能使得eax变为10(mprotect),先修改alarm的got表里面改成syscall,之后利用这个syscall结合ret2csu完成一个mprotect修改特定位置的权限。这里选择修改堆为可执行。
- 在堆上写好一些rep要用到的指令。包括
pop rbp rbx rcx
(用于magic方法)、mov rsi rsp ret
(用于rep)、rep movs
,就是rep,后面在栈上就可以直接使用。注意这里要直接写入16进制的指令数值。用下面的方法
- 在栈上写好shellcode,主要是socket+connect+sendfile,使用rep把shellcode拷贝到bss段执行(需要提前在堆上构造一些rep使用所需要的指令)。因为如果不用rep,就要使用上面提到的gardet,可能导致长度不够的问题。
- 跳转到堆上执行shellcode
调试过程十分复杂,光复现就花了五个小时…
截图
修改alarm
修改alarm为syscall。能够实现的原因是用那个magic能够控制任意地址加上一个任意数值。然后alarm在libc里面的内容里面有syscall。因此我们直接把地址加上这么多即可。
下面是改掉之后的结果。
调用mprotect
可以看到这里的参数,以及最后的alarm+9的内容。
我们把堆映射为RWX的。
寻找相关汇编
下面这个rep的好难找,真的要自己输入这样的指令。算是借此学到了吧,因为网上真的查不到这样的指令。下面指令的意思是把rsi指针中的数据中内容复制到rdi指针中。(类似于字符串拷贝的汇编)一次拷贝8byte。拷贝总次数由rcx决定。是和rep配合起来使用的。
相关系统调用
一共用到三个系统调用。第一个是本地创建一个socket,用来后续bind。
第二个是connect,相当于这个socket连接到addr指针中的ip地址和端口(到这里大概明白了为什么要公网IP,因为打远程的时候远程的binary没发链接到自己的虚拟机上,自己的虚拟机本地调试的时候,这里可以在本地监听)
第三个是sendfile。其中out_fd表示输出到什么文件,in_fd表示从什么文件读。这里我先开的socket,再打开的文件,所以是下面的顺序。offset就不用管了,直接写0,最后是长度。
最终反向shell拿到flag
exp
首先上一个完整的exp。当本地打开127.0.0.1:10001端口监听之后,可以获得一个flag。
1 | from pwn import * |
下面是socket和connect。注意不需要编写汇编,直接用下面给好的字节码就能成功。字节码里面remote_port和remote_ip填自己公网的ip和port。(已验证可以成功,注意调试的时候碰到push rsp之后需要用si而不是ni,否则会失败)
1 | s1 = """ |
这里是open+sendfile。可以根据情况改文件名。
1 | # 下面是open并且sendfile |
总结
这道题学到了很多
- magic指令(我就这样命名了hhh)可以结合csu的gadget实现任意地址写任意数值。这是很强大的。
add dword ptr [rbp - 0x3d], ebx; nop xxxxx; ret
- 使用alarm剩余的秒数设置rax。这也是新学到的方法。
- 反向shell。这也是第一次碰到。并且知道了shellcode如何构造。
- 在栈空间不够的时候,使用rep指令传递到堆上,学习了rep指令和配套的movs。
- 没有syscall的时候利用magic改掉alarm的got为syscall,并利用ret2cus调用任意的系统调用(结合第二部设置的rax)
pwnhub的题目确实很有收获。最后,放上官方的wp,里面对汇编代码有更好的解释。
1 | #!/usr/bin/python3 |