SINO (Socket In, No Out)
SINO (Socket In, No Out)
Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-02 | Status: Solved Techniques: dlsym_rtld_next, docker_linux_amd64_execution, heap_buffer_exfiltration, ld_preload_free_interception
Summary
Task: not-stripped x86-64 ELF network client receives a flag over a socket into a heap buffer, but the print is commented out and stdout/stderr are fclose'd before free(theFlag); get_msg is a 15k-line obfuscated function using an x32-ABI raw syscall to defeat strace/gdb. Solution: instead of reversing the socket protocol, hook free() via LD_PRELOAD and dump the freed buffer to a file (stdout/stderr are closed), running the binary under Docker linux/amd64 so it reaches the live server. First free() call is theFlag.
SINO (Socket In, No Out) — spbctf
Description
Title: "Socket In, No Out" "We accidentally compiled this code and deleted the entire source!"
c read-only • copy allowedchar * theFlag = get_msg(argv[1], atoi(argv[2])); //printf("The flag is %s\n", theFlag); fclose(stdout); fclose(stderr); free(theFlag);Download binary: sino.elf Run:
./sino.elf 109.233.56.90 11038
The binary is a network client: it connects to 109.233.56.90:11038, receives the flag over the socket into a heap buffer returned by get_msg(), but the printf that would display it is commented out, and stdout/stderr are fclose()'d before free(theFlag). So the flag is received but never emitted — "Socket In, No Out".
Goal: recover the flag that the client fetches but refuses to print.
Analysis
file sino.elf → ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped.
main (0x400950)
main faithfully matches the leaked snippet:
asm read-only • copy allowed400966: cmp dword [rbp-0x8], 0x3 ; argc == 3 ? 40096a: je 400990 ... ; else printf usage, return -1 400994: mov rdi, [rax+0x8] ; argv[1] (IP) 4009a7: call atoi@plt ; atoi(argv[2]) (port) 4009b2: call get_msg ; theFlag = get_msg(argv[1], atoi(argv[2])) 4009b7: mov [rbp-0x18], rax ; save theFlag pointer 4009bb: mov rdi, [0x6110a0] ; stdout 4009c3: call fclose@plt ; fclose(stdout) 4009c8: mov rdi, [0x6110c0] ; stderr 4009d3: call fclose@plt ; fclose(stderr) 4009d8: mov rdi, [rbp-0x18] ; theFlag 4009df: call free@plt ; free(theFlag) <-- flag still intact here
At 0x4009df the flag is still in the heap buffer in RDI, right before free(). Everything after get_msg is designed to make the flag unreadable (no print, closed FDs).
get_msg (0x400a00) — obfuscated
get_msg is a ~15,000-line bloated/obfuscated function. It contains ~3 repeated blocks of socket -> connect -> raw syscall() -> read -> close and branches on booleans computed from globals at 0x6110cc / 0x6110d0 (derived from port number / argc parity). It performs the actual byte-wise flag retrieval over the socket. Reversing this to reconstruct the protocol is a large, unnecessary effort.
Anti-analysis: x32-ABI raw syscall
get_msg calls the libc syscall() wrapper using a raw syscall via the x32 ABI. This is the trick that makes analysis tools report the process "runs in x32 mode", and it defeats both strace and gdb/ptrace:
strace -f -e trace=network,read,write ./sino.elf ...→strace: exec: Permission denied/ "Process runs in x32 mode".gdb -batchbreakpoint on the free call (break *0x4009df) to dumpRDI→ "Couldn't get registers: Input/output error", even with--cap-add=SYS_PTRACEandseccomp=unconfined. The x32-ABI syscall confuses ptrace on an otherwise amd64 process.
Both failures show why static protocol reconstruction and debugger hooking are dead ends here.
Solution
Key insight: the flag lives intact in the heap buffer right up until free(theFlag). No need to reverse the obfuscated get_msg or reconstruct the socket protocol. Since stdout/stderr are closed (so we can't printf), and ptrace is blocked (so we can't debug), intercept free() at runtime with an LD_PRELOAD shim that writes the buffer contents to a file (not to the closed stdout/stderr), then chains to the real free(). LD_PRELOAD needs no ptrace, so the x32 anti-debug is irrelevant.
The binary must actually reach the live server, so run the linux/amd64 ELF inside Docker (ubuntu:22.04, --platform linux/amd64) — on the macOS host the client then talks to the real challenge server.
hookfree.c
c read-only • copy allowed#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <dlfcn.h> static void (*real_free)(void*) = NULL; void free(void *p) { if (!real_free) real_free = dlsym(RTLD_NEXT, "free"); if (p) { // dump buffer to a file (stdout/stderr are closed by the program) FILE *f = fopen("/w/flagdump.bin", "ab"); if (f) { fprintf(f, "--- free(%p) ---\n", p); // print first 256 bytes as string + hex char *c = (char*)p; fprintf(f, "STR: %.256s\n", c); fprintf(f, "HEX: "); for (int i = 0; i < 128; i++) fprintf(f, "%02x", (unsigned char)c[i]); fprintf(f, "\n"); fclose(f); } } real_free(p); }
Build and run (inside Docker linux/amd64)
bash read-only • copy allowed# Host: launch a linux/amd64 container with the working dir mounted at /w docker run --rm -it --platform linux/amd64 -v "$PWD":/w -w /w ubuntu:22.04 bash # Inside the container: apt-get update && apt-get install -y gcc gcc -shared -fPIC -o hookfree.so hookfree.c -ldl LD_PRELOAD=./hookfree.so ./sino.elf 109.233.56.90 11038
The very first free() call is theFlag from main, and /w/flagdump.bin immediately revealed:
STR: spbctf{REDACTED}
HEX: 7370626374667b61663133383861333532383736306338373530326538353133353262373234627d
echo <hex> | xxd -r -p confirms the ASCII flag.