// HackTheBox  ·  writeup

Payload

HackTheBox Easy pefileobjdumpstringssha256sumx64dbgPython
Malware Analysis

Scenario

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.

Executive Summary

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.

Artifacts

Note: Static code reading was done from objdump -d output. All addresses carry the 0x140000000 image base because the binary has no ASLR (Q4, Q5).

Execution

Q1 — What is the SHA256 hash of func_pointer.exe?

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
Click flag to reveal edd41b4a819f917f81203424730aaf0c24cc95e40acfc0f1bd90b11dadf58015
Q2 — What compiler is being used?

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:'
Click to reveal answer MinGW
Q3 — What is the compilation date?

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.

Click flag to reveal 2023-04-06 15:21:17
Q4 — Is ASLR enabled? (True or False)

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))"
Click to reveal answer false
Q5 — What is the image base address?

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))"
Click flag to reveal 0x140000000
Q6 — What is the entry point?

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 the 0x**** mask.

Click to reveal answer 0x1125
Q9 — What is the address of the decrypted payload?

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 r8 in the dump pane. The stack address can differ between runs and machines, so confirm the bytes at it rather than trusting a remembered value.

Click flag to reveal 0x5FFC40

Defense Evasion

Q7 — What are the first 8 bytes of the encrypted payload that is being moved to allocated memory? (format: daffd563616c632e)

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
Click flag to reveal 8d098d59a01f830a
Q8 — What is the key for decryption in hex?

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
Click to reveal answer 0x5555555555555555
Q10 — What are the first 8 bytes of the decrypted payload that is being moved to allocated memory? (format: daffd563616c632e)

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.

Click flag to reveal fc4881e4f0ffffff
Q11 — There are several functions that are not in the import table but are invoked. Which of these functions starts with `V`?

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'
Click flag to reveal VirtualAllocEx

Attack Summary

PhaseAction
Defense EvasionShellcode stored encrypted in .data with an 8-byte key beside it
Defense EvasionMemory APIs resolved with GetProcAddress, keeping them out of the import table
ExecutionCustom stream cipher decrypts the 0x150-byte blob into a stack buffer
ExecutionDecrypted bytes handed to WriteProcessMemory for execution as shellcode

IOCs

TypeValue
SHA256edd41b4a819f917f81203424730aaf0c24cc95e40acfc0f1bd90b11dadf58015
Filefunc_pointer.exe
Decryption key0x5555555555555555
Encrypted blob.data at 0x140004040, length 0x150
Decrypt function0x1400017d0

MITRE ATT&CK

TechniqueIDDescription
Obfuscated Files or Information: Encrypted/Encoded FileT1027.013Shellcode held encrypted in .data and decrypted at runtime
Obfuscated Files or Information: Dynamic API ResolutionT1027.007Allocator resolved via GetProcAddress instead of the import table
Native APIT1106Direct use of Windows memory APIs to stage the payload

Defender Takeaways

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.

Conclusion

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.