A reverse engineering challenge for beginners.
Three tiny executables from the same author were triaged statically. The first was fingerprinted, its architecture and symbol state checked, and the leftover PDB path gave away the author’s IDE and Windows account. Disassembly of all three then showed how each build passes arguments: a 32-bit stack-based convention, a 32-bit register-assisted one, and the 64-bit Windows register convention. No sample was executed, and there is no attack chain here: the case is about reading compiler artifacts and calling conventions.
file, strings, objdump -dobjdump -dobjdump -dFormat note: The argument questions use stack notation such as
esp+4and register names in lowercase. The answer masks (***+*,***+**) tell you which notation is expected.
Hash the sample as delivered, before touching anything else, so every later note can be tied to the same file.
sha256sum argument_baby_1.exe
The PDB path embedded in the debug directory (Q5) points at a Visual Studio style project layout with a Release build folder, and the PE’s linker version fits MSVC 14.35, which ships with Visual Studio 2022. The environment is read from those two clues together rather than from a single “IDE” field.
strings -a argument_baby_1.exe | grep -i pdb
file reads the PE header machine field directly. It reports a PE32 image for Intel i386, so this is a 32-bit build.
file argument_baby_1.exe
There are no COFF symbols in the file, which is what a naive reading of “stripped” suggests. The debug directory still carries a PDB reference (Q5), so debug information was left in and the binary is not stripped in the sense the question means.
file argument_baby_1.exe
Gotcha:
truelooks right because the symbol table is empty, but the PDB path in the debug directory means debug info is present.falseis the accepted answer.
Linkers write the build-time PDB path into the RSDS debug record. A plain strings pass with a case-insensitive pdb filter surfaces it.
strings -a argument_baby_1.exe | grep -i pdb
From the Q5 path, the profile directory under C:\Users\ is the account that built the binary.
strings -a argument_baby_1.exe | grep -i 'users'
In objdump -d the arguments are pushed onto the stack before the call, nothing is loaded into ecx or edx for them, and the caller cleans the stack afterwards. That combination is the default 32-bit C convention.
objdump -d argument_baby_1.exe
With stack passing, the first argument sits at the lowest address at the call site. The offsets for Q8 to Q12 run in 4-byte steps from the stack pointer, in the notation the answer masks ask for. The offsets follow from the push order and the masks, so they were reasoned rather than read as literal [esp+n] operands in one listing.
objdump -d argument_baby_1.exe | grep -B8 call
Next slot up the stack, 4 bytes above the first.
objdump -d argument_baby_1.exe | grep -B8 call
Third slot, 8 bytes above the stack pointer.
objdump -d argument_baby_1.exe | grep -B8 call
Fourth slot, 12 bytes up, written in hex as the mask expects.
objdump -d argument_baby_1.exe | grep -B8 call
Fifth slot, 16 bytes up, again in hex.
objdump -d argument_baby_1.exe | grep -B8 call
The function computes its result into the accumulator just before ret.
objdump -d argument_baby_1.exe | grep -B5 ret
This 32-bit build takes its first two arguments in ecx and edx, with the rest on the stack, and the function ends in ret 0xc, so the callee pops the stack arguments. That is the fastcall pattern.
objdump -d argument_baby_2.exe
First integer argument goes in the first fastcall register.
objdump -d argument_baby_2.exe | grep -B8 call
Second argument goes in the second fastcall register.
objdump -d argument_baby_2.exe | grep -B8 call
Registers are exhausted, so the third argument is the first one on the stack, at the stack pointer.
objdump -d argument_baby_2.exe | grep -B8 call
Next stack slot, 4 bytes up.
objdump -d argument_baby_2.exe | grep -B8 call
Last stack slot, 8 bytes up. Three stack arguments at 4 bytes each match the ret 0xc from Q14.
objdump -d argument_baby_2.exe | grep -B8 call
file reports PE32+, the 64-bit PE format.
file argument_baby_3.exe
The 64-bit Windows convention passes the first four integer arguments in registers, starting with rcx.
objdump -d argument_baby_3.exe | grep -B8 call
Second register in the sequence.
objdump -d argument_baby_3.exe | grep -B8 call
The extended registers r8 and r9 carry the third and fourth arguments, which is why the masks drop to two characters.
objdump -d argument_baby_3.exe | grep -B8 call
Fourth register slot.
objdump -d argument_baby_3.exe | grep -B8 call
The fifth argument spills to the stack, above the 32-byte shadow space the caller reserves for the register arguments, which puts it at rsp+20 in hex.
objdump -d argument_baby_3.exe | grep -B8 call
Same accumulator as the 32-bit builds, but at full 64-bit width.
objdump -d argument_baby_3.exe | grep -B5 ret
Gotcha:
eaxis the reflex answer carried over from the 32-bit samples. In a 64-bit build the question wants the 64-bit register name.
There is no intrusion in this case. The three files are training binaries.
| Phase | Action |
|---|---|
| Triage | Hash, architecture and symbol state recorded for sample 1 |
| Attribution artefacts | PDB path exposes the author’s IDE, project folder and Windows account |
| Analysis | Calling convention and argument locations read from disassembly for all three builds |
| Type | Value |
|---|---|
| SHA-256 (argument_baby_1.exe) | dc6267608ddfcc5c80571ccd46475a03fb667baf8620d0e91c93ccacacf97ef6 |
| PDB path | C:\Users\HTB\Desktop\argument\Release\argument.pdb |
No ATT&CK techniques apply: the case covers static reverse engineering of benign training binaries, with no adversary behaviour.
Build paths leak the author. The PDB path named the IDE project folder and the Windows account that compiled the sample. Collect PDB paths from every sample as a pivot for clustering related tooling.
“Stripped” needs a precise check. An empty symbol table does not mean the debug directory is empty. Check both before calling a binary stripped.
Know the calling convention before reading assembly. Cdecl, fastcall and the x64 convention put arguments in different places, and misreading them sends a decompiler review in the wrong direction. Confirm architecture with file first, then read the call sites and the ret form.
Register width follows architecture. Return values live in eax on 32-bit builds and rax on 64-bit builds, a small difference that is easy to get wrong when switching between samples.
Three small binaries covered the three conventions an analyst meets most often on Windows. The PDB path was the one piece of attribution-relevant data, and it came from a compiler default rather than anything the author chose to hide.