VC黑防日记:DLL隐藏和逆向(续)
2020-02-27 18:58:00 Author: mp.weixin.qq.com(查看原文) 阅读量:62 收藏

本文为看雪论坛优秀文章
看雪论坛作者ID:小迪xiaodi 
【实验平台】:Win7 x64
【开发平台】:Win10 x64 + VS2017
【调试工具】:Ollydbg
经过几天的努力,我的毕业论文终于写的差不多了,老师说:
OK,那咱们继续回论坛吹水~
前段日子呢,写了个《VC黑防日记(一):DLL隐藏和逆向(上)》的水文,也得到论坛的大佬对我“能力”的认可:
那我们今天,继续来混一下字数,谈谈上次遗留的的问题——干掉吾爱破解OD这类工具对HOOK FreeLibrary方式隐藏DLL的检测。
怎么干呢,很简单,就是让它没了。
注入进去DLL之后,拷贝DLL镜像到一块内存地址,然后卸载DLL后,再把内存镜像还原回原地址。
这样子,只是简单的内存数据复制,而DLL的确被卸载了,拷贝后的DLL内存数据,只要计算下导出函数地址便可以调用,相当于远程调Call。
在此,也要感谢上次各位老哥推荐的灵活的方法思路,本文章并未进行复现,有机会可以一块儿写出来,在此表示感谢!

0x01 回顾
我们上次是通过分析FreeLibrary函数的流程得知大概分为四个步骤:
1. 判断DLL句柄是否有效,有效就说明该DLL存在于进程中
2. 递减模块的引用计数,且判断是否为0
3. 调用模块的DllMain函数响应 DLL_PROCESS_DETACH消息
4. 从进程空间撤销对DLL的内存映射
然后我们通过对第四个步骤的分析,采用HOOK  FreeLibrary 函数中的 ZwUnmapViewOfSection函数,实现了擦除DLL痕迹隐藏DLL的功能,但是出现了一丢丢的问题,那就是被某些“强大”的工具检测并枚举了出来,那么为什么会这样呢?今天我们就来分析分析~
0x02 分析
既然我们上次HOOK  ZwUnmapViewOfSection出现了小问题,能不能对 ZwUnmapViewOfSection做一些小手脚呢,暂时我的知识储备还不够,处理不了,但是我选择了其他的绕过方式,在讲这个方式之前,我们先了解一部分逆向的理论知识。
虚拟地址描述符:
大家都知道,在x86的平台上Windows操作系统为每个进程描述了一个完整的4G的地址空间,这4G空间由低位2G的用户地址空间和高位2G的系统地址空间构成。每个进程的用户地址空间是相互隔离的,不可见的。但是系统地址空间是各个进程间共享的,对于进程有不同的视图。用户的私有的数据代码还有加载的动态链接库(DLL)都存放在用户地址空间中。现在有个问题是,总要有个地方记录着这2G地址空间,到底那些被预留了,那些被提交了,那些被访问了吧。还有个问题就是,对于程序来讲,地址不是连续的,是分段的。代码段、数据段、堆、栈等等。可是进程的空间是连续的,从0x00000000x7FFFFFF。总要有个数据结构描述程序的各个段对应那段地址空间。这两个重要任务就交个了VAD,即虚拟地址描述符(Virtual Address Descriptor)。 VAD组织成了一个AVL自平衡二叉树(参考Mark Russinovich的《深入解析Windows操作系统》),这种组织方式完全是方便快速查找。树中的每一个节点代表了一段虚拟地址空间。所以程序的代码段,数据段,堆段都会各种占用一个或多个VAD节点,由一个MMVAD结构完整描述。内核空间并不受VAD的管理。 https://baike.baidu.com/item/%E8%99%9A%E6%8B%9F%E5%9C%B0%E5%9D%80%E6%8F%8F%E8%BF%B0%E7%AC%A6/295257?fr=aladdin

分析一下:
1.有个玩意儿叫做“虚拟地址描述符”,存放了DLL的内存空间使用情况数据结构。
2.如果正常调用 ZwUnmapViewOfSection,内存地址所对应的“VAD”就会从树中摘除。
3.我们上次hook了 ZwUnmapViewOfSection,刚好使得被注入的DLL的 “内存空间数据结构”无法从整体的“二叉树”结构中消除。
结论:
1. 如果不HOOK ZwUnmapViewOfSection ,就假戏真做,DLL就真的被卸载了。
2. 如果HOOK了  ZwUnmapViewOfSection,就很难受,虚拟地址描述符清除不掉,还是过不了吾爱破解OD的检测。
3. 我们需要找一个方法在假戏真做的同时,保存DLL的内存镜像。

