~/tiramisaks
← back to /home

GEMASTIK XIX: Hexlock Reverse Engineering

GEMASTIK XIX 2026-08-23 Reverse Engineering
#GEMASTIK19 #AES-128-GCM #Anti-Debug #Golang #ELF 64-bit

Write-up Reverse Engineering GEMASTIK XIX: Hexlock

Ringkasan Challenge

InformasiNilai
EventGEMASTIK XIX
KategoriReverse Engineering
Binaryhexlock
FormatELF 64-bit, x86-64, statically linked, stripped
BahasaGo
ProteksiRekonstruksi key saat runtime dan korupsi key melalui anti-debug
KriptografiAES-128-GCM
FlagGEMASTIK19{6_sh4rd5_r34ss3mbl3_th3_g0ph3r5_s3cr3t}

Write-up ini mengasumsikan file challenge tersedia sebagai ./hexlock. File awal memiliki ekstensi .txt, tetapi magic bytes-nya menunjukkan bahwa file tersebut merupakan executable ELF.


1. Recon Awal

Jangan menentukan jenis file hanya berdasarkan ekstensi. Periksa magic bytes dan metadata ELF terlebih dahulu.

cp 'hexlock (this is file not txt).txt' hexlock
chmod +x hexlock

file hexlock
sha256sum hexlock
readelf -h hexlock
readelf -S hexlock

Informasi penting yang diperoleh:

ELF 64-bit LSB executable, x86-64, statically linked, stripped

Empat byte pertama file adalah magic ELF:

7f 45 4c 46

Daftar section juga memuat beberapa section khas Go:

.go.buildinfo
.gopclntab
.gosymtab
.typelink
.itablink

Cari indikator tambahan dengan:

strings -a -n 5 hexlock | grep -E \
'go.buildinfo|gopclntab|runtime\.|crypto/aes|crypto/cipher|flag>|Correct!|Wrong\.'

Banyaknya string runtime Go, static linkage, dan metadata Go menunjukkan bahwa binary ini dibuat dengan Go. Karena binary sudah stripped dan standard library Go ikut ter-link secara statis, hasil strings dan disassembly sangat ramai. Analisis perlu difokuskan ke string aplikasi dan alur yang menggunakannya.


2. Mencari String Antarmuka

Binary menampilkan tiga string yang sangat membantu:

flag>
Wrong.
Correct!

Cari file offset masing-masing string:

grep -aob 'flag> ' hexlock
grep -aob 'Wrong.' hexlock
grep -aob 'Correct!' hexlock

Hasilnya:

flag>     0xd3607
Wrong.    0xd360d
Correct!  0xd3755

Section .rodata dipetakan pada:

Virtual address : 0x4ae000
File offset     : 0xae000

Rumus konversi file offset ke virtual address:

VA = section_VA + (file_offset - section_file_offset)

Contoh untuk string Correct!:

0x4ae000 + (0xd3755 - 0xae000) = 0x4d3755

Go biasanya merepresentasikan string menggunakan descriptor yang berisi pointer menuju data dan panjang string. Karena itu, kode belum tentu mereferensikan byte string secara langsung. Pencarian pointer little-endian ke alamat string membawa kita ke descriptor, lalu xref descriptor membawa ke fungsi utama aplikasi.

Fungsi validasi utama berada di sekitar:

0x4ace40

Dump fungsi tersebut:

objdump -d -M intel \
  --start-address=0x4ace40 \
  --stop-address=0x4ad120 \
  hexlock > main.asm

less main.asm

Alamat penting yang diperoleh:

0x4ace40  fungsi validasi utama
0x4acba0  pembentuk key
0x4acd40  wrapper enkripsi
0x4ad040  pemrosesan hasil enkripsi
0x4ad0ae  loop perbandingan byte

Jika menggunakan Ghidra, tekan G, masukkan alamat, lalu rename fungsi berdasarkan perilakunya:

0x4ace40 -> main_like
0x4acba0 -> build_key
0x4acd40 -> encrypt_input

Pada tahap ini fungsi encrypt_input belum perlu langsung disebut AES-GCM. Algoritmanya harus dibuktikan dari data flow.


3. Merekonstruksi Alur Validasi

Fungsi pada 0x4ace40 melakukan alur berikut:

  1. Menampilkan prompt flag> .
  2. Membaca satu baris input.
  3. Menghapus newline di akhir input.
  4. Membentuk key sepanjang 16 byte.
  5. Mengenkripsi input pengguna.
  6. Membandingkan hasil enkripsi dengan target global.
  7. Menampilkan Correct! atau Wrong..

Bagian perbandingan dapat dilihat dengan:

objdump -d -M intel \
  --start-address=0x4ad030 \
  --stop-address=0x4ad117 \
  hexlock

Instruksi penting:

