Windows下API_HASH

前言

很久以前学习的时候记的笔记,这里主要是跟着https://www.ired.team/offensive-security/defense-evasion/windows-api-hashing-in-malware这篇文章进行的记录和学习,感兴趣的朋友可以看看原文。

什么是API Hashing

Windows API Hashing技术是一种将输入数据(如函数名称或参数)转换为固定长度哈希值的算法。过哈希化 API 函数名称或参数,可以对它们进行隐藏或混淆,使得恶意软件或攻击者更难直接识别或利用系统中的特定 API 调用。核心是利用API Hashing技术来隐藏IAT中的可疑API调用。
在恶意代码分析中,Windows 平台的 API 函数是非常重要的信息来源,比如通过分析恶意代码中使用的 API 函数,我们不需要对已加壳的文件进行逆向分析,因为我们只需要对恶意代码所执行的 API 调用来进行动态分析,就可以知道某个特定文件具体的功能了。通过这样的方法(分析 API 调用),我们可以确定一个文件是否具有恶意性,而有些 API 调用只有某些特殊类型的恶意软件才会去使用。
为了防止恶意软件的分析者可以直接分析出我们的代码,提高恶意软件的分析难度,可以借助API Hashing技术来隐藏IAT中的一些可以API调用,这样一来当安全人员分析处理时由于windows API被隐藏起来了,就需要更加深入和细致的分析才能搞清楚这些代码的具体行为。

API hash的意义

将API进行hash从不同的角度看可能有不同的作用,这里从免杀的角度进行考虑。
这么做的意义是什么:
1.规避杀毒引擎的检测:

  • 杀毒引擎会扫描可执行文件中的 API 调用,并与病毒库进行对比,以此来检测和拦截恶意代码。
  • 通过对 API 进行哈希化处理,可以有效地 bypass 杀毒引擎的特征库检测,降低被杀的概率。

2.混淆代码逻辑:

  • 将 API 名称进行哈希化处理,可以混淆代码逻辑,增加逆向分析的难度,从而提高免杀效果。

3.动态解密 API 调用:

  • 在某些免杀技术中,恶意代码会对 API 调用进行动态解密,以此来规避静态检测。
  • 将 API 名称哈希化后,可以将解密逻辑集中在哈希值的解密过程中,进一步提高免杀能力。

4.绕过 API 监控:

  • 一些安全产品会对系统 API 调用进行监控和分析,试图检测恶意行为。 将 API 名称哈希化可以绕过这些监控机制,降低被检测的概率。

实例

这里假设编写如下代码的恶意软件,其中使用到了CreateThread函数。在这里插入图片描述
编译该代码,然后使用CFF Explorer打开可以很明显的看到该程序从kernel32中导入了13个函数,而Create Thread就是其中之一。
在这里插入图片描述
这样仅仅通过查看二进制的IAT或者运行strings工具就知道了该程序将调用Create Thread,为了使该程序更难被分析出来,这里就可以采用API Hashing技术,在运行时解析Create Thread函数的地址,这样Create Thread就从IAT表中凭空消失了。

这里我们和文章中一样,编写文章中使用的powershell的脚本和一个C程序,powershell脚本用来计算给定函数的哈希值,例如,向该脚本输入一个字符串CreateThread时,将返回其哈希值;C程序用来解析Create Thread函数的虚拟地址,方法是遍历kernel32模块(即Create Thread所在的模块)所有导出函数的名称,并计算它们的哈希值,并与我们的Create Thread函数的哈希值进行比较。

上述过程,如下图:
在这里插入图片描述
API Hashing并不复杂,只需要提供一个加密算法来计算给定文本字符串的哈希值即可。

这里我们就直接使用文章中的加密算法。
作者定义的哈希算法原理为:
1、取要计算哈希值的函数名(如CreateThread);
2、将字符串转换为char数组;
3、将一个变量$hash设置为任意初始值。在我们的例子中,我们将这个初值设为0x35(没有特别的原因);如前所述,哈希计算可以是您选择的任意算法,只要能够可靠地创建哈希值而不发生碰撞即可,也就是说,只要两个不同的API调用不会生成相同的哈希值即可;
4、遍历每个字符,并执行以下处理(哈希计算):
(1)将字符转换为十六进制表示;
(2)执行以下运算: $hash += $hash * 0xab10f29f + $c-band 0xffffffff,其中:
① 0xab10f29f是我们选择的另一个随机值;
② $c是我们要计算其哈希值的函数名的十六进制表示;
③ -band 0xffffff用于屏蔽哈希值的高位。
5、返回字符串“CreateThread”的哈希表示形式。

使用上面的哈希函数来处理字符串Create Thread,得到的哈希值为:0x00544e304
在这里插入图片描述

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

通过哈希值解析地址

这里编写的C程序将使用两个函数:
GetHashFromString函数:计算给定字符串的哈希值,作用和上面的Powershell脚本中用于计算函数名CreateThread的哈希值的函数的作用是一样的。

在下图中,右边是C程序中的getHashFromString函数,左边是Powershell版本的哈希计算算法:
在这里插入图片描述
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 <Windows.h>
#include <iostream>
#include <stdio.h>

//计算给定字符串的hash,最后返回计算后的hash
DWORD getHashFromString(char* string)
{
	size_t stringLength = 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;
}

