RE2: "Secure Data Vault" (Reverse Engineering)

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_paddr values (normally p_paddr == p_vaddr), which is what makes readelf complain 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 it jmp 0x16d0 (the decoy front door). If argc == 2 it runs the hidden routine.
  • 0x16d0 (front door): reads a password, SHA-256 → hex, strcmp against the stored hash; on match it just XOR-0x55-decodes the 0x2340 decoy and prints DECRYPTED SECRET. Dead end.

The hidden routine at 0x1220 (argc == 2):

  1. Validates its own in-memory ELF header (magic 0x464c457f, class 2, e_phentsize == 0x38, e_phnum > 6).
  2. Harvests the p_paddr fields of program headers 6..12 ([phdr+0x18]) into a 56-byte buffer H: the “garbage” values were the hidden payload.
  3. Derives a key byte and a length from the ELF header: start from e_entry’s low byte, XOR with e_phnum and 0x37, then for every PT_LOAD phdr XOR in (p_flags * counter) ^ (p_align >> 12), then ^0xa5, ^ first dword of H, ^0x5a; low byte = length (54).
  4. Decodes: out[i] = H[i+1] ^ (0x29 + 0x13*i) ^ rol8(key, i & 7), then reverses out.
  5. strcmp-style compares the reversed string to argv[1]; equal → prints correct.

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 = 0 hides sections from readelf/objdump but 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 argc branch: the “obvious” main path 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.