Anatomy of a Fake Windows Security Warning That Just Writes Your PIN to a Text File

I recently pulled apart a small malware sample calling itself WindowsPassKey.exe (SHA-256 a93c946c237b981189d2668d938a9d4d1d9681757e48dae8d9d65ed25b5da657, 25,600 bytes, SHA-1 1798612c9445ea7c411f269d984e2aaed4bfcaf0, MD5 e0f775585bd8580d5cdda0e7d91a370a). It's a 32-bit .NET Windows Forms binary, and it's about as unsophisticated as malware gets — which is exactly what makes it a useful teardown.

Caveat on method: my usual container-backed toolchain (capa, FLOSS, Ghidra) wasn't reachable for this sample — Docker/Podman wasn't running — so everything below comes from direct PE/.NET metadata parsing, managed IL inspection, and string/resource review on the host. No dynamic execution was performed. Static analysis can't prove the absence of every runtime behavior, but the application-defined method set here is small enough that I'm confident in the coverage.

The lure

On load, the form calls SetWindowPos (user32.dll) with SWP_NOMOVE, SWP_NOSIZE, and topmost flags — pinning itself above other windows without moving or resizing. It then displays:

Windows Security found a threat.
Please verify it's you in order to remove threat
Please enter your login PIN

Classic pretext: borrow the visual authority of a system dialog, manufacture urgency, and ask for a credential people are conditioned to hand over to "Windows" itself. PE version metadata identifies the binary as WindowsPassKey, version 1.0.0.0, original filename WindowsPassKey.exe, with a 2025 copyright string.

The payload

The entire theft routine lives in button1_Click, and it's a short, linear sequence:

  1. Read textBox1.Text via get_Text.
  2. Compare against an empty string.
  3. If non-empty, construct new StreamWriter("output.txt", true) — the true opens in append mode.
  4. WriteLine the submitted text.
  5. Dispose() the writer.
  6. Environment.Exit(0).

Because the file is opened in append mode, every successful submission across multiple runs stacks up as a new line in the same output.txt, written to the process's current working directory. The second button's click handler is a bare return — dead weight, no action.

There's no network stack invoked anywhere in the reviewed IL: no sockets, no HttpClient/WebRequest, no DNS resolution. No persistence (no run-key writes, no scheduled-task or service creation observed). No browser credential-store access, no process injection, no follow-on payload staging. The mscoree.dll!_CorExeMain import just confirms this is a standard managed executable — unremarkable on its own.

Obfuscation and anti-analysis: essentially none

This is the part that made static analysis fast. Namespaces, classes, and handlers keep their original descriptive names: WindowsPassKey.Form1, button1_Click, textBox1. Framework calls resolve cleanly (get_Text, StreamWriter, WriteLine, Dispose, Environment.Exit) with no reflection-based indirection or dynamic assembly loading. No control-flow flattening, opaque predicates, or string decoders in the application-defined methods.

The .text section does run ~7.23 bits/byte entropy, which on its own might suggest packing — but the executable is managed and carries embedded resources, both of which push entropy up without implying compression or encryption. Given the fully transparent metadata and IL, I'd weight that entropy reading as noise, not packing evidence.

On the anti-debugging side: no Debugger.IsAttached, no IsDebuggerPresent/CheckRemoteDebuggerPresent/NtQueryInformationProcess, no timing checks, no analysis-tool or VM/sandbox fingerprinting, no exception-based detection, no self-modifying or dynamically generated code. There is a System.Diagnostics.DebuggableAttribute reference and a PE debug directory, but those are ordinary build artifacts, not controls. One curiosity: the PE COFF timestamp decodes to 2078-09-15T04:25:14Z — implausibly future-dated, likely altered or non-semantic, and not itself an anti-analysis mechanism.

Does a harvested PIN actually matter?

Windows Hello PINs are device-bound, so a PIN sitting in output.txt isn't a portable credential the way a password would be — it doesn't grant remote authentication on its own. But it's still authentication material: combined with physical access to the machine, or paired with other stolen data, it closes the gap between "has the laptop" and "is fully authenticated on it." That's an inference about consequence, not an additional capability observed in the binary.

Hunting notes

Type Value
SHA-256 a93c946c237b981189d2668d938a9d4d1d9681757e48dae8d9d65ed25b5da657
SHA-1 1798612c9445ea7c411f269d984e2aaed4bfcaf0
MD5 e0f775585bd8580d5cdda0e7d91a370a
Internal filename WindowsPassKey.exe
.NET type WindowsPassKey.Form1

No domains, IPs, mutexes, registry paths, or scheduled-task/service names were present to pivot on. output.txt is too generic a filename to serve as a standalone IOC — treat it as corroborating context (an unexplained append-mode text file next to a suspicious binary) rather than a detection signature by itself.

Takeaway

Zero obfuscation, zero anti-analysis, zero network activity, zero persistence — and yet the sample achieves its objective if even one victim believes the dialog. This is a good specimen for the argument that the hard part of a credential-theft attack is rarely the code; it's getting the target to trust the box on their screen. The if (!string.IsNullOrEmpty()) WriteLine() routine here is trivial. The social engineering is doing 100% of the work.

Commentaires