This challenge started with exactly two artifacts:
-
capture.pcapng -
DbgInfo.DMP
There was no additional context to rely on, so the investigation had to begin directly from the two artifacts.
The practical approach was to split the problem in half.
The PCAP would tell me what the implant said on the wire. The minidump would show what survived in runtime memory. Together, they gave two independent views of the same incident.
This write-up follows that trail from initial triage to final binary analysis. It covers C2 traffic, AES key recovery, screenshot extraction, the recovered executable, and the reverse engineering path that tied everything together.
Initial triage
I started by confirming the provided files:
ls -lh capture.pcapng DbgInfo.DMP
That gave me exactly what I expected and nothing else:
-
capture.pcapng -
DbgInfo.DMP
That was a clean starting point, and it defined the scope immediately.
The plan from there was simple enough:
- use the PCAP to reconstruct the C2 behavior
- use the minidump to confirm what existed in memory
- use both to recover the screenshot and the uploaded binary
- reverse the binary and see what it was hiding
The plan was simple on paper, but the execution still required careful iteration.
Inspecting the PCAP
I started with the packet capture because network traffic usually reveals the attack shape quickly: protocol, cadence, and task flow.
My first attempt was basic:
tshark -r capture.pcapng -q -z io,phs
That failed immediately with Exit code 127, so tshark was missing from the environment.
After installing it, I reran the same checks.
Once tshark was available, I asked for the protocol breakdown and the HTTP view. That was the first useful shape of the traffic.
The capture was centered around HTTP. The flow showed an implant checking in to an internal service on a single port, receiving tasks, and returning results. The endpoints included login, ping, and query-style requests consistent with a C2 API.
I also exported the HTTP objects so I could inspect each artifact directly:
tshark -r capture.pcapng --export-objects http,http_objects
That gave me a bunch of small objects and at least one larger one that immediately looked worth attention. The small ones were the interesting part first. One of them contained the login response, which held the implant ID and an XOR-obfuscated AES key blob.
That was the first real clue. Not the flag, not the screenshot, not the binary. Just enough cryptographic structure to show the traffic was not plain text.
The key exchange was the part that made the rest of the traffic readable
At this point the PCAP was doing the usual thing where it looks like a normal web service until it suddenly stops being normal on purpose.
The login response had a short JSON object with an implant ID and a key field. The key was not directly usable. It was base64, and the bytes inside were XOR-obfuscated before AES came into play.
That meant I needed a way to reverse the pre-crypto layer first, then test whether the result actually decrypted something useful.
The helper script for the implant family was the obvious place to look. I cloned it, opened it, and checked how it expected to recover keys. Then I installed the crypto dependency it required.
cd /tmp/nimplanted
cat NimPlanted.py | head -100
pip3 install pycryptodome
That immediately made the structure of the recovery clearer. The helper was built around the same idea I was already inferring from the capture, which was reassuring. It meant I was not inventing the shape of the problem from thin air.
The actual recovery logic was:
- reverse the XOR obfuscation over the key blob
- test candidate 16-character alphanumeric AES keys
- use AES-CTR with the decoded key material
- stop when the decrypted sample turns into printable JSON
I did not brute-force the entire keyspace first. I tested a smaller range to validate the approach before scaling it.
The small-range test came back with a real key and a real decrypted sample. That sample decrypted to a short JSON task that looked like a normal implant check-in, which was the proof I needed that the key was correct.
Once that worked, the rest of the traffic became readable structured data.
Decrypting the whole conversation turned the capture into a transcript
With the key validated, I walked through the exported HTTP objects and decrypted the encrypted JSON fields one by one.
The pattern was the same every time:
- base64 decode the field
- split off the IV
- use AES-CTR with the recovered key
- decode the plaintext as JSON or text
That turned the PCAP from "a lot of packets" into an actual transcript.
The decrypted traffic showed the implant registration and a sequence of tasks. The operator started with basic host and identity checks before moving to data collection.
The tasks were things like:
-
whoami -
pwd -
shell ipconfig -
ps -
getAv -
env - a ping to a private IP
-
screenshot - an upload of a binary
-
shell whoami /all
That sequence is significant. It shows a progression from host and identity checks to environment discovery, security-tool enumeration, and artifact collection.
The registration record also told me what sort of host this was. I am not going to list every machine metadata field, but the important part was that the PCAP and the minidump were clearly talking about the same Windows host and the same process family. That cross-check was important because it stopped me from treating them as separate puzzles.
The capture was already giving me the outline of the attack. The screenshot and the binary would fill in the details.
Extracting the screenshot
The screenshot task was the next obvious thing to chase. The operator had clearly asked the implant to grab the screen, which meant the result should exist somewhere in the decrypted traffic.
The result object was layered: base64, then gzip, then PNG, but not in a way that was obvious on first pass.
My first attempt was too shallow. I tried to decompress what I thought was the gzip blob and got this error:
gzip.BadGzipFile: Not a gzipped file (b'H4')
That indicated I was feeding gzip a base64 layer instead of the compressed payload.
So I backed up and decoded it correctly.
The working flow was:
- decrypt the task result with the AES key
- parse the JSON wrapper
-
base64 decode the
resultfield - base64 decode that again
- gzip decompress the final blob
- save the output as a PNG
The key detail was the second base64 layer. Once that was handled correctly, the gzip header appeared and the payload turned into an image.
I saved the screenshot from the PCAP as ss_pcap.png and checked it visually. OCR quality was poor at first, which was expected because screenshot text in C2 captures is often compressed and scaled.
I tried tesseract anyway:
tesseract ss_pcap.png stdout --psm 6
That did not immediately produce the required text. I cropped the relevant region, tried again, and also checked the image with zsteg to rule out obvious steganographic content.
The important result was that the screenshot contained the first visible fragment of the flag inside the editor window. The main issue was the image quality and text rendering, which made the fragment harder to read directly.
So I took the screenshot seriously, made it readable, and recovered the first part of the flag from the image itself.
That step was important because it provided direct, verifiable evidence for the first flag fragment.
The minidump confirmed the screenshot was not a one-off artifact
Once I had the screenshot from the PCAP, I wanted to know whether the same visual artifact also existed in memory. That would make the case much stronger. If the screenshot appeared in the minidump too, then I was not dealing with a random transmission artifact. I was looking at a live runtime trace of the same thing.
I started with a basic parser, and the first call failed immediately.
pip3 install minidump -q
python3 -c '
from minidump.minidumpfile import MinidumpFile
mf = MinidumpFile.parse("DbgInfo.DMP")
print(mf.get_streams())
'
This failed with an AttributeError because the method I used was not available in the installed library version. I inspected the object and adjusted the approach.
So I stepped back, inspected the object with dir(), and checked the system info directly. That gave me the important part first: the dump was from a Windows 11 22H2 system, build 22621. That was consistent with the other runtime evidence and provided a useful basis for the memory analysis.
From there I searched the dump for the obvious marker of a gzip-wrapped base64 blob, the H4sI prefix. That was the clue I needed.
The minidump contained a screenshot artifact too, but this one was larger and lived as an embedded base64 gzip blob. I decoded it, decompressed it, and saved it as a separate screenshot image.
That was useful for two reasons.
First, it proved the screenshot was not just a network artifact. It was present in memory too.
Second, the higher resolution version confirmed that the desktop state was consistent with the PCAP version. Same environment, same artifact family, same underlying session. This provided an independent cross-check between the network and memory evidence.
So at this point I had:
- a screenshot from the PCAP
- a screenshot from the minidump
- the first flag fragment visible in the screenshot
- confirmation that the screenshot artifact existed in runtime memory
That was enough to move on without guessing.
The large upload object turned out to be the recovered executable
The traffic also contained a large uploaded object, which the decrypted C2 transcript made impossible to ignore. That upload was the next thing I pulled apart.
The encrypted object decrypted to compressed data. Once decompressed, the bytes turned into a Windows PE file. That was the recovered executable, which I referred to as rev.exe for clarity.
The chain there was simple in hindsight:
- AES-decrypt the upload object
- decompress the result
-
verify the output starts with
MZ - save it as a PE file
That gave me the binary I needed for the reverse engineering part of the investigation.
This is where the PCAP stopped being just network evidence and became the source of a second stage. The operator had pushed a file to the host, and the capture preserved it. No guessing, no assumptions, just a concrete executable that belonged in the session.
That binary was the bridge between the network side and the code side of the investigation. It was the part that turned the forensic case into a reverse engineering case.
From there, the next step was binary analysis.
Initial PE analysis
The first pass on the recovered executable was the usual triage.
file rev.exe
sha256sum rev.exe
objdump -h rev.exe
objdump -p rev.exe | grep -A300 'The Import Tables'
strings -a -n 5 rev.exe > strings.txt
strings -el -n 5 rev.exe > strings_unicode.txt
The file was a PE32+ x64 console executable with six sections. Nothing there was exotic on its own. The imports were also not screaming for attention at first glance, which is usually how these things prefer to start their nonsense.
Then the string scan and import table started narrowing the field.
The binary imported a lot of generic Windows APIs, but the ones that actually mattered were:
-
VirtualProtect -
GetProcAddress -
LoadLibraryExW -
IsDebuggerPresent
That import combination is a common loader signal: memory-permission changes, dynamic API resolution, and debugger checks.
There were also a lot of generic runtime and console APIs, plus exception-handling and thread-related calls. The important part was what was missing. There was no obvious network-stack import in the table. This suggested either that the binary did not handle networking directly or that relevant APIs were resolved dynamically at runtime.
This one was doing the second thing.
Analyzing the loader entry point
I loaded the executable into radare2 with relocations applied and ran analysis.
r2 -e bin.relocs.apply=true -A rev.exe
The entry point was not the payload. It was a small wrapper that called into another function and then jumped onward.
The entry point primarily performed setup before transferring control to the code that handled the main logic.
I opened main and the pattern became clearer.
The function did three important things:
- it checked a timing condition
-
it made a
VirtualProtectcall on a fixed memory region - it ran a decryption loop over 687 bytes and then called into that same region
The relevant address was the one that kept showing up around the decryption logic. The size was 0x2af, which is 687 bytes. That number mattered because it matched exactly with the data buffer I extracted later.
The decryption loop was the big clue. It was not just a random XOR. It looked like a stream cipher setup, with a 256-byte state array, repeated swaps, and a keystream XOR over the buffer. RC4 was the obvious fit.
At this point, the binary could be characterized as a loader containing an encrypted stage.
That is a much more useful sentence to be able to say.
Recovering the encrypted stage
Once I understood that the code was decrypting and executing a buffer in memory, I extracted exactly that region from the executable.
r2 -q -c 'p8 687 @ 0x1400014b0' rev.exe | tr -d '\n' | xxd -r -p > stage.enc
wc -c stage.enc
xxd -l 64 stage.enc
The size matched exactly: the code referenced 687 bytes and the extracted buffer was also 687 bytes. This confirmed that the buffer identified in the disassembly was the correct region.
The raw bytes were not readable code yet, which was exactly what I expected. They were not directly readable as code, which was consistent with the decryption logic identified in the loader.
So I rebuilt the decryption routine in Python, used the static key string from the binary, and ran the buffer through it.
The output was immediately recognizable as x64 shellcode.
The first bytes looked like this:
fc 48 83 e4 f0 e8 ...
This is consistent with a Windows shellcode entry sequence that establishes its execution context before resolving APIs dynamically. At this point, the buffer could be treated as executable shellcode rather than opaque data.
The observed behavior matched the loader logic identified during analysis.
It decrypted a second stage in memory, changed the memory protection, and executed the result.
That is the core loader pattern: hide the real stage until runtime, then jump into decrypted memory.
Analyzing the decrypted shellcode
At this point, the loader's role was clear. The encrypted 687-byte region had been recovered, the RC4 routine had been reproduced, and the decrypted bytes were confirmed as x64 shellcode.
The next thing I did was squeeze every readable string out of the shellcode.
strings -a -n 4 stage.dec
That output included useful strings such as wininet, the private IP seen in the C2 workflow, and a slash-heavy path that looked like a request or resource name. Those strings tied the shellcode back to the network behavior seen in the PCAP.
But there was one more thing in there. A readable string that was not just a network hint, not just a config stub, but the missing second fragment of the flag.
The second fragment was present in the decrypted output. Once that string was confirmed, the chain was straightforward: the screenshot provided the first fragment, and the shellcode provided the second.
That was the point where I stopped treating the shellcode as "interesting output" and started treating it as the answer.
Validating the screenshot path
Even after the shellcode path became clear, I kept validating the minidump screenshot. The memory image was higher resolution, so it was still worth checking for additional artifacts.
The dump had already given me a few reasons to keep looking there:
-
the
screenshot.pngartifact was present in memory -
the screenshot dimensions were
2560x1440 -
the desktop showed a PDF icon named
EULA, which looked potentially relevant
So I kept testing the image anyway.
convert screenshot.png -resize 50% -contrast-stretch 0x5% -colorspace Gray ocr_prep.png
tesseract ocr_prep.png stdout --psm 6
The output was not useful.
I tried additional crop and threshold passes as well.
convert ss_pcap.png -colorspace Gray -negate -threshold 50% bin.png
tesseract bin.png stdout --psm 6
That produced even less reliable text.
I also tried the easy obvious forensic checks on the dump screenshot.
steghide extract -sf screenshot.png -p ""
exiftool screenshot.png
Those went exactly as well as you'd expect when the package is not installed or the file is not hiding anything obvious. steghide was not available, exiftool was not there either, and the image metadata was basically empty when I checked it through Pillow.
python3 -c '
from PIL import Image
img = Image.open("screenshot.png")
print(img.info)
print(img.size)
'
That confirmed the file contained a normal image without additional metadata or embedded content exposed by this check.
I also searched the dump directly for flag-shaped strings.
strings DbgInfo.DMP | grep -oE 'HTB\{[^}]+\}'
strings DbgInfo.DMP | grep -oE 'flag\{[^}]+\}'
strings DbgInfo.DMP | grep -i htb | head
That did not produce the final flag. It returned noisy data and an embedded base64 artifact, but nothing that directly yielded the remaining fragment.
So the dump screenshot stayed a clue, but not the last clue.
The remaining flag fragment was obtained from the decrypted shellcode.
Why the evidence was sufficient
At that point the chain was finally clean.
The screenshot from the PCAP gave me the first fragment. The decrypted shellcode gave me the second fragment. The minidump screenshot was useful runtime evidence, but it did not directly provide the remaining flag fragment. It helped validate the relationship between the memory and network artifacts.
Once the shellcode string lined up with the rest of the evidence, there was no reason to keep hunting for a more exciting answer.
The second fragment came directly from the decrypted stage that the loader pulled into memory and executed. This was consistent with the network capture, the recovered executable, and the runtime memory evidence.
That was enough to close the loop confidently because the evidence aligned across network, binary, and memory artifacts.
The final assembly
So the investigation ended with the two fragments coming from two different layers of the same incident:
- the first fragment from the screenshot
- the second fragment from the decrypted shellcode
That is also why I kept the screenshot, the dump, and the binary in the same story instead of treating them like separate mini challenges. They were all pieces of the same chain, and the answer only became obvious after I stopped asking each artifact to be the hero on its own.
The final result was the complete flag assembled from both halves, verified against the actual artifacts instead of guessed from a pretty string or a convenient coincidence.
Conclusion
The investigation was easier to reason about when treated as a chain of transformations rather than a search for an isolated flag string.
The PCAP became decrypted JSON.
The JSON became a screenshot and a binary upload.
The binary became an executable PE.
The PE became an encrypted 687-byte stage.
The stage became shellcode.
The shellcode gave up the missing fragment.
The early dead ends were useful because they prevented premature conclusions and helped narrow the investigation step by step.
Each layer initially looked like a separate problem: C2 traffic, memory noise, a generic executable, and shellcode bytes.
In practice they were all parts of the same chain.
When a loader decrypts a stage in memory and jumps into it, static bytes become direct behavioral evidence.