1、做外挂,本质上就改变目标软件的执行流程。为了达到目的,肯定要给目标软件额外添加代码才能达到自己的目的。3环下为了添加代码实现自己的逻辑,要么写shellcode,要么加载dll;两种方式对比如下:
相比之下,dll的优点非常明显,尤其是第2点,可以用C实现复杂的逻辑,这能极大加快外挂、补丁等的开发;缺点只有1条,本文就介绍一个隐藏dll的办法!
2、windwos作为一个复杂的操作系统,需要运营和管理的模块太多了,大家最熟悉的莫过于进程了。 打开任务管理器,就能查到所有的进程;相应的,通过OD、x32dbg这些调试器打开进程,也能查到进程所有的模块,比如dll、exe等。任务管理器、调试器是怎么知道这些模块详细信息的了?
个人观点:进程本质上是个资源分配的最小单位。用户双击exe的时候,windwos会在内存分配空间,然后把exe从磁盘加载到内存执行。为了提高效率,windwos是“同时”运行多个进程的。为了管理这些进程所使用的资源,windows使用了一个叫做PEB的结构体! 进程并不真正执行exe的代码,线程才是。1个进程至少有1个主线程。进程和线程是1对多的关系。为了管理这些线程,windwos又使用了一个TEB的结构体。关于PEB和TEB的介绍,网上已经烂大街了,建议各位小伙伴google一下,有很多比较成熟的讲解(这里推荐一下滴水逆向海哥的windows逆向课程,讲解很详细,B站的链接下面有)。
每个进程都有PEB,每个线程也都有TEB,操作系统该怎么管理这么多的结构体了? 比如用户点击磁盘上的exe,此时需要分配物理内存存放exe的数据和代码;用户退出程序后,操作系统需要回收这个程序的内存并释放,相应的各种元数据(内存地址、内存大小、读写属性、栈、模块地址、模块大小、各种sector大小基址等等)也都要释放,操作系统怎么精准找到这些需要释放的PEB和TEB的位置了(一旦找错,把还在运行进程的PEB、TEB释放掉,岂不是错杀了)?
微软的研发人员在开发操作系统时,是不可能预测到用户到底会启动多少进程的,所以这么多PEB和TEB的实例是不可能用数组来存储和检索的。由于PEB之间、TEB之间、PEB内部的模块之间都是平行关系,不是上下游或父子关系,所以也不会用树形结构来存储,现在只剩链表了;限于篇幅,本文介绍PEB内部模块之间存储的数据结构——链表,如下:
每个模块都有自己的属性,比较重要的有模块名称、内存基址、大小、磁盘上的路径、入口点等,这些数据都存放在结构体内。结构体之间通过链表首尾相接,操作系统就可以通过遍历链表查到每个模块的属性。当exe执行loadlibrary时,需要从磁盘加载dll,这时新生成这样的一个结构体,然后加入链表(一般是尾部);当exe执行freelibrary时,操作系统释放dll占据的内存,同时也释放这个结构体,然后从现有的这个链表摘除(业界俗称“断链”),这样下次遍历这个链表时就找不到了。根据这个原理,目前网上99%的dll隐藏教程都是根据fullDllName找到目标dll,然后把三个关键字段(如上)的上下游链表分别连接,自己从链表中断开,代码如下:
#include "stdafx.h" #include "windows.h" typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; }UNICODE_STRING, *PUNICODE_STRING; typedef struct _PEB_LDR_DATA { ULONG Length; // +0x00 BOOLEAN Initialized; // +0x04 PVOID SsHandle; // +0x08 LIST_ENTRY InLoadOrderModuleList; // +0x0c LIST_ENTRY InMemoryOrderModuleList; // +0x14 LIST_ENTRY InInitializationOrderModuleList;// +0x1c PVOID EntryInProgress; // +0x24 } PEB_LDR_DATA,*PPEB_LDR_DATA; typedef struct _LDR_DATA_TABLE_ENTRY { LIST_ENTRY InLoadOrderLinks; LIST_ENTRY InMemoryOrderLinks; LIST_ENTRY InInitializationOrderLinks; PVOID DllBase; PVOID EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; //后面不写了,用不到 } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY; int STRCMP(unsigned short *str1,unsigned short *str2) //自己实现一个UNICODE字符串比较 { int flag = 0; while (*str1) { if (*str1 != *str2) { flag = 1; break; } str1++; str2++; } return flag; } void HideModule(UNICODE_STRING DllName) { PPEB_LDR_DATA ldr; PLDR_DATA_TABLE_ENTRY Node; PLIST_ENTRY Head,Temp; __asm { mov eax,fs:[0x30] //只有在ring3_TEB的值为fs[0],偏移0x30到_PEB mov ecx,[eax+0xC] //_PEB偏移0xC到PEB_LDR mov ldr,ecx } // 分别将三个链表断链处理 //1 Head = &(ldr->InLoadOrderModuleList); //第一项是自己的exe,windbg显示不出来 Temp = Head->Flink; printf("以下为所有模块名: "); do { //CONTAINING_RECORD宏的作用就是根据结构体类型和结构体中成员变量地址和名称,则可求出该变量所在结构体的指针 Node = (PLDR_DATA_TABLE_ENTRY)Temp; //InLoadOrderLinks就是结构体第一个成员,不必CONTAINING_RECORD printf("%ls ",Node->BaseDllName.Buffer); //打印所有模块名 if (!STRCMP(Node->BaseDllName.Buffer,DllName.Buffer)) { Node->InLoadOrderLinks.Blink->Flink = Node->InLoadOrderLinks.Flink; Node->InLoadOrderLinks.Flink->Blink = Node->InLoadOrderLinks.Blink; } Temp = Temp->Flink; } while(Head != Temp); //2 Head = &(ldr->InMemoryOrderModuleList); Temp = Head->Flink; do { //CONTAINING_RECORD宏的作用就是根据结构体类型和结构体中成员变量地址和名称,则可求出该变量所在结构体的指针 Node = CONTAINING_RECORD(Temp, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks); if (!STRCMP(Node->BaseDllName.Buffer,DllName.Buffer)) { Node->InMemoryOrderLinks.Blink->Flink = Node->InMemoryOrderLinks.Flink; Node->InMemoryOrderLinks.Flink->Blink = Node->InMemoryOrderLinks.Blink; } Temp = Temp->Flink; } while(Head != Temp); //3 Head = &(ldr->InInitializationOrderModuleList); Temp = Head->Flink; do { //CONTAINING_RECORD宏的作用就是根据结构体类型和结构体中成员变量地址和名称,则可求出该变量所在结构体的指针 Node = CONTAINING_RECORD(Temp, LDR_DATA_TABLE_ENTRY, InInitializationOrderLinks); if (!STRCMP(Node->BaseDllName.Buffer,DllName.Buffer)) { Node->InInitializationOrderLinks.Blink->Flink = Node->InInitializationOrderLinks.Flink; Node->InInitializationOrderLinks.Flink->Blink = Node->InInitializationOrderLinks.Blink; } Temp = Temp->Flink; } while(Head != Temp); } int main() { printf("未断链 "); getchar(); WCHAR MoudleName[] = L"ntdll.dll"; //要隐藏的dll名称 UNICODE_STRING YourMoudle; YourMoudle.MaximumLength = strlen((char*)MoudleName) + 1; YourMoudle.Length = strlen((char*)MoudleName); YourMoudle.Buffer = MoudleName; HideModule(YourMoudle); printf("断链后 "); printf("隐藏的模块名为:%ls",YourMoudle.Buffer); getchar(); return 0; }
网上99%的教程都到此为止了;运行这段代码后,调用windwos提供的CreateToolhelp32Snapshot确实已经查不到了,用OD、x32dbg这些调试器也查不到了,自此已经完事大吉了?
我们双击exe的时候,操作系统会先逐个检查,看看这个到底是不是可执行文件;在windwos下就是PE格式(linux和android时ELF)。PE格式文件的教程网上也烂大街了,感兴趣的小伙伴自己google一下吧;如果不是PE格式,windwos直接报错,是不会加载执行的(可以把其他格式,比如txt、jpg等常见改成exe试试)。
Dll、exe、sys本质上都是PE格式的文件,所以windwos操作系统遇到这类文件,会根据特定的格式找到代码段的入口加载到内存后开始执行。PE大体的构成如下:有各种头部和sector;头部的作用主要是描述文件的类型、各个sector的偏移等,相当于整个文件的“元数据”;text、data等才是核心的代码、数据段;一旦加载到内存开始执行,说明windwos已经认识到了这个文件就是PE,那么头部这些描述整个文件的信息就没用了(就像厕纸一样,上完厕所后用之即弃,完全没必要继续保留);
根据以上的推理,可以进一步抹掉我们自己dll的PE头信息,只留关键的sector,比上面仅仅断链的隐藏更进了一步!核心代码如下:
IMAGE_DOS_HEADER dosH{}; IMAGE_NT_HEADERS ntH{}; PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)_hMod; PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)(dosHeader->e_lfanew + (unsigned)_hMod); DWORD dOld; VirtualProtect(dosHeader, sizeof(dosH), PAGE_EXECUTE_READWRITE, &dOld); VirtualProtect(ntHeader, sizeof(ntH), PAGE_EXECUTE_READWRITE, &dOld); memcpy(dosHeader, &dosH, sizeof(dosH)); memcpy(ntHeader, &ntH, sizeof(ntH));
断链前用CreateToolhelp32Snapshot查询到所有的模块,其中有我们拦截消息的dll,在0x78DF0000处,大小是0x1EA000。断链并且抹掉dll的PE头后,x32dbg在原dll处还是能看到有一块内存的属性是ERWC,但这时已经看不到dll的任何名字了;但此时我们拦截消息的功能依然能正常使用,说明dll的代码本身还是正常运行的!
其他dll模块在内存视图能看到明显的PE头特征:
我们注入的dll中,已经看不到MZ这些PE头的特征了。当然下面还有this is program..... 也有可能被检测到,所以可以进一步去掉这些信息;
到这里完了么? dll是不是隐藏地很好,再也无法明了地找到了?这么想就too yong, too simple了,用process hacker照样能找到我们自己注入的dll,如下:
最新的PCHUNTER(V1.57)也能查到:而且居然还标红提示,说明察觉到了异常!
注意的地方:
1、不同版本的windwos中,PEB结构体有细微差异,建议自己用windbg查查,再写代码
2、各种结构体建议根据windbg查到的重新定义,不要使用windwos原始的结构体
3、PChunter停更了1年多后终于再次更新,能在1909版本继续使用,但试用旗截至2021.2.1,今天2021.2.14,已经过期。但更改本地的时间还能正常使用,不知道后续会不会联网比对时间;
参考:
1、https://blog.csdn.net/Mikasys/article/details/109260609 PEB断链隐藏模块