(xxxx)七:3环下注入的dll模块隐藏(一)

     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("以下为所有模块名:\n");
    do
    {
        //CONTAINING_RECORD宏的作用就是根据结构体类型和结构体中成员变量地址和名称,则可求出该变量所在结构体的指针
        Node = (PLDR_DATA_TABLE_ENTRY)Temp;    //InLoadOrderLinks就是结构体第一个成员,不必CONTAINING_RECORD
        printf("%ls\n",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("未断链\n");
    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("断链后\n");
    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断链隐藏模块

2、https://www.bilibili.com/video/BV1m7411p7TM?p=30 进程与线程

posted @ 2021-02-14 23:56  第七子007  阅读(1321)  评论(0编辑  收藏  举报