鹏城杯2023WriteUp-Pwn
这个鹏城杯才是我真正意义上的第一场CTF。虽然之前有过校赛,但是校赛上简单题非常多,还是可以混混分的。这场应该算是像样的CTF(甚至还有点恶心),直接给我虐昏了。只能赛后补补题了。
而且比赛当天感冒了,当然这不是关键问题。我学的内容还是比较少的,先把能补的补了,持续更新。
silent
唯一有希望做出来的题,看完题解后发现是唯一没希望做出来的题。
首先谈谈思路:由于开启了seccomp禁掉了execve,所以one_gadget直接被毙掉。反编译发现程序只有输入(read函数),没有输出,system更是无从谈起。但是输出是必须的。没有输出就没有地址,没有地址就拿不到libc,没有libc什么函数都用不了(这题显然应该是orw拿flag)。于是这里我们要谈一个叫magic gadget的东西。
我们可以在程序中发现一个叫stdout的东西,具体来说叫:stdout@@GLIBC_2_2_5
。实际上它存在于libc中,是libc中的一个symbol。我们知道在进程中libc函数的相对位置是固定的,那么我们就可以通过相对的偏移将stdout所在的位置的地址改为其他libc函数,再call stdout的地址即可执行想要的函数。
magic gadget就是通过寄存器做到任意修改内存地址的gadgets,它在ida中是找不到的,因为是通过错位的字节码来获得的。使用ROPgadget工具可以找到它的地址:
|
|
即:add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; repz ret
这三条指令。我们仅需关注第一条指令:它会将rbp - 0x3d所代表的内存处的数值增加ebx。rbp
和ebx
是很容易控制的,因此可以实现任意内存地址的读写。
根据大佬们的博客,掌握了上面的内容,下面的内容基本属于固定套路。为了泄露libc基地址,我们需要输出已知函数(read)的实际地址(GOT地址)。我们只需要将stdout偏移至syscall,然后控制rax为1,即可完成write
的功能。
如何控制rax为1?我们知道,当执行成功的时候,write
和read
的返回值分别是成功输出和读入的字节数,而返回值就存放在rax寄存器中。于是我们只需要read1个字节的数据到任意的地点即可完成对rax寄存器的控制。
总结下,如果遇到可用函数很少的情况:
- 找到存在于libc中的一个函数(通常是stdout),使用magic gadget偏移到syscall上。
- 使用read函数控制rax为1,然后执行syscall泄露libc地址。
那么再讨论实现的细节。由于溢出空间比较小(实际上是ret2csu所需要空间比较大),首先要做的是将之后的payload读取到bss区然后将栈迁移过去。这个非常简单,具体实现如下所示:
|
|
csu的两段代码如下,可以在IDA中轻松的找到:
|
|
接下来我们就使用magic gadget将stdout偏移为syscall。这里有个细节,为了加上一个负数,这里需要将负数写成补码,方法如下:
|
|
为什么要加27呢?因为这个syscall执行前会有很多额外操作,实测会影响下面进程,于是这里直接偏移到call syscall
这一条指令上来最为方便。另外,stdout在libc中的名称是_IO_2_1_stdout_
!!!而且也有名叫stdout的symbol,千万不要弄混。这两者的区别尚不明确。
接下来,我们使用read读取一个字节来控制rax,结束后将下一段payload读取到另外一个位置,再次栈迁移。这是因为我们要在当前的payload末尾放入一个/flag
字符串为接下来orw做准备。如果接着这个栈的位置使用会有连续性上的问题。这里我比较懒,直接再开一个空间放下一个栈好了。
|
|
然后就是泄露地址,然后计算orw三个函数的真实地址:
|
|
最后就是orw的基本过程,这里pop rsi
和pop rdx
的gadgets在程序中找不到,只能去libc中找了。这里为什么不用ret2csu呢?因为用ret2csu中的call执行open函数会直接死掉,这里原因尚不明确,等知道了再来补。
|
|
因为是复盘所以这里没有用题目给的libc,也不想再patch了。但是和实际情况基本没差,只要将exp中的libc加载成题目给的即可。
实际运行结果:
flag文件是自己创建在根目录下的。下面给出完整的exp:
|
|
不枉我补了1天,确实是好题。