从 RCTF 看 x86-64 syscall

从 RCTF only_rev 看 x86-64 syscall

题目背景

先简单看下题目关键内容

image-20251118205440429

关键函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
v0 = addr;
*addr = 0x3148DB3148C03148LL;
v0[1] = 0x48FF3148D23148C9LL;
v0[2] = 0xC9314DC0314DF631LL;
v0[3] = 0x314DDB314DD2314DLL;
v0[4] = 0x4DF6314DED314DE4LL;
v0[5] = 0x345678C48148FF31LL;
v0[6] = 0x12345678C5814812LL;
printf("your code:");
v5 = read(0, addr + 56, 9uLL);
v1 = addr + v5 + 56;
*v1 = 0x4812345678EC8148LL;
*(v1 + 7) = 0xC312345678ED8148LL;
(addr)();
munmap(addr, 0x1000uLL);
puts("run success");

只能读入九个字节的shellcode,并且ban掉execve、execveat

当我们调试时候,在执行sc位置打断点,发现寄存器全被清零,而且栈被破坏(rsp和rbp都被+0x12345678):

image-20251118205222461

现在面临如下问题:

  1. 不能拿shell,要orw。

  2. 读入长度很少,需要再次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描述如下:

image-20251118210941322

提取关键部分:

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 调用操作系统的系统调用处理程序。其实现方式为:

  1. 将 SYSCALL 后续指令的地址存入 RCX 寄存器
  2. 把 RIP 替换为 IA32_LSTAR MSR 寄存器里的值
  3. 将标志寄存器(RFLAGS)的值保存至 R11 寄存器,然后把 RFLAGS 的值与 IA32_FMASK MSR 里的值做掩码运算
  4. 把 IA32_STAR MSR 寄存器里第32~47位加载到 CS 和 SS 段寄存器

主要关注第一步,也就是将 syscall 指令的下一条命令的地址 (RIP) 放入 RCX 中

PWN IT

根据上面的内容,我们可以通过 syscall 在 rcx 中得到 rip,而 rcx 是可以使用 mov 命令的。也就是说,我们可以用如下方式得到 rip,并把它放入 rsi 用于后续read:

1
2
syscall
mov rsi, rcx

不要忘记,执行shellcode前,除了rip、rsp、rbp以外,所有寄存器都是0。所以,如果直接执行shellcode,相当于:

1
read(0, 0, 0)

这样操作是无害的,程序不会进行读取,而且返回值 rax 也是 0。但是,syscall 指令使 rip 内容存到了 rcx,通过 mov,我们就得到了一个可写可执行的地址:

image-20251118213211708

此时,rdi、rsi都被设置好了,只需要设置rdx然后再次syscall,即可再次read

1
2
3
4
syscall
mov rsi, rcx
mov dl, 0xff
syscall

接下来就很简单了,通过 sub 来恢复 rsp 和 rbp,然后进行常规 orw