Bypassing the PIN prompt during smart card container creation
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.
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.
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 must be the raw byte count of the narrow string.
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},
};
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.
CredUIPromptForWindowsCredentialsW in the process remain unaffected.
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 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.
basecsp.dllandscksp.dllalways callCredUIPromptForWindowsCredentialsWon container creation, ignoringCRYPT_SILENT.- Register the hook in
s_Hooks[]undercredui.dll— one entry is enough. setUpHook()resolves originals, loads the three modules (msclmd.dll,basecsp.dll,scksp.dll), and callsPatchIAT()on each before exposingg_pStaticInstance.- The hook is
static— it cannot usethis. Supply the PIN via a#defineor a static/TLS mechanism. - The output buffer is a flat
CoTaskMemAllocallocation: copy of the input buffer with the ANSI PIN appended. - Trap 1:
Pin.Bufferis a relative offset (pbPinDst - pLogon), not an absolute address. - Trap 2:
Pin.Lengthis the ANSI byte count (strlen), not a wide-char count.