Source code of Windows XP (NT5)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

987 lines
24 KiB

/*++
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