4ad040: call   0x4acd40
4ad045: test   rax,rax
4ad048: je     0x4ad062
4ad04a: cmp    QWORD PTR [rip+...],rbx   # panjang target
4ad051: jne    0x4ad062
4ad053: mov    rdx,QWORD PTR [rip+...]   # pointer target
4ad05a: xor    ecx,ecx
4ad05c: xor    esi,esi

4ad0ae: movzx  r8d,BYTE PTR [rax+rcx]
4ad0b3: movzx  r9d,BYTE PTR [rdx+rcx]
4ad0b8: xor    r8d,r9d
4ad0bb: inc    rcx
4ad0be: or     esi,r8d
4ad0c1: cmp    rbx,rcx
4ad0c4: jg     0x4ad0ae
4ad0c6: test   sil,sil
4ad0c9: jne    0x4ad062

Pseudocode-nya:

encrypted = encrypt_input(user_input, &encrypted_len);

if (encrypted == NULL)
    wrong();

if (encrypted_len != expected_len)
    wrong();

difference = 0;

for (i = 0; i < encrypted_len; i++)
    difference |= encrypted[i] ^ expected[i];

if (difference != 0)
    wrong();

correct();

Jadi input tidak dibandingkan secara langsung dengan flag. Binary mengenkripsi kandidat input terlebih dahulu, kemudian membandingkan output autentikasinya dengan buffer target.


4. Membuktikan Penggunaan AES-GCM

Dump wrapper enkripsi:

objdump -d -M intel \
  --start-address=0x4acd40 \
  --stop-address=0x4ace40 \
  hexlock > encrypt.asm

less encrypt.asm

Data flow menunjukkan karakteristik berikut:

Panjang key       = 16 byte
Panjang nonce     = 12 byte
Overhead output   = 16 byte
Additional data   = kosong

Nilai-nilai ini cocok dengan AES-GCM standar pada Go:

  • Key AES 16 byte berarti AES-128.
  • GCM standar menggunakan nonce 12 byte.
  • GCM standar menambahkan authentication tag 16 byte.
  • AEAD.Seal pada Go menghasilkan ciphertext || tag.

Target memiliki panjang 66 byte. Karena tag GCM berukuran 16 byte:

Panjang plaintext = 66 - 16
                  = 50 byte

Konsep kode Go yang direkonstruksi:

block, err := aes.NewCipher(key)
if err != nil {
    return nil
}

gcm, err := cipher.NewGCM(block)
if err != nil {
    return nil
}

sealed := gcm.Seal(nil, nonce, input, nil)

Dengan demikian, algoritma yang digunakan adalah AES-128-GCM dengan AAD kosong.


5. Mengambil Target Ciphertext

Loop perbandingan mengakses descriptor slice Go global:

0x56f490  pointer data
0x56f498  length
0x56f4a0  capacity

Representasi slice Go pada target ini dapat dibaca sebagai:

struct GoSlice {
    void *data;
    uint64_t len;
    uint64_t cap;
};

Melalui GDB

gdb -q ./hexlock

Di dalam GDB:

set pagination off
starti
x/3gx 0x56f490

Descriptor tersebut menghasilkan:

data = 0x567660
len  = 0x42
cap  = 0x42

0x42 sama dengan 66 desimal. Dump tepat 66 byte:

dump binary memory target.bin 0x567660 0x5676a2
quit

Alamat akhir bersifat eksklusif:

0x567660 + 0x42 = 0x5676a2

Tampilkan sebagai satu baris hex:

od -An -v -tx1 target.bin | tr -d ' \n'
echo

Target yang diperoleh:

4ea13b5cc7a00ff3b8f3654beabe5bcc83a4c1710772371126e70c45528d8a7e1e2c472e373bd87fd6e68247c3a865e2b3ed12fc30641557730e00a28224efdb8594

Dua byte terakhir 8594 tetap merupakan bagian target 66 byte, bukan field terpisah. Parsing yang benar:

ciphertext = target[:-16]
tag = target[-16:]

Mengambil Langsung dari ELF

Mapping section .data:

.data VA          = 0x56efc0
.data file offset = 0x16efc0

Konversi alamat target ke file offset:

0x16efc0 + (0x567660 - 0x56efc0) = 0x167660

Extract tanpa GDB:

dd if=hexlock \
  of=target.bin \
  bs=1 \
  skip=$((0x167660)) \
  count=66 \
  status=none

6. Mengambil Nonce

Wrapper enkripsi memuat pointer 0x567148 dan panjang 0x0c:

Alamat nonce = 0x567148
Panjang      = 0x0c = 12 byte

Melalui GDB

x/12bx 0x567148
dump binary memory nonce.bin 0x567148 0x567154

Nonce yang diperoleh:

194b00b0922d969e007055a4

Langsung dari ELF

