In this challenge, players will need to apply some reverse engineering concepts to understand how a binary is trying to run encrypted shellcode and how its moving in memory space. Users will also need to find decrypted shellcode at runtime.
The sample is a 64-bit console executable built with MinGW GCC. It has no ASLR, so it loads at a fixed base and every address in the disassembly is the real one. The loader keeps an encrypted shellcode blob in .data beside a short key, decrypts it with a hand-written LFSR-style stream cipher, and passes the result to a memory-writing API. The allocator and writer are resolved at runtime through GetProcAddress, so they never appear in the import table. Static work was enough to recover the key, the ciphertext and the first plaintext bytes, which form a valid shellcode prologue. The decrypted buffer lives on the runtime stack, so its address needed a debugger run.
pefile, objdump, stringsNote: Static code reading was done from
objdump -doutput. All addresses carry the0x140000000image base because the binary has no ASLR (Q4, Q5).
Unzip the sample with the case password, run file to confirm a PE32+ executable, and hash it. The PE type decides the tooling for everything that follows.
sha256sum func_pointer.exe
GCC-built images embed a GCC: (...) producer string. The full identifier names the Windows port, the target flavour (x86_64, ucrt, posix threads, SEH) and version 12.2.0. The answer is the name of that Windows port.
strings -a func_pointer.exe | grep 'GCC:'
FILE_HEADER.TimeDateStamp is an epoch value. Convert it to UTC and format it to match the task mask.
python3 -I -c "import pefile,datetime;p=pefile.PE('func_pointer.exe');print(datetime.datetime.utcfromtimestamp(p.FILE_HEADER.TimeDateStamp))"
Tip: The timestamp is whatever the linker wrote. It is the expected answer here, but on other samples treat it as an attacker-controllable field.
ASLR is the DYNAMIC_BASE bit (0x40) in OPTIONAL_HEADER.DllCharacteristics. The field reads 0x100, which is only NX compatibility, so the bit is clear and the image never relocates.
python3 -I -c "import pefile;p=pefile.PE('func_pointer.exe');print(hex(p.OPTIONAL_HEADER.DllCharacteristics))"
With ASLR off (Q4), the preferred base in the optional header is also the real load address. It prefixes every address in the objdump -d listing used later.
python3 -I -c "import pefile;p=pefile.PE('func_pointer.exe');print(hex(p.OPTIONAL_HEADER.ImageBase))"
AddressOfEntryPoint is an RVA, so it is reported relative to the Q5 base, not as a full virtual address. The four-digit mask in the task fits the RVA.
python3 -I -c "import pefile;p=pefile.PE('func_pointer.exe');print(hex(p.OPTIONAL_HEADER.AddressOfEntryPoint))"
Gotcha: Adding the image base gives
0x140001125, which does not fit the0x****mask.
Static analysis shows the copy but not where the stack lands, because the decrypted bytes go into a runtime stack buffer. Run the sample in x64dbg on an isolated analysis VM and break on WriteProcessMemory. Under the x64 fastcall convention the third argument (r8, lpBuffer) points at the shellcode, and the dump at that address starts FC 48 81 E4, matching the plaintext from Q10. This answer came from a live run rather than from the file.
bp kernel32.WriteProcessMemory
Hint: Check the argument registers at the breakpoint and follow
r8in the dump pane. The stack address can differ between runs and machines, so confirm the bytes at it rather than trusting a remembered value.
Disassemble main and follow the data references. It calls a decrypt function at 0x1400017d0 with a 0x150-byte buffer in .data at 0x140004040. Dump .data and read the first 8 bytes at that address.
objdump -d func_pointer.exe
objdump -s -j .data func_pointer.exe
The key is an 8-byte value in .data at 0x140004020, 0x20 bytes before the blob from Q7, and the same dump shows it. The sixteen-digit mask matches 8 bytes written as one 64-bit constant.
objdump -s -j .data func_pointer.exe
Re-implement the routine at 0x1400017d0 instead of stepping through it. It is a stream cipher driven by three state registers (an LFSR-style keystream) seeded from the Q8 key. Run the Python version over the 0x150 bytes from Q7. The output opens with a standard x64 shellcode prologue, which confirms the cipher was reproduced correctly.
python3 -I dec_payload.py
Tip: A recognisable shellcode prologue is the correctness check. Random-looking output means the cipher or key handling is wrong.
Compare the import table with the strings in .rdata. The loader stores API names as plain strings and resolves them with GetProcAddress, so the allocator is absent from the imports. The name starting with V is the remote-process variant of the virtual allocation API.
strings -a func_pointer.exe | grep -i '^Virtual'
| Phase | Action |
|---|---|
| Defense Evasion | Shellcode stored encrypted in .data with an 8-byte key beside it |
| Defense Evasion | Memory APIs resolved with GetProcAddress, keeping them out of the import table |
| Execution | Custom stream cipher decrypts the 0x150-byte blob into a stack buffer |
| Execution | Decrypted bytes handed to WriteProcessMemory for execution as shellcode |
| Type | Value |
|---|---|
| SHA256 | edd41b4a819f917f81203424730aaf0c24cc95e40acfc0f1bd90b11dadf58015 |
| File | func_pointer.exe |
| Decryption key | 0x5555555555555555 |
| Encrypted blob | .data at 0x140004040, length 0x150 |
| Decrypt function | 0x1400017d0 |
| Technique | ID | Description |
|---|---|---|
| Obfuscated Files or Information: Encrypted/Encoded File | T1027.013 | Shellcode held encrypted in .data and decrypted at runtime |
| Obfuscated Files or Information: Dynamic API Resolution | T1027.007 | Allocator resolved via GetProcAddress instead of the import table |
| Native API | T1106 | Direct use of Windows memory APIs to stage the payload |
Import tables undersell what a binary does. The allocator is missing from the imports because it is resolved at runtime. Pair import hashing with a scan of .rdata for API name strings, and treat a small import table alongside GetProcAddress as a loader signal.
A fixed image base is a useful fingerprint. Without DYNAMIC_BASE, code and data sit at predictable addresses. That makes static signatures on the encrypted blob reliable, and it is a hardening gap on any binary you build yourself.
Encrypted payloads in .data still leave a decryptable trail. The key sat 0x20 bytes from the blob, so a static unpack was straightforward. Detecting the shellcode prologue after decryption matters more than hunting for the cipher, which can change per build.
Memory-based detection is the backstop. The plaintext exists only at runtime in a stack buffer and then in target memory. A breakpoint on WriteProcessMemory exposes it in one step, and EDR memory scanning catches the same moment at scale.
The loader hid its payload behind a custom cipher and a runtime-resolved allocator, and both gave way to static analysis. Only the address of the decrypted buffer needed a live run, and one breakpoint on the write API was enough to get it.