0x03 解决方案—内存拷贝法
由上一节的分析我们得知,如果我们假戏真做,DLL会被完整的卸载掉,那么我们如果在卸载之前先保存自身DLL内存镜像到另外的位置,然后真正的卸载掉,最后再把内存镜像拷贝回去就可以了,拷贝的时候拷贝回原地址是最好的方法,因此,我们开始解决问题吧!
1. 拷贝DLL内存镜像需要得知内存镜像的大小,根据PE文件结构操作,解:
      char szExePath[MAX_PATH] = "C:\\Users\\86186\\Desktop\\mydll.dll";     HANDLE hFile = CreateFile(szExePath, GENERIC_ALL, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, NULL, NULL);     //获得PE文件句柄         HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);     //创建一个新的文件映射内核对象          PVOID pbFile = MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0);    //将一个文件映射对象映射到内存,得到指向映射到内存的第一个字节的指针pbFile      if (INVALID_HANDLE_VALUE == hFile || NULL == hMapping || NULL == pbFile)    {        printf("\n\t---------- The File Inexistence! ----------\n");        if (NULL != pbFile)        {            UnmapViewOfFile(pbFile);        }         if (NULL != hMapping)        {            CloseHandle(hMapping);        }         if (INVALID_HANDLE_VALUE != hFile)        {            CloseHandle(hFile);        }         return 0;    }     //pDosHeader指向DOS头起始位置    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pbFile;    printf("PE Header e_lfanew:0x%x\n", pDosHeader->e_lfanew);     //计算PE头位置    PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pbFile + pDosHeader->e_lfanew);     //计算DLL模块镜像大小    DWORD dwSizeOfImage = (DWORD)pNTHeader->OptionalHeader.SizeOfImage;    printf("SizeOfImage: 0x%08X\n", dwSizeOfImage);

得到DLL模块镜像大小 == 我们要复制写的内存的大小。
2. 分配内存地址方便写入
使用VirtualAllocEx函数: https://docs.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex
VirtualAllocEx(hProcess, NULL, dwSizeOfImage, MEM_COMMIT, PAGE_READWRITE);

然后修改内存属性为可读可写:
BOOL VirtualProtectEx(  HANDLE hProcess,  LPVOID lpAddress,  SIZE_T dwSize,  DWORD  flNewProtect,  PDWORD lpflOldProtect);

3. 内存拷贝DLL数据
//保存数组BYTE code[] = { 0 };DWORD lp_copy = (DWORD)lpaddress;DWORD lp_start = addr_start; for (int i = 0; i < dwSizeOfImage; i++, lp_start++, lp_copy++) {    ReadProcessMemory(hProcess, (LPCVOID)lp_start, code, 1, NULL);    WriteProcessMemory(hProcess, (LPVOID)lp_copy, code, 1, NULL);}printf("原地址 = 0x%x 拷贝地址 = 0x%x\n", addr_start, lpaddress);MessageBox(NULL, "拷贝完成!", "Cap", MB_OK);

4. 卸载DLL
卸载DLL后就可以真正的无影无踪了:
UnInject(Pid, DLL路径);

