前言
如何写shellcode是二进制选手的基本素养了,本题中通过syscall的一些trick用法来写shellcode,getshell。
赛题链接
https://github.com/eternalsakura/ctf_pwn/tree/master/sixstar/sixstar/primepwn
https://github.com/sixstars/starctf2018/tree/master/pwn-primepwn
前置知识
Linux系统调用
举个例子
1 | .data |
输出结果是
1 | ./test |
实际上这里是直接使用了write的syscall,它的系统调用号是1.
其他的syscall的系统调用号可以在系统调用表查看。
我们可以看到我们在示例程序里用到的,1是write,60是exit退出程序。
1 | 1 common write sys_write |
对于64位程序,通过syscall进入系统调用,将系统调用号传入rax,各个参数按照rdi、rsi、rdx的顺序传递到寄存器中,系统调用返回值储存到rax寄存器。
syscall的trick
syscall在所有寄存器都为0的情况下,执行的时候会把rip赋值给rcx。
分析
首先输入一个unsigned long也就是8字节的数据,然后判定输入的数据(写到0x601038)是不是素数,如果不是素数,程序就结束掉,如果是素数,那么继续运行。
可以看到0x601030存放的是指向0x601038的指针。
之后程序会把寄存器都清零,然后跳到0x601030指向的空间执行,也就是跳到0x601038,执行我们写入的数据。
利用
为了方便读者自己调试分析,我就走一遍exp,然后描述一下利用过程。
exp如下,我加了一行gdb.attach用来调试。
1 | from pwn import * |
然后我们开始调试,如果你使用pwndbg插件,看到的应该和我一样。
这样我们就断在了main函数执行之前,我们的目的是进入main函数单步看exp执行.
所以我们按5次finish(finsh的作用是执行到当前函数结束返回)
现在你应该看到下面这样:
然后单步n,一直单步到main函数最后的jmp。
由程序分析可知,首先我们要准备一个是素数的shellcode,且不能超过8个字节。
利用我们之前提到的syscall trick就可以做到,后面会分析为什么。
1 | start: |
1 | .text:0000000000400A57 loc_400A57: ; CODE XREF: main+56↑j |
由于在这里我们通过xor把寄存器都清空了,所以在执行syscall的时候,把rip的值赋给了rcx。
syscall执行前
syscall执行后
然后传递参数。
1 | dec edx |
edx从0减去1就是-1,即0xFFFFFFFF。
esi=ecx=rip。
rdi和eax在之前就已经被清零了。
jmp跳到syscall执行,相当于调用sys_read(0,rip,0xFFFFFFFF)
sys_read执行结束后,0x60103a(即rip,也就是下一条要执行的命令)及其之后的指令被覆盖,如下图可以看到我们将继续执行真正的shellcode。
将真正的shellcode读入到rip中,这样,继续执行就执行到了我们的shellcode。
1 | code=""" |
因为之前rsp被置0了,所以为了在栈上保存/bin/sh,用在后面pop rdi。
现在要让rsp指向一个可写的地址,于是把rcx赋值给它。
然后将execve的系统调用号赋值给rax。
1 | 0x3b execve sys_execve/ptregs |
传递参数,执行execve(‘/bin/sh’,0,0)来getshell
注意在call的时候,会先把返回地址压栈,而返回地址就是下一条指令,也就是0x601056。
gdb里si跟入call的函数。
然后pop rdi就把参数传进去了,接着syscall就执行了execve(‘/bin/sh’,0,0)
getshell