/*++ Copyright (c) 2000 Microsoft Corporation Module Name: EmulateMissingEXE.cpp Abstract: Win9x had scandskw.exe and defrag.exe in %windir%, NT does not. Whistler has a hack in the shell32 for scandisk for app compatability purposes. Whistler can also invoke defrag via "%windir%\system32\mmc.exe %windir%\system32\dfrg.msc". This shim redirects CreateProcess and Winexec to execute these two substitutes, as well as FindFile to indicate their presence. Notes: This is a general purpose shim. History: 01/02/2001 prashkud Created 02/18/2001 prashkud Merged HandleStartKeyword SHIM with this. 02/21/2001 prashkud Replaced most strings with CString class. --*/ #include "precomp.h" IMPLEMENT_SHIM_BEGIN(EmulateMissingEXE) #include "ShimHookMacro.h" APIHOOK_ENUM_BEGIN APIHOOK_ENUM_ENTRY(CreateProcessA) APIHOOK_ENUM_ENTRY(CreateProcessW) APIHOOK_ENUM_ENTRY(WinExec) APIHOOK_ENUM_ENTRY(FindFirstFileA) APIHOOK_ENUM_ENTRY(FindFirstFileW) APIHOOK_ENUM_ENTRY_COMSERVER(SHELL32) APIHOOK_ENUM_END IMPLEMENT_COMSERVER_HOOK(SHELL32) // Type for the functions that builds the New EXES typedef BOOL (*_pfn_STUBFUNC)(CString&, CString&, BOOL); // Main Data structure to hold the New strings struct REPLACEENTRY { WCHAR *OrigExeName; // original EXE to be replaced _pfn_STUBFUNC pfnFuncName; // function to call to correct the name }; CRITICAL_SECTION g_CritSec; WCHAR g_szSysDir[MAX_PATH]; // system directory for stubs to use BOOL StubScandisk(CString&, CString&, BOOL); BOOL StubDefrag(CString&, CString&, BOOL); BOOL StubStart(CString&, CString&, BOOL); BOOL StubControl(CString&, CString&, BOOL); BOOL StubDxDiag(CString&, CString&, BOOL); BOOL StubWinhlp(CString&, CString&, BOOL); BOOL StubRundll(CString&, CString&, BOOL); BOOL StubPbrush(CString&, CString&, BOOL); // Add variations of these missing Exes like in HandleStartKeyword // Start has been put at the top of the list as there seem to be more apps // that need the SHIM for this EXE than others. In fact there was a // seperate SHIM HandleStartKeyword that was merged with this. REPLACEENTRY g_ReplList[] = { {L"start", StubStart }, {L"start.exe", StubStart }, {L"scandskw", StubScandisk }, {L"scandskw.exe", StubScandisk }, {L"defrag", StubDefrag }, {L"defrag.exe", StubDefrag }, {L"control", StubControl }, {L"control.exe", StubControl }, {L"dxdiag", StubDxDiag }, {L"dxdiag.exe", StubDxDiag }, {L"winhelp", StubWinhlp }, {L"winhelp.exe", StubWinhlp }, {L"rundll", StubRundll }, {L"rundll.exe", StubRundll }, {L"Pbrush", StubPbrush }, {L"Pbrush.exe", StubPbrush }, // Always the last one {L"", NULL } }; // Added to merge HandleStartKeyword // Link list of shell link object this pointers. struct THISPOINTER { THISPOINTER *next; LPCVOID pThisPointer; }; THISPOINTER *g_pThisPointerList; /*++ Function Description: Add a this pointer to the linked list of pointers. Does not add if the pointer is NULL or a duplicate. Arguments: IN pThisPointer - the pointer to add. Return Value: None History: 12/14/2000 maonis Created --*/ VOID AddThisPointer( IN LPCVOID pThisPointer ) { EnterCriticalSection(&g_CritSec); if (pThisPointer) { THISPOINTER *pPointer = g_pThisPointerList; while (pPointer) { if (pPointer->pThisPointer == pThisPointer) { return; } pPointer = pPointer->next; } pPointer = (THISPOINTER *) malloc(sizeof THISPOINTER); if (pPointer) { pPointer->pThisPointer = pThisPointer; pPointer->next = g_pThisPointerList; g_pThisPointerList = pPointer; } } LeaveCriticalSection(&g_CritSec); } /*++ Function Description: Remove a this pointer if it can be found in the linked list of pointers. Arguments: IN pThisPointer - the pointer to remove. Return Value: TRUE if the pointer is found. FALSE if the pointer is not found. History: 12/14/2000 maonis Created --*/ BOOL RemoveThisPointer( IN LPCVOID pThisPointer ) { THISPOINTER *pPointer = g_pThisPointerList; THISPOINTER *last = NULL; BOOL lRet = FALSE; EnterCriticalSection(&g_CritSec); while (pPointer) { if (pPointer->pThisPointer == pThisPointer) { if (last) { last->next = pPointer->next; } else { g_pThisPointerList = pPointer->next; } free(pPointer); lRet = TRUE; break; } last = pPointer; pPointer = pPointer->next; } LeaveCriticalSection(&g_CritSec); return lRet; } /*++ We are here because the application name: scandskw.exe, matches the one in the static array. Fill the News for scandskw.exe as: rundll32.exe shell32.dll,AppCompat_RunDLL SCANDSKW --*/ BOOL StubScandisk( CString& csNewApplicationName, CString& csNewCommandLine, BOOL bExists ) { csNewApplicationName = g_szSysDir; csNewApplicationName += L"\\rundll32.exe"; csNewCommandLine = L"shell32.dll,AppCompat_RunDLL SCANDSKW"; return TRUE; } /*++ We are here because the application name: defrag.exe, matches the one in the static array. Fill the News for .exe as: %windir%\\system32\\mmc.exe %windir%\\system32\\dfrg.msc --*/ BOOL StubDefrag( CString& csNewApplicationName, CString& csNewCommandLine, BOOL bExists ) { csNewApplicationName = g_szSysDir; csNewApplicationName += L"\\mmc.exe"; csNewCommandLine = g_szSysDir; csNewCommandLine += L"\\dfrg.msc"; return TRUE; } /*++ We are here because the application name: start.exe, matches the one in the static array. Fill the News for .exe as: %windir%\\system32\\cmd.exe" "/c start" Many applications have a "start.exe" in their current working directories which needs to take precendence over any New we make. --*/ BOOL StubStart( CString& csNewApplicationName, CString& csNewCommandLine, BOOL bExists ) { // // First check the current working directory for start.exe // if (bExists) { return FALSE; } // // There is no start.exe in the current working directory // csNewApplicationName = g_szSysDir; csNewApplicationName += L"\\cmd.exe"; csNewCommandLine = L"/d /c start \"\""; return TRUE; } /*++ We are here because the application name: control.exe, matches the one in the static array. Fill the News for .exe as: %windir%\\system32\\control.exe --*/ BOOL StubControl( CString& csNewApplicationName, CString& csNewCommandLine, BOOL bExists ) { csNewApplicationName = g_szSysDir; csNewApplicationName += L"\\control.exe"; csNewCommandLine = L""; return TRUE; } /*++ We are here because the application name: dxdiag.exe, matches the one in the static array. Fill the News for .exe as: %windir%\system32\dxdiag.exe --*/ BOOL StubDxDiag( CString& csNewApplicationName, CString& csNewCommandLine, BOOL bExists ) { csNewApplicationName = g_szSysDir; csNewApplicationName += L"\\dxdiag.exe"; csNewCommandLine = L""; return TRUE; } /*++ We are here because the application name: Winhlp.exe, matches the one in the static array. Fill the News for .exe as: %windir%\system32\winhlp32.exe --*/ BOOL StubWinhlp( CString& csNewApplicationName, CString& csNewCommandLine, BOOL bExists ) { csNewApplicationName = g_szSysDir; csNewApplicationName += L"\\winhlp32.exe"; // Winhlp32.exe needs the app name to be in the commandline. csNewCommandLine = csNewApplicationName; return TRUE; } /*++ We are here because the application name: rundll.exe matches the one in the static array. Fill the News for .exe as: %windir%\system32\rundll32.exe --*/ BOOL StubRundll( CString& csNewApplicationName, CString& csNewCommandLine, BOOL bExists ) { csNewApplicationName = g_szSysDir; csNewApplicationName += L"\\rundll32.exe"; csNewCommandLine = L""; return TRUE; } /*++ We are here because the application name: Pbrush.exe matches the one in the static array. Fill the New for .exe as: %windir%\system32\mspaint.exe --*/ BOOL StubPbrush( CString& csNewApplicationName, CString& csNewCommandLine, BOOL bExists ) { csNewApplicationName = g_szSysDir; csNewApplicationName += L"\\mspaint.exe"; csNewCommandLine = L""; return TRUE; } /*++ GetTitle takes the app path and returns just the EXE name. --*/ VOID GetTitle(CString& csAppName,CString& csAppTitle) { csAppTitle = csAppName; int len = csAppName.ReverseFind(L'\\'); if (len) { csAppTitle.Delete(0, len+1); } } /*++ This is the main function where the New logic happens. This function goes through the static array and fills the suitable New appname and the commandline. --*/ BOOL Redirect( const CString& csApplicationName, const CString& csCommandLine, CString& csNewApplicationName, CString& csNewCommandLine, BOOL bJustCheckExePresence ) { BOOL bRet = FALSE; CSTRING_TRY { CString csOrigAppName; CString csOrigCommandLine; BOOL bExists = FALSE; AppAndCommandLine AppObj(csApplicationName, csCommandLine); csOrigAppName = AppObj.GetApplicationName(); csOrigCommandLine = AppObj.GetCommandlineNoAppName(); if (csOrigAppName.IsEmpty()) { goto Exit; } // // Loop through the list of redirectors // REPLACEENTRY *rEntry = &g_ReplList[0]; CString csAppTitle; GetTitle(csOrigAppName, csAppTitle); while (rEntry && rEntry->OrigExeName[0]) { if (_wcsicmp(rEntry->OrigExeName, csAppTitle) == 0) { // // This final parameter has been added for the merger // of HandleStartKeyword Shim. If this is TRUE, we don't // go any further but just return. // if (bJustCheckExePresence) { bRet = TRUE; goto Exit; } // // Check if the current working directory contains the exe in question // WCHAR szCurrentDirectory[MAX_PATH]; if (szCurrentDirectory && GetCurrentDirectoryW(MAX_PATH, szCurrentDirectory)) { CString csFullAppName(szCurrentDirectory); csFullAppName += L"\\"; csFullAppName += csAppTitle; // Check if the file exists and is not a directory DWORD dwAttr = GetFileAttributesW(csFullAppName); if ((dwAttr != 0xFFFFFFFF) && !(dwAttr & FILE_ATTRIBUTE_DIRECTORY)) { DPFN( eDbgLevelInfo, "[Redirect] %s found in current working directory"); bExists = TRUE; } } // // We have a match, so call the corresponding function // if (bRet = (*(rEntry->pfnFuncName))(csNewApplicationName, csNewCommandLine, bExists)) { // // Append the original command line // csNewCommandLine += L" "; csNewCommandLine += csOrigCommandLine; } // We matched an EXE, so we're done break; } rEntry++; } if (bRet) { DPFN( eDbgLevelWarning, "Redirected:"); DPFN( eDbgLevelWarning, "\tFrom: %S %S", csApplicationName, csCommandLine); DPFN( eDbgLevelWarning, "\tTo: %S %S", csNewApplicationName, csNewCommandLine); } } CSTRING_CATCH { DPFN( eDbgLevelError, "Not Redirecting: Exception encountered"); bRet = FALSE; } Exit: return bRet; } /*++ Hooks the CreateProcessA function to see if any News need to be substituted. --*/ BOOL APIHOOK(CreateProcessA)( LPCSTR lpApplicationName, LPSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCSTR lpCurrentDirectory, LPSTARTUPINFOA lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation ) { if ((NULL == lpApplicationName) && (NULL == lpCommandLine)) { // If both are NULL, return FALSE. SetLastError(ERROR_INVALID_PARAMETER); return FALSE; } CSTRING_TRY { CString csNewApplicationName; CString csNewCommandLine; CString csPassedAppName(lpApplicationName); CString csPassedCommandLine(lpCommandLine); if ((csPassedAppName.IsEmpty()) && (csPassedCommandLine.IsEmpty())) { goto exit; } // // Run the list of New stubs: call to the main New routine // if (Redirect(csPassedAppName, csPassedCommandLine, csNewApplicationName, csNewCommandLine, FALSE)) { LOGN( eDbgLevelWarning, "[CreateProcessA] \" %s %s \": changed to \" %s %s \"", lpApplicationName, lpCommandLine, csNewApplicationName.GetAnsi(), csNewCommandLine.GetAnsi()); } else { csNewApplicationName = lpApplicationName; csNewCommandLine = lpCommandLine; } // Convert back to ANSI using the GetAnsi() method exposed by the CString class. return ORIGINAL_API(CreateProcessA)( csNewApplicationName.IsEmpty() ? NULL : csNewApplicationName.GetAnsi(), csNewCommandLine.IsEmpty() ? NULL : csNewCommandLine.GetAnsi(), lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation); } CSTRING_CATCH { DPFN( eDbgLevelError, "[CreateProcessA]:Original API called.Exception occured!"); } exit: return ORIGINAL_API(CreateProcessA)(lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation); } /*++ Hooks the CreateProcessW function to see if any News need to be substituted. --*/ BOOL APIHOOK(CreateProcessW)( LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation ) { if ((NULL == lpApplicationName) && (NULL == lpCommandLine)) { // If both are NULL, return FALSE. SetLastError(ERROR_INVALID_PARAMETER); return FALSE; } CSTRING_TRY { CString csNewApplicationName; CString csNewCommandLine; CString csApplicationName(lpApplicationName); CString csCommandLine(lpCommandLine); if ((csApplicationName.IsEmpty()) && (csCommandLine.IsEmpty())) { goto exit; } // // Run the list of New stubs // if (Redirect(csApplicationName, csCommandLine, csNewApplicationName, csNewCommandLine, FALSE)) { LOGN( eDbgLevelWarning, "[CreateProcessW] \" %S %S \": changed to \" %S %S \"", lpApplicationName, lpCommandLine, csNewApplicationName, csNewCommandLine); } else { csNewApplicationName = lpApplicationName; csNewCommandLine = lpCommandLine; } return ORIGINAL_API(CreateProcessW)( csNewApplicationName.IsEmpty() ? NULL : csNewApplicationName.Get(), csNewCommandLine.IsEmpty() ? NULL : (LPWSTR)csNewCommandLine.Get(), lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation); } CSTRING_CATCH { DPFN( eDbgLevelError, "[CreateProcessW] Original API called. Exception occured!"); } exit: return ORIGINAL_API(CreateProcessW)(lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation); } /*++ Hooks WinExec to redirect if necessary. --*/ UINT APIHOOK(WinExec)( LPCSTR lpCmdLine, UINT uCmdShow ) { if (NULL == lpCmdLine) { SetLastError(ERROR_INVALID_PARAMETER); return ERROR_PATH_NOT_FOUND; } CSTRING_TRY { CString csNewApplicationName; CString csNewCommandLine; CString csAppName; CString csNewCmdLine; CString csCommandLine(lpCmdLine); if (csCommandLine.IsEmpty()) { goto exit; } // Check for redirection if (Redirect(csAppName, csCommandLine, csNewApplicationName, csNewCommandLine, FALSE)) { // Modification for the WinHlp32 strange behaviour if (csNewCommandLine.Find(csNewApplicationName.Get()) == -1) { // If the new Command line does not contain the new application // name as the substring, we are here. csNewCmdLine = csNewApplicationName; csNewCmdLine += L" "; } csNewCmdLine += csNewCommandLine; // Assign to csCommandLine as this can be commonly used csCommandLine = csNewCmdLine; LOGN( eDbgLevelInfo, "[WinExec] \" %s \": changed to \" %s \"", lpCmdLine, csCommandLine.GetAnsi()); } return ORIGINAL_API(WinExec)(csCommandLine.GetAnsi(), uCmdShow); } CSTRING_CATCH { DPFN( eDbgLevelError, "[WinExec]:Original API called.Exception occured!"); } exit: return ORIGINAL_API(WinExec)(lpCmdLine, uCmdShow); } /*++ Hooks the FindFirstFileA function to see if any replacements need to be substituted. This is a requirement for cmd.exe. --*/ HANDLE APIHOOK(FindFirstFileA)( LPCSTR lpFileName, LPWIN32_FIND_DATAA lpFindFileData ) { CSTRING_TRY { CString csNewApplicationName; CString csNewCommandLine; CString csFileName(lpFileName); CString csAppName; // Call the main replacement routine. if (Redirect(csFileName, csAppName, csNewApplicationName, csNewCommandLine, FALSE)) { // Assign to csFileName csFileName = csNewApplicationName; LOGN( eDbgLevelInfo, "[FindFirstFileA] \" %s \": changed to \" %s \"", lpFileName, csFileName.GetAnsi()); } return ORIGINAL_API(FindFirstFileA)(csFileName.GetAnsi(), lpFindFileData); } CSTRING_CATCH { DPFN( eDbgLevelError, "[FindFirstFileA]:Original API called.Exception occured!"); return ORIGINAL_API(FindFirstFileA)(lpFileName, lpFindFileData); } } /*++ Hooks the FindFirstFileW function to see if any replacements need to be substituted. This is a requirement for cmd.exe. --*/ HANDLE APIHOOK(FindFirstFileW)( LPCWSTR lpFileName, LPWIN32_FIND_DATAW lpFindFileData ) { CSTRING_TRY { CString csNewApplicationName(lpFileName); CString csNewCommandLine; CString csFileName(lpFileName); CString csAppName; // Call the main replacement routine. if (Redirect(csFileName, csAppName, csNewApplicationName, csNewCommandLine, FALSE)) { LOGN( eDbgLevelInfo, "[FindFirstFileW] \" %S \": changed to \" %S \"", lpFileName, (const WCHAR*)csNewApplicationName); } return ORIGINAL_API(FindFirstFileW)(csNewApplicationName, lpFindFileData); } CSTRING_CATCH { DPFN( eDbgLevelError, "[FindFirstFileW]:Original API called.Exception occured!"); return ORIGINAL_API(FindFirstFileW)(lpFileName, lpFindFileData); } } // Added for the merge of HandleStartKeyword /*++ Hook IShellLinkA::SetPath - check if it's start, if so change it to cmd and add the this pointer to the list. --*/ HRESULT STDMETHODCALLTYPE COMHOOK(IShellLinkA, SetPath)( PVOID pThis, LPCSTR pszFile ) { _pfn_IShellLinkA_SetPath pfnSetPath = ORIGINAL_COM( IShellLinkA, SetPath, pThis); CSTRING_TRY { CString csExeName; CString csCmdLine; CString csNewAppName; CString csNewCmdLine; CString cscmdCommandLine(pszFile); // Assign the ANSI string to the WCHAR CString csExeName = pszFile; csExeName.TrimLeft(); // Check to see whether the Filename conatains the "Start" keyword. // The last parameter to the Rediect function controls this. if (Redirect(csExeName, csCmdLine, csNewAppName, csNewCmdLine, TRUE)) { // Found a match. We add the this pointer to the list. AddThisPointer(pThis); DPFN( eDbgLevelInfo, "[SetPath] Changing start.exe to cmd.exe\n"); // Prefix of new "start" command line, use full path to CMD.EXE // Append the WCHAR global system directory path to ANSI CString cscmdCommandLine = g_szSysDir; cscmdCommandLine += L"\\cmd.exe"; } return (*pfnSetPath)(pThis, cscmdCommandLine.GetAnsi()); } CSTRING_CATCH { DPFN( eDbgLevelError, "[SetPath] Original API called. Exception occured!"); return (*pfnSetPath)(pThis, pszFile); } } /*++ Hook IShellLinkA::SetArguments - if the this pointer can be found in the list, remove it from the list and add "/d /c start" in front of the original argument list. --*/ HRESULT STDMETHODCALLTYPE COMHOOK(IShellLinkA, SetArguments)( PVOID pThis, LPCSTR pszFile ) { _pfn_IShellLinkA_SetArguments pfnSetArguments = ORIGINAL_COM(IShellLinkA, SetArguments, pThis); CSTRING_TRY { CString csNewFile(pszFile); if (RemoveThisPointer(pThis)) { csNewFile = "/d /c start \"\" "; csNewFile += pszFile; DPFN( eDbgLevelInfo, "[SetArguments] Arg list is now %S", csNewFile); } return (*pfnSetArguments)( pThis, csNewFile.GetAnsi()); } CSTRING_CATCH { DPFN( eDbgLevelError, "[SetArguments]:Original API called.Exception occured!"); return (*pfnSetArguments)( pThis, pszFile ); } } /*++ Register hooked functions --*/ BOOL NOTIFY_FUNCTION( DWORD fdwReason ) { if (fdwReason == DLL_PROCESS_ATTACH) { if (!GetSystemDirectory(g_szSysDir, MAX_PATH)) { DPFN( eDbgLevelError, "[Notify] GetSystemDirectory failed"); return FALSE; } InitializeCriticalSection(&g_CritSec); } return TRUE; } HOOK_BEGIN CALL_NOTIFY_FUNCTION APIHOOK_ENTRY(KERNEL32.DLL, CreateProcessA) APIHOOK_ENTRY(KERNEL32.DLL, CreateProcessW) APIHOOK_ENTRY(KERNEL32.DLL, WinExec) APIHOOK_ENTRY(KERNEL32.DLL, FindFirstFileA) APIHOOK_ENTRY(KERNEL32.DLL, FindFirstFileW) APIHOOK_ENTRY_COMSERVER(SHELL32) COMHOOK_ENTRY(ShellLink, IShellLinkA, SetPath, 20) COMHOOK_ENTRY(ShellLink, IShellLinkA, SetArguments, 11) HOOK_END IMPLEMENT_SHIM_END