在PE文件中,存在iat导入表,记录了PE文件使用的API以及相关的dll模块。编译一个MessageBox文件,查看其导入表:
int main(){
printf("hello worldn");
MessageBox(0, TEXT("hello world"), 0, 0);
return 0;
}
可以看到使用了MessageBoxW这个api。
可以通过自定义API的方式隐藏导入表中的恶意API,自定义api试一下。
#include<stdio.h>
#include<Windows.h>
typedef int(WINAPI* pMessageBox) (
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
);
int main()
{
printf("hello worldn");
pMessageBox MyMessageBox = (pMessageBox)GetProcAddress(LoadLibrary(L"User32
.dll"), "MessageBoxW");MyMessageBox(0, TEXT("hello world"), 0, 0);
return 0;
}
这里就隐藏了api,但是使用这种方式,不可避免的是去使用windows中的loadlibary与GetProcAddress两大api。而这两个函数的组合使用已经被标记烂了。
于是存在了网上分享过的,使用PEB模块去获取GetProcAddress函数。流程如下:
1. 找到kernel32.dll的地址
2. 遍历啊kernel32.dll的导入表,找到GetProcAddress的地址
3. 使用GetProcAddress获取LoadLibrary函数的地址
4. 然后使用 LoadLibrary加载DLL文件
5. 使用 GetProcAddress查找某个函数的地址
如下函数,返回kernel32.dll地址,使用汇编,返回kernel32的基址。对此汇编代码感兴趣的师傅,可以在文章末链接查看。
DWORD GetKernel32Address() {
DWORD dwKernel32Addr = 0;
_asm {
mov eax, fs: [0x30]
mov eax, [eax + 0x0c]
mov eax, [eax + 0x14]
mov eax, [eax]
mov eax, [eax]
mov eax, [eax + 0x10]
mov dwKernel32Addr, eax
}
return dwKernel32Addr;
}
获取GetProcAddress的地址,如下函数。
DWORD RGetProcAddress() {
//获取kernel32的地址
DWORD dwAddrBase = GetKernel32Address();
//获取Dos头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)dwAddrBase;
//获取Nt头
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + dwAddrBase);
//数据目录表 扩展头 数据目录表 + 导出表 定位导出表
PIMAGE_DATA_DIRECTORY pDataDir = pNt->OptionalHeader.DataDirectory +
IMAGE_DIRECTORY_ENTRY_EXPORT;//导出表
//导出表地址
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(dwAddrBase +
pDataDir->VirtualAddress);//函数总数
DWORD dwFunCount = pExport->NumberOfFunctions;
//函数名称数量
DWORD dwFunNameCount = pExport->NumberOfNames;
//函数地址
PDWORD pAddrOfFun = (PDWORD)(pExport->AddressOfFunctions +dwAddrBase);
//函数名称地址
PDWORD pAddrOfNames = (PDWORD)(pExport->AddressOfNames + dwAddrBase);
//序号表
PWORD pAddrOfOrdinals = (PWORD)(pExport->AddressOfNameOrdinals + dwAddrBase);
for (size_t i = 0; i < dwFunCount; i++) {
if (!pAddrOfFun[i]) {
continue;
}
DWORD dwFunAddrOffset = pAddrOfFun[i];
for (size_t j = 0; j < dwFunNameCount; j++) {
if (pAddrOfOrdinals[j] == i) {
DWORD dwNameOffset = pAddrOfNames[j];
char * pFunName = (char *)(dwAddrBase + dwNameOffset);
if (strcmp(pFunName, "GetProcAddress") == 0) {
return dwFunAddrOffset + dwAddrBase;
}
}
}
}
}
使用汇编语言实现了获取kernerl32.dll里获取到了GetProcAddress函数的地址,然后再用自定义函数将参数装进去,实现调用。从而实现了获取其他函数的地址。
typedef HMODULE(WINAPI* pGetModuleHandle)(
_In_ LPCSTR lpLibFileName
);
pGetModuleHandle MyGetModuleHandle = (pGetModuleHandle)MyGetProcAddress(hKernel3
2, "GetModuleHandle");
查看一下函数,在kernel32中已经查不到,结合shellcode加密360也简单绕过。
但是visual studio的X64的架构的语言不支持内联汇编。X64可以通过单独写一个汇编语言来调用,但是我不会编写汇编代码 。还有别的方式来调用loadlibray与GetProcAddress函数呢?
我换了一种思维来实现了隐藏。首先来看一下调用GetProcAddress的时发生了什么。很简单传入了kernel32.dll的地址然后进行对它导出函数的调用。
那么这里编译一个dll进行一个测试,实现了一个导出函数为cior的编写,输出内容为!!!!!!!!。
extern "C" __declspec(dllexport) void cior(const char* c1)
{
printf("!!!!!!");
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
printf("999");
}
DLL准备就绪,关于怎么加载进去DLL。Windows提供了一个函数叫做loadlibray。很多师傅都听过DLL内存反射,DLL映射等等。实际上无论Dll怎么变,Dll的本质其实就是一个PE文件。回归到本质,它准确的名称应该叫做PE映射。那么什么是PE文件呢?如下就是一个PE文件使用16进制打开的DOS头。
重点关注的是下面这张图。这张图就是PE装载在内存的大概过程,也可以叫做PE拉伸。实现过程的话懂的师傅都懂。
编写一个自定义的loadlibary函数。
LPVOID loadlibary111(LPBYTE lpData)
{
ImportHeapCreate MyHeapCreate = (ImportHeapCreate)GetMemProcAddress(GetMemModuleHandleA("kernel32.dll"), "HeapCreate");
ImportHeapAlloc MyHeapAlloc = (ImportHeapAlloc)GetMemProcAddress(GetMemModuleHandleA("kernel32.dll"), "HeapAlloc");
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpData;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)pDosHeader + pDosHeader->e_lfanew);
DWORD dwSizeOfImage = pNtHeader->OptionalHeader.SizeOfImage;
HANDLE hc = MyHeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
LPVOID lpBaseAddress = MyHeapAlloc(hc, 0, dwSizeOfImage);
RtlZeroMemory(lpBaseAddress, dwSizeOfImage);
DWORD dwSizeOfHeaders = pNtHeader->OptionalHeader.SizeOfHeaders;
WORD wNumberOfSections = pNtHeader->FileHeader.NumberOfSections;
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);
RtlCopyMemory(lpBaseAddress, lpData, dwSizeOfHeaders);
for (WORD i = 0; i < wNumberOfSections; i++)
{
if (0 == pSectionHeader->VirtualAddress || 0 == pSectionHeader->SizeOfRawData)
{
pSectionHeader++;
continue;
}
LPVOID lpSrcMem = (LPVOID)((DWORD_PTR)lpData + pSectionHeader->PointerToRawData);
LPVOID lpDestMem = (LPVOID)((DWORD_PTR)lpBaseAddress + pSectionHeader->VirtualAddress);
DWORD dwSizeOfRawData = pSectionHeader->SizeOfRawData;
RtlCopyMemory(lpDestMem, lpSrcMem, dwSizeOfRawData);
pSectionHeader++;
}
PIMAGE_DOS_HEADER pDosHeader1 = (PIMAGE_DOS_HEADER)lpBaseAddress;
PIMAGE_NT_HEADERS pNtHeader1 = (PIMAGE_NT_HEADERS)((LPBYTE)lpBaseAddress + pDosHeader1->e_lfanew);
PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)(pNtHeader1->OptionalHeader.DataDirectory[5].VirtualAddress + (DWORD_PTR)pDosHeader1);
while (pReloc->VirtualAddress != 0 && pReloc->SizeOfBlock != 0)
{
int nNumberOfReloc = (pReloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
for (int i = 0; i < nNumberOfReloc; i++)
{
WORD* pRelocData = (WORD*)((DWORD_PTR)pReloc + sizeof(IMAGE_BASE_RELOCATION));
if ((WORD)(pRelocData[i] & 0xF000) == 0x3000)
{
DWORD_PTR pAddress = (DWORD_PTR)pDosHeader1 + pReloc->VirtualAddress + (pRelocData[i] & 0x0FFF);
pAddress += (DWORD_PTR)pDosHeader1 - pNtHeader1->OptionalHeader.ImageBase;
}
}
pReloc = (PIMAGE_BASE_RELOCATION)((DWORD_PTR)pReloc + pReloc->SizeOfBlock);
}
PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD_PTR)pDosHeader1 + pNtHeader1->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
char* pDllName = nullptr;
PIMAGE_THUNK_DATA pIat = NULL;
PVOID pFuncAddress = NULL;
PIMAGE_IMPORT_BY_NAME pImportByName = NULL;
while (pImport->Name)
{
pDllName = (char*)((DWORD_PTR)pDosHeader1 + pImport->Name);
PVOID hDll = GetMemModuleHandleA(pDllName);
if (NULL == hDll)
{
hDll = LoadLibraryA(pDllName);
if (NULL == hDll)
{
pImport++;
continue;
}
}
pIat = (PIMAGE_THUNK_DATA)((DWORD_PTR)pDosHeader1 + pImport->FirstThunk);
while (pIat->u1.Ordinal)
{
if (pIat->u1.Ordinal & 0x80000000)
{
pFuncAddress = GetMemProcAddress(hDll, (LPCSTR)(pIat->u1.Ordinal & 0x7FFFFFFF));
}
else
{
pImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)pDosHeader1 + pIat->u1.AddressOfData);
pFuncAddress = GetMemProcAddress(hDll, pImportByName->Name);
}
pIat->u1.Function = (DWORD_PTR)pFuncAddress;
pIat++;
}
pImport++;
}
return lpBaseAddress;
}
那么再结合到自定义写的GetMemProcAddress实际使用,看看效果。
到这里完成了自定义的loadlibray与GetProcAddress的实现。那么实现这样一个技术能够完成什么呢?在编写免杀马的时候不可避免会用到一些ring3的api函数。而杀软又是在ring0层hook的。常规BypassAV的方式,无非是选择冷门api或者自定义实现的函数。自定义函数能够更好隐藏api的调用。这里我们可以将shellcodeload放在导出函数,然后用回调函数去调用。简单完成免杀。
除了常规的应用方式,我恰巧碰见了,计划任务过不了360核晶。核心代码如下:
int main()
{
.......
hello = CoCreateInstance(CLSID_TaskScheduler, //com
NULL,
CLSCTX_INPROC_SERVER,
IID_ITaskService,
(void**)&pService);
.......
hello = pRootFolder->RegisterTaskDefinition( //注册创建计划任务
_bstr_t(wszTaskName.c_str()),
pTask,
TASK_CREATE_OR_UPDATE,
_variant_t("SYSTEM"),
_variant_t(),
TASK_LOGON_PASSWORD,
_variant_t(L""),
&pRegisteredTask);
}
这个是过不了核晶的,查杀提示为DCOMCallback。
本意尝试用loadlibary去加载dll然后用GetProcAddress去获取RegisterTaskDefinition的地址。结果是失败并报错。经测试,这个计划任务是通过实例com对象去调用的。Com的DLL与一般的DLL并不相同。
可以发现最终调用的是COM方法,而不是导出函数。
关于COM的hook也与常规的hook并不相同,国内并没有太多关于COM的文章。恰巧看到了一篇17年的文章。感兴趣的师傅可以看文章末链接。这里详细介绍了关于com hook的可行性。准确的叫做com仿真。于是尝试对com仿真进行测试。
于是我仿真了bits实现功能的调用。核心代码:
STDMETHODIMP CFakeBitsManager::CreateJob(LPCWSTR DisplayName, BG_JOB_TYPE Type, GUID* pJobId, IBackgroundCopyJob** ppJob)
{
printf("123");
return pUnk->CreateJob(DisplayName, Type, pJobId, ppJob);
}
调用代码:
IBackgroundCopyManager* pManager=NULL;
HRESULT hr = CoInitialize(NULL);
hr = CoCreateInstance(__uuidof(FakeBitsManager), NULL, CLSCTX_INPROC_SERVER, __uuidof(IFakeBitsManager), (void**)&pManager);
if (SUCCEEDED(hr))
{
printf("888");
}
GUID guidJob;
IBackgroundCopyJob* pBackgroundCopyJob;
hr = pManager->GetJob2(L"T123",BG_JOB_TYPE_DOWNLOAD,&guidJob, (IBackgroundCopyJob**) &pBackgroundCopyJob);
if (SUCCEEDED(hr))
{
printf("999");
}
这里完成了bits 的仿真。于是我尝试着去仿真TaskScheduler。
HRESULT hr = CoInitialize(NULL);
LPCWSTR wszTaskName = L"Test";
wstring wstrExecutablePath = L"C://Windows//System32//cmd.exe";
ITaskService* pService = NULL;
hr = CoCreateInstance(CLSID_TaskScheduler, NULL, CLSCTX_INPROC_SERVER, IID_ITaskService, (void**)&pService);
hr = pService->Connect(_variant_t(), _variant_t(), _variant_t(), _variant_t());
ITaskFolder* pRootFolder = NULL;
hr = pService->GetFolder(_bstr_t(L"\"), &pRootFolder);
这里我开始尝试去实列化我写的仿真TaskScheduler,最终结果却失败。
技术的运用是灵活的,在实现自定义loadlibray将dll加载进内存后,可以免杀一大部分杀软。但对于内存空间的查杀还需要更高更复杂的技巧,最后附上一张内存攻防思路图。
https://bbs.kanxue.com/thread-266678.htm
https://bbs.kanxue.com/thread-121488.htm
https://www.anquanke.com/post/id/86821
原文始发于微信公众号(千寻安服):杂谈免杀
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论