Blackbeard's Tomb - Rev
Points: 470 | Flag: BCCTF{Buri3d_at_sea_Ent0m3d_amm0nG_th3_w4v3S} | Solved by: Smothy @ 0xN1umb

what we got
single binary tomb - 64-bit statically linked ELF with OpenSSL baked in. takes flag as argv[1].
$ file tomb
tomb: ELF 64-bit LSB executable, x86-64, statically linked, not stripped
$ checksec tomb
Partial RELRO | Canary | NX disabled | No PIE | RWX stackrwx stack is a big hint - this thing runs code off the stack
the solve
ngl this one was a JOURNEY. tl;dr the binary has like 4 layers of protection wrapping the actual flag check
layer 1 - anti-debug (proc)
the real entrypoint isnt main(), its proc() at 0x403385. it reads /proc/self/status to grab TracerPid. if you're running under gdb/strace, TracerPid is nonzero and it XORs the master key (tomb_key) with a garbage value, making everything downstream break
// proc() pseudocode
v13 = TracerPid + 4950; // 4950 when not debugged, garbage when debugged
for(k=0; k<=7; k++)
tomb_key[k*4] ^= v13; // XOR 8 DWORDs
main(argc, argv);also the binary crashed on kali due to OpenSSL 3.x provider init in a static binary lmao. fix: OPENSSL_CONF=/tmp/empty_ssl.cnf
layer 2 - SHA256 hash chain
main() validates the known part of the flag using SHA256 hash chains:
- SHA256("BCCTF{") checked against hardcoded hash, XORs result into tomb_key
- checks flag is exactly 45 chars, XORs flag[44] ('}') into tomb_key
- loop j=6..25: computes
SHA256(flag[j]) ^ SHA256(flag[j+1]), compares with 20 stored hashes, XORs matches into tomb_key
this gives us the first 27 characters: BCCTF{Buri3d_at_sea_Ent0m3d
layer 3 - self-decrypting shellcode (stage1)
39 bytes of encrypted shellcode on the stack, XORed with 0xCC to decrypt. this is stage1 - a simple XOR loop that decrypts stage2 (52 bytes) using the repeating 32-byte tomb_key:
; stage1 - XOR decryptor
push rdi ; save tomb_key ptr
push rdx ; save stage2 ptr
xor r8, r8 ; counter = 0
loop:
mov bl, [rdi] ; bl = tomb_key[i]
xor [rdx], bl ; stage2[i] ^= tomb_key[i]
inc rdi / rdx / r8
cmp r8, 0x20 ; wrap tomb_key at 32
je wrap
cmp rcx, r8 ; done at 52?
jne loop
pop rdx / rdi
jmp rdx ; jump to decrypted stage2!layer 4 - stage2 (the actual flag checker)
this is where it got spicy. under gdb the decrypted stage2 was garbage (anti-debug corrupts tomb_key). even after patching out the TracerPid check, the code STILL crashed in gdb. my computed tomb_key was subtly wrong.
the breakthrough: patch the binary with eb fe (infinite loop) right before call r8, run it WITHOUT gdb, then read /proc/PID/mem to grab the REAL tomb_key from the live process
# patch call r8 -> jmp $ (infinite loop)
python3 -c "..." > tomb_loop
OPENSSL_CONF=/tmp/empty_ssl.cnf ./tomb_loop 'BCCTF{...AAAA...}' &
python3 -c "
pid = $!
with open(f'/proc/{pid}/mem', 'rb') as f:
f.seek(0x8413e0)
print(f.read(32).hex())
"with the REAL tomb_key, stage2 decrypts to clean shellcode:
add rsi, 0x1a ; rsi = &flag[26]
mov rdx, 0x539 ; success return value
xor rcx, rcx ; counter = 0
loop:
mov al, 5
mul cl ; al = 5 * counter
add al, 0x40 ; al = 5*i + 0x40
xor al, [rsi] ; al ^= flag[26+i]
mov bl, [rdi] ; bl = tomb_key[i]
cmp bl, al ; must match!
jne fail
inc rsi / rdi / rcx
cmp rcx, 0x12 ; 18 iterations (positions 26-43)
jne loop
jmp done
fail:
inc rdx ; rdx = 0x53A (wrong)
done:
mov rax, rdx ; return 0x539 if all match
retso the check is: tomb_key[i] == (5*i + 0x40) ^ flag[26+i] for 18 chars
rearrange: flag[26+i] = (5*i + 0x40) ^ tomb_key[i]
tomb_key = bytes([0x24, 0x1a, 0x2b, 0x22, 0x39, 0x69, 0x30, 0x24,
0x37, 0x19, 0x1a, 0x44, 0x23, 0xf6, 0xb2, 0xfd,
0xa3, 0xc6])
for i in range(18):
print(chr(((5*i + 0x40) & 0xFF) ^ tomb_key[i]), end='')
# d_amm0nG_th3_w4v3Sputting it together
BCCTF{Buri3d_at_sea_Ent0m3d_amm0nG_th3_w4v3S}
Buried at sea Entombed among the waves
$ OPENSSL_CONF=/tmp/empty_ssl.cnf ./tomb 'BCCTF{Buri3d_at_sea_Ent0m3d_amm0nG_th3_w4v3S}'
You have entered my tomb! None will escape...
You have defied the odds and made it back to shore. Great job
flag
BCCTF{Buri3d_at_sea_Ent0m3d_amm0nG_th3_w4v3S}
fr this one had everything - anti-debug, SHA256 hash chains, self-modifying shellcode, and the ultimate troll of making the stage2 code crash under ANY debugger even after patching the TracerPid check. the /proc/PID/mem trick to read the live process memory was the key. lowkey one of the hardest revs ive done ngl
smothy out ✌️