Konversi virtual address ke file offset:

0x16efc0 + (0x567148 - 0x56efc0) = 0x167148

Extract:

dd if=hexlock \
  bs=1 \
  skip=$((0x167148)) \
  count=12 \
  status=none | od -An -v -tx1 | tr -d ' \n'
echo

Output:

194b00b0922d969e007055a4

7. Analisis Pembentuk Key

Fungsi 0x4acba0 membangun key 16 byte dari beberapa shard.

objdump -d -M intel \
  --start-address=0x4acba0 \
  --stop-address=0x4acd40 \
  hexlock > key_builder.asm

less key_builder.asm

Pada awal fungsi, area lokal 16 byte dibersihkan:

movups XMMWORD PTR [rsp+0x20],xmm15

Artinya buffer key lokal dimulai dari:

rsp + 0x20

Instruksi seperti:

mov BYTE PTR [rsp+rax+0x20],bl

setara dengan:

key[rax] = bl;

Contoh Transformasi Shard Pertama

Salah satu shard di-decode dengan XOR:

movzx  ebx,BYTE PTR [rdx+rax]
xor    ebx,0x5a
mov    BYTE PTR [rsp+rax+0x20],bl

Pseudocode:

key[i] = shard[i] ^ 0x5a;

Contoh Transformasi Shard Kedua

Shard lain menggunakan transformasi affine:

movzx  ebx,BYTE PTR [rdx+rax]
lea    esi,[rbx+rbx*2]
lea    ebx,[rbx+rsi*2]
lea    ebx,[rbx+0x3d]

Perhitungannya:

esi = x + 2x = 3x
ebx = x + 2(3x) = 7x
ebx = 7x + 0x3d

Pseudocode:

key_part[i] = (7 * shard[i] + 0x3d) & 0xff;

Rekonstruksi Berbasis Bit

Tabel lain digunakan untuk membentuk byte satu bit demi satu bit:

movzx ebx,BYTE PTR [rbx+rsi]
and   ebx,0x1
shl   ebx,cl
or    edx,ebx

Pseudocode:

value = 0;

for (bit = 0; bit < 8; bit++)
    value |= (table[index] & 1) << bit;

Fungsi lengkap menggabungkan beberapa bagian seperti ini. Hal tersebut sesuai dengan isi flag yang menyebut enam shard sedang merakit kembali sebuah rahasia.


8. Menemukan Anti-Debug dan Mengambil Key yang Benar

Di dalam pembentuk key terdapat pemanggilan helper anti-debug:

4acc45: call   0x4ac700
4acc4a: test   al,al
4acc4c: je     0x4acc5a
4acc4e: movzx  eax,BYTE PTR [rsp+0x25]
4acc53: xor    eax,0x11
4acc56: mov    BYTE PTR [rsp+0x25],al

Karena key dimulai dari rsp+0x20, byte yang dimodifikasi adalah:

(rsp + 0x25) - (rsp + 0x20) = key[5]

Pseudocode:

if (anti_debug_check())
    key[5] ^= 0x11;

Artinya, key yang diambil langsung ketika debugger terdeteksi dapat sengaja dirusak.

Caller memanggil pembentuk key di:

4ad015: call   0x4acba0
4ad01a: movups xmm0,XMMWORD PTR [rsp]

Sesaat setelah fungsi kembali, hasil 16 byte tersedia pada $rsp milik caller.

Mengambil Key Normal dengan GDB

gdb -q ./hexlock

Pasang dua breakpoint:

set pagination off
set confirm off
set disassembly-flavor intel
break *0x4acc4a
break *0x4ad01a
run

Ketika prompt muncul, masukkan tepat 50 karakter dummy:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Breakpoint pertama berada tepat setelah fungsi anti-debug kembali. Paksa hasil boolean di AL menjadi false:

p/x $al
set $al = 0
p/x $al
continue

Ketika berhenti di 0x4ad01a, dump key 16 byte:

x/16bx $rsp
dump binary memory key.bin $rsp $rsp+16
quit

Tampilkan key:

od -An -v -tx1 key.bin | tr -d ' \n'
echo

Key normal:

7f8a7205095edb11515d302c2695808a

Byte normal pada indeks kelima adalah 0x5e. Jika cabang anti-debug dijalankan:

0x5e ^ 0x11 = 0x4f

Key palsu yang dihasilkan:

7f8a7205094fdb11515d302c2695808a

Penggunaan key palsu akan membuat verifikasi authentication tag AES-GCM gagal.


9. Material Kriptografi yang Berhasil Direcover

Algoritma : AES-128-GCM
Key       : 7f8a7205095edb11515d302c2695808a
Nonce     : 194b00b0922d969e007055a4
AAD       : kosong
Target    : ciphertext || authentication tag 16 byte

Target lengkap:

