Bypass the PIN prompt when creating a smart card container
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.
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.
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.
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},
};
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.
CredUIPromptForWindowsCredentialsW in the same process still reach the real function.
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 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.
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
staticand can't usethis. Pass the PIN through a#define, a static, or TLS. - 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.