//从指定的library中解析特定hash对应的函数地址
PDWORD getFunctionAddressByHash(char* library, DWORD hash)
{
	PDWORD functionAddress = (PDWORD)0;

	// 加载指定的DLL,这里是kernel32
	HMODULE libraryBase = LoadLibraryA(library);
	
	PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)libraryBase;//将kernel32强制转换为dosHeader这种结构体指针,这个结构体是PE的DOS头信息,包含PE基本信息,例如e_lfanew
	PIMAGE_NT_HEADERS imageNTHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)libraryBase + dosHeader->e_lfanew);//从 DOS 头部中获取了指向 IMAGE_NT_HEADERS 结构体的指针

	DWORD_PTR exportDirectoryRVA = imageNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;	//获取导出表,通过 IMAGE_DIRECTORY_ENTRY_EXPORT 定位导出表的位置

	PIMAGE_EXPORT_DIRECTORY imageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD_PTR)libraryBase + exportDirectoryRVA);	//libraryBase加上导出表的RVA,得到导出表在内存中的地址

	
	PDWORD addresOfFunctionsRVA = (PDWORD)((DWORD_PTR)libraryBase + imageExportDirectory->AddressOfFunctions);//获取函数地址表AddressOfFunctions的RVA,该表存储了导出函数的地址,通过这个地址可以访问到模块中导出函数的地址列表
	PDWORD addressOfNamesRVA = (PDWORD)((DWORD_PTR)libraryBase + imageExportDirectory->AddressOfNames);//获取函数名称表AddressOfNames的RVA,函数名称表存储了导出函数的名称字符串的地址。通过这个地址,可以访问到模块中导出函数名称的列表。
	PWORD addressOfNameOrdinalsRVA = (PWORD)((DWORD_PTR)libraryBase + imageExportDirectory->AddressOfNameOrdinals);//获取函数序号表AddressOfNameOrdinals的RVA,函数序号表存储了导出函数在函数地址表中的索引序号。通过这个地址,可以访问到模块中导出函数的序号列表。

	// Iterate through exported functions, calculate their hashes and check if any of them match our hash of 0x00544e304 (CreateThread)
	// If yes, get its virtual memory address (this is where CreateThread function resides in memory of our process)
	for (DWORD i = 0; i < imageExportDirectory->NumberOfFunctions; i++)		//NumberOfFunctions是所有导出函数的个数
	{
		DWORD functionNameRVA = addressOfNamesRVA[i];	//依次获取函数名称表AddressOfNames中函数的RVA
		DWORD_PTR functionNameVA = (DWORD_PTR)libraryBase + functionNameRVA;	//kernel32加上导出函数的RVA,获取到该函数名称在内存中的RVA
		char* functionName = (char*)functionNameVA;	//将获取到的函数RVA赋值给functionName
		DWORD_PTR functionAddressRVA = 0;

		// Calculate hash for this exported function
		DWORD functionNameHash = getHashFromString(functionName);	//计算该函数名称的hash

		// If hash for CreateThread is found, resolve the function address
		if (functionNameHash == hash)	//比较hash是否一致
		{
			functionAddressRVA = addresOfFunctionsRVA[addressOfNameOrdinalsRVA[i]];//获取函数地址表中与当前函数名称对应的函数地址的 RVA,通过addressOfNameOrdinalsRVA[i]获取了当前函数在函数序号表中的索引,然后通过这个索引获取了函数地址表中对应的 RVA。
			functionAddress = (PDWORD)((DWORD_PTR)libraryBase + functionAddressRVA);//通过将模块基地址 libraryBase 与函数地址的 RVA 相加,得到了目标函数的绝对虚拟地址,并将其转换为指向 DWORD 类型的指针
			printf("%s : 0x%x : %p\n", functionName, functionNameHash, functionAddress);//打印函数名称、哈希值以及函数地址
			return functionAddress;
		}
	}
}

// 重新定义一个CreateThread的函数
using customCreateThread = HANDLE(NTAPI*)(
	LPSECURITY_ATTRIBUTES   lpThreadAttributes,
	SIZE_T                  dwStackSize,
	LPTHREAD_START_ROUTINE  lpStartAddress,
	__drv_aliasesMem LPVOID lpParameter,
	DWORD                   dwCreationFlags,
	LPDWORD                 lpThreadId
	);

int main()
{

	PDWORD functionAddress = getFunctionAddressByHash((char*)"kernel32", 0x00544e304);	//使用getFunctionAddressByHash函数获取kernel32中哈希值为0x00544e304的函数的地址

	customCreateThread CreateThread = (customCreateThread)functionAddress;	//将CreateThread函数指针指向由其哈希解析的CreateThread虚拟地址
	DWORD tid = 0;

	// 调用 CreateThread
	HANDLE th = CreateThread(NULL, NULL, NULL, NULL, NULL, &tid);

	return 1;
}

编译并运行可以看到以下结果:
在这里插入图片描述
其中,从左到右依次为:
1、CreateThread:函数名,已被解析为给定的哈希值0x00544e304。
2、0x00544e304:函数名CreateThread对应的哈希值。
3、00007FFB3FAFA810:函数CreateThread在ap_hash.exe进程中的虚拟内存地址。

下图说明VA虚拟地址0x00007ffb3fafa810确实指向api_hash.exe进程中的CreateThread函数
在这里插入图片描述
最重要的是,通过IAT已经很难发现CreateThread了
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦里捡到一只猫丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值