5. 这样,DLL就真的没了,被拷贝在别的位置,只要知道起始地址,那么就可以正常的调用DLL的导出函数了。
本次实验的代码如下:
int main(){    const char* a = "C:\\Users\\86186\\Desktop\\mydll.dll";     HANDLE hToken = NULL;    //打开当前进程的访问令牌    int hRet = OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken);     if (hRet)    {        TOKEN_PRIVILEGES tp;        tp.PrivilegeCount = 1;        //取得描述权限的LUID        LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);        tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;        //调整访问令牌的权限        AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);         CloseHandle(hToken);    }      char szExePath[MAX_PATH] = "C:\\Users\\86186\\Desktop\\mydll.dll";     HANDLE hFile = CreateFile(szExePath, GENERIC_ALL, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, NULL, NULL);     //获得PE文件句柄         HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);     //创建一个新的文件映射内核对象          PVOID pbFile = MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0);    //将一个文件映射对象映射到内存,得到指向映射到内存的第一个字节的指针pbFile      if (INVALID_HANDLE_VALUE == hFile || NULL == hMapping || NULL == pbFile)    {        printf("\n\t---------- The File Inexistence! ----------\n");        if (NULL != pbFile)        {            UnmapViewOfFile(pbFile);        }         if (NULL != hMapping)        {            CloseHandle(hMapping);        }         if (INVALID_HANDLE_VALUE != hFile)        {            CloseHandle(hFile);        }         return 0;    }       //pDosHeader指向DOS头起始位置    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pbFile;    printf("PE Header e_lfanew:0x%x\n", pDosHeader->e_lfanew);     //计算PE头位置    PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pbFile + pDosHeader->e_lfanew);     //计算DLL模块镜像大小    DWORD dwSizeOfImage = (DWORD)pNTHeader->OptionalHeader.SizeOfImage;    printf("SizeOfImage: 0x%08X\n", dwSizeOfImage);     UnmapViewOfFile(pbFile);    CloseHandle(hMapping);    CloseHandle(hFile);     Inject(GetProcessIDByName("代码注入器.exe"), (char*)a);     MessageBox(NULL, "注入完成!", "Cap", MB_OK);     int Pid = 0;    while (1)    {        Pid = GetProcessIDByName("代码注入器.exe");        if (Pid > 0)        {            MessageBox(NULL, "检测到进程,点击确定开始申请内存!", "Cap", MB_OK);            break;        }    }     //申请内存    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);    DWORD dwOldProtect;    LPVOID lpaddress = VirtualAllocEx(hProcess, NULL, dwSizeOfImage, MEM_COMMIT, PAGE_READWRITE);    printf("分配地址:0x%x\n",lpaddress);    VirtualProtectEx(hProcess, lpaddress, dwSizeOfImage + 1, PAGE_EXECUTE_READWRITE, &dwOldProtect);     DWORD addr_start = 0;    while (1)    {        addr_start = GetProcessModuleHandleByName(Pid, "mydll.dll");        if (addr_start > 0)        {            MessageBox(NULL, "检测到DLL被注入了,点击确定开始拷贝DLL!", "Cap", MB_OK);            break;        }      }     //保存数组    BYTE code[] = { 0 };    DWORD lp_copy = (DWORD)lpaddress;    DWORD lp_start = addr_start;     for (int i = 0; i < dwSizeOfImage; i++, lp_start++, lp_copy++) {        ReadProcessMemory(hProcess, (LPCVOID)lp_start, code, 1, NULL);        WriteProcessMemory(hProcess, (LPVOID)lp_copy, code, 1, NULL);    }    printf("原地址 = 0x%x 拷贝地址 = 0x%x\n", addr_start, lpaddress);    MessageBox(NULL, "拷贝完成!", "Cap", MB_OK);     //真正去卸载    UnInject(GetProcessIDByName("代码注入器.exe"), (char*)a);      printf("原地址 = 0x%x \n", addr_start);    //还原DLL镜像至原地址    DWORD VirtualAddress = lpaddress;     VirtualFreeEx(hProcess, addr_start, dwSizeOfImage + 1, MEM_RELEASE);     DWORD returnValue = VirtualAllocEx(hProcess, addr_start, dwSizeOfImage + 1, MEM_COMMIT, PAGE_READWRITE);    int error1 = GetLastError();    printf("the value = %d  error = %d \n", returnValue, error1);    MessageBox(NULL, "原内存地址恢复完成!", "Cap", MB_OK);     VirtualProtectEx(hProcess, addr_start, dwSizeOfImage + 1, PAGE_EXECUTE_READWRITE, &dwOldProtect);    for (int i = 0; i < dwSizeOfImage; i++, addr_start++, VirtualAddress++) {        ReadProcessMemory(hProcess, (LPCVOID)VirtualAddress, code, 1, NULL);        WriteProcessMemory(hProcess, (LPVOID)addr_start, code, 1, NULL);    }     MessageBox(NULL, "还原完成!DLL隐藏完成!", "Cap", MB_OK);     getchar();    return 0;}

0x04 未解决不完美之处
本来打算把DLL的内存镜像复制回卸载DLL之前DLL在程序中的内存地址的,无奈分配地址会出错,暂时还没找到解决的方法。
如果能够复制回原来的地址就比较完美了。
    DWORD returnValue = VirtualAllocEx(hProcess, addr_start, dwSizeOfImage + 1, MEM_COMMIT, PAGE_READWRITE);    int error1 = GetLastError();    printf("the value = %d  error = %d \n", returnValue, error1);    MessageBox(NULL, "原内存地址恢复完成!", "Cap", MB_OK);

通过GetLastError得到结果:

发现错误487来源于:

暂时还没解“初始化的安全区”导致的分配原地址失败的问题,如果有大佬会的话希望大佬能够评论一下~
结语
把一些新奇的想法用代码去做实验实现还是比较有意思的,技术有些粗陋,重要的是享受过程(其实就是我菜)
- End -

看雪ID:小迪xiaodi

https://bbs.pediy.com/user-680946.htm 

*本文由看雪论坛 小迪xiaodi 原创,转载请注明来自看雪社区。

推荐文章++++

攻防世界fakebook关卡攻略

Win32 Shellcode编写

**游戏逆向分析笔记

对宝马车载apps协议的逆向分析研究

x86_64架构下的函数调用及栈帧原理

好书推荐


公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]
“阅读原文”一起来充电吧!

文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&amp;mid=2458303714&amp;idx=1&amp;sn=46d322f9dd280629ba73d05073fb791e&amp;chksm=b1818c6886f6057e4e512fbc413e2e6af6c81b9e70c0f2e6082610a8e061950cb05c9d751bef#rd
如有侵权请联系:admin#unsafe.sh