被VMP HOOK的ZwProtectVirtualMemory介绍
ZwProtectVirtualMemory,是一个修改内存输入的API函数,VirtualProtect和VirtualProtectEx修改内存属性都会通过ZwProtectVirtualMemory这个API函数.
在VMP壳的程序中,注入DLL进行一定的内存修改,但会发现,对WINAPI或者程序的代码段等进行WriteProcessMemory操作,会发现修改错误,错误提示是没有修改权限.
而WriteProcessMemory在调用更底层的ZwWriteVirtualMemory前会先调用ZwProtectVirtualMemory修改原有程序的内存属性.
但VMP HOOK了ZwProtectVirtualMemory,就导致了无法随意修改内存属性
778D0068 E9 A2FFA088 jmp 002E000F ; 被HOOK的ZwProtectVirtualMemory
778D006D 33C9 xor ecx,ecx
778D006F 8D5424 04 lea edx,dword ptr ss:[esp+0x4]
778D0073 64:FF15 C000000>call dword ptr fs:[0xC0]
778D007A 83C4 04 add esp,0x4
778D007D C2 1400 retn 0x14
778D0068 B8 4D000000 mov eax,0x4D ; 没被HOOK前的ZwProtectVirtualMemory
778D006D 33C9 xor ecx,ecx
778D006F 8D5424 04 lea edx,dword ptr ss:[esp+0x4]
778D0073 64:FF15 C000000>call dword ptr fs:[0xC0]
778D007A 83C4 04 add esp,0x4
778D007D C2 1400 retn 0x14
从上面的代码可以看出,被修改的5字节是SSDT操作码,我们需要精准的知道原操作码是多少,但在WIN7下ZwProtectVirtualMemory的操作码是0x4D,而WIN10以及XP,WIN8又是不同的操作码,怎么样可以准确的获取到当前系统的操作码,并且在不修改HOOK的前提下调用ZwProtectVirtualMemory修改内存属性就是本贴的主题.
思路
首先想获取操作码有很多方法.
1.写驱动在驱动层获取SSDT操作码.
首先驱动就不写了.......复杂...而且x64驱动没签名...放弃....
2.读取其他进程的前5字节.
从其他进程读前5字节是可以考虑的,但电脑安装了某些安全软件,他们的防护逻辑是全局DLL注入HOOK....所以你读出来的也可能是被HOOK后的opcode........这个方法也不考虑....放弃
3.从ntdll.dll文件内获取操作码.
这个方法相对而言是最好的...因为你电脑的ntdll.dll被修改的机率还是非常非常低的(反正我没见过),就这个方法了吧,盘他.
详解
1.获取文件数据
首先把ntdll.dll读入到内存中,然后才可以进行下一步操作,直接贴代码好了,没什么难度
HANDLE hFile = CreateFile(_T("C:\\Windows\\System32\\ntdll.dll"), GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
MessageBox(0, _T("CreateFileError"), _T("error"), MB_OK);
return;
}
SIZE_T ntdllAddr = (SIZE_T)LoadLibrary(_T("ntdll.dll"));
SIZE_T apiAddr = (SIZE_T)GetProcAddress((HMODULE)ntdllAddr, "ZwProtectVirtualMemory");
if (apiAddr == NULL || ntdllAddr == NULL)
{
MessageBox(0, _T("LoadLibrary|GetProcAddress,Error"), _T("error"), MB_OK);
CloseHandle(hFile);
return;
}
//把Ntdll读入进内存
DWORD dwHighSize = 0;
DWORD dwLowSize = GetFileSize(hFile, &dwHighSize);
BYTE* pBuff = new BYTE[dwLowSize + dwHighSize]();
DWORD dwFileSize = 0;
ReadFile(hFile, pBuff, dwLowSize + dwHighSize, &dwFileSize, NULL);
2.获取ZwProtectVirtualMemory在文件中的位置
ZwProtectVirtualMemory地址 - ntdll地址 = RVA
先通过RVA找到所在区段,再用RVA - 当前区段RVA + 当前区段文件偏移 = F0A
最后用F0A找到相对的opcode
//寻找所在区段
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBuff;
PIMAGE_NT_HEADERS32 pNtHeader = (PIMAGE_NT_HEADERS32)(pDosHeader->e_lfanew + ((SIZE_T)pDosHeader));
PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)(((SIZE_T)(pNtHeader)) + sizeof(IMAGE_NT_HEADERS32));
SIZE_T Deviation = 0;
//ZwProtectVirtualMemory 地址 - ntdll地址 = RVA
SIZE_T Rva = apiAddr - ntdllAddr;
// 先通过RVA找到所在区段,再用RVA-当前区段RVA+当前区段文件偏移 = F0A
// 最后用F0A找到相对的opcode,复制到新内存执行再跳转
for (WORD i = 0; i < pNtHeader->FileHeader.NumberOfSections; ++i)
{
if (Rva > pSectionHeader[i].VirtualAddress && Rva < pSectionHeader[i].VirtualAddress + pSectionHeader[i].SizeOfRawData)
{//通过RVA找到所在区段,再用RVA-当前区段RVA+文件偏移 = F0A
Deviation = Rva - pSectionHeader[i].VirtualAddress + pSectionHeader[i].PointerToRawData;
break;
}
}
if (Deviation == 0)
{
MessageBoxA(0, "计算段内偏移错误", "error", MB_OK);
delete[] pBuff;
CloseHandle(hFile);
return;
}
3.绕过HOOK
得到函数在文件内的位置后,可以获取原opcode进行调用,然后再跳转回没被HOOK的位置,还是来个图片好解释
...应该
// 最后用段内偏移找到相对的opcode
g_pAddr = (BYTE*)VirtualAlloc(0, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);//申请一段堆空间执行被修改的汇编代码
if (g_pAddr == NULL)
{
MessageBoxA(0, "VirtualAlloc Error", "error", MB_OK);
delete[] pBuff;
CloseHandle(hFile);
return;
}
memcpy(g_pAddr, pBuff + Deviation, 5);//x32下VMP修改了前5字节,把文件内的原5字节拷贝过去
g_pAddr[5] = 0x68;//push
apiAddr += 5;
memcpy(g_pAddr + 6, &apiAddr, 4);//push的地址是ZwProtectVirtualMemory+5的地方
g_pAddr[10] = 0xc3;//使用retn过去
delete[] pBuff;
CloseHandle(hFile);
代码
上面的代码都是一个函数被拆分上传了.x64的也有,和x32不同的地方就只有x64不能直接push太长的地址,所以用了下面的方式来跳转
mov rax ,地址 48 b8 00 00 00 00 00 00 00 00 len == 9
push rax 50 len == 10
mov r10,rcx
mov eax,操作码
retn
下面是x32和x64完整的代码
#define DefineFuncPtr(name,base) decltype(name) *My_##name = (decltype(name)*)base
#include "windows.h"
#include "tchar.h"
BYTE* g_pAddr = NULL;//ZwProtectVirtualMemory的调用函数
BYTE* g_pHookAddr = NULL;
typedef SIZE_T(CALLBACK* PZwProtectVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
SIZE_T* NumberOfBytesToProtect,
ULONG NewAccessProtection,
PULONG OldAccessProtection);
PZwProtectVirtualMemory pZwProtectVirtualMemory;
void Init64()
{
if (g_pAddr != NULL)
{
return;
}
HANDLE hFile = CreateFile(_T("C:\\Windows\\System32\\ntdll.dll"), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
MessageBox(0, _T("CreateFileError"), _T("error"), MB_OK);
return;
}
SIZE_T ntdllAddr = (SIZE_T)LoadLibrary(_T("ntdll.dll"));
SIZE_T apiAddr = (SIZE_T)GetProcAddress((HMODULE)ntdllAddr, "ZwProtectVirtualMemory");
if (apiAddr == NULL || ntdllAddr == NULL)
{
MessageBox(0, _T("LoadLibrary|GetProcAddress,Error"), _T("error"), MB_OK);
CloseHandle(hFile);
return;
}
//把Ntdll读入进内存
DWORD dwHighSize = 0;
DWORD dwLowSize = GetFileSize(hFile, &dwHighSize);
BYTE* pBuff = new BYTE[dwLowSize + dwHighSize]();
DWORD dwFileSize = 0;
ReadFile(hFile, pBuff, dwLowSize + dwHighSize, &dwFileSize, NULL);
//寻找所在区段
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBuff;
PIMAGE_NT_HEADERS64 pNtHeader = (PIMAGE_NT_HEADERS64)(pDosHeader->e_lfanew + ((SIZE_T)pDosHeader));
PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)(((SIZE_T)(pNtHeader)) + sizeof(IMAGE_NT_HEADERS64));
SIZE_T Deviation = 0;
//ZwProtectVirtualMemory 地址 - ntdll地址 = RVA
SIZE_T Rva = apiAddr - ntdllAddr;
// 先通过RVA找到所在区段,再用RVA-当前区段RVA+当前区段文件偏移 = F0A
// 最后用F0A找到相对的opcode,复制到新内存执行再跳转
for (WORD i = 0; i < pNtHeader->FileHeader.NumberOfSections; ++i)
{
if (Rva > pSectionHeader[i].VirtualAddress && Rva < pSectionHeader[i].VirtualAddress + pSectionHeader[i].SizeOfRawData)
{//通过RVA找到所在区段,再用RVA-当前区段RVA+文件偏移 = F0A
Deviation = Rva - pSectionHeader[i].VirtualAddress + pSectionHeader[i].PointerToRawData;
break;
}
}
if (Deviation == 0)
{
MessageBoxA(0, "计算段内偏移错误", "error", MB_OK);
delete[] pBuff;
CloseHandle(hFile);
return;
}
// 最后用段内偏移找到相对的opcode
g_pAddr = (BYTE*)VirtualAlloc(0, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);//申请一段堆空间执行被修改的汇编代码
if (g_pAddr == NULL)
{
MessageBoxA(0, "VirtualAlloc Error", "error", MB_OK);
delete[] pBuff;
CloseHandle(hFile);
return;
}
/*
mov rax ,地址 48 b8 00 00 00 00 00 00 00 00 len == 9
push rax 50 len == 10
mov r10,rcx
mov eax,操作码
retn
*/
g_pAddr[0] = 0x48;
g_pAddr[1] = 0xb8;
apiAddr += 8;
memcpy(g_pAddr + 2, &apiAddr, 8);//地址是ZwProtectVirtualMemory+7
g_pAddr[10] = 0x50;//push rax
memcpy(g_pAddr + 11, pBuff + Deviation, 8);//x64下VMP修改了前8字节,把文件内的原8字节拷贝过去
g_pAddr[19] = 0xc3;//retn过去
delete[] pBuff;
CloseHandle(hFile);
}
void Init32()
{
if (g_pAddr != NULL)
{
return;
}
HANDLE hFile = CreateFile(_T("C:\\Windows\\System32\\ntdll.dll"), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
MessageBox(0, _T("CreateFileError"), _T("error"), MB_OK);
return;
}
SIZE_T ntdllAddr = (SIZE_T)LoadLibrary(_T("ntdll.dll"));
SIZE_T apiAddr = (SIZE_T)GetProcAddress((HMODULE)ntdllAddr, "ZwProtectVirtualMemory");
if (apiAddr == NULL || ntdllAddr == NULL)
{
MessageBox(0, _T("LoadLibrary|GetProcAddress,Error"), _T("error"), MB_OK);
CloseHandle(hFile);
return;
}
//把Ntdll读入进内存
DWORD dwHighSize = 0;
DWORD dwLowSize = GetFileSize(hFile, &dwHighSize);
BYTE* pBuff = new BYTE[dwLowSize + dwHighSize]();
DWORD dwFileSize = 0;
ReadFile(hFile, pBuff, dwLowSize + dwHighSize, &dwFileSize, NULL);
//寻找所在区段
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBuff;
PIMAGE_NT_HEADERS32 pNtHeader = (PIMAGE_NT_HEADERS32)(pDosHeader->e_lfanew + ((SIZE_T)pDosHeader));
PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)(((SIZE_T)(pNtHeader)) + sizeof(IMAGE_NT_HEADERS32));
SIZE_T Deviation = 0;
//ZwProtectVirtualMemory 地址 - ntdll地址 = RVA
SIZE_T Rva = apiAddr - ntdllAddr;
// 先通过RVA找到所在区段,再用RVA-当前区段RVA+当前区段文件偏移 = F0A
// 最后用F0A找到相对的opcode,复制到新内存执行再跳转
for (WORD i = 0; i < pNtHeader->FileHeader.NumberOfSections; ++i)
{
if (Rva > pSectionHeader[i].VirtualAddress && Rva < pSectionHeader[i].VirtualAddress + pSectionHeader[i].SizeOfRawData)
{//通过RVA找到所在区段,再用RVA-当前区段RVA+文件偏移 = F0A
Deviation = Rva - pSectionHeader[i].VirtualAddress + pSectionHeader[i].PointerToRawData;
break;
}
}
if (Deviation == 0)
{
MessageBoxA(0, "计算段内偏移错误", "error", MB_OK);
delete[] pBuff;
CloseHandle(hFile);
return;
}
// 最后用段内偏移找到相对的opcode
g_pAddr = (BYTE*)VirtualAlloc(0, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);//申请一段堆空间执行被修改的汇编代码
if (g_pAddr == NULL)
{
MessageBoxA(0, "VirtualAlloc Error", "error", MB_OK);
delete[] pBuff;
CloseHandle(hFile);
return;
}
memcpy(g_pAddr, pBuff + Deviation, 5);//x32下VMP修改了前5字节,把文件内的原5字节拷贝过去
g_pAddr[5] = 0x68;//push
apiAddr += 5;
memcpy(g_pAddr + 6, &apiAddr, 4);//push的地址是ZwProtectVirtualMemory+5的地方
g_pAddr[10] = 0xc3;//使用retn过去
delete[] pBuff;
CloseHandle(hFile);
}
SIZE_T MyZwProtectVirtualMemory(
HANDLE ProcessHandle, //进程句柄
PVOID* BaseAddress,//地址的指针
SIZE_T* NumberOfBytesToProtect,//修改的大小,函数调用后会改成成功修改的大小
ULONG NewAccessProtection,//新的内存属性
PULONG OldAccessProtection)//旧的内存属性
{
#ifdef _WIN64
Init64();
#else
Init32();
#endif
pZwProtectVirtualMemory = (PZwProtectVirtualMemory)g_pAddr;
return pZwProtectVirtualMemory(ProcessHandle, BaseAddress, NumberOfBytesToProtect, NewAccessProtection, OldAccessProtection);
}
验证下效果
1.程序介绍
一个加了VMP 3.2壳的易语言程序.
已知程序是获取CPU序号的程序,并且这个程序只调用了CPUID这个汇编代码获取了CPU序号,无法对相关的API进行HOOK,而且是个加了VMP壳的程序,只能让其代码段解密后修改,但想精准的修改还需在代码没运行前进行拦截,这里可以通过DLL进行API HOOK判断代码段是否解码,然后再HOOK拦截修改EAX或者EDX
2.寻找代码段解码后一个调用的函数
先看连接器信息,10.00,可能是一个vs2010连接器生成的EXE,vs2010连接器生成的代码在运行main函数之前第一个调用的API是GetSystemTimeAsFileTime
通过测试发现,GetSystemTimeAsFileTime 这个API第一次调用的时候,0x0040112B已经解密完成,并且对其下断能正常断下,可以使用这个API进行代码段是否解码完成.
3.HOOK GetSystemTimeAsFileTime
这里就没什么好说的,相信大家都对HOOK很熟悉了,调用上面我写好的MyZwProtectVirtualMemory函数来修改内存属性.
HOOK GetSystemTimeAsFileTime-> 判断代码是否解码->修改内存属性,然后HOOK 0040112B->稍微修改下CPUID调用后的EAX
case DLL_PROCESS_ATTACH:
{
// HOOK GetSystemTimeAsFileTime来判断代码段是否解密
//保存GetSystemTimeAsFileTime前5字节,并且push retn跳到GetSystemTimeAsFileTime + 5 位置
g_pHookAddr = (BYTE*)VirtualAlloc(0, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);//GetSystemTimeAsFileTime调用接口
PVOID addr = (PVOID)GetProcAddress(LoadLibraryA("KernelBase.dll"), "GetSystemTimeAsFileTime");
memcpy(g_pHookAddr, addr, 5);//拷贝原5字节
g_pHookAddr[5] = 0x68;//psuh GetSystemTimeAsFileTime + 5
SIZE_T TempAddr = ((SIZE_T)addr) + 5;//GetSystemTimeAsFileTime + 5
memcpy(g_pHookAddr + 6, &TempAddr, 4);
g_pHookAddr[10] = 0xc3;//retn
SIZE_T size = 5;//e9 00 00 00 00 size
ULONG OldProtect;
MyZwProtectVirtualMemory((HANDLE)-1, &addr, &size, PAGE_EXECUTE_READWRITE, &OldProtect);//修改内存属性
addr = (PVOID)GetProcAddress(LoadLibraryA("KernelBase.dll"), "GetSystemTimeAsFileTime");
BYTE opcode[8] = {};
opcode[0] = 0xe9;//jmp
SIZE_T MyApiAddr = (((SIZE_T)MyGetSystemTimeAsFileTime) - ((SIZE_T)addr) - 5);
memcpy(opcode + 1, &MyApiAddr, 4);
memcpy(addr, opcode, 5);//HOOK GetSystemTimeAsFileTime这个函数,并且JMP到MyGetSystemTimeAsFileTime
MyZwProtectVirtualMemory((HANDLE)-1, &addr, &size, OldProtect, &OldProtect);//恢复内存属性
break;
}
判断代码是否解码
VOID WINAPI MyGetSystemTimeAsFileTime(
_Out_ LPFILETIME lpSystemTimeAsFileTime)
{
WORD opcode;
memcpy(&opcode, (PVOID)(((SIZE_T)GetModuleHandleA(nullptr)) + 0x112B), 2);
if (opcode == 0xA20F)//EXEtest.vmp.exe + 0x112B
{
PVOID addr = (PVOID)(((SIZE_T)GetModuleHandleA(nullptr)) + 0x112B);
SIZE_T size = 5;
SIZE_T OldProtect;
MyZwProtectVirtualMemory((HANDLE)-1, &addr, &size, PAGE_EXECUTE_READWRITE, &OldProtect);//修改属性
addr = (PVOID)(((SIZE_T)GetModuleHandleA(nullptr)) + 0x112B);
memcpy(addr, new BYTE[5]{ 0xE9, 0x54 , 0x9E , 0x09 , 0x00 }, 5);
size = 5;
MyZwProtectVirtualMemory((HANDLE)-1, &addr, &size, OldProtect, &OldProtect);//恢复属性
addr = (PVOID)(((SIZE_T)GetModuleHandleA(nullptr)) + 0x9AF84);
size = 15;
MyZwProtectVirtualMemory((HANDLE)-1, &addr, &size, PAGE_EXECUTE_READWRITE, &OldProtect);//修改属性
addr = (PVOID)(((SIZE_T)GetModuleHandleA(nullptr)) + 0x9AF84);
size = 15;
memcpy(addr, new BYTE[15]{ 0x0F ,0xA2 ,0x89 ,0x55 ,0xFC ,0xB8 ,0x66 ,0x66 ,0x66 ,0x66 ,0xE9 ,0x9D ,0x61 ,0xF6 ,0xFF }, 15);
MyZwProtectVirtualMemory((HANDLE)-1, &addr, &size, OldProtect, &OldProtect);//恢复属性
}
DefineFuncPtr(GetSystemTimeAsFileTime, g_pHookAddr);
return My_GetSystemTimeAsFileTime(lpSystemTimeAsFileTime);
}
HOOK后对0x0040112B的处理
0040112B E9 549E0900 jmp EXEtest_.0049AF84
0049AF84 0FA2 cpuid
0049AF86 8955 FC mov dword ptr ss:[ebp-0x4],edx
0049AF89 B8 66666666 mov eax,0x66666666
0049AF8E E9 9D61F6FF jmp EXEtest_.00401130
完整的DLL代码在附件内,同测试程序同时上传吧.
WIN10测试图片: