RE2: "Secure Data Vault" (Reverse Engineering)
On this page
Flag: KPMG_CTF{nvDSMQtzeCo_kKTpE55G6qtK2Psp4CfieVHP6qE62VzZ}
File: RE2, ELF 64-bit LSB PIE, x86-64, dynamically linked, section headers removed
Category: Reverse engineering (ELF-structure / anti-analysis)
Brief
The Secure Data Vault is heavily guarded by a SHA-256 password prompt, but the front door is a complete distraction. To find the true prize you must tear apart the binary’s very DNA: dissect the ELF structure and extract the hidden truth.
Two traps: (1) the SHA-256 password prompt, and (2) a decoy flag it reveals. The real flag is encoded inside the ELF program-header table and only reconstructed when the binary is run with a command-line argument.
I analyzed a copy under /tmp/re2/ so the original in ~/Downloads stayed untouched.
Step 1: Triage: the ELF is deliberately broken
$ cp ~/Downloads/RE2 /tmp/re2/RE2 && cd /tmp/re2 && chmod +x RE2
$ file RE2
RE2: ELF 64-bit LSB pie executable, x86-64, ... bad note description size 0x989dd6fc,
... no section header
$ readelf -S RE2 # "There are no sections in this file."
$ readelf -l RE2 # 13 program headers; several have garbage p_paddr / bad PhysAddr
Two anomalies stand out:
- Section header table is hidden (
e_shoff = 0,e_shnum = 0). - Program headers 6–12 have nonsensical
p_paddrvalues (normallyp_paddr == p_vaddr), which is what makesreadelfcomplain about a “bad note description size.”
Step 2: The strings already show the two traps
$ strings -n 5 RE2 | grep -iE 'flag|vault|sha|pass|correct|denied|access|\{'
Notables: EVP_sha256, SECURE DATA VAULT, ENCRYPTION: SHA-256, a stored hex hash
936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af, DECRYPTED SECRET: %s,
correct, wrong, challenge metadata is damaged.
Step 3: Rebuild the section headers (restore the “DNA”)
The section header table is still physically present at the end of the file; only e_shoff was
zeroed. A hex dump of the tail shows the .shstrtab names and a 64-byte NULL section header entry
at file offset 0x3140 (so e_shoff = 0x3140, (0x3840-0x3140)/64 = 28 sections).
$ od -A x -t x1z -j 0x3018 RE2 | head # GCC comment + .shstrtab + SHT at the tail
Patch the ELF header so tools can parse it again:
# patch_shoff.py (run as: python3 -I patch_shoff.py)
import struct
p='/tmp/re2/RE2.fix'
f=bytearray(open('/tmp/re2/RE2','rb').read())
struct.pack_into('<Q', f, 0x28, 0x3140) # e_shoff
struct.pack_into('<H', f, 0x3c, 28) # e_shnum
struct.pack_into('<H', f, 0x3e, 27) # e_shstrndx (.shstrtab = last section)
open(p,'wb').write(f)
$ readelf -S RE2.fix # now lists 28 sections: .text, .rodata, .data, ...
$ readelf -sW RE2.fix # dynsym: EVP_sha256/EVP_Digest*, strcmp, __sprintf_chk, fgets ...
Step 4: Read .rodata, find (and dismiss) the decoy
$ objdump -s -j .rodata RE2.fix
At the very end of .rodata (0x2340, 43 bytes) sits a blob with no normal string around it. The
front-door “unlock” path XOR-decodes it with 0x55. Testing KPMG against it shows the key is a
single byte 0x55:
$ python3 -I -c "
f=open('/tmp/re2/RE2','rb').read()
blob=f[0x2340:0x236b]
print(bytes(b^0x55 for b in blob).decode())"
KPMG_CTF{y0ur_4r3_w45t1g_t1m3_1_d3c0y_fl4g}
Decoded, it reads “your are wasting time, I’m a decoy flag”, a trap. A whole-file single-byte-XOR
sweep for KPMG confirms this is the only plaintext-XOR flag in the file, so the real one is
computed, not stored:
$ python3 -I -c "
f=open('/tmp/re2/RE2','rb').read()
for k in range(256):
d=bytes(b^k for b in f); i=d.find(b'KPMG')
if i>=0: print(hex(k),hex(i),d[i:i+60])"
0x55 0x2340 b'KPMG_CTF{y0ur_4r3_w45t1g_t1m3_1_d3c0y_fl4g}...'
Step 5: Disassemble: two code paths
$ objdump -d -M intel --no-show-raw-insn RE2.fix > dis.txt
The entry stub passes 0x1220 to __libc_start_main; that is the real main.
main@0x1220:cmp edi,2(argc). If argc != 2 itjmp 0x16d0(the decoy front door). If argc == 2 it runs the hidden routine.0x16d0(front door): reads a password, SHA-256 → hex,strcmpagainst the stored hash; on match it just XOR-0x55-decodes the0x2340decoy and printsDECRYPTED SECRET. Dead end.
The hidden routine at 0x1220 (argc == 2):
- Validates its own in-memory ELF header (magic
0x464c457f, class 2,e_phentsize == 0x38,e_phnum > 6). - Harvests the
p_paddrfields of program headers 6..12 ([phdr+0x18]) into a 56-byte bufferH: the “garbage” values were the hidden payload. - Derives a key byte and a length from the ELF header: start from
e_entry’s low byte, XOR withe_phnumand0x37, then for everyPT_LOADphdr XOR in(p_flags * counter) ^ (p_align >> 12), then^0xa5,^ first dword of H,^0x5a; low byte = length (54). - Decodes:
out[i] = H[i+1] ^ (0x29 + 0x13*i) ^ rol8(key, i & 7), then reversesout. strcmp-style compares the reversed string toargv[1]; equal → printscorrect.
So the reversed decoded string is the flag/expected argument.
Step 6: Reimplement the decode and recover the flag
# decode.py (python3 -I decode.py)
import struct
f=open('/tmp/re2/RE2','rb').read()
u16=lambda o: struct.unpack_from('<H',f,o)[0]
u32=lambda o: struct.unpack_from('<I',f,o)[0]
u64=lambda o: struct.unpack_from('<Q',f,o)[0]
e_phoff=u64(0x20); e_phnum=u16(0x38); sz=u16(0x36)
H=bytearray()
for i in range(e_phnum-7, e_phnum): # phdrs 6..12
H += f[e_phoff+i*sz+0x18 : e_phoff+i*sz+0x20]
edi = f[0x18] ^ (e_phnum & 0xff) ^ 0x37 # key seed from e_entry low byte
ecx = 3
for i in range(e_phnum):
o=e_phoff+i*sz
if u32(o)==1: # PT_LOAD
edi ^= ((f[o+4]*ecx) & 0xffffffff) ^ ((u64(o+0x30)>>12) & 0xffffffff)
ecx+=1
edi = (edi ^ 0xffffffa5) & 0xffffffff
r10 = (edi ^ struct.unpack_from('<I',H,0)[0] ^ 0x5a) & 0xff # length = 54
key = edi & 0xff
rol8=lambda v,c:((v<<(c&7))|(v>>(8-(c&7))))&0xff
out=bytes((H[i+1] ^ ((0x29+0x13*i)&0xff) ^ rol8(key,i&7)) & 0xff for i in range(r10))
print(bytes(reversed(out)).decode())
$ python3 -I decode.py
KPMG_CTF{nvDSMQtzeCo_kKTpE55G6qtK2Psp4CfieVHP6qE62VzZ}
Step 7: Verify against the binary
$ ./RE2 'KPMG_CTF{nvDSMQtzeCo_kKTpE55G6qtK2Psp4CfieVHP6qE62VzZ}'
correct
Takeaways
e_shoff = 0hides sections fromreadelf/objdumpbut the table is usually still in the file; restore the three header fields (e_shoff,e_shnum,e_shstrndx) to analyze normally.- Non-standard
p_paddr(≠p_vaddr) is a classic place to smuggle data; here the program read its own phdrs at runtime to rebuild the key. - Watch the
argcbranch: the “obvious”mainpath was a decoy; the real logic only ran with an argument. - Treat the first flag you find as suspect: this one literally said it was a decoy.