Blackbeards Tomb

BearcatCTFby smothy

Blackbeard's Tomb - Rev

Points: 470 | Flag: BCCTF{Buri3d_at_sea_Ent0m3d_amm0nG_th3_w4v3S} | Solved by: Smothy @ 0xN1umb

pirate treasure

what we got

single binary tomb - 64-bit statically linked ELF with OpenSSL baked in. takes flag as argv[1].

bash
$ file tomb
tomb: ELF 64-bit LSB executable, x86-64, statically linked, not stripped
$ checksec tomb
Partial RELRO | Canary | NX disabled | No PIE | RWX stack

rwx 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

c
// 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:

  1. SHA256("BCCTF{") checked against hardcoded hash, XORs result into tomb_key
  2. checks flag is exactly 45 chars, XORs flag[44] ('}') into tomb_key
  3. 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:

asm
; 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

bash
# 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:

asm
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
  ret

so 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]

python
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_w4v3S

putting it together

BCCTF{Buri3d_at_sea_Ent0m3d_amm0nG_th3_w4v3S} Buried at sea Entombed among the waves
bash
$ 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

crack the code

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 ✌️