4ea13b5cc7a00ff3b8f3654beabe5bcc83a4c1710772371126e70c45528d8a7e1e2c472e373bd87fd6e68247c3a865e2b3ed12fc30641557730e00a28224efdb8594

10. Solver

Install PyCryptodome jika belum tersedia:

python3 -m pip install pycryptodome

Buat solve.py:

from Crypto.Cipher import AES

TARGET = bytes.fromhex(
    "4ea13b5cc7a00ff3b8f3654beabe5bcc"
    "83a4c1710772371126e70c45528d8a7e"
    "1e2c472e373bd87fd6e68247c3a865e2"
    "b3ed12fc30641557730e00a28224efdb"
    "8594"
)

KEY = bytes.fromhex(
    "7f8a7205095edb11515d302c2695808a"
)

NONCE = bytes.fromhex(
    "194b00b0922d969e007055a4"
)

ciphertext = TARGET[:-16]
tag = TARGET[-16:]

cipher = AES.new(KEY, AES.MODE_GCM, nonce=NONCE)
plaintext = cipher.decrypt_and_verify(ciphertext, tag)

flag = plaintext.decode()

assert len(plaintext) == 50
assert flag.startswith("GEMASTIK19{")
assert flag.endswith("}")

print(flag)

Jalankan:

python3 solve.py

Output:

GEMASTIK19{6_sh4rd5_r34ss3mbl3_th3_g0ph3r5_s3cr3t}

11. Verifikasi ke Binary Asli

printf '%s\n' \
'GEMASTIK19{6_sh4rd5_r34ss3mbl3_th3_g0ph3r5_s3cr3t}' |
./hexlock

Output yang diharapkan:

flag> Correct!

Dekripsi yang lolos autentikasi dan respons Correct! dari binary menjadi dua verifikasi independen bahwa flag benar.


12. Flag

GEMASTIK19{6_sh4rd5_r34ss3mbl3_th3_g0ph3r5_s3cr3t}

13. Referensi dan Challenge Serupa

Write-up CTF Serupa

  1. DefCamp CTF 2020: Stripped Go

    Challenge reverse engineering Go yang sudah stripped. Write-up tersebut membahas pemulihan simbol Go, penggunaan Ghidra, referensi AES-GCM, dan inspeksi data di sekitar fungsi enkripsi. Ini merupakan referensi yang paling dekat secara konsep dengan hexlock.

  2. BTCTF 2024: golang2 Reverse Engineering

    Write-up reverse engineering binary Go yang membahas kesulitan static linkage serta penggunaan tooling seperti GoReSym dan integrasi dengan Ghidra atau IDA.

Reverse Engineering Binary Go

  1. CUJO AI: Reverse Engineering Go Binaries with Ghidra

    Penjelasan mengenai tantangan analisis binary Go, termasuk static linkage, hilangnya nama fungsi, metadata Go, dan penggunaan script Ghidra.

  2. Mandiant GoReSym

    Tool untuk memulihkan metadata dari binary Go, termasuk informasi fungsi, source filename, line metadata, embedded structures, dan type.

  3. Go Reverse Engineering Tool Kit

    Koleksi tool untuk menganalisis binary Go, termasuk GoRE, Redress, Libgore, dan PyGoRE.

  4. Kumpulan Catatan dan Tool Reverse Engineering Go

    Kumpulan metode, tool, dan referensi untuk reverse engineering executable Go.

Perilaku AES-GCM

  1. Dokumentasi Go crypto/cipher

    Dokumentasi interface AEAD, NonceSize, Overhead, Seal, dan Open. Fungsi Seal mengenkripsi sekaligus mengautentikasi plaintext, lalu menambahkan hasilnya ke destination slice.

  2. Dokumentasi Go crypto/aes

    Dokumentasi bahwa AES menerima key 16, 24, atau 32 byte untuk memilih AES-128, AES-192, atau AES-256.

  3. Source Code GCM pada Go

    Implementasi GCM Go mendefinisikan nonce standar 12 byte dan authentication tag 16 byte. Ukuran tersebut sama dengan nilai yang ditemukan pada hexlock.


14. Ringkasan Alur Analisis

Urutan yang dapat digunakan untuk challenge stripped Go serupa:

identifikasi file
-> fingerprint metadata dan runtime Go
-> cari string aplikasi
-> cari descriptor string dan xref
-> temukan fungsi validasi utama
-> analisis transformasi input atau wrapper crypto
-> analisis loop perbandingan output
-> recover global Go slice
-> telusuri data flow key dan nonce
-> periksa anti-debug
-> lakukan dekripsi terautentikasi secara offline
-> verifikasi hasil ke binary

Alur ini menjaga analisis tetap berbasis bukti dan mencegah kesimpulan algoritma hanya berdasarkan string yang ditemukan.