从 RCTF only_rev 看 x86-64 syscall
题目背景
先简单看下题目关键内容

关键函数:
1 | v0 = addr; |
只能读入九个字节的shellcode,并且ban掉execve、execveat
当我们调试时候,在执行sc位置打断点,发现寄存器全被清零,而且栈被破坏(rsp和rbp都被+0x12345678):

现在面临如下问题:
-
不能拿shell,要orw。
-
读入长度很少,需要再次read,但是目前没有可写可执行的地址。
如果能找到一个可写可执行的地址,也就能read一个更长的shellcode,也就能orw。
之前的博客中有一个关于shellcode爆破可写地址的文章。但是九个字节太少了,没办法照抄。
一个理想的地址就是执行shellcode这部分区域,但是不能使用 mov 指令对 rip 进行操作,如果使用 lea rsi, [rip] ,字节长度会超出9个字节。
这里就要提到syscall指令的特殊用法了。
syscall 流程
众所周知,x64通过寄存器传参。当进行系统调用时,各个参数先被放入对应的寄存器,寄存器填满后,更多的参数被放入栈。rax 寄存器被设为系统调用号,然后通过 syscall 指令进行系统调用,而关键在于 syscall 指令的流程。
通过 Intel® 64 and IA-32 Architectures Software Developer Manuals 的 Volume 2B 第 4.3 节 (P689) 对syscall描述如下:

提取关键部分:
SYSCALL invokes an OS system-call handler at privilege level 0. It does so by loading RIP from the IA32_LSTAR MSR (after saving the address of the instruction following SYSCALL into RCX). (The WRMSR instruction ensures that the IA32_LSTAR MSR always contain a canonical address.)
SYSCALL also saves RFLAGS into R11 and then masks RFLAGS using the IA32_FMASK MSR (MSR address C0000084H); specifically, the processor clears in RFLAGS every bit corresponding to a bit that is set in the IA32_FMASK MSR.
SYSCALL loads the CS and SS selectors with values derived from bits 47:32 of the IA32_STAR MSR.
也就是:
SYSCALL 指令会在 ring 0 调用操作系统的系统调用处理程序。其实现方式为:
- 将 SYSCALL 后续指令的地址存入 RCX 寄存器
- 把 RIP 替换为 IA32_LSTAR MSR 寄存器里的值
- 将标志寄存器(RFLAGS)的值保存至 R11 寄存器,然后把 RFLAGS 的值与 IA32_FMASK MSR 里的值做掩码运算
- 把 IA32_STAR MSR 寄存器里第32~47位加载到 CS 和 SS 段寄存器
主要关注第一步,也就是将 syscall 指令的下一条命令的地址 (RIP) 放入 RCX 中
PWN IT
根据上面的内容,我们可以通过 syscall 在 rcx 中得到 rip,而 rcx 是可以使用 mov 命令的。也就是说,我们可以用如下方式得到 rip,并把它放入 rsi 用于后续read:
1 | syscall |
不要忘记,执行shellcode前,除了rip、rsp、rbp以外,所有寄存器都是0。所以,如果直接执行shellcode,相当于:
1 | read(0, 0, 0) |
这样操作是无害的,程序不会进行读取,而且返回值 rax 也是 0。但是,syscall 指令使 rip 内容存到了 rcx,通过 mov,我们就得到了一个可写可执行的地址:

此时,rdi、rsi都被设置好了,只需要设置rdx然后再次syscall,即可再次read
1 | syscall |
接下来就很简单了,通过 sub 来恢复 rsp 和 rbp,然后进行常规 orw