Escape From Evasion: Dominating Windows Functions with Detour
The security landscape is constantly evolving, and malware authors are always finding new ways to evade detection and bypass security measures. One of the techniques they use is to check if their malware is running in a virtual environment or sandbox, and if so, to modify their behavior or stop running altogether. This is done to prevent their malicious activities from being detected and analyzed by security researchers. On the other hand, security researchers and defenders use various techniques to detect and analyze malware in a safe environment.
One such approach is the use of detour functions. Detours are a powerful technique used to intercept and modify the behavior of existing functions in a running program. By hooking into specific functions, we can redirect their execution to a custom implementation that can modify the input, output, or behavior of the original function. Detour functions can be used to bypass checks performed by malware to detect virtual environments or sandboxes, or to modify the behavior of malware to prevent it from carrying out its malicious activities. This cat-and-mouse game between attackers and defenders continues, and new methods are needed to stay ahead of the game.
Test Case: Al-Khaser
Al-Khaser is a popular tool used by malware analysts to test the effectiveness of their detection and prevention mechanisms. One of its features is the ability to detect if it’s running in a virtual environment or sandbox by checking specific Windows functions’ return values, pointer addresses, or buffer content. Al-Khaser and many malware use Windows functions to determine if they are running in a virtual environment, so if we can control the Windows functions, we can deceive them. With the help of detour functions and known techniques malware authors uses, we can get the control over Windows functions and if our rules match we manipulate the data Windows functions’ return.
In this project, Evasion Escaper, we will demonstrate how detour functions can be used to bypass Al-Khaser’s virtual environment detection mechanisms by intercepting the Windows functions that it uses and modifying their behavior or output. We will use a DLL to inject our detour functions into the target process and hook into specific functions like EnumSystemFirmwareTables or GetSystemInfo. Our detour functions will return customized values, point to fake buffer addresses, or modify existing buffers to make the system look like a normal computer, not a virtual environment. By doing so, we will bypass the virtual environment detection checks of Al-Khaser and demonstrate the power of detour functions in bypassing malware’s controls.
Methodology
The goal of this Evasion Escaper is to modify the behavior of Windows functions that are called by Al-Khaser, with the aim of preventing detection by certain evasion techniques. This is achieved by using detour functions to intercept the original Windows function calls and replace them with custom implementations.
The following steps are involved in this methodology:
- Identify the base address of the DLL functions that the program is likely to interact with.
- Use detour functions to hook into the Windows functions that the program calls, and modify their behavior as required.
- Implement a set of rules that specify possible words that may be present in the data returned by the Windows functions. If the return value or requested data includes any of these words, use detour functions to modify the parameters or return value of the Windows function to prevent detection.
- Test the modified program to ensure that it behaves as intended and does not trigger any detection mechanisms.
Overall, this methodology can be a useful tool for researchers and developers who need to modify the behavior of Windows functions for various purposes, including security and testing. By using detour functions and implementing rules to prevent detection, it is possible to modify the behavior of a program without being detected by certain evasion techniques.
Demonstration Over Examples
Loaded Modules
To determine the base address of the GetModuleHandleW function, a serial if condition is utilized to verify if GetModuleHandleW’s base address is NULL. This approach is implemented to ensure that this code section is not executed when DetourFunction() is used to replace the original function with our own function hGetModuleHandleW.
//Declaration of variables
LPVOID lpGetModuleHandleW = NULL;
HMODULE hKernel32 = NULL;
//Identify the base address of DLL function
if (lpGetModuleHandleW == NULL) {
hKernel32 = GetModuleHandleW(L"kernel32.dll");
lpGetModuleHandleW = GetProcAddress(hKernel32, "GetModuleHandleW");
// Replace the DLL function with modified twin
if (lpGetModuleHandleW != NULL)
oGetModuleHandleW = reinterpret_cast<tGetModuleHandleW>(DetourFunction(reinterpret_cast<PBYTE>(lpGetModuleHandleW), reinterpret_cast<PBYTE>(hGetModuleHandleW)));
}
Furthermore, it is imperative to define a custom structure for the modified Windows function prior to its replacement. This ensures that the original function is preserved and can be called when necessary. To achieve this, we declare a typedef named tGetModuleHandleW that points to the function signature of the original GetModuleHandleW function. We also declare a variable named oGetModuleHandleW of type tGetModuleHandleW to store a pointer to the original function.
typedef HMODULE (WINAPI* tGetModuleHandleW)(LPCWSTR);
tGetModuleHandleW oGetModuleHandleW = NULL;
Once the original function is successfully replaced with the modified one, it is necessary to check if its parameters are present in the ruleset.txt file to determine if the function attempts to detect if it is running in a virtual machine or sandbox environment.
To compare the module name passed to the function with the words in our ruleset.txt, we use the StrStrIW function. It should be noted that prior to the process of checking the function parameters against the ruleset.txt file, we have already parsed and saved the words from the ruleset.txt file to a vector array called rules_GetModuleHandleW. If the module name passed to the function matches with the rules in rules_GetModuleHandleW, we return NULL. If it is not, we return the value of the original Windows function, which is set as oGetModuleHandleW.
Note: We chose to use StrStrIW function to compare the module name passed to the function because it is a case-insensitive string comparison function. This means that it will match strings regardless of the case of the letters. The “IW” suffix in the function name stands for “Insensitive Wide-character” which means that it operates on Unicode strings. By using StrStrIW, we can compare the module name with the words in our ruleset.txt even if they are written in different cases. This helps to increase the accuracy of our detection and prevention of evasion techniques used by malware.
HMODULE WINAPI hGetModuleHandleW(const LPCWSTR lpModuleName)
{
HMODULE result = oGetModuleHandleW(lpModuleName);
for (const auto& rule : rules_GetModuleHandleW)
{
if (StrStrIW(rule[0].c_str(), lpModuleName))
return NULL;
}
return result;
}
File Avaliablity Controls
Similar to GetModuleHandleW, we need to define a custom structure for the modified Windows function, PathFindFileNameW, before replacing it with our own function hPathFindFileNameW. This ensures that the original function is preserved and can be called when necessary. We declare a typedef named tPathFindFileNameW that points to the function signature of the original PathFindFileNameW function. We also declare a variable named oPathFindFileNameW of type tPathFindFileNameW to store a pointer to the original function.
typedef PWSTR(WINAPI* tPathFindFileNameW)(LPCWSTR);
tPathFindFileNameW oPathFindFileNameW = NULL;
We then use a similar approach to identify the base address of the PathFindFileNameW function and replace it with our own function hPathFindFileNameW, using the DetourFunction() function.
HMODULE hShlwapi = NULL;
LPVOID lpPathFindFileNameW = NULL;
if (lpPathFindFileNameW == NULL) {
hShlwapi = GetModuleHandleW(L"Shlwapi.dll");
lpPathFindFileNameW = GetProcAddress(hShlwapi, "PathFindFileNameW");
if (lpPathFindFileNameW != NULL)
oPathFindFileNameW = reinterpret_cast<tPathFindFileNameW>(DetourFunction(reinterpret_cast<PBYTE>(lpPathFindFileNameW), reinterpret_cast<PBYTE>(hPathFindFileNameW)));
}
Once the original function is successfully replaced with the modified one, we can check if its parameters are present in the ruleset.txt file to determine if the function attempts to detect if it is running in a virtual machine or sandbox environment.
To detect if the string passed to the function is a hexadecimal value, we use the IsHexString() function from the Al-Khaser project. If the string is a hexadecimal value, we replace it with a random string from our vector array of random strings. If the string contains a match with any of the rules in the ruleset.txt file, we also replace it with a random string.
BOOL IsHexString(WCHAR* szStr) {
std::wstring s(szStr);
if (std::find_if(s.begin(), s.end(), [](wchar_t c) {return !std::isxdigit(static_cast<unsigned char>(c)); }) == s.end())
return TRUE;
else
return FALSE;
}
PWSTR WINAPI hPathFindFileNameW(LPWSTR pszPath)
{
PWSTR pwResult = oPathFindFileNameW(pszPath);
PWSTR clean_pwResult = pwResult;
PathRemoveExtensionW(clean_pwResult);
std::wstring randomstring = random_strings[0];
if (pwResult != NULL) {
for (const auto& rule : rules_Get)
{
PWSTR match = wcsstr(pwResult, rule[0].c_str());
if (match != nullptr) {
wcscpy_s(pwResult, wcslen(randomstring.c_str()) + 1, randomstring.c_str());
break;
}
if ((wcslen(clean_pwResult) == 32 || wcslen(clean_pwResult) == 40 || wcslen(clean_pwResult) == 64) && IsHexString(clean_pwResult))
{
wcscpy_s(pwResult, wcslen(randomstring.c_str()) + 1, randomstring.c_str());
break;
}
}
}
return pwResult;
}
Registry Controls
The provided functions hRegQueryValueExW, hRegEnumKeyExW and hRegOpenKeyExW are designed to modify certain registry information to evade detection by security mechanisms.
hRegQueryValueExW is a hook for the RegQueryValueExW function, which is used to retrieve the data associated with a particular value name for a given registry key. This function intercepts the call to RegQueryValueExW and checks whether the returned data buffer contains any of the forbidden strings specified in the rules_RegQueryValueExW vector. If any forbidden string is found, it returns the error code ERROR_FILE_NOT_FOUND, indicating that the requested value does not exist.
typedef LSTATUS(WINAPI* tRegQueryValueExW)(HKEY, LPCWSTR, LPDWORD, LPDWORD, LPBYTE, LPDWORD);
tRegQueryValueExW oRegQueryValueExW = NULL;
LSTATUS WINAPI hRegQueryValueExW(HKEY hKey, LPCWSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType, LPBYTE lpData, LPDWORD lpcbData)
{
LSTATUS result = oRegQueryValueExW(hKey, lpValueName, lpReserved, lpType, lpData, lpcbData);
WCHAR* pData = reinterpret_cast<WCHAR*>(lpData);
for (const auto& rule : rules_RegQueryValueExW)
{
if (StrStrIW(pData, rule[0].c_str())) {
// The buffer contains, so we return an error code
return ERROR_FILE_NOT_FOUND;
}
}
return result;
}
hRegEnumKeyExW is a hook for the RegEnumKeyExW function, which is used to enumerate the subkeys of a given registry key. This function intercepts the call to RegEnumKeyExW and checks whether the returned key name contains any of the forbidden strings specified in the rules_RegEnumKeyExW vector. If any forbidden string is found, it returns the error code ERROR_FILE_NOT_FOUND, indicating that the requested key does not exist.
typedef LSTATUS(WINAPI* tRegEnumKeyExW)(HKEY, DWORD, LPWSTR, LPDWORD, LPDWORD, LPWSTR, LPDWORD, PFILETIME);
tRegEnumKeyExW oRegEnumKeyExW = NULL;
LSTATUS WINAPI hRegEnumKeyExW(HKEY hKey, DWORD dwIndex, LPWSTR lpName, LPDWORD lpcchName, LPDWORD lpReserved, LPWSTR lpClass, LPDWORD lpcchClass, PFILETIME lpftLastWriteTime)
{
LSTATUS result = oRegEnumKeyExW(hKey, dwIndex, lpName, lpcchName, lpReserved, lpClass, lpcchClass, lpftLastWriteTime);
for (const auto& rule : rules_RegEnumKeyExW)
{
if (StrStrIW(lpName, rule[0].c_str())) {
// The buffer contains, so we return an error code
return ERROR_FILE_NOT_FOUND;
}
}
return result;
}
hRegOpenKeyExW is a hook for the RegOpenKeyExW function, which is used to open a specified registry key. This function intercepts the call to RegOpenKeyExW and checks whether the specified subkey contains any of the forbidden strings specified in the rules_RegOpenKeyEx vector. If any forbidden string is found, it returns the error code ERROR_FILE_NOT_FOUND, indicating that the requested key does not exist.
typedef LSTATUS(WINAPI* tRegOpenKeyExW)(HKEY, LPCSTR, DWORD, REGSAM, PHKEY);
tRegOpenKeyExW oRegOpenKeyExW = NULL;
LSTATUS WINAPI hRegOpenKeyExW(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired, PHKEY phkResult)
{
LSTATUS result = oRegOpenKeyExW(hKey, lpSubKey, ulOptions, samDesired, phkResult);
for (const auto& rule : rules_RegOpenKeyEx)
{
if (StrStrIW(converter.from_bytes(lpSubKey).c_str(), rule[0].c_str()))
{
result = ERROR_FILE_NOT_FOUND;
break;
}
}
return result;
}
Disk Space
The provided function hGetDiskFreeSpaceExW overrides the behavior of the Windows API function GetDiskFreeSpaceExW, which is commonly used by malware such as Al-Khaser to check if the computer system is running in a virtual machine or sandbox environment.
The function takes the same input parameters as GetDiskFreeSpaceExW, but modifies the value of lpTotalNumberOfBytes to make it appear as if the computer has 490 GB of free disk space, regardless of the actual amount of free space. By doing so, the function can deceive malware that uses disk space checks as part of its detection or evasion mechanism.
BOOL WINAPI hGetDiskFreeSpaceExW(LPCWSTR lpDirectoryName, PULARGE_INTEGER lpFreeBytesAvailableToCaller, PULARGE_INTEGER lpTotalNumberOfBytes, PULARGE_INTEGER lpTotalNumberOfFreeBytes)
{
BOOL result = oGetDiskFreeSpaceExW(lpDirectoryName, lpFreeBytesAvailableToCaller, lpTotalNumberOfBytes, lpTotalNumberOfFreeBytes);
// Modify the free disk space to 490 GB
if (lpTotalNumberOfBytes != NULL)
{
ULARGE_INTEGER newSize;
newSize.QuadPart = 490LL * 1024LL * 1024LL * 1024LL; // Set new size to 490 GB
*lpTotalNumberOfBytes = newSize;
}
return result;
}
WMI Queries
The provided functions hExecQueryFunc and hGetFunc in fastprox.dll are designed to modify the behavior of the WMI service.
The hExecQueryFunc function intercepts the execution of WMI queries and searches for the “FROM” keyword in the query string. If found, it extracts the name of the class being queried and compares it to a list of predefined rules. If the class name matches a rule, the function modifies the query string by replacing the class name with a different value specified in the rule. The modified query is then executed using the original WMI service.
typedef HRESULT ( __stdcall* tExecQueryFunc)(void* pThis, BSTR , BSTR , long , IWbemContext* , IEnumWbemClassObject** );
tExecQueryFunc oExecQueryFunc = NULL;
std::wstring queryFrom;
HRESULT __stdcall hExecQueryFunc(void* pThis, const BSTR strQueryLanguage, BSTR strQuery, long lFlags, IWbemContext* pCtx, IEnumWbemClassObject** ppEnum)
{
std::wstring EQ = L"ExecQuery";
std::wstring Get = L"Get";
std::wstring queryStr = strQuery;
int fromPos = queryStr.find(L"FROM");
if (fromPos != std::wstring::npos) {
queryFrom = queryStr.substr(fromPos + 5);
}
for (const auto& rule : rules_ExecQuery) {
std::wstring loc_ruleFrom = rule[0];
if (StrStrIW(queryFrom.c_str(), loc_ruleFrom.c_str()) != NULL) {
// Create a BSTR and a wstring to concat
BSTR bstr = SysAllocString(L"Select * From ");
std::wstring wstr = rule[1];
// Allocate memory for BSTRs
BSTR combinedBstr = SysAllocStringLen(bstr, SysStringLen(bstr) + wstr.length());
// Concatenate BSTR and wstr
wcscat_s(combinedBstr, MAX_PATH, wstr.c_str());
strQuery = combinedBstr;
}
}
HRESULT hResult = oExecQueryFunc(pThis, strQueryLanguage, strQuery, lFlags, pCtx, ppEnum);
return hResult;
}
The hGetFunc function intercepts the retrieval of WMI properties and searches for the name of the class being queried and the name of the property being retrieved. If both names match a predefined rule, the function replaces the original value of the property with a different value specified in the rule. The modified property value is then returned to the calling process.
typedef HRESULT(__stdcall* tGetFunc)(void* pThis, LPCWSTR, LONG, PVOID, LONG, LONG);
tGetFunc oGetFunc = NULL;
HRESULT __stdcall hGetFunc(void* pThis, LPCWSTR wszName, LONG lFlags, VARIANTARG* pValue, LONG type, LONG plFlavor)
{
HRESULT hResult = oGetFunc(pThis, wszName, lFlags, pValue, type, plFlavor);
for (const auto& rule : rules_Get)
{
LPCWSTR rule_queryFrom = rule[0].c_str();
LPCWSTR queryGet = rule[1].c_str();
if (StrStrIW(wszName, queryGet) && StrStrIW(queryFrom.c_str(), rule_queryFrom))
{
VariantClear(pValue);
pValue->vt = VT_BSTR;
pValue->bstrVal = SysAllocString(rule[2].c_str());
}
}
return hResult;
}
The code snippet provided replaces functions in the “fastprox.dll” library with modified versions of those functions. However, since “fastprox.dll” loads with dynamic resolving, we cannot get its DLL handle before it loads. To solve this issue, the code uses a DLL verifier method to determine which DLL loads and what the base address of the loaded DLL is. The code given below, first checks if the loaded DLL is “fastprox.dll” by comparing its name with “fastprox.dll”, if the comparison is true, the code proceeds with the replacement of the original functions.
To find the base address of the loaded DLL, the code uses the lpDLLBase variable provided by DllLoadCallback function. Next, the code initializes the symbol handler using the SymInitialize function, passing the handle of the current process obtained using OpenProcess function.
The SymFromName function is then used to find the address of the CWbemSvcWrapper::XWbemServices::ExecQuery function. The address is stored in the symbolInfo structure. Next, the code uses the GetProcAddress function to retrieve the address of the original Get function in the “fastprox.dll” library. The DetourFunction function is then used to replace the original Get function with the modified version of the function. The same process is followed for the ExecQuery function.
The following code snippet demonstrates the use of the DLL verifier method to determine which DLLs are loaded and their corresponding base addresses. If the loaded DLL is fastprox.dll, the procedure starts to find relevant function addresses.
Please note that only the relevant portion of the code has been included for the purposes of this explanation, and the actual code likely contains additional functionality and complexity beyond what is shown here.
VOID NTAPI DllLoadCallback(PWSTR lpDLLName, PVOID lpDLLBase, SIZE_T size, PVOID lpReserved)
{
// Parse ruleset and save them in corresponding vector arrays
static bool parsed = false;
if (!parsed) {
parse_ruleset("ruleset.txt");
parsed = true;
}
if (wcscmp(lpDLLName, L"fastprox.dll") == 0)
{
parse_ruleset("ruleset.txt");
if (!lpDLLBase)
return;
HANDLE proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, findMyProc("target.exe"));
SymSetOptions(SYMOPT_DEFERRED_LOADS | SYMOPT_UNDNAME);
if (!SymInitialize(proc, NULL, TRUE)) {
std::cout << "Error initializing symbols: " << GetLastError() << std::endl;
return;
}
SYMBOL_INFO symbolInfo;
symbolInfo.SizeOfStruct = sizeof(SYMBOL_INFO);
symbolInfo.MaxNameLen = MAX_SYM_NAME;
DWORD64 dwAddress = 0;
if (!SymFromName(proc, "fastprox!CWbemSvcWrapper::XWbemServices::ExecQuery", &symbolInfo)) {
std::cout << "Error finding symbol: " << GetLastError() << std::endl;
return;
}
LPVOID lpGetFunc = GetProcAddress(reinterpret_cast<HMODULE>(lpDLLBase), "?Get@CWbemObject@@UAGJPBGJPAUtagVARIANT@@PAJ2@Z");
oGetFunc = reinterpret_cast<tGetFunc>(DetourFunction(reinterpret_cast<PBYTE>(lpGetFunc), reinterpret_cast<PBYTE>(hGetFunc)));
LPVOID lpExecQueryFunc = reinterpret_cast<unsigned char*>(symbolInfo.Address);
oExecQueryFunc = reinterpret_cast<tExecQueryFunc>(DetourFunction(reinterpret_cast<PBYTE>(lpExecQueryFunc), reinterpret_cast<PBYTE>(hExecQueryFunc)));
}
}
VOID RegisterProvider(VOID)
{
RtlSecureZeroMemory(&g_AVRFProvider, sizeof(RTL_VERIFIER_PROVIDER_DESCRIPTOR));
g_AVRFProvider.Length = sizeof(RTL_VERIFIER_PROVIDER_DESCRIPTOR);
g_AVRFProvider.ProviderDlls = AVRFDlls;
g_AVRFProvider.ProviderDllLoadCallback = reinterpret_cast<RTL_VERIFIER_DLL_LOAD_CALLBACK>(&DllLoadCallback);
}
BOOL WINAPI DllMain(PVOID DllHandle, DWORD fdwReason, PRTL_VERIFIER_PROVIDER_DESCRIPTOR* lpDescriptor)
{
switch (fdwReason)
{
case DLL_PROCESS_VERIFIER:
RegisterProvider();
*lpDescriptor = &g_AVRFProvider;
break;
case DLL_PROCESS_ATTACH:
case DLL_PROCESS_DETACH:
default:
break;
}
return TRUE;
}
Firmware Table Controls
The provided functions hEnumServicesStatusExW and hGetSystemFirmwareTable are designed to modify certain system information to evade detection by security mechanisms.
hEnumServicesStatusExW is a hook function for the Windows API EnumServicesStatusExW which enumerates services in the specified service control manager database. This hook function intercepts calls to EnumServicesStatusExW and modifies the service information returned by the function. In this particular implementation, if the number of firmware tables is less than four, the function creates a dummy firmware table and returns the table count after adding the dummies.
typedef BOOL(WINAPI* tEnumServicesStatusExW)(SC_HANDLE, SC_ENUM_TYPE, DWORD, DWORD, LPBYTE, DWORD, LPDWORD, LPDWORD, LPDWORD, LPCWSTR);
tEnumServicesStatusExW oEnumServicesStatusExW = NULL;
BOOL WINAPI hEnumServicesStatusExW(SC_HANDLE hSCManager, SC_ENUM_TYPE InfoLevel, DWORD dwServiceType, DWORD dwServiceState, LPBYTE lpServices, DWORD cbBufSize, LPDWORD pcbBytesNeeded, LPDWORD lpServicesReturned, LPDWORD lpResumeHandle, LPCWSTR pszGroupName)
{
LPBYTE lpNewServices = new BYTE[cbBufSize];
ZeroMemory(lpNewServices, cbBufSize);
DWORD dwNewServicesReturned = 0;
DWORD dwBytesNeeded = 0;
LPENUM_SERVICE_STATUS_PROCESSW newServices = reinterpret_cast<LPENUM_SERVICE_STATUS_PROCESSW>(lpNewServices);
// Copy the service information from the original buffer to the new buffer, somehow original buffer resisted to changes
CopyMemory(lpNewServices, lpServices, cbBufSize);
BOOL result = oEnumServicesStatusExW(hSCManager, InfoLevel, dwServiceType, dwServiceState, lpNewServices, cbBufSize, pcbBytesNeeded, lpServicesReturned, lpResumeHandle, pszGroupName);
if (result && lpNewServices && *lpServicesReturned > 0)
{
LPENUM_SERVICE_STATUS_PROCESSW services = reinterpret_cast<LPENUM_SERVICE_STATUS_PROCESSW>(lpServices);
for (DWORD i = 0; i < *lpServicesReturned; i++)
{
for (const auto& rule : rules_EnumServicesStatusExW)
{
if (StrStrIW(newServices[i].lpServiceName, rule[0].c_str()))
{
// To reduce the errors we set randomstring length to actual string
std::wstring randomstring = random_strings[0].substr(0, wcslen(newServices[i].lpServiceName));
wcscpy_s(newServices[i].lpServiceName, wcslen(randomstring.c_str()) + 1, randomstring.c_str());
wcscpy_s(newServices[i].lpDisplayName, wcslen(randomstring.c_str()) + 1, randomstring.c_str());
break;
}
}
}
}
return result;
}
hGetSystemFirmwareTable is a hook function for the Windows API GetSystemFirmwareTable which retrieves firmware table information from the firmware of a specified device. This hook function intercepts calls to GetSystemFirmwareTable and modifies the firmware table information returned by the function. Specifically, it replaces any occurrence of a specific string in the firmware table with randomly generated characters to evade detection by security mechanisms that rely on the string to identify and block malicious firmware.
typedef ULONG(WINAPI* tGetSystemFirmwareTable)(DWORD, DWORD, PVOID, DWORD);
tGetSystemFirmwareTable oGetSystemFirmwareTable = NULL;
ULONG WINAPI hGetSystemFirmwareTable(DWORD FirmwareTableProviderSignature, DWORD FirmwareTableID, PVOID pFirmwareTableBuffer, DWORD BufferSize)
{
ULONG result = oGetSystemFirmwareTable(FirmwareTableProviderSignature, FirmwareTableID, pFirmwareTableBuffer, BufferSize);
PBYTE firmwareTable = reinterpret_cast<PBYTE>(pFirmwareTableBuffer);
size_t firmwareTableSize = static_cast<size_t>(result);
for (const auto& rule : rules_GetSystemFirmwareTable)
{
std::string needle = converter.to_bytes(rule[0].c_str());
size_t needleLen = needle.length();
std::string randomChars = converter.to_bytes(random_strings[0].c_str());
for (size_t i = 0; i < firmwareTableSize - needleLen; i++)
{
if (memcmp(&firmwareTable[i], reinterpret_cast<PBYTE>(&needle), needleLen) == 0)
{
memcpy(&firmwareTable[i], &randomChars[0], needleLen);
}
}
}
return result;
}
Timing Attacks
The provided functions are designed to prevent timing attacks by limiting the maximum delay time. Specifically, each function limits the input delay time to a maximum of 100 milliseconds. This ensures that the functions will not take significantly longer to execute if an attacker attempts to use timing attacks to exploit them. Timing attacks are used to exploit the time taken by a program to perform certain operations. In the context of sandboxing, these attacks aim to delay the execution of malicious code until after the sandbox has timed out, thus evading its time limits. By limiting the maximum delay time that can be used by an attacker, the functions make it more difficult to execute a successful timing attack. This reduces the risk of exploitation of any time-based vulnerabilities in a system.
typedef NTSTATUS(NTAPI* tNtDelayExecution)(BOOLEAN, PLARGE_INTEGER);
tNtDelayExecution oNtDelayExecution = NULL;
NTSTATUS WINAPI hNtDelayExecution(BOOL Alertable, PLARGE_INTEGER DelayInterval)
{
DelayInterval->QuadPart = 1;
NTSTATUS result = oNtDelayExecution(Alertable, DelayInterval);
return result;
}
typedef UINT(WINAPI* tSetTimer)(HWND hWnd, UINT_PTR nIDEvent, UINT uElapse, TIMERPROC lpTimerFunc);
tSetTimer oSetTimer = NULL;
UINT WINAPI hSetTimer(HWND hWnd, UINT_PTR nIDEvent, UINT uElapse, TIMERPROC lpTimerFunc)
{
if (uElapse > 100)
uElapse = 100;
UINT result = oSetTimer(hWnd, nIDEvent, uElapse, lpTimerFunc);
return result;
}
typedef void (CALLBACK* LPTIMECALLBACK)(UINT, UINT, DWORD_PTR, DWORD_PTR, DWORD_PTR);
typedef MMRESULT(WINAPI* tTimeSetEvent)(UINT, UINT, LPTIMECALLBACK, DWORD_PTR, UINT);
tTimeSetEvent oTimeSetEvent = NULL;
MMRESULT WINAPI hTimeSetEvent(UINT uDelay, UINT uResolution, LPTIMECALLBACK lpTimeProc, DWORD_PTR dwUser, UINT fuEvent)
{
if (uDelay > 100)
uDelay = 100;
MMRESULT result = oTimeSetEvent(uDelay, uResolution, lpTimeProc, dwUser, fuEvent);
return result;
}
When the WaitForSingleObject function is called with an infinite timeout value after a SetWaitableTimer function call, the WaitForSingleObject function should return immediately. However, if the infinite timeout value is set by another function, we should not interfere with it to avoid introducing errors.
Therefore, to handle this situation, we can create a boolean variable called called_SetWaitableTimer, which is set to true when SetWaitableTimer is called. Then, when WaitForSingleObject is called with an infinite timeout value, we can check the called_SetWaitableTimer variable. If it is true, we immediately return WAIT_OBJECT_0 without waiting. After returning from WaitForSingleObject, we set called_SetWaitableTimer to false to indicate that SetWaitableTimer has not been called again.
typedef DWORD(WINAPI* tWaitForSingleObject)(HANDLE, DWORD);
tWaitForSingleObject oWaitForSingleObject = NULL;
BOOL called_SetWaitableTimer = FALSE;
DWORD WINAPI hWaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds)
{
if (dwMilliseconds > 100 && dwMilliseconds != INFINITE)
dwMilliseconds = 100;
if (dwMilliseconds == INFINITE && called_SetWaitableTimer == TRUE) {
DWORD result = oWaitForSingleObject(hHandle, 1000);
called_SetWaitableTimer = FALSE;
return WAIT_OBJECT_0;
}
DWORD result = oWaitForSingleObject(hHandle, dwMilliseconds);
return result;
}
typedef DWORD(WINAPI* tIcmpSendEcho)(HANDLE, IPAddr, LPVOID, WORD, PIP_OPTION_INFORMATION, LPVOID, DWORD, DWORD);
tIcmpSendEcho oIcmpSendEcho = NULL;
DWORD WINAPI hIcmpSendEcho(HANDLE IcmpHandle, IPAddr DestinationAddress, LPVOID RequestData, WORD RequestSize, PIP_OPTION_INFORMATION RequestOptions, LPVOID ReplyBuffer, DWORD ReplySize, DWORD Timeout)
{
if (Timeout > 100)
Timeout = 100;
DWORD result = oIcmpSendEcho(IcmpHandle, DestinationAddress, RequestData, RequestSize, RequestOptions, ReplyBuffer, ReplySize, Timeout);
return result;
}
typedef BOOL(WINAPI* tSetWaitableTimer)(HANDLE, LARGE_INTEGER*, LONG, PTIMERAPCROUTINE, LPVOID, BOOL);
tSetWaitableTimer oSetWaitableTimer = NULL;
BOOL WINAPI hSetWaitableTimer(HANDLE hTimer, LARGE_INTEGER* pDueTime, LONG lPeriod, PTIMERAPCROUTINE pfnCompletionRoutine, LPVOID lpArgToCompletionRoutine, BOOL fResume)
{
LARGE_INTEGER correctedDueTime;
correctedDueTime.QuadPart = pDueTime->QuadPart;
// if due time is greater than 1 second
if (correctedDueTime.QuadPart > -1000000LL)
correctedDueTime.QuadPart = -1000000LL;
BOOL result = oSetWaitableTimer(hTimer, &correctedDueTime, lPeriod, pfnCompletionRoutine, lpArgToCompletionRoutine, fResume);
called_SetWaitableTimer = TRUE;
return result;
}
typedef BOOL(WINAPI* tCreateTimerQueueTimer)(HANDLE*, HANDLE, WAITORTIMERCALLBACK, PVOID, DWORD, DWORD, ULONG);
tCreateTimerQueueTimer oCreateTimerQueueTimer = NULL;
BOOL WINAPI hCreateTimerQueueTimer(PHANDLE phNewTimer, HANDLE TimerQueue, WAITORTIMERCALLBACK Callback, PVOID Parameter, DWORD DueTime, DWORD Period, ULONG Flags)
{
if (DueTime > 100)
DueTime = 100;
BOOL result = oCreateTimerQueueTimer(phNewTimer, TimerQueue, Callback, Parameter, DueTime, Period, Flags);
return result;
}
Conclusion
In conclusion, Evasion Escaper has demonstrated the power of detour functions in bypassing malware’s virtual environment detection mechanisms. By intercepting and modifying the behavior of Windows functions called by Al-Khaser, we were able to prevent detection by certain evasion techniques. Evasion Escaper provides a useful tool for security researchers and developers who need to modify the behavior of Windows functions for various purposes, including testing and security. As the security landscape continues to evolve, new methods are needed to stay ahead of the game, and detour functions provide a powerful approach to achieving this goal.
This project is open to ongoing development and contribution, with plans to add additional improvements to the current bypass methods in future commits. The code has been structured to enhance readability and maintainability. As the project progresses, documentation for the bypass methods will be continuously updated and refined. Contributions from the community are highly welcomed and appreciated.
Check out the code for this project on my Github page: Evasion Escaper