abominate_language
abominate_language
Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-06-24 | Status: Solved Techniques: cgo_call_tracing, gopclntab_recovery, static_disassembly, string_reconstruction
Summary
Task: stripped Go+cgo ELF that reads a string and prints OK! only for the correct flag. Solution: statically disassemble main.main in radare2 (gopclntab gives symbols), read the ordered per-character lea rbx string references that the checker concatenates, and reconstruct the expected input.
abominate_language — SPbCTF (rev-kids20.forkbomb.ru)
Description
App reads a string, and will print 'OK!' if it's flag
Language hint from the organizers: "C+Go" — i.e. a cgo binary mixing C and Go. The goal is to recover the exact string the program accepts.
Source: rev-kids20.forkbomb.ru (SPbCTF kids rev track).
Binary: https://rev-kids20.forkbomb.ru/files/rev/re5/abominate_language
Recon (file / strings / symbols)
text read-only • copy allowed$ file abominate_language ELF 64-bit LSB executable, x86-64, dynamically linked, stripped Go build ID "a54840cebb9ead1c5a7564cf8a7fccba626c6188"
Although the binary is stripped, it is a Go executable, so its function table
(gopclntab) lets tools recover Go symbol names. The cgo footprint is visible
in the symbol/string data:
_cgo_2d05b70430d8_Cfunc_myprint,x_cgo_*— the C side glue.- Go-side wrappers:
main._Cfunc_CString,main._Cfunc_myprint,main._Ctype_char,main._Ctype_void.
Relevant recovered Go functions:
| Address | Symbol |
|---|---|
0x401460 | main._Cfunc_CString |
0x401550 | main._Cfunc_myprint |
0x402020 | main.init |
0x4015f0 | main.main (size 2604 B) |
| |
The presence of main._Cfunc_* confirms the "C+Go" (cgo) hint: the Go side
builds the success message and hands it to a C function for printing.
Static analysis with radare2
The ELF could not be executed locally (macOS: exec format error), so it was
solved purely statically. Analyze and disassemble main.main:
bash read-only • copy allowedr2 -2 -q -e scr.color=0 \ -c 'aa; s 0x004015f0; af; pdf' \ abominate_language
main.main reads the user input, then compares it byte-by-byte against an
expected string. The expected string is not stored as one contiguous blob.
Instead, the Go fmt-style code assembles it character-by-character: each piece is
referenced with a lea rbx, [<addr>] instruction pointing at a small .rodata
string entry (mostly single characters, plus a few multi-char fragments).
radare2 annotates each lea with the string content it points to.
How the flag string is reconstructed
Reading the lea rbx, ... references in execution order spells out the
expected input:
text read-only • copy allowed0x0040173b lea rbx, [0x004b3458] ; "f" 0x00401791 lea rbx, [0x004b37f8] ; "l" 0x004017e7 lea rbx, [0x004b2fd8] ; "a" 0x0040183d lea rbx, [0x004b3578] ; "g" 0x00401893 lea rbx, [0x004b43a0] ; "{" 0x004018e9 lea rbx, [0x004b2820] ; "G" 0x0040193f lea rbx, [0x004b2b28] ; "O" 0x00401995 lea rbx, [0x004b2f78] ; "_" 0x004019eb lea rbx, [0x004b36e8] ; "i" 0x00401a41 lea rbx, [0x004b2c88] ; "S" 0x00401a97 lea rbx, [0x004b2d80] ; "T" 0x00401aed lea rbx, [0x004b3358] ; "e" 0x00401b43 lea rbx, [0x004b3cd0] ; "r" 0x00401b99 lea rbx, [0x004b3cd0] ; "r" 0x00401bef lea rbx, [0x004b36e8] ; "i" 0x00401c45 lea rbx, [0x004b30c8] ; "b" 0x00401c9b lea rbx, [0x004b37f8] ; "l" 0x00401cf1 lea rbx, [0x004b3358] ; "e" 0x00401d47 lea rbx, str._lan ; "_lan" 0x00401d9d lea rbx, [0x004b3660] ; "gua" 0x00401df3 lea rbx, [0x004b35e8] ; "ge" 0x00401e49 lea rbx, [0x004b43a8] ; "}"
Concatenating the fragments in order:
text read-only • copy allowedf l a g { G O _ i S T e r r i b l e + "_lan" + "gua" + "ge" + } = flag{REDACTED}
Note that several addresses repeat (r→0x004b3cd0 twice, i→0x004b36e8,
e→0x004b3358, l→0x004b37f8), which is just .rodata string deduplication —
the same character entry is referenced multiple times.
The cgo print path
Once every byte matches, main.main takes the success branch and invokes the
cgo bridge:
text read-only • copy allowedcall 0x401460 ; main._Cfunc_CString — convert Go string -> C char* call 0x401550 ; main._Cfunc_myprint — C side prints "OK!"
So the Go side converts its message into a C string via CString, then calls the
C function myprint (the _cgo_..._Cfunc_myprint glue) which prints "OK!".
The whole challenge is a pun: cgo ("abomination") mixes C and Go, and the flag itself jokes that Go is a terrible language.