Bypassing the PIN prompt during smart card container creation

The problem

When a CSP or KSP creates a new key container on a smart card, basecsp.dll and scksp.dll always prompt for the PIN through CredUIPromptForWindowsCredentialsW, regardless of CRYPT_SILENT or any KSP equivalent. There is no official API to inject the PIN for this specific operation.

The only solution is to hook CredUIPromptForWindowsCredentialsW in the IAT of both modules and return a pre-built credential buffer containing the PIN, so the dialog is never shown.

How BaseCSP calls CredUIPromptForWindowsCredentials

BaseCSP calls the function with the flag CREDUIWIN_IN_CRED_ONLY (0x20) and passes a KERB_CERTIFICATE_LOGON structure as the input buffer. It expects the same structure back with the Pin field filled in.

The hook checks for this flag to distinguish PIN prompts from other credential dialogs, then builds the output buffer and returns ERROR_SUCCESS without showing any UI.

Two non-obvious constraints apply to the output buffer:
1. All UNICODE_STRING.Buffer fields are relative offsets from the buffer base, not absolute pointers.
2. The Pin field is declared as UNICODE_STRING but holds an ANSI stringLength must be the raw byte count of the narrow string.
Registering the hook in s_Hooks[]

The hook is registered in the static s_Hooks[] table alongside all other SCard hooks. Only one entry is needed — for credui.dll:

HookEntry MsCryptoBase::s_Hooks[] = {
    // ... SCard hooks for Winscard.dll ...
    {"Winscard.dll", "SCardTransmit",   (PROC)MsCryptoBase::HookTransmit,  NULL, false},
    // ...

    // PIN injection hook — targets credui.dll imported by basecsp.dll / scksp.dll
    {"credui.dll", "CredUIPromptForWindowsCredentialsW",
                   (PROC)MsCryptoBase::HookCredUIPromptForWindowsCredentials,
                   NULL, false},
};
Installing and removing the hook

setUpHook() first resolves all original function addresses from their DLLs into s_Hooks[].pfnOriginal, then calls PatchIAT() on each target module. The global instance pointer g_pStaticInstance is set last so hooks cannot fire before setup is complete. tearDownHook() reverses the process.

void MsCryptoBase::setUpHook()
{
    // 1. Resolve all original addresses into s_Hooks[].pfnOriginal
    for (auto& entry : s_Hooks) {
        HMODULE hMod = GetModuleHandleA(entry.pszDllName);
        if (!hMod) hMod = LoadLibraryA(entry.pszDllName);
        if (!hMod) continue;
        entry.pfnOriginal = GetProcAddress(hMod, entry.pszFuncName);
    }

    // 2. Load the three modules whose IATs must be patched
    m_hMinidriver = LoadLibrary(L"msclmd.dll");
    m_hBaseCsp    = LoadLibrary(L"basecsp.dll");
    m_hKsp        = LoadLibrary(L"scksp.dll");   // KSP = scksp.dll, not baseksp.dll

    // 3. Patch their IATs — PatchIAT iterates s_Hooks[] internally
    PatchIAT(m_hMinidriver, /*restore=*/false);
    PatchIAT(m_hBaseCsp,    /*restore=*/false);
    PatchIAT(m_hKsp,        /*restore=*/false);

    // 4. Expose the instance AFTER hooks are in place
    g_pStaticInstance = this;
}

void MsCryptoBase::tearDownHook()
{
    PatchIAT(m_hMinidriver, /*restore=*/true);
    PatchIAT(m_hBaseCsp,    /*restore=*/true);
    PatchIAT(m_hKsp,        /*restore=*/true);
    g_pStaticInstance = nullptr;
}

PatchIAT() walks the standard and delay-load import descriptor tables. For each entry it checks s_Hooks[] by DLL name, then calls ApplyPatchStandard() or ApplyPatchDelayLoad() which compare the current IAT slot value against entry.pfnOriginal (patch direction) or entry.pfnHook (restore direction) and swap under VirtualProtect.

Scope: IAT patching only affects the import table of the patched module. Other callers of CredUIPromptForWindowsCredentialsW in the process remain unaffected.
The hook function — building the output buffer

The hook is a static member — it has no access to this. The PIN is a compile-time constant (replace with a secure retrieval mechanism for production use). When the call does not look like a BaseCSP PIN prompt (CREDUIWIN_IN_CRED_ONLY absent), the original function is called via GetOriginal<>.

