Skip to content

Bypass the PIN prompt when creating a smart card container

The problem

When an application creates a new key container on a smart card, basecsp.dll and scksp.dll always ask for the PIN through CredUIPromptForWindowsCredentialsW, even with CRYPT_SILENT or the KSP equivalent. No documented API passes the PIN for this operation.

The workaround is to hook CredUIPromptForWindowsCredentialsW in the IAT of both modules and return a credential buffer that already holds the PIN. The dialog never shows up.

How BaseCSP calls CredUIPromptForWindowsCredentials

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

The hook uses this flag to tell PIN prompts apart from other credential dialogs. It builds the output buffer and returns ERROR_SUCCESS without showing any UI.

Two details of the output buffer are easy to miss:
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 string. Length is its byte count.
Register the hook in s_Hooks[]

The hook goes in the static s_Hooks[] table, next to the SCard hooks. It needs a single entry, 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. It sets the global instance pointer g_pStaticInstance last, so no hook can run before setup is complete. tearDownHook() undoes all of it.

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 looks up s_Hooks[] by DLL name and calls ApplyPatchStandard() or ApplyPatchDelayLoad(). These compare the IAT slot with entry.pfnOriginal (to patch) or entry.pfnHook (to restore), and swap it under VirtualProtect.

Scope: IAT patching only changes the import table of the patched modules. Other callers of CredUIPromptForWindowsCredentialsW in the same process still reach the real function.
The hook function: building the output buffer

The hook is a static member, so it has no this. The PIN here is a compile-time constant; production code needs a secure source. When the call is not a BaseCSP PIN prompt (no CREDUIWIN_IN_CRED_ONLY), the hook forwards it to the original function through 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: the hook is a static member and can't reach instance data through this. The PIN has to come from a static source: a #define, a process-wide singleton, or a thread-local variable set before the CSP/KSP call.
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 and can't use this. Pass the PIN through a #define, a static, or TLS.
  • 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.