I built a thing called WinRecon. it's a python script that audits a windows box and hands you back a security score. 20 checks, runs on the standard library only, no pip, no internet. you point it at a machine and it tells you the firewall is off on the public profile, smbv1 is still enabled, rdp has network level auth turned off, defender is disabled, that kind of stuff. it writes one self-contained html report and a json file you can feed into a SIEM.
the check i spent the most time on reads scheduled tasks and startup entries and tries to spot an attacker living off the land. encoded powershell, certutil pulling down a file, regsvr32 running a remote scriptlet, bitsadmin, msiexec, the usual lolbin crowd. the idea was simple. attackers reuse the same handful of signed binaries, so just scan the command lines for those keywords.
first time i ran it on my own laptop it threw four criticals. every one of them was rundll32 or certutil. none of them were malware.
the keyword scanner was technically correct
here's roughly what the first version did. i had a flat list of bad strings and i checked if any of them showed up in the task's command line.
SUSPICIOUS = [
"-enc", "-encodedcommand", "frombase64string", "bypass",
"certutil", "bitsadmin", "regsvr32", "rundll32", "msiexec",
"invoke-expression", "downloadstring", "ngrok.io",
"raw.githubusercontent", "pastebin.com",
]
def scan_command(cmd):
hits = [s for s in SUSPICIOUS if s in cmd.lower()]
if hits:
return Finding("CRITICAL", f"suspicious task: {hits}")
return None
the problem is that windows ships with a pile of scheduled tasks that legitimately call rundll32. there's one that runs rundll32.exe advpack.dll,DelNodeRunDLL32. there's printer stuff, there's a microsoft compatibility appraiser task. certutil shows up in cert maintenance. so the scanner was right that the binary was there. it just had no idea whether the binary was doing something bad.
that's the actual hard part of lolbin detection and i'd basically skipped it. presence of certutil isn't the signal. certutil reaching out to a url is the signal. rundll32 loading a dll out of %temp% is the signal. rundll32 firing off a signed microsoft task is just tuesday.
what i changed
i stopped treating the keyword list as one bucket. i split it by how much the match actually tells you.
a bare lolbin name on its own is weak. it only gets interesting when it's paired with something else. so a hit became critical only if the binary keyword showed up with a second-stage indicator, like a url, a base64 blob, -windowstyle hidden, or a path that points at a temp or appdata directory. a lolbin by itself with none of that drops down to INFO, which in the report means "here, look at this, but i'm not going to scare you about it."
STAGE2 = ["http://", "https://", "-enc", "frombase64string",
"-windowstyle hidden", "%temp%", "%appdata%", "downloadstring"]
def scan_command(cmd):
c = cmd.lower()
lol = [s for s in LOLBINS if s in c]
if not lol:
return None
stage2 = [s for s in STAGE2 if s in c]
if stage2:
return Finding("CRITICAL", f"{lol} with {stage2}")
return Finding("INFO", f"{lol} present, no second-stage indicators")
after that, my laptop went from four criticals to zero, with a handful of INFO notes for the microsoft tasks i now know are fine. and the one time i tested it against a fake task that ran powershell -enc <base64> out of appdata, it lit up critical like it should.
it's still keyword matching. i want to be honest about that. it doesn't parse the command line into a real argument tree, it doesn't follow what the dll actually does, and a half-clever attacker who renames their payload path or splits the command can walk right past it. it's a tripwire, not a verdict. for a tier 1 "is anything obviously wrong on this box" pass that's about the right altitude, but i wouldn't call it detection.
the part i'm actually happy with
the constraint that shaped the whole project was no dependencies. it had to run on a locked-down windows machine with no pip and no outbound internet, because that's the machine you actually want to audit. so everything is subprocess against built-in windows commands and stdlib parsing. netsh advfirewall for the firewall, net user and net localgroup for accounts, the registry for the powershell logging and uac settings.
that sounds annoying and it sort of was, but it means you can drop a single .py file on a fresh box and run it. no install step, nothing to flag, nothing to phone home. the report is one html file with the css inlined so it opens with no network either.
scoring is deliberately dumb. you start at 100, every critical costs 20, every warning costs 10, and you land on an A through F grade. i went back and forth on weighting findings more cleverly and decided against it. a crude score that a hiring manager or a non-security person can read in two seconds beats a precise one nobody trusts. exit code is 2 if anything critical fired, so you can wire it into a pipeline.
what's next / what's broken
- the lolbin scanner still can't tell a renamed binary or a split command from a clean one. real argument parsing is the obvious next step.
- no native event log correlation yet. it checks that the event log service is healthy but doesn't read the logs.
- the roadmap has compliance mapping (CIS, NIST), but i didn't want to claim a mapping i hadn't actually verified against the benchmark text, so it's not in there yet.
repo's here if you want to poke at it or tell me where the detection logic is naive: https://github.com/TiltedLunar123/WinRecon
it's MIT, and it's for boxes you're allowed to audit. it works. not perfect, but it works.
Top comments (0)