静态扫描之ImpHash检测法

使用 Import Hashing 检测恶意软件

翻译自:https://www.fireeye.com/blog/threat-research/2014/01/tracking-malware-import-hashing.html

一种特殊的检测恶意软件的方法是检测其PE文件导入表(Imports),导入表就是一个包含所有调用函数(一般是调用自Windows系统各种DLL)的表。对于每个软件(恶意软件),其ImpHash是唯一的,因为编译器是根据源码中每个函数出现的顺序来制定IAT(Import Address TableI)的。


下面以两个源码示例来进行演示:

#include 
#include 
#include 
#include 
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "wininet.lib")
int makeMutexA()
{
    CreateMutexA(NULL, FALSE, "TestMutex");
    return 0;
}
int makeMutexW()
{
    CreateMutexW(NULL, FALSE, L"TestMutex");
    return 0;
}
int makeUserAgent()
{
    HANDLE hInet=0, hConn=0;
    char buf[sizeof(struct hostent)] = {0};
    hInet = InternetOpenA("User-Agent: (Windows; 5.1)", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
    hConn = InternetConnectA(hInet, "www.google.com", 443, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
    WSAAsyncGetHostByName(NULL, 3, "www.yahoo.com", buf, sizeof(struct hostent));
    return 0;
}
int main(int argc, char *argv[])
{
    makeMutexA();
    makeMutexW();
    makeUserAgent();
    return 0;
}

将上述源码编译时,生成的IAT如下所示:

ws2_32.dll
ws2_32.dll.WSAAsyncGetHostByName

wininet.dll
wininet.dll.InternetOpenA
wininet.dll.InternetConnectA

kernel32.dll
kernel32.dll.InterlockedIncrement
kernel32.dll.IsProcessorFeaturePresent
kernel32.dll.GetStringTypeW
kernel32.dll.MultiByteToWideChar
kernel32.dll.LCMapStringW
kernel32.dll.CreateMutexA
kernel32.dll.CreateMutexW
kernel32.dll.GetCommandLineA
kernel32.dll.HeapSetInformation
kernel32.dll.TerminateProcess

Imphash: 0c6803c4e922103c4dca5963aad36ddf

我简化了导入表以节省空间,其中粗体的API是源代码中引用到的函数。 请注意它们在导入表中出现的顺序,并将它们与源码中显示的顺序进行比较。

如果作者调整下函数的调用顺序,那么相应的,导入表也会随之变化,还是上面的源码实例:

#include 
#include 
#include 
#include 
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "wininet.lib")
int makeMutexW()
{
    CreateMutexW(NULL, FALSE, L"TestMutex");
    return 0;
}
int makeMutexA()
{
    CreateMutexA(NULL, FALSE, "TestMutex");
    return 0;
}
int makeUserAgent()
{
    HANDLE hInet=0, hConn=0;
    char buf[sizeof(struct hostent)] = {0};
    hConn = InternetConnectA(hInet, "www.google.com", 443, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
  hInet = InternetOpenA("User-Agent: (Windows; 5.1)", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
    WSAAsyncGetHostByName(NULL, 3, "www.yahoo.com", buf, sizeof(struct hostent));
  return 0;
}
int main(int argc, char *argv[])
{
    makeMutexA();
    makeMutexW();
    makeUserAgent();
    return 0;
}

在这个例子中,我们调换了makeMutexW和makeMutexA以及InternetConnectA和InternetOpenA的顺序。(请注意,这将是一个错误的API调用逻辑,但我只是在这里作为讲解示范)下面是该源码的IAT,我们来跟上面的导入表和ImpHash进行比较:

ws2_32.dll
ws2_32.dll.WSAAsyncGetHostByName

wininet.dll
wininet.dll.InternetConnectA
wininet.dll.InternetOpenA

kernel32.dll
kernel32.dll.InterlockedIncrement
kernel32.dll.IsProcessorFeaturePresent
kernel32.dll.GetStringTypeW
kernel32.dll.MultiByteToWideChar
kernel32.dll.LCMapStringW
kernel32.dll.CreateMutexW
kernel32.dll.CreateMutexA
kernel32.dll.GetCommandLineA
kernel32.dll.HeapSetInformation
kernel32.dll.TerminateProcess

Imphash: b8bb385806b89680e13fc0cf24f4431e

我们发现随着函数调用顺序的改变, ImpHash 也会变得截然不同。


下面这个例子,将展示在编译时,包含文件的排序是如何影响IAT的(以及由此产生的ImpHash)。 在此示例,原来的imphash.c将包含进imphash1.c和imphash2.c:

-- imphash1.c --
int makeNamedPipeA()
{
HANDLE ph = CreateNamedPipeA("\\.\pipe    est_pipe", PIPE_ACCESS_DUPLEX,
    PIPE_TYPE_MESSAGE, 1, 128,
    64, 200, NULL);
    return 0;
}
-- imphash2.c --
int makeNamedPipeW()
{
    HANDLE ph2 = CreateNamedPipeW(L"\\.\pipe    est_pipeW", PIPE_ACCESS_DUPLEX,
        PIPE_TYPE_MESSAGE, 1, 128,
        64, 200, NULL);
    return 0;
}
-- imphash.c --
#include 
#include 
#include 
#include 
#include "imphash1.h"
#include "imphash2.h"
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "wininet.lib")
int makeMutexW()
{
    CreateMutexW(NULL, FALSE, L"TestMutex");
    return 0;
}
int makeMutexA()
{
    CreateMutexA(NULL, FALSE, "TestMutex");
    return 0;
}
int makeUserAgent()
{
    HANDLE hInet = 0, hConn = 0;
    char buf[sizeof(struct hostent)] = {0};
    hConn = InternetConnectA(hInet, "www.google.com", 443, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
    hInet = InternetOpenA("User-Agent: (Windows; 5.1)", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
    WSAAsyncGetHostByName(NULL, 3, "www.yahoo.com", buf, sizeof(struct hostent));
    return 0;
}
int main(int argc, char *argv[])
{
    makeMutexA();
    makeMutexW();
    makeUserAgent();
    makeNamedPipeA();
    makeNamedPipeW();
    return 0;
}

使用以下命令编译:

cl imphash.c imphash1.c imphash2.c /W3 /WX /link

IAT顺序如下:

ws2_32.dll
ws2_32.dll.WSAAsyncGetHostByName
wininet.dll
wininet.dll.InternetConnectA
wininet.dll.InternetOpenA
kernel32.dll
kernel32.dll.TlsFree
kernel32.dll.IsProcessorFeaturePresent
kernel32.dll.GetStringTypeW
kernel32.dll.MultiByteToWideChar
kernel32.dll.LCMapStringW
kernel32.dll.CreateMutexW
kernel32.dll.CreateMutexA
kernel32.dll.CreateNamedPipeA
kernel32.dll.CreateNamedPipeW
kernel32.dll.GetCommandLineA
kernel32.dll.HeapSetInformation
kernel32.dll.TerminateProcess

Imphash: 9129bdbc18cfd1aba498c94e809567d5

在imphash.c中更改imphash1.h和imphash2.h的include顺序将不会影响IAT的顺序。 但是,更改编译命令中文件imphash1.h和imphash2.h的顺序则会影响IAT(PS:注意CreateNamedPipeW和CreateNamedPipeA的重新排序):

cl imphash.c imphash2.c imphash1.c /W3 /WX /link

ws2_32.dll
ws2_32.dll.WSAAsyncGetHostByName
wininet.dll
wininet.dll.InternetConnectA
wininet.dll.InternetOpenA
kernel32.dll
kernel32.dll.TlsFree
kernel32.dll.IsProcessorFeaturePresent
kernel32.dll.GetStringTypeW
kernel32.dll.MultiByteToWideChar
kernel32.dll.LCMapStringW
kernel32.dll.CreateMutexW
kernel32.dll.CreateMutexA
kernel32.dll.CreateNamedPipeW
kernel32.dll.CreateNamedPipeA
kernel32.dll.GetCommandLineA
kernel32.dll.HeapSetInformation
kernel32.dll.TerminateProcess

Imphash: c259e28326b63577c31ee2c01b25d3fa

这些例子表明,源代码中函数导入的顺序以及编译时的参数顺序都会影响IAT,从而影响产生的ImpHash值。也就是说,即使两个不同的二进制文件的函数导入模式一样,但因为编码时的方式不一样,最终得到的ImpHash值也会不同。相反,如果两个文件具有相同的ImpHash值,则它们具有相同的IAT,这意味着这些文件是从相同的源代码编译的,并且编码方式相同。


对于一些加壳样本、小型的工具和程序(因为其导入函数很少,编译方式也可能相同),它们的ImpHash值就可能相同。 换句话说,两个不同恶意软件可能会因为这些因素具有相同的ImpHash。

然而,对于更复杂的工具(如恶意软件)来说,在存在足够数量的导入函数的情况下,ImpHash应该是唯一的,因此可以用于识别结构相似的恶意软件家族。 虽然具有相同ImpHash的恶意软件不能保证来自相同的家族,但至少可以给你提供一个额外的线索。

我们在使用ImpHash检测恶意软件上取得了巨大的成功。官方也将此方法加入了pefile库,示例代码:

import pefile
pe = pefile.PE(sys.argv[1])
print "Import Hash: %s" % pe.get_imphash()

设想一个场景,一个攻击者根据不同的C&C地址和代号ID编译30个后门变种,并将它们部署到不同的公司。 如果有博客文章讨论其公司正被某MD5的后门监视,那么基于该MD5,攻击者立即知道哪些C&C服务器处于危险之中。 但是,如果博客只是讨论后门的ImpHash值,则攻击者并不知道究竟是30个变种中的哪个被发现。

为了展示这种检测方法的效果,我们来看一看Mandiant APT1报告中分享的几个恶意软件家族的ImpHash值:

Family Name          Import Hash          Total Imports Number of matched Samples

GREENCAT    2c26ec4a570a502ed3e8484295581989      74              23
GREENCAT    b722c33458882a1ab65a13e99efe357e      74              18
GREENCAT    2d24325daea16e770eb82fa6774d70f1     113              13
GREENCAT    0d72b49ed68430225595cc1efb43ced9     100              13
STARSYPOUND 959711e93a68941639fd8b7fba3ca28f      62              31
COOKIEBAG   4cec0085b43f40b4743dc218c585f2ec      79              10
NEWSREELS   3b10d6b16f135c366fc8e88cba49bc6c      77              41
NEWSREELS   4f0aca83dfe82b02bbecce448ce8be00      80              10
TABMSGSQL   ee22b62aa3a63b7c17316d219d555891     102               9
WEBC2       a1a42f57ff30983efda08b68fedd3cfc      63              25
WEBC2       7276a74b59de5761801b35c672c9ccb4      52              13

Mandiant APT1报告展示了几个恶意软件家族的ImpHash值以及各个ImpHash值所匹配到的样本数。由此我们可以看出,同一个恶意软件家族的样本大都具有相同的ImpHash值,我们可以使用此方法来对各个样本变种进行归类。

最后,即使ImpHash检测法是个简单高效的方法,但是,不能单纯使用ImpHash对恶意样本进行检测,它应该配合其他检测法一起使用。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值