详解Windows API Hashing技术
在本文中,我们将为大家介绍一种称为API Hashing的技术,恶意软件开发者通常利用这种技术来隐藏从PE的IAT中导入的可疑WindowsAPI,从而提高恶意软件的分析难度。本文旨在帮助安全分析人员了解该技术的运行机制,从而提高分析效率。
恶意软件开发者所面临的问题
如果PE的IAT是完好无损的,那么,在理解PE的功能的时候,还是比较容易的——例如,如果我们看到二进制文件加载的是Ws2_32.dll,那么基本上就可以断定该二进制文件包含了一些网络功能;如果发现导入了一个RegCreateKeyEx函数,我们就知道这个二进制文件提供了修改注册表的功能,等等。
恶意软件开发者的解决方案
恶意软件开发者当然希望避免上述情况,为了提高恶意软件的分析难度,他们会借助于API hashing技术来隐藏IAT中的可疑API调用。这样一来,当安全分析人员通过实用程序strings处理恶意二进制文件,或在某个PE解析器中打开恶意软件的二进制代码时,由于Windows API被隐藏了起来,如果不进行深入分析的话,很难搞清楚这些二进制代码的具体行为。
假设攻击者编写了一个名为api-hashing.exe的恶意软件,其中用到了CreateThread函数。
如果我们编译上面的代码,并通过PE解析器进行检查,就会发现它从kernel32库中导入了28个函数,而CreateThread就是其中之一。
IAT完好无损:CreateThread暴露无遗
出于某些原因,攻击者决定提高恶意软件分析人员的工作强度:仅仅通过查看二进制的IAT/运行strings工具就知道他们的二进制代码将调用CreateThread,这样也太轻松了。为了达到这个目的,他们可以采用APIhashing技术,在运行时解析CreateThread函数的地址。这样一来,攻击者可以让CreateThread从PE的IAT中“凭空消失”,这正是本文的目的——为安全分析人员介绍这个技术的运行机制,从而提高工作效率。
目标
为了完成本实验,我们将编写:
i. 一个简单的powershell脚本,用于计算给定函数名称的哈希值。例如,向该脚本输入一个字符串CreateThread时,将返回其哈希值;在我们的实验室中,正如我们稍后看到的那样,将返回0x00544e304。
ii. 一个简单的C程序,用于在api-hashing.exe中解析CreateThread函数的虚拟地址,方法是遍历kernel32模块(CreateThread所在的模块)所有导出函数的名称,并计算它们的哈希值(这里将使用我们的哈希算法),并与我们的哈希值0x00544e304(对应于CreateThread函数)进行比较。在我们的例子中,这个程序会返回一个虚拟地址:00007FF89DAFB5A0,具体见下文。
上述过程,可以用下图说明:
计算哈希值
如上文所说,API hashing技术并不复杂,只需提供一个函数/算法,来计算给定文本字符串的哈希值即可。
在本例中,我们定义的哈希算法的工作方式为:
1、取要计算哈希值的函数名(如CreateThread);
2、将字符串转换为char数组;
3、将一个变量$hash设置为任意初始值。在我们的例子中,我们将这个初值设为0x35(没有特别的原因);如前所述,哈希计算可以是您选择的任意算法,只要能够可靠地创建哈希值而不发生碰撞即可,也就是说,只要两个不同的API调用不会生成相同的哈希值即可;
4、遍历每个字符,并执行以下处理(哈希计算):
1、将字符转换为十六进制表示;
2、执行以下运算: $hash += $hash * 0xab10f29f + $c-band 0xffffffff,其中:
1、0xab10f29f是我们选择的另一个随机值;
2、$c是我们要计算其哈希值的函数名的十六进制表示;
3、-band 0xffffff用于屏蔽哈希值的高位。
5、返回字符串“CreateThread”的哈希表示形式。
$APIsToHash = @("CreateThread")
$APIsToHash | % {
$api = $_
$hash = 0x35
[int]$i = 0
$api.ToCharArray() | % {
$l = $_
$c = [int64]$l
$c = '0x{0:x}' -f $c
$hash += $hash * 0xab10f29f + $c -band 0xffffff
$hashHex = '0x{0:x}' -f $hash
$i++
write-host "Iteration $i : $l : $c : $hashHex"
}
write-host "$api`t $('0x00{0:x}' -f $hash)"
}
如果我们上面的哈希函数来处理字符串CreateThread,得到的哈希值为0x00544e304。
CreateThread对应的哈希值为0x00544e304
下面,我们开始编写C程序,用于解析CreateThread函数的地址,方法是解析Kernel32模块的导出地址表,并根据我们刚才计算出的哈希值--0x00544e304,得出CreateThread函数在恶意进程的内存地址。
通过哈希值解析地址
我们的C程序将用到两个函数:
getHashFromString函数:计算给定字符串的哈希值。实际上,它与前面的Powershell脚本中用于计算函数名CreateThread的哈希值的函数的作用是一样的。
在下图中,左边是我们C程序中的getHashFromString函数,右边是Powershell版本的哈希计算算法:
C语言和Powershell语言版本的API hashing函数
getFunctionAddressByHash函数:该函数的输入为哈希值(在我们的例子中,就是函数CreateThread对应的哈希值0x00544e304),并返回该哈希值对应的函数的虚拟地址——在我们的例子中,将返回00007FF89DAFB5A0。
这个函数的工作原理如下所示:
1、获取我们感兴趣的函数(本例中,为CreateThread)所在程序库(在本例中,为kernel32.dll)的基址。
2、查找kernel32导出地址表。
3、通过kernel32模块遍历每个导出的函数名。
4、对于每个导出的函数名,使用getHashFromString计算其哈希值。
5、如果计算出的哈希值等于0x00544e304(对应于CreateThread),则计算该函数的虚拟地址。
6、这时,可以输入CreateThread函数原型,使其指向第5步中解析的地址,并用它来创建新线程,但这次CreateThread将不会显示在恶意软件PE的导入地址表中!
下面是C程序的源码,用于通过哈希值(0x00544e304)来解析CreateThread函数的地址:
●#include <iostream>
#include <Windows.h>
DWORD getHashFromString(char *string)
{
size_tstringLength = strnlen_s(string, 50);
DWORD hash =0x35;
for (size_t i= 0; i < stringLength; i++)
{
hash +=(hash * 0xab10f29f + string[i]) & 0xffffff;
}
//printf("%s: 0x00%x\n", string, hash);
return hash;
}
PDWORD getFunctionAddressByHash(char *library, DWORDhash)
{
PDWORDfunctionAddress = (PDWORD)0;
// Get ba seaddress of the module in which our exported function of interest resides(kernel32 in the case of CreateThread)
HMODULElibraryba se = LoadLibraryA(library);
PIMAGE_DOS_HEADERdosHeader = (PIMAGE_DOS_HEADER)libraryba se;
PIMAGE_NT_HEADERSimageNTHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)libraryba se +dosHeader->e_lfanew);
DWORD_PTRexportDirectoryRVA =imageNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
PIMAGE_EXPORT_DIRECTORYimageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD_PTR)libraryba se +exportDirectoryRVA);
// Get RVAsto exported function related information
PDWORDaddresOfFunctionsRVA = (PDWORD)((DWORD_PTR)libraryba se +imageExportDirectory->AddressOfFunctions);
PDWORDaddressOfNamesRVA = (PDWORD)((DWORD_PTR)libraryba se +imageExportDirectory->AddressOfNames);
PWORDaddressOfNameOrdinalsRVA = (PWORD)((DWORD_PTR)libraryba se +imageExportDirectory->AddressOfNameOrdinals);
// Iteratethrough exported functions, calculate their hashes and check if any of themmatch our hash of 0x00544e304 (CreateThread)
// If yes,get its virtual memory address (this is where CreateThread function resides inmemory of our process)
for (DWORD i= 0; i < imageExportDirectory->NumberOfFunctions; i++)
{
DWORDfunctionNameRVA = addressOfNamesRVA[i];
DWORD_PTRfunctionNameVA = (DWORD_PTR)libraryba se + functionNameRVA;
char*functionName = (char*)functionNameVA;
DWORD_PTRfunctionAddressRVA = 0;
//Calculate hash for this exported function
DWORD functionNameHash= getHashFromString(functionName);
// Ifhash for CreateThread is found, resolve the function address
if(functionNameHash == hash)
{
functionAddressRVA= addresOfFunctionsRVA[addressOfNameOrdinalsRVA[i]];
functionAddress= (PDWORD)((DWORD_PTR)libraryba se + functionAddressRVA);
printf("%s: 0x%x : %p\n", functionName, functionNameHash, functionAddress);
returnfunctionAddress;
}
}
}
// Define CreateThread function prototype
using customCreatThread = HANDLE(NTAPI*)(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
__drv_aliasesMemLPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
int main()
{
// ResolveCreateThread address by hash
PDWORDfunctionAddress = getFunctionAddressByHash((char *)"kernel32",0x00544e304);
// PointCreateThread function pointer to the CreateThread virtual address resolved byits hash
customCreatThreadCreateThread = (customCreatThread)functionAddress;
DWORD tid =0;
// CallCreateThread
HANDLE th =CreateThread(NULL, NULL, NULL, NULL, NULL, &tid);
return 1;
}
● 关于解析PE可执行文件的更多信息,请参见 Parsing PE File Headers with C++。
如果我们编译并运行这段代码,我们将看到以下输出结果:
其中,从左到右依次为:
1、CreateThread:函数名,已被解析为给定的哈希值0x00544e304。
2、0x00544e304:函数名CreateThread对应的哈希值。
3、00007FF89DAFB5A0:函数CreateThread在api-hashing.exe进程中的虚拟内存地址。
下图说明虚拟地址00007FF89DAFB5A0确实指向api-hashing.exe进程中的CreateThread函数。
通过哈希值成功解析出CreateThread函数的内存地址
更重要的是,现在已经很难通过IAT发现CreateThread了:
IAT已经被篡改,CreateThread“凭空消失”了
测试CreateThread是否可用
下图表明,我们现在可以成功调用CreateThread了,并且它是在运行时通过哈希值0x00544e304进行解析的——这一点可以通过获得新创建线程的句柄0x84来确认。
下图还显示了在我们调用CreateThread时创建的线程ID:
参考资料
API-Hashing in the Sodinokibi/REvil Ransomware– Why and How?
本文由secM整理并翻译,不代表白帽汇任何观点和立场
来源:https://www.ired.team/offensive-security/defense-evasion/windows-api-hashing-in-malware
最新评论