// PIN to inject — hardcoded here for clarity.
// In a real scenario, retrieve this from a secure configuration source.
#define INJECT_PIN  "12345678"

template<typename T>
static T GetOriginal(LPCSTR pszFuncName) {
    for (auto& entry : MsCryptoBase::s_Hooks)
        if (_stricmp(entry.pszFuncName, pszFuncName) == 0)
            return (T)entry.pfnOriginal;
    return nullptr;
}

DWORD WINAPI MsCryptoBase::HookCredUIPromptForWindowsCredentials(
    _In_opt_  PCREDUI_INFOW pUiInfo,
    _In_      DWORD         dwAuthError,
    _Inout_   ULONG*        pulAuthPackage,
    _In_opt_  LPCVOID       pvInAuthBuffer,
    _In_      ULONG         ulInAuthBufferSize,
    _Out_     LPVOID*       ppvOutAuthBuffer,
    _Out_     ULONG*        pulOutAuthBufferSize,
    _Inout_   BOOL*         pfSave,
    _In_      DWORD         dwFlags)
{
    // Not a BaseCSP PIN prompt — forward to the real function
    if (!(dwFlags & CREDUIWIN_IN_CRED_ONLY) || !pvInAuthBuffer || !ppvOutAuthBuffer)
    {
        auto pfnReal = GetOriginal<PFN_CredUIPromptForWindowsCredentials>(
            "CredUIPromptForWindowsCredentialsW");
        return pfnReal(pUiInfo, dwAuthError, pulAuthPackage,
                       pvInAuthBuffer, ulInAuthBufferSize,
                       ppvOutAuthBuffer, pulOutAuthBufferSize,
                       pfSave, dwFlags);
    }

    // BaseCSP PIN prompt: build the output buffer with the PIN injected
    LPCSTR szPin = INJECT_PIN;
    DWORD  cbPin = (DWORD)strlen(szPin);

    // Output = copy of input buffer + ANSI PIN appended at the end
    // BaseCSP frees this buffer with CoTaskMemFree
    ULONG  cbOut = ulInAuthBufferSize + cbPin + sizeof(CHAR);
    LPBYTE pbOut = (LPBYTE)CoTaskMemAlloc(cbOut);
    if (!pbOut) return ERROR_OUTOFMEMORY;

    memcpy(pbOut, pvInAuthBuffer, ulInAuthBufferSize);

    // Write the ANSI PIN bytes at the end of the buffer
    LPBYTE pbPinDst = pbOut + ulInAuthBufferSize;
    memcpy(pbPinDst, szPin, cbPin + 1);

    // Patch the UNICODE_STRING Pin field in the copied header
    KERB_CERTIFICATE_LOGON* pLogon = (KERB_CERTIFICATE_LOGON*)pbOut;

    // *** Trap 1: Buffer is a RELATIVE offset from pLogon, not an absolute pointer ***
    pLogon->Pin.Buffer = (PWSTR)((ULONG_PTR)pbPinDst - (ULONG_PTR)pLogon);

    // *** Trap 2: Length is the ANSI byte count, not a wide-char count ***
    pLogon->Pin.Length        = (USHORT)cbPin;
    pLogon->Pin.MaximumLength = (USHORT)(cbPin + 1);

    *ppvOutAuthBuffer   = pbOut;
    *pulOutAuthBufferSize = cbOut;
    if (pfSave) *pfSave = FALSE;

    return ERROR_SUCCESS;
}
Static function constraint: because the hook is a static member, it cannot access instance data via this. The PIN must come from a static source — a #define, a process-wide singleton, or a thread-local variable set before calling the CSP/KSP API.
Summary
  • basecsp.dll and scksp.dll always call CredUIPromptForWindowsCredentialsW on container creation, ignoring CRYPT_SILENT.
  • Register the hook in s_Hooks[] under credui.dll — one entry is enough.
  • setUpHook() resolves originals, loads the three modules (msclmd.dll, basecsp.dll, scksp.dll), and calls PatchIAT() on each before exposing g_pStaticInstance.
  • The hook is static — it cannot use this. Supply the PIN via a #define or a static/TLS mechanism.
  • The output buffer is a flat CoTaskMemAlloc allocation: copy of the input buffer with the ANSI PIN appended.
  • Trap 1: Pin.Buffer is a relative offset (pbPinDst - pLogon), not an absolute address.
  • Trap 2: Pin.Length is the ANSI byte count (strlen), not a wide-char count.