动态内存加密解密技术是计算机安全领域中重要的一部分,它保护程序在运行时所使用的内存数据,防止被恶意程序或攻击者窃取敏感信息,也被用于软件的反调试序列。本文将介绍动态内存加密解密技术的实现,包括动态加密技术的作用、简单异或加密内存以及动态 AES 加密解密方法实现内存中变量以及函数的动态加密。
动态加密技术旨在提高程序运行时内存中敏感数据的安全性,防范针对运行中程序的攻击。在传统的程序运行中,内存中的数据通常是明文存储的,这使得攻击者可以通过直接读取内存的方式获取敏感信息。动态加密技术通过在运行时对内存中的数据进行加密,有效地降低了敏感信息泄露的风险。
简单异或加密是一种基本的内存加密方法,它通过对内存中的数据执行异或运算来实现加密和解密。以下是简单异或加密内存的基本步骤:
步骤:
代码示例(使用 C 语言):
#include
#include
// 异或加密函数
void xor_encrypt_decrypt(void* data, size_t data_size, const char* key) {
for (size_t i = 0; i < data_size; ++i) {
((char*)data)[i] ^= key[i % strlen(key)];
}
}
int main() {
char plaintext[14] = "Hello, World!"; // 明文
const char key[14] = "123456789sgfd"; // 密钥
printf("Original: %s\n", plaintext);
// 加密
xor_encrypt_decrypt(plaintext, sizeof(plaintext), key);
printf("Encrypted: %s\n", plaintext);
// 解密
xor_encrypt_decrypt(plaintext, sizeof(plaintext), key);
printf("Decrypted: %s\n", plaintext);
return 0;
}
运行的效果如图:
AES 加密是一种常见的对称加密算法,它使用高级加密标准(AES, Advanced Encryption Standard)算法对内存中的数据进行加密和解密。由于网上有很多对该算法进行讲解的资料,本文就不进行算法本身的讲解了。以下是动态 AES 加密解密内存中变量的基本步骤:
步骤:
代码示例(使用 C 语言和 OpenSSL 库):
#include
#include
#include
#include
int main() {
unsigned char key[23] = "1234567890dfsfsdfsdfs";
unsigned char iv[23] = "123456";
unsigned char iv_copy[23];
unsigned char buf_normal[64] = "mytestword......................hello world.";
unsigned char buf_encrypt[64] = "";
AES_KEY aesKey;
// 加密
// 向量在运算过程中会被改变,为了之后可以正常解密,拷贝一份副本使用
memcpy(iv_copy, iv, 23);
AES_set_encrypt_key(key, 256, &aesKey);
AES_cbc_encrypt(buf_normal, buf_encrypt, sizeof(buf_normal), &aesKey, iv_copy, 1);
// 输出加密后的密码
printf("加密后的密码:\n");
for (size_t i = 0; i < sizeof(buf_encrypt); i++) {
printf("%02x", buf_encrypt[i]);
}
printf("\n");
//解密
memcpy(iv_copy, iv, 23);
AES_set_decrypt_key(key, 256, &aesKey);
AES_cbc_encrypt(buf_encrypt, buf_normal, sizeof(buf_encrypt), &aesKey, iv_copy, 0);
printf("解密后的密码:%s\n", buf_normal);
return 0;
}
执行结果:
对于函数的加密涉及到很多细节方面的问题,比如函数的适用性、代码何时加密以及何时解密等。并且以下因素可能对实现产生影响:
编译器优化、代码生成、硬件处理的差异对内存动态加密技术的使用具有重要影响。
对于指令区间的加密一般涉及到动态计算函数入口地址以及指令序列的可能区间等过程。计算函数地址受不同编译器编译的影响,这需要提前通过逆向工程了解该编译器编译代码的特征、指令的结构和数据存储方式等。对区间的加密不能选择太小的区块,不然加密失去作用;也不能选择远超过被调用函数的栈帧大小的块大小,因为超出限制的加密将导致未定义的行为(例如,加密了即将执行的代码,导致程序发生堆栈异常而崩溃,试图加密不可访问的地址,导致写失败等)。
下文均以 MSVC v143 (VS 2022)编译环境为例进行分析:
首先代码编译的方式有多种,其中可以分为 Release 和 Debug 版本,在两种编译模式下,编译器的处理具有差异。
在调试模式(Debug)下,ObjectFunction 函数的调用首先跳转到内置的跳板函数(Trampoline)地址上,跳板函数里面第一条指令是一个无条件跳转,即 jmp _rel_ObjectFunction,这是为了便于获取附加调试信息。
我们可以在调试器中分析示例代码并验证该结论:
#include
#include
#include
// 示例函数,模拟需要保护的代码块
void ProtectedFunction()
{
std::cout << "This is a protected function." << std::endl;
}
BOOL ThreadDbg()
{
while (true) {
// 模拟等待
Sleep(3000);
// 模拟调用函数
ProtectedFunction();
system("pause");
}
return TRUE;
}
int main()
{
HANDLE hThread_1 = NULL;
DWORD threadID1 = 0;
hThread_1 = CreateThread(NULL,
0, (LPTHREAD_START_ROUTINE)ThreadDbg,
NULL, 0, &threadID1);
while (true) {
// do nothing
Sleep(3000);
}
return 0;
}
首先,在 x64dbg 工具中打开一个以 Debug 模式编译的程序,该程序的特点为在 ThreadDbg 线程执行函数内调用目标函数 ProtectedFunction 。
由于已知代码,我们通过符号表找到 ThreadDbg 函数:
然后向下找我们写的 system 函数:
根据光标显示的指令预览页面,定位到调用 _Trampoline_ObjectFunction 的部分:
可以看到这里的上下文都是无条件跳转:
当执行跳转指令后,我们才进入了真正的 ObjectFunction 函数:
而在 Release 编译模式下,函数调用往往不经过 Trampoline 中转,直接进入真实函数的入口点。我们试着编译运行对应代码的发布版本:
由于编译器在Release 下采取较大的优化模式,不同的优化参数会导致不同的代码结构,可以通过下图所示的项目设置修改编译优化模式:
1)开启O2优化、使用内部函数和全程序优化(Release 默认选项)
这种情况下,会减少函数调用,因为传参和分支过程影响执行效率,ObjectFunction 函数的指令会被合并到 ThreadDbg 里面,并且 &ObjectFunction 获取的是 ObjectFunction 在 ThreadDbg 里面的地址:
&ObjectFunction 计算 ObjectFunction 在 ThreadDbg 中的物理地址:
2)禁用优化、不使用内部函数、不使用程序优化
此时,首先可以看到直接的 call 实际函数,并且解析出了函数参数、返回值和调用约定:
光标查看确实是重定位到了目标函数入口点:
无论是上面哪种优化级别,Release 编译模式都不会生成跳板函数,而 Debug 则会生成跳板函数。
示例函数的代码为:
// 示例函数,模拟需要保护的代码块
void ProtectedFunction()
{
std::cout << "This is a protected function." << std::endl;
}
由于其汇编代码简单,可以直接使用返回时的 ret 指令(机器码是 0xC3)进行终止位置的判断,来计算该函数的结束位置。
下面的函数给出了在一定范围内,根据一个特征字节进行遍历,计算具有跳板模式的函数地址以及块大小的辅助函数:
// 获取函数实际地址(用于调试模式)
unsigned long long DebugGetFunctionActualAddress(
unsigned long long functionAddress,
unsigned char* baseCode,
size_t* trunkSize,
size_t stackSearchSize
)
{
DWORD oldProtect; // 用于记录旧的内存保护属性
// 获得 jmp 指令的相对偏移量(memcpy 注意大小端问题)
unsigned long relativeAddressLittleEndian;
memcpy(&relativeAddressLittleEndian, reinterpret_cast(functionAddress) + 1, sizeof(relativeAddressLittleEndian));
// 计算实际地址
unsigned long long lpProc = functionAddress + relativeAddressLittleEndian + 0x5;
// 计算结束位置
unsigned char refCode = 0;
for (size_t i = 0; i < stackSearchSize; i++)
{
// 修改内存权限以允许读取
VirtualProtect(reinterpret_cast(lpProc + i), sizeof(unsigned char), PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(&refCode, reinterpret_cast(lpProc) + i, sizeof(refCode));
// 比较特征字节
if (refCode == *baseCode)
{
*trunkSize = i + 1; // 修正块大小的数值记录
break;
}
}
// 恢复内存页保护
VirtualProtect(reinterpret_cast(lpProc), sizeof(stackSearchSize), oldProtect, &oldProtect);
return lpProc;
}
其中,baseCode 就是特征字节、stackSearchSize 是对搜索范围的绝对限制,trunkSize 可以初始化一个值,表示对结束位置的最小估计,在计算中,如果找到了该特征值,则修正块大小。
这里内存页保护其实可以不用改。
当编译模式是 release 时,我们不需要越过一个 jmp 指令。此时,函数地址就是实际入口地址,我们只需要根据一定的逻辑计算块大小即可:
// 获取函数区间范围(用于非调试模式)
BOOL RelGetFunctionRange(
unsigned long long functionAddress,
unsigned char* baseCode,
size_t* trunkSize,
size_t stackSearchSize
)
{
// 计算结束位置
unsigned char refCode = 1;
for (size_t i = 0; i < stackSearchSize; i++)
{
// 判断地址是否有效
if (IsBadReadPtr(reinterpret_cast(functionAddress), sizeof(unsigned char)))
{
// 处理非法内存访问
MessageBoxW(NULL, L"非法访问内存!", L"FatalError", MB_OK);
return FALSE; // 或者采取其他适当的措施
}
memcpy(&refCode, (unsigned char*)(functionAddress)+i, sizeof(refCode));
// 比较是否为特征字节
if (refCode == *baseCode)
{
*trunkSize = i + 1; // 修正块大小的数值记录
break;
}
}
return TRUE;
}
随后,我们只需要在调用方函数内这样写即可:
// 获取函数地址和大小
unsigned long long functionAddress = (reinterpret_cast(&ProtectedFunction));
// 最小估计块大小
size_t functionSize = 45;
// 特征字节(这里是 ret 指令)
unsigned char baseCode = 0xc3;
#ifdef _DEBUG /* 调试模式下,需要增加 jmp 指令的越过处理 */
functionAddress = DebugGetFunctionActualAddress(functionAddress, &baseCode, &functionSize, 0x100);
#else
RelGetFunctionRange(functionAddress, &baseCode, &functionSize, 0x100);
#endif
异或运算具有可逆性、反身性,经过同一个密钥异或过的数据,再次异或将得到明文。根据这个特性,我们可以创建一个密钥提供函数,用于动态生成密钥,然后通过计算得到的目标函数地址以及块大小,对代码的执行进行读写,读取原文并回写加密后的密文。下面的代码是一个简单的提供加密服务的函数程式:
// 用于随机生成密钥
unsigned char GenerateRandomKey()
{
return rand() % 256;
}
// 对代码块进行加密或解密
void WriteCodeBlock(unsigned long long address, size_t size, unsigned char key)
{
// 修改内存权限以允许写入
DWORD oldProtect;
VirtualProtect(reinterpret_cast(address), size, PAGE_EXECUTE_READWRITE, &oldProtect);
// 对内存进行异或加密或解密
for (size_t i = 0; i < size; ++i)
{
unsigned char* bytePtr = reinterpret_cast(address) + i;
*bytePtr ^= key;
}
// 恢复内存权限
VirtualProtect(reinterpret_cast(address), size, oldProtect, &oldProtect);
}
过程中需要对内存页保护进行脱保护才能直接对内存进行修改,使用 VirtualProtect 修改当前进程的内存保护属性。
完整示例代码:
#include
#include
#include
// 用于随机生成密钥
unsigned char GenerateRandomKey()
{
return rand() % 256;
}
// 对代码块进行加密或解密
void WriteCodeBlock(unsigned long long address, size_t size, unsigned char key)
{
// 修改内存权限以允许写入
DWORD oldProtect;
VirtualProtect(reinterpret_cast(address), size, PAGE_EXECUTE_READWRITE, &oldProtect);
// 对内存进行异或加密或解密
for (size_t i = 0; i < size; ++i)
{
unsigned char* bytePtr = reinterpret_cast(address) + i;
*bytePtr ^= key;
}
// 恢复内存权限
VirtualProtect(reinterpret_cast(address), size, oldProtect, &oldProtect);
}
// 获取函数实际地址(用于调试模式)
unsigned long long DebugGetFunctionActualAddress(
unsigned long long functionAddress,
unsigned char* baseCode,
size_t* trunkSize,
size_t stackSearchSize
)
{
DWORD oldProtect; // 用于记录旧的内存保护属性
// 获得 jmp 指令的相对偏移量(memcpy 注意大小端问题)
unsigned long relativeAddressLittleEndian;
memcpy(&relativeAddressLittleEndian, reinterpret_cast(functionAddress) + 1, sizeof(relativeAddressLittleEndian));
// 计算实际地址
unsigned long long lpProc = functionAddress + relativeAddressLittleEndian + 0x5;
// 计算结束位置
unsigned char refCode = 0;
for (size_t i = 0; i < stackSearchSize; i++)
{
// 修改内存权限以允许读取
VirtualProtect(reinterpret_cast(lpProc + i), sizeof(unsigned char), PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(&refCode, reinterpret_cast(lpProc) + i, sizeof(refCode));
if (refCode == *baseCode)
{
*trunkSize = i + 1;
break;
}
}
// 恢复内存页保护
VirtualProtect(reinterpret_cast(lpProc), sizeof(stackSearchSize), oldProtect, &oldProtect);
return lpProc;
}
// 获取函数区间范围(用于非调试模式)
BOOL RelGetFunctionRange(
unsigned long long functionAddress,
unsigned char* baseCode,
size_t* trunkSize,
size_t stackSearchSize
)
{
// 计算结束位置
unsigned char refCode = 1;
for (size_t i = 0; i < stackSearchSize; i++)
{
if (IsBadReadPtr(reinterpret_cast(functionAddress), sizeof(unsigned char)))
{
// 处理非法内存访问
MessageBoxW(NULL, L"非法访问内存!", L"FatalError", MB_OK);
return FALSE; // 或者采取其他适当的措施
}
memcpy(&refCode, (unsigned char*)(functionAddress)+i, sizeof(refCode));
// 比较特征字节
if (refCode == *baseCode)
{
*trunkSize = i + 1; // 修正块大小的数值记录
break;
}
}
return TRUE;
}
// 示例函数,模拟需要保护的代码块
void ProtectedFunction()
{
std::cout << "This is a protected function." << std::endl;
}
BOOL ThreadDbg()
{
// 获取函数地址和块大小
unsigned long long functionAddress = (reinterpret_cast(&ProtectedFunction));
size_t functionSize = 45; // 最小估计块大小
unsigned char baseCode = 0xc3;
#ifdef _DEBUG /* 调试模式下,需要增加 jmp 指令的越过处理 */
functionAddress = DebugGetFunctionActualAddress(functionAddress, &baseCode, &functionSize, 0x100);
#else
RelGetFunctionRange(functionAddress, &baseCode, &functionSize, 0x100);
#endif
printf("Address: 0x%I64X; Size: %zd\n", functionAddress, functionSize);
while (true) {
// 生成随机密钥
unsigned char key = GenerateRandomKey();
// 加密代码块
WriteCodeBlock(functionAddress, functionSize, key);
// 模拟等待
Sleep(3000);
// 解密代码块,确保正常执行下一步的代码
WriteCodeBlock(functionAddress, functionSize, key);
// 模拟调用函数
ProtectedFunction();
}
return TRUE;
}
int main()
{
HANDLE hThread_1 = NULL;
DWORD threadID1 = 0;
hThread_1 = CreateThread(NULL,
0, (LPTHREAD_START_ROUTINE)ThreadDbg,
NULL, 0, &threadID1);
while (true) {
// do nothing
Sleep(3000);
}
return 0;
}
AES 算法有很多模式,也有很多库提供了 AES 加密解密算法,比如 OpenSSL、CryptoPP、CNG 等。这里为了理解方便,直接使用微软的 CNG 接口进行动态 AES 加密函数代码段的实现。(实际开发环境应采用自己实现的算法流程完成加密解密过程,使用第三方库容易被绕过)
由于 AES 加密消耗更多的性能和时间,需要考虑分支和循环对加密解密的影响,尤其是处理器的分支预测机制。尤其是,直接对循环中的代码进行加密解密操作,并进行调用的过程,可能存在解密延时导致分支预测失败、段错误或者非法指令的异常。这是为什么呢?
因为对进程内存进程操作,尤其是对指令内存进程动态修改时,由于 Cache 中的指令数据不会立即同步,这可能造成 Cache 里面存在旧的指令数据,CPU 可能执行这些脏数据,在我们本章的情境下,若在线程的 while 循环中执行加密解密,首先通过指令加密,指令一定是不能被正常执行的。在第一轮加密时不会出现问题,而在解密时,涉及到的指令内存地址上的内存覆盖写入,由于复杂系统的目标指令或者相邻的指令用到的 Cache 块内数据没来得及及时更新, while 循环就进行下一条指令了,此时 CPU 是读取的脏数据,导致寻址失败或者非法指令,可能因段错误立即崩溃,也可能在未来出现问题。这是很危险的。
对于我们代码中采用了 memcpy 函数和 BCryptDecrypt 直接对内存地址进行操作时,它们并不具备内部刷新 Cache 的作用,这就会导致 CPU 在未来执行脏数据。微软提供了 API 供程序在修改内存中指令时刷新对应区间的 Cache 数据,这个函数就是 FlushInstructionCache。
这个函数的原型是:
BOOL FlushInstructionCache(
[in] HANDLE hProcess,
[in] LPCVOID lpBaseAddress,
[in] SIZE_T dwSize
);
关于参数和返回值:
[in] hProcess
要刷新其指令缓存的进程句柄。
[in] lpBaseAddress
指向要刷新的区域基的指针。 此参数可以为 NULL。
[in] dwSize
如果 lpBaseAddress 参数不是 NULL(以字节为单位),则要刷新的区域的大小。
返回值
如果该函数成功,则返回值为非零值。
如果函数失败,则返回值为零。 要获得更多的错误信息,请调用 GetLastError。
下面的代码是错误发生的地方,当 while 指令执行后面几轮循环时,程序发生异常。异常发生的时机取决于原加密解密内存块的位置和大小、相近指令的功能等。
while(true) // 不刷新 Cache 引发非法指令以及分段错误
{
BCRYPT_ALG_HANDLE hAesAlg = NULL;
BCRYPT_KEY_HANDLE hKey = NULL;
NTSTATUS status = STATUS_UNSUCCESSFUL;
DWORD cbCipherText = 0,
cbPlainText = 0,
cbData = 0,
cbKeyObject = 0,
cbBlockLen = 0,
cbBlob = 0;
PBYTE pbCipherText = NULL,
pbKeyObject = NULL,
pbBlob = NULL,
pbIV = NULL;
PBYTE rgbIV = new BYTE[CODE_LENGTH];
PBYTE rgbAES128Key = new BYTE[CODE_LENGTH];
unsigned long long functionAddress = NULL;
size_t functionSize = 22;
BYTE* pbPlainText = nullptr;
// 首先生成密钥数据
if (!GenerateKeyData(&rgbAES128Key, &rgbIV))
{
wprintf(L"GenerateKeyData failed\n");
CleanupResources(hAesAlg, hKey, pbCipherText,
pbKeyObject, cbKeyObject,
pbIV, cbBlockLen, pbBlob, cbBlob);
return 0;
}
// 随后初始化算法句柄
if (!InitializeAlgorithmHandle(&hAesAlg, &hKey, &rgbAES128Key, rgbIV,
&pbIV, &pbKeyObject, &pbBlob, &cbData, &cbKeyObject, &cbBlockLen, &cbBlob))
{
wprintf(L"InitializeAlgorithmHandle failed\n");
CleanupResources(hAesAlg, hKey, pbCipherText,
pbKeyObject, cbKeyObject,
pbIV, cbBlockLen, pbBlob, cbBlob);
return 0;
}
引发错误的原因以及修改方案:
// 修改内存权限以允许写入
VirtualProtect(reinterpret_cast(*pbPlainText), dwTrunkSize, PAGE_EXECUTE_READWRITE, &oldProtect);
// 覆盖内存区域
memcpy(*pbPlainText, *pbCipherText, *cbCipherText);
// 这里加上刷新 Cache
FlushInstructionCache(GetCurrentProcess(), *pbPlainText, *cbCipherText);
// 使用 memcpy 更改当前进程的内存指令数据需要刷新 Cache,
// 而使用 WriteProcessMemory 则不需要,它内部会调用刷新函数
// 恢复内存页保护
VirtualProtect(reinterpret_cast(*pbPlainText), dwTrunkSize, PAGE_EXECUTE_READ, &oldProtect);
经过查阅资料,我了解到,WriteProcessMemory 写内存时候内部调用了 NtFlushInstructionCache ,此时就不需要刷新 Cache 了。下面是引用的 WriteProcessMemory 函数的逆向代码:
BOOL
NTAPI
WriteProcessMemory(IN HANDLE hProcess,
IN LPVOID lpBaseAddress,
IN LPCVOID lpBuffer,
IN SIZE_T nSize,
OUT SIZE_T *lpNumberOfBytesWritten)
{
NTSTATUS Status;
ULONG OldValue;
SIZE_T RegionSize;
PVOID Base;
BOOLEAN UnProtect;
/* Set parameters for protect call */
RegionSize = nSize;
Base = lpBaseAddress;
/* Check the current status */
Status = NtProtectVirtualMemory(hProcess,
&Base,
&RegionSize,
PAGE_EXECUTE_READWRITE,
&OldValue);
if (NT_SUCCESS(Status))
{
/* Check if we are unprotecting */
UnProtect = OldValue & (PAGE_READWRITE |
PAGE_WRITECOPY |
PAGE_EXECUTE_READWRITE |
PAGE_EXECUTE_WRITECOPY) ? FALSE : TRUE;
if (UnProtect)
{
/* Set the new protection */
Status = NtProtectVirtualMemory(hProcess,
&Base,
&RegionSize,
OldValue,
&OldValue);
/* Write the memory */
Status = NtWriteVirtualMemory(hProcess,
lpBaseAddress,
(LPVOID)lpBuffer,
nSize,
lpNumberOfBytesWritten);
if (!NT_SUCCESS(Status))
{
/* We failed */
SetLastErrorByStatus(Status);
return FALSE;
}
/* Flush the ITLB */
NtFlushInstructionCache(hProcess, lpBaseAddress, nSize);
return TRUE;
}
else
{
/* Check if we were read only */
if ((OldValue & PAGE_NOACCESS) || (OldValue & PAGE_READONLY))
{
/* Restore protection and fail */
NtProtectVirtualMemory(hProcess,
&Base,
&RegionSize,
OldValue,
&OldValue);
SetLastErrorByStatus(STATUS_ACCESS_VIOLATION);
return FALSE;
}
/* Otherwise, do the write */
Status = NtWriteVirtualMemory(hProcess,
lpBaseAddress,
(LPVOID)lpBuffer,
nSize,
lpNumberOfBytesWritten);
/* And restore the protection */
NtProtectVirtualMemory(hProcess,
&Base,
&RegionSize,
OldValue,
&OldValue);
if (!NT_SUCCESS(Status))
{
/* We failed */
SetLastErrorByStatus(STATUS_ACCESS_VIOLATION);
return FALSE;
}
/* Flush the ITLB */
NtFlushInstructionCache(hProcess, lpBaseAddress, nSize);
return TRUE;
}
}
else
{
/* We failed */
SetLastErrorByStatus(Status);
return FALSE;
}
}
而像 memcpy 设计初衷可能就不是用来修改内存中指令代码的,他不会自我调用 Cache 的更新函数。如果使用 mempy 就需要紧接着刷新 Cache。但显然,我发现了加密解密内存再刷新 Cache 这些操作明显消耗资源影响性能(建议线程执行函数改成 for 循环或者不用循环,减少这种代码的使用)。
所以,对代码段内存的加密涉及到很多方面的考虑,尤其是对调用频繁并且结构复杂的函数区域加密。
注意:以下代码仅支持通用 MSVC Debug 模式、MSVC Release 无优化模式,对于 Release 且开启了优化模式的程序,加密的写入过程会被优化掉。(其他编译器还未测试)
【完整代码】
#include
#include
#include
#include
#pragma comment(lib, "Bcrypt.lib")
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#define STATUS_UNSUCCESSFUL ((NTSTATUS)0xC0000001L)
#define CODE_LENGTH 256 // 加密密钥的长度
// 打印字节数组的辅助函数
void PrintBytes(
IN BYTE* pbPrintData,
IN DWORD cbDataLen)
{
DWORD dwCount = 0;
for (dwCount = 0; dwCount < cbDataLen; dwCount++)
{
printf("0x%02x, ", pbPrintData[dwCount]);
// 每10个字节换行
if (0 == (dwCount + 1) % 10) putchar('\n');
}
}
// 获取函数实际地址(用于调试模式)
unsigned long long DebugGetFunctionActualAddress(
unsigned long long functionAddress,
unsigned char* baseCode,
size_t* trunkSize,
size_t stackSearchSize
)
{
// 获得 jmp 指令的相对偏移量(大端模式)
unsigned long relativeAddressLittleEndian;
memcpy(&relativeAddressLittleEndian,
reinterpret_cast(functionAddress) + 1,
sizeof(relativeAddressLittleEndian));
// 计算实际地址
unsigned long long lpProc = functionAddress + relativeAddressLittleEndian + 0x05;
// 计算结束位置
unsigned char refCode = 1;
for (size_t i = 0; i < stackSearchSize; i++)
{
if (IsBadReadPtr(reinterpret_cast(lpProc), sizeof(unsigned char)))
{
// 处理非法内存访问
MessageBoxW(NULL, L"非法访问内存!", L"FatalError", MB_OK);
return 0; // 或者采取其他适当的措施
}
memcpy(&refCode, (unsigned char*)(lpProc) + i, sizeof(refCode));
if (refCode == *baseCode)
{
*trunkSize = i + 1;
break;
}
}
return lpProc;
}
// 获取函数区间范围(用于非调试模式)
BOOL RelGetFunctionRange(
unsigned long long functionAddress,
unsigned char* baseCode,
size_t* trunkSize,
size_t stackSearchSize
)
{
// 计算结束位置
unsigned char refCode = 1;
for (size_t i = 0; i < stackSearchSize; i++)
{
if (IsBadReadPtr(reinterpret_cast(functionAddress), sizeof(unsigned char)))
{
// 处理非法内存访问
MessageBoxW(NULL, L"非法访问内存!", L"FatalError", MB_OK);
return FALSE; // 或者采取其他适当的措施
}
memcpy(&refCode, (unsigned char*)(functionAddress) + i, sizeof(refCode));
if (refCode == *baseCode)
{
*trunkSize = i + 1;
break;
}
}
return TRUE;
}
// 生成AES密钥和IV
BOOL GenerateKeyData(BYTE** rgbAES128Key, BYTE** rgbIV)
{
HCRYPTPROV hCryptProv = NULL;
if (!CryptAcquireContextW(&hCryptProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) {
wprintf(L"**** Error acquiring cryptographic context\n");
goto Cleanup;
}
// 生成随机密钥
if (!CryptGenRandom(hCryptProv, CODE_LENGTH, *rgbAES128Key)) {
wprintf(L"**** Error generating random key\n");
goto Cleanup;
}
// 生成随机 IV
if (!CryptGenRandom(hCryptProv, CODE_LENGTH, *rgbIV)) {
wprintf(L"**** Error generating random IV\n");
goto Cleanup;
}
if (hCryptProv) {
CryptReleaseContext(hCryptProv, 0);
}
return TRUE;
Cleanup:
if (hCryptProv) {
CryptReleaseContext(hCryptProv, 0);
}
return FALSE;
}
// 初始化加密算法句柄
BOOL InitializeAlgorithmHandle(
BCRYPT_ALG_HANDLE *hAesAlg,
BCRYPT_KEY_HANDLE *hKey,
BYTE** rgbAES128Key,
BYTE* rgbIV,
BYTE** pbIV,
BYTE** pbKeyObject,
BYTE** pbBlob,
DWORD* cbData,
DWORD* cbKeyObject,
DWORD* cbBlockLen,
DWORD* cbBlob
)
{
NTSTATUS status = STATUS_UNSUCCESSFUL;
// 打开算法句柄
if (!NT_SUCCESS(status = BCryptOpenAlgorithmProvider(
hAesAlg,
BCRYPT_AES_ALGORITHM,
NULL,
0)))
{
wprintf(L"**** Error 0x%x returned by BCryptOpenAlgorithmProvider\n", status);
return FALSE;
}
// 计算保存KeyObject所需的缓冲区大小
if (!NT_SUCCESS(status = BCryptGetProperty(
*hAesAlg,
BCRYPT_OBJECT_LENGTH,
(PBYTE)cbKeyObject,
sizeof(DWORD),
cbData,
0)))
{
wprintf(L"**** Error 0x%x returned by BCryptGetProperty\n", status);
return FALSE;
}
// 在堆上分配KeyObject
*pbKeyObject = (PBYTE)HeapAlloc(GetProcessHeap(), 0, *cbKeyObject);
if (NULL == *pbKeyObject)
{
wprintf(L"**** memory allocation failed\n");
return FALSE;
}
// 计算IV的块长度
if (!NT_SUCCESS(status = BCryptGetProperty(
*hAesAlg,
BCRYPT_BLOCK_LENGTH,
(PBYTE)cbBlockLen,
sizeof(DWORD),
cbData,
0)))
{
wprintf(L"**** Error 0x%x returned by BCryptGetProperty\n", status);
return FALSE;
}
// 判断cbBlockLen是否不超过IV长度
if (*cbBlockLen > sizeof(BYTE) * CODE_LENGTH)
{
wprintf(L"**** block length is longer than the provided IV length\n");
return FALSE;
}
// 为IV分配缓冲区,在加密/解密过程中会使用该缓冲区
*pbIV = (PBYTE)HeapAlloc(GetProcessHeap(), 0, *cbBlockLen);
if (NULL == *pbIV)
{
wprintf(L"**** memory allocation failed\n");
return FALSE;
}
memcpy(*pbIV, rgbIV, *cbBlockLen);
// 设置 CNG 对象的命名属性的值,设置加密算法的链式模式。
if (!NT_SUCCESS(status = BCryptSetProperty(
*hAesAlg,
BCRYPT_CHAINING_MODE,
(PBYTE)BCRYPT_CHAIN_MODE_CBC, // 将算法的链模式设置为 加密块链接。
sizeof(BCRYPT_CHAIN_MODE_CBC),
0)))
{
wprintf(L"**** Error 0x%x returned by BCryptSetProperty\n", status);
return FALSE;
}
// 创建一个密钥对象,用于提供的密钥中的对称密钥加密算法
if (!NT_SUCCESS(status = BCryptGenerateSymmetricKey(
*hAesAlg,
hKey,
*pbKeyObject,
*cbKeyObject,
(PBYTE)rgbAES128Key,
sizeof(BYTE) * CODE_LENGTH,
0)))
{
wprintf(L"**** Error 0x%x returned by BCryptGenerateSymmetricKey\n", status);
return FALSE;
}
// 为未来恢复准备一份密钥的拷贝
if (!NT_SUCCESS(status = BCryptExportKey(
*hKey,
NULL,
BCRYPT_OPAQUE_KEY_BLOB,
NULL,
0,
cbBlob,
0)))
{
wprintf(L"**** Error 0x%x returned by BCryptExportKey\n", status);
return FALSE;
}
// 在堆上分配密钥 Blob
*pbBlob = (PBYTE)HeapAlloc(GetProcessHeap(), 0, *cbBlob);
if (NULL == *pbBlob)
{
wprintf(L"**** memory allocation failed\n");
return FALSE;
}
// 将密钥导出到内存 BLOB,该密钥可以保留供以后使用。
if (!NT_SUCCESS(status = BCryptExportKey(
*hKey,
NULL,
BCRYPT_OPAQUE_KEY_BLOB,
*pbBlob,
*cbBlob,
cbBlob,
0)))
{
wprintf(L"**** Error 0x%x returned by BCryptExportKey\n", status);
return FALSE;
}
return TRUE;
}
// 加密函数数据
BOOL EncryptFunctionData(
BCRYPT_KEY_HANDLE hKey,
PBYTE pbIV,
PBYTE* pbCipherText,
PBYTE* pbPlainText,
DWORD cbData,
//DWORD cbKeyObject,
DWORD cbBlockLen,
PDWORD cbCipherText,
PDWORD cbPlainText,
DWORD dwTrunkSize
)
{
NTSTATUS status = STATUS_UNSUCCESSFUL;
DWORD oldProtect = 0;
//
// 获取加密需要的缓冲区长度
//
if (!NT_SUCCESS(status = BCryptEncrypt(
hKey,
*pbPlainText,
*cbPlainText,
NULL,
pbIV,
cbBlockLen,
NULL,
0,
cbCipherText,
BCRYPT_BLOCK_PADDING)))
{
wprintf(L"**** Error 0x%x returned by BCryptEncrypt\n", status);
return FALSE;
}
*pbCipherText = (PBYTE)HeapAlloc(GetProcessHeap(), 0, *cbCipherText);
if (NULL == *pbCipherText)
{
wprintf(L"**** memory allocation failed\n");
return FALSE;
}
// 使用密钥加密明文缓冲区。
// 对于块大小的消息,块填充将添加一个额外的块。
if (!NT_SUCCESS(status = BCryptEncrypt(
hKey,
*pbPlainText,
*cbPlainText,
NULL,
pbIV,
cbBlockLen,
*pbCipherText,
*cbCipherText,
&cbData,
BCRYPT_BLOCK_PADDING)))
{
wprintf(L"**** Error 0x%x returned by BCryptEncrypt\n", status);
return FALSE;
}
// 修改内存权限以允许写入
VirtualProtect(reinterpret_cast(*pbPlainText), dwTrunkSize, PAGE_EXECUTE_READWRITE, &oldProtect);
// 覆盖内存区域
//memcpy(*pbPlainText, *pbCipherText, *cbCipherText);
//FlushInstructionCache(GetCurrentProcess(), *pbPlainText, *cbCipherText);
// 使用 memcpy 更改当前进程的内存指令数据需要刷新 Cache,
// 而使用 WriteProcessMemory 则不需要,它内部会调用刷新函数
SIZE_T dwNumberOfBytesWritten = 0;
WriteProcessMemory(GetCurrentProcess(), *pbPlainText, *pbCipherText, *cbCipherText, &dwNumberOfBytesWritten);
// 恢复内存页保护
VirtualProtect(reinterpret_cast(*pbPlainText), dwTrunkSize, PAGE_EXECUTE_READ, &oldProtect);
// 销毁密钥并从保存的 BLOB 中重新导入。
if (!NT_SUCCESS(status = BCryptDestroyKey(hKey)))
{
wprintf(L"**** Error 0x%x returned by BCryptDestroyKey\n", status);
return FALSE;
}
hKey = 0;
return TRUE;
}
// 解密函数数据
BOOL DecryptFunctionData(
BCRYPT_ALG_HANDLE* hAesAlg,
BCRYPT_KEY_HANDLE* hKey,
BYTE* rgbIV,
PBYTE* pbIV,
DWORD cbBlockLen,
PBYTE* pbBlob,
DWORD cbBlob,
PBYTE* pbCipherText,
PDWORD cbCipherText,
PBYTE* pbKeyObject,
DWORD cbKeyObject,
PBYTE* pbPlainText,
PDWORD cbPlainText,
DWORD dwTrunkSize
)
{
NTSTATUS status = STATUS_UNSUCCESSFUL;
DWORD oldProtect = 0;
// 重新初始化 Key 和 IV 数据
//
// 我们可以重用关键对象。
memset(*pbKeyObject, 0, cbKeyObject);
// 重新初始化IV,因为加密会修改它。
memcpy(*pbIV, rgbIV, cbBlockLen);
if (!NT_SUCCESS(status = BCryptImportKey(
*hAesAlg,
NULL,
BCRYPT_OPAQUE_KEY_BLOB,
hKey,
*pbKeyObject,
cbKeyObject,
*pbBlob,
cbBlob,
0)))
{
wprintf(L"**** Error 0x%x returned by BCryptGenerateSymmetricKey\n", status);
return FALSE;
}
//
// 获取解密需要的缓冲区大小
//
if (!NT_SUCCESS(status = BCryptDecrypt(
*hKey,
*pbCipherText,
*cbCipherText,
NULL,
*pbIV,
cbBlockLen,
NULL,
0,
cbPlainText,
BCRYPT_BLOCK_PADDING)))
{
wprintf(L"**** Error 0x%x returned by BCryptDecrypt\n", status);
return FALSE;
}
// 修改内存权限以允许写入
VirtualProtect(reinterpret_cast(*pbPlainText), dwTrunkSize, PAGE_EXECUTE_READWRITE, &oldProtect);
// 解密数据
if (!NT_SUCCESS(status = BCryptDecrypt(
*hKey,
*pbCipherText,
*cbCipherText,
NULL,
*pbIV,
cbBlockLen,
*pbPlainText,
*cbPlainText,
cbPlainText,
BCRYPT_BLOCK_PADDING)))
{
wprintf(L"**** Error 0x%x returned by BCryptDecrypt\n", status);
// 恢复内存页保护
VirtualProtect(reinterpret_cast(*pbPlainText), dwTrunkSize, PAGE_EXECUTE_READ, &oldProtect);
return FALSE;
}
// 刷新 cache
FlushInstructionCache(GetCurrentProcess(), *pbPlainText, *cbCipherText);
// 恢复内存页保护
VirtualProtect(reinterpret_cast(*pbPlainText), dwTrunkSize, PAGE_EXECUTE_READ, &oldProtect);
return TRUE;
}
// 示例函数,模拟函数内调用
int addarg(int a, int b, int c)
{
return a > b ? a : c;
}
// 示例函数,模拟需要保护的代码块
void ProtectedFunction()
{
std::cout << "This is a protected function." << std::endl;
int a = 1;
int b = 5;
int c = a + b;
int d = addarg(a, b, c);
printf("addrag = %d\n", d);
}
// 函数指针类型,用于指向加密前或加密后的函数
typedef void (*FunctionPointer)();
// 函数执行器,用于执行函数并捕获异常
bool ExecuteFunctionSafely(FunctionPointer function)
{
__try
{
function(); // 尝试执行函数
return true; // 执行成功
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return false; // 捕获异常,执行失败
}
}
// 清理资源辅助函数
void CleanupResources(
BCRYPT_ALG_HANDLE hAesAlg,
BCRYPT_KEY_HANDLE hKey,
PBYTE pbCipherText,
PBYTE pbKeyObject,
DWORD cbKeyObject,
PBYTE pbIV,
DWORD cbBlockLen,
PBYTE pbBlob,
DWORD cbBlob
)
{
if (hKey) // 释放密钥句柄
{
BCryptDestroyKey(hKey);
}
if (hAesAlg) // 释放算法句柄
{
BCryptCloseAlgorithmProvider(hAesAlg, 0);
}
// 释放内存
//
//
if (pbCipherText)
{
HeapFree(GetProcessHeap(), 0, pbCipherText);
}
if (pbKeyObject)
{
SecureZeroMemory(pbKeyObject, cbKeyObject); // 释放之前,安全地将密钥对象归零
HeapFree(GetProcessHeap(), 0, pbKeyObject);
}
if (pbIV)
{
SecureZeroMemory(pbIV, cbBlockLen); // 释放之前,安全地将 IV 对象归零
HeapFree(GetProcessHeap(), 0, pbIV);
}
if (pbBlob)
{
SecureZeroMemory(pbBlob, cbBlob); // 释放之前,安全地将 Blob 对象归零
HeapFree(GetProcessHeap(), 0, pbBlob);
}
}
// 线程执行函数
BOOL ThreadDbg()
{
for(int i = 0;; i++) // while 循环可能产生分支预测异常问题,且性能不如 for
//while(true)
{
BCRYPT_ALG_HANDLE hAesAlg = NULL;
BCRYPT_KEY_HANDLE hKey = NULL;
NTSTATUS status = STATUS_UNSUCCESSFUL;
DWORD cbCipherText = 0,
cbPlainText = 0,
cbData = 0,
cbKeyObject = 0,
cbBlockLen = 0,
cbBlob = 0;
PBYTE pbCipherText = NULL,
pbKeyObject = NULL,
pbBlob = NULL,
pbIV = NULL;
PBYTE rgbIV = new BYTE[CODE_LENGTH];
PBYTE rgbAES128Key = new BYTE[CODE_LENGTH];
unsigned long long functionAddress = NULL;
size_t functionSize = 22;
BYTE* pbPlainText = nullptr;
// 首先生成密钥数据
if (!GenerateKeyData(&rgbAES128Key, &rgbIV))
{
wprintf(L"GenerateKeyData failed\n");
CleanupResources(hAesAlg, hKey, pbCipherText,
pbKeyObject, cbKeyObject,
pbIV, cbBlockLen, pbBlob, cbBlob);
return 0;
}
/*
* // 不输出密钥
wprintf(L"Random Key:\n");
PrintBytes(rgbAES128Key, sizeof(BYTE) * CODE_LENGTH);
wprintf(L"\n");
wprintf(L"Random IV:\n");
PrintBytes(rgbIV, sizeof(BYTE) * CODE_LENGTH);
wprintf(L"\n");
*/
// 随后初始化算法句柄
if (!InitializeAlgorithmHandle(&hAesAlg, &hKey, &rgbAES128Key, rgbIV,
&pbIV, &pbKeyObject, &pbBlob, &cbData, &cbKeyObject, &cbBlockLen, &cbBlob))
{
wprintf(L"InitializeAlgorithmHandle failed\n");
CleanupResources(hAesAlg, hKey, pbCipherText,
pbKeyObject, cbKeyObject,
pbIV, cbBlockLen, pbBlob, cbBlob);
return 0;
}
// 获取需要加密的函数地址和函数区块大小
functionAddress = (reinterpret_cast(&ProtectedFunction));
unsigned char baseCode = 0xc3;
#ifdef _DEBUG /* 调试模式下,需要增加 jmp 指令的越过处理 */
functionAddress = DebugGetFunctionActualAddress(functionAddress, &baseCode, &functionSize, 0x100);
#else
RelGetFunctionRange(functionAddress, &baseCode, &functionSize, 0x100);
#endif
printf("Address: 0x%I64X; Size: %zd\n", functionAddress, functionSize);
// 数据类型转换
pbPlainText = (BYTE*)functionAddress;
cbPlainText = (DWORD)functionSize;
wprintf(L"Original Plaintext:\n");
PrintBytes(pbPlainText, cbPlainText);
wprintf(L"\n");
// 调用加密函数对目标地址区间进行加密
if (!EncryptFunctionData(hKey, pbIV, &pbCipherText, &pbPlainText, cbData, cbBlockLen, &cbCipherText, &cbPlainText, cbPlainText)) {
wprintf(L"EncryptFunctionData failed\n");
CleanupResources(hAesAlg, hKey, pbCipherText,
pbKeyObject, cbKeyObject,
pbIV, cbBlockLen, pbBlob, cbBlob);
return 0;
}
wprintf(L"Ciphertext:\n");
PrintBytes(pbPlainText, cbPlainText);
wprintf(L"\n");
Sleep(3000); // 模拟休眠函数
// 调用解密函数对目标地址区间进行数据解密
if (!DecryptFunctionData(&hAesAlg, &hKey, rgbIV, &pbIV, cbBlockLen, &pbBlob, cbBlob,
&pbCipherText, &cbCipherText, &pbKeyObject, cbKeyObject, &pbPlainText, &cbPlainText, cbPlainText)) {
wprintf(L"DecryptFunctionData failed\n");
CleanupResources(hAesAlg, hKey, pbCipherText,
pbKeyObject, cbKeyObject,
pbIV, cbBlockLen, pbBlob, cbBlob);
return 0;
}
wprintf(L"Decrypted Plaintext:\n");
PrintBytes(pbPlainText, cbPlainText);
wprintf(L"\n");
// 测试解密后的函数可执行性
if (ExecuteFunctionSafely(&ProtectedFunction))
{
std::cout << "Function executed successfully." << std::endl;
}
else
{
std::cerr << "Access violation detected in the function." << std::endl;
}
}
// 程序的返回
return 0;
}
// 主函数
int __cdecl wmain(
int argc,
__in_ecount(argc) LPWSTR* wargv)
{
UNREFERENCED_PARAMETER(argc);
UNREFERENCED_PARAMETER(wargv);
// 创建线程处理
HANDLE hThread_1 = NULL;
DWORD threadID1 = 0;
hThread_1 = CreateThread(NULL,
0, (LPTHREAD_START_ROUTINE)ThreadDbg,
NULL, 0, &threadID1);
while (true)
{
Sleep(3000); // do nothing in main
}
return 0;
}
运行结果:
混淆调试器的效果(解析为错误指令):
测试视频(本人 B 站视频,欢迎关注呀):
【反调试】动态 AES 内存加密技术
更新于:2023.12.27