详解Windows API Hashing技术

secM  278天前

在本文中,我们将为大家介绍一种称为API Hashing的技术,恶意软件开发者通常利用这种技术来隐藏从PE的IAT中导入的可疑WindowsAPI,从而提高恶意软件的分析难度。本文旨在帮助安全分析人员了解该技术的运行机制,从而提高分析效率。

恶意软件开发者所面临的问题

如果PE的IAT是完好无损的,那么,在理解PE的功能的时候,还是比较容易的——例如,如果我们看到二进制文件加载的是Ws2_32.dll,那么基本上就可以断定该二进制文件包含了一些网络功能;如果发现导入了一个RegCreateKeyEx函数,我们就知道这个二进制文件提供了修改注册表的功能,等等。

恶意软件开发者的解决方案

恶意软件开发者当然希望避免上述情况,为了提高恶意软件的分析难度,他们会借助于API hashing技术来隐藏IAT中的可疑API调用。这样一来,当安全分析人员通过实用程序strings处理恶意二进制文件,或在某个PE解析器中打开恶意软件的二进制代码时,由于Windows API被隐藏了起来,如果不进行深入分析的话,很难搞清楚这些二进制代码的具体行为。

假设攻击者编写了一个名为api-hashing.exe的恶意软件,其中用到了CreateThread函数。

 1.png

如果我们编译上面的代码,并通过PE解析器进行检查,就会发现它从kernel32库中导入了28个函数,而CreateThread就是其中之一。

 1.png

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,具体见下文。

上述过程,可以用下图说明:

 1.png

计算哈希值

如上文所说,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。

Image

CreateThread对应的哈希值为0x00544e304

下面,我们开始编写C程序,用于解析CreateThread函数的地址,方法是解析Kernel32模块的导出地址表,并根据我们刚才计算出的哈希值--0x00544e304,得出CreateThread函数在恶意进程的内存地址。

通过哈希值解析地址

我们的C程序将用到两个函数:

getHashFromString函数:计算给定字符串的哈希值。实际上,它与前面的Powershell脚本中用于计算函数名CreateThread的哈希值的函数的作用是一样的。

在下图中,左边是我们C程序中的getHashFromString函数,右边是Powershell版本的哈希计算算法:

1.png

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.png

其中,从左到右依次为:

1、CreateThread:函数名,已被解析为给定的哈希值0x00544e304。

2、0x00544e304:函数名CreateThread对应的哈希值。


3、00007FF89DAFB5A0:函数CreateThread在api-hashing.exe进程中的虚拟内存地址。

下图说明虚拟地址00007FF89DAFB5A0确实指向api-hashing.exe进程中的CreateThread函数。

 1.png

通过哈希值成功解析出CreateThread函数的内存地址

更重要的是,现在已经很难通过IAT发现CreateThread了:

 1.png

IAT已经被篡改,CreateThread“凭空消失”了

测试CreateThread是否可用

下图表明,我们现在可以成功调用CreateThread了,并且它是在运行时通过哈希值0x00544e304进行解析的——这一点可以通过获得新创建线程的句柄0x84来确认。

 api-hashing-calculating-hash.gif

下图还显示了在我们调用CreateThread时创建的线程ID:

1.png

参考资料

API-Hashing in the Sodinokibi/REvil Ransomware&#8211; Why and How?

本文由secM整理并翻译,不代表白帽汇任何观点和立场
来源:https://www.ired.team/offensive-security/defense-evasion/windows-api-hashing-in-malware

最新评论

昵称
邮箱
提交评论