• x64下隐藏可执行内存


    前言

    我们如果想要实现进程隐藏在3环通常会使用到PEB断链去达到隐藏进程的效果,但是那只是表面上的进程隐藏,所有内存的详细信息都会被储存在vad树里面,这里我们就来探究在64位下如何隐藏可执行内存

    vad

    VAD是管理虚拟内存的,每一个进程有自己单独的一个VAD树,使用VirtualAlloc申请一个内存,则会在VAD树上增加一个结点,其是_MMVAD结构体

    dt _MMVAD

    image-20220329154007738.png

    这里找一个进程,因为是根节点所以没有父节点

    image-20220329154131189.png

    然后往左遍历二叉树,在下一个节点处的父节点指向了上一个二叉树

    image-20220329154230061.png

    注意StartingVpnEndingVpn这两个结构,描述了当前页的位置,以4kb为单位,即0x400000到0x488000这一块内存空间已经被占用了

    image-20220329154654585.png

    在0x18有一个ControlArea结构,描述了这块结构体到底被谁占用,这里跟进去看0x24有一个FilePointer结构,如果这里的值为0就是一个真正的物理页,如果有值继续往里面找

    image-20220329155005328.png

    这里对应了Dbgview.exe

    image-20220329155328329.png

    在操作系统里面分配的内存只可能有两种类型,一种是VirtualAlloc自己分配的内存,一种是文件映射使用CreateFileMapping的内存,当ControlAreaFilePointer值为空的时候则是我们自己用VirtualAlloc分配的内存,还没有对应,如果值不为空则是文件映射的内存

    分页机制

    在32位里面有2-9-9-1210-10-12两种分页模式,而在64位下只有一种分页模式,即9-9-9-9-12分页模式

    随着计算机技术的发展,64位系统逐渐占据主流地位,那么也就表示CPU的最大寻址范围为64位。但实际上,CPU只使用了其中的48位用于寻址,并使用9-9-9-9-12分页模式。即便如此,在未来较长一段时间里,48位寻址范围也足够大部分人的日常使用了

    9-9-9-9-12分页表示物理地址拥有四级页表,在Intel开发手册中,将这四级页表分别称为PML4EPDPTEPDEPTE,但微软的命名方式略有不同,将这四级页表分别称为PXEPPEPDEPTEWinDbg中也是如此

    image-20220504165636253.png

    启用分页模式条件:cr0.PG = 1 且 cr0.PE = 1

    根据不同CPU架构及特性主要分为三种模式,处于哪种模式视寄存器属性不同:

    • • 32-bit paging(32位OS): cr0.PG = 1 、 cr4.PAE = 0

    • • PAE paging(32位OS且开启了PAE): cr0.PG = 1 、 cr4.PAE = 1 、 IA32_EFER.LME = 0

    • • IA-32e paging(64位OS): cr0.PG = 1 、 cr4.PAE = 1 、 IA32_EFER.LME = 1

    需要注意的是

    1. 1. 32bit下,每个entry(表项)是4字节大小;而在PAE和IA-32e下,每个entry是8字节大小

    2. 2. 在x64体系中只实现了48位的virtual address,高16位被用作符号扩展,这高16位要么全是0,要么全是1。所以在讨论64bit地址的时候,高16位不使用

    我们主要研究的是IA-32e模式下的内存,这里IA-32e提供了三种页转换模型:

    • • 4k:PML4t,PDPT,PDT和PT

    • • 2M:PML4T,PDPT和PDT

    • • 1G:PML4T和PDPT

    在4kb小页的情况下,64位可以拆分为一下几段,即9-9-9-9-9-12分页

    sign extended -- 符号扩展位 --- 在线性地址48~63bit 

    PML4 entry -- 在线性地址39~47bit用于索引PML4 entry,指向PDP 

    PDP entry -- 在线性地址的30~38bit用来索引PDP entry,指向PDE 

    PDE entry -- 在线性地址的21~29bit用来索引PDEentry,指向PTE 

    PTE entry -- 在线性地址的12~20bit用来索引PTE entry,指向page offset

    page offse t -- 在线性地址的0~11bit提供在页中的offset

    这里我们手动去找一下,前3位为符号扩展位,直接去掉

    image-20220420103031572.png

    可以看到PXEPPEPDEPTE都是能够对应上的

    image-20220420103147049.png

    页表基址

    • • 一个进程该如何访问自己的物理页呢?可以通过读取Cr3的值进行访问吗?

    答案是不行,Cr3中保存的页表基址是物理地址,程序如果直接访问这个地址,虽然看上去值是一样的,但实际上访问的是一个线性地址,会被虚拟内存管理器解析成另一个地址

    实际上,操作系统会将当前进程的物理页映射在某个线性地址中,以供程序读取自己的页表内容

    在x86系统中,页表基址是固定的,位于0xC0000000,将这个线性地址进行解析,访问其物理页的内容,会发现从这个地址开始,里面保存的数据为当前程序的所有物理页地址

    而在x64系统中,页表基址不再是固定的值,而是每次系统启动后随机生成的可以在WinDbg中查看0地址对应的线性地址来确定当前的页表基址

    可以看到,当前系统的页表基址的线性地址为0xFFFFF38000000000,注意,只有后48位才是有效地址

    其中,每个物理页占8个字节,例如,第一个物理页地址位于线性地址0xFFFFF38000000000,第二个物理页地址位于线性地址0xFFFF800000000008,每个物理页中包含1024个字节的数据

    image-20220419212509051.png

    MiIsAddressValid

    我们在这里初步了解了windows的内存管理,那么这里我们去看一下windows是如何实现分页机制的,这里使用到MiIsAddressValid这个API

    image-20220504170429995.png

    我们首先看一下win7下MiIsAddressValid的实现

    image-20220504171516062.png

    shr eax, 14hand eax, 0FFC相当于eax右移16位再乘以4,然后判断PS位是否为0,如果为0则不合法,则将al清零

    image-20220504204400160.png

    在64位下,存在三种不同大小的页面,分别为大页、中页、小页。其大小分别为1GB、2MB、4KB。这里判断al不等于0则继续向下执行,这里jns是通过判断SF=0,如果SF=0成立则跳转

    image-20220504205545977.png

    这里仍然后一个右移的操作,是为了将段选择子分为9-9-9-9-12五部分,然后判断P位是否有效和PAT是否为1

    image-20220504205653192.png

    这里其实看伪代码逻辑会更清晰一点,我们可以可以发现通过一系列的移位操作得到对应的PXEPPEPDEPTE并判断P位验证是否有效

    image-20220504210754899.png

    那么这里我们就可以通过减去的数值取反,然后加1即可得到对应基址,通过计算得到win7 64位下的PTE_Base = fffff68000000000

    我们再去看一下win10下的MiIsAddressValid函数

    1. .text:00000001400AD930 _MmIsAddressValid proc near             .text:000000014000FB6E
    2. .text:00000001400AD930                                         
    3. .text:00000001400AD930                 mov     rax, rcx
    4. .text:00000001400AD933                 sar     rax, 48                               
    5.                                                  ;取得线性地址的   高16
    6. .text:00000001400AD937                 inc     rax
    7. .text:00000001400AD93A                 cmp     rax, 1
    8. .text:00000001400AD93E                 ja      loc_1400AD9D3   
    9.                                  ; 高16位要么全0, 要么全1 ,加一后大于1则不合法,直接返回false
    10. .text:00000001400AD944                 mov     rax, rcx
    11. .text:00000001400AD947                 mov     rdx, 0FFFFF6FB7DBED000h
    12.                                                      ; PML4T 的虚拟地址
    13. .text:00000001400AD951                 shr     rax, 39        
    14.                                                    ; 将虚拟地址右移39
    15. .text:00000001400AD955                 and     eax, 1FFh      
    16.                                                    ; 拿到pml4数组的下标
    17. .text:00000001400AD95A                 test    byte ptr [rdx+rax*8], 1
    18.                                                  ; 检测PML4T项p位
    19. .text:00000001400AD95E                 jz      short loc_1400AD9D3
    20.                                                   ; p=0 则直接返回false
    21. .text:00000001400AD960                 mov     rax, rcx
    22. .text:00000001400AD963                 mov     rdx, 0FFFFF6FB7DA00000h
    23. .text:00000001400AD96D                 shr     rax, 27         
    24.                                                 ; 右移30位 ,再 乘 8 ,相当于右移27
    25. .text:00000001400AD971                 and     eax, 1FFFF8h   
    26.                                                 ; 8字节对齐 得到PDPT的偏移
    27. .text:00000001400AD976                test    byte ptr [rax+rdx], 1
    28.                                                ; 检测PDPT项的p位 rax+rdx=PDPT的首地址
    29. .text:00000001400AD97A                jz      short loc_1400AD9D3
    30. .text:00000001400AD97C                 mov     rdx, -0FFFFF6FB40000000h
    31. .text:00000001400AD986                 mov     rax, rcx
    32. .text:00000001400AD989                 shr     rax, 18        
    33.                                                   ; 右移21位,再乘8 ,相当于右移18
    34. .text:00000001400AD98D                 and     eax, 3FFFFFF8h       
    35.                                                    ;得到PDT的偏移
    36. .text:00000001400AD992                 sub     rax, rdx      
    37.                                                  ; rax = rax + 0FFFFF6FB40000000h  
    38. .text:00000001400AD995                 mov     rdx, [rax]               
    39.                                                 ;此时rax指向PDT中的 某一项
    40. .text:00000001400AD998                 test    dl, 1         
    41.                                                   ; 检测PDT项p位
    42. .text:00000001400AD99B                 jz      short loc_1400AD9D3
    43. .text:00000001400AD99D                 test    dl, dl
    44. .text:00000001400AD99F                 js      short loc_1400AD9D6
    45.                                                      ; 是否开启PSE,是的话直接返回真
    46. .text:00000001400AD9A1                 shr     rcx, 9         
    47.                                                     ; 右移12位,再乘 8 ,相当于右移9
    48. .text:00000001400AD9A5                 mov     rax, 7FFFFFFFF8h ; 8字节对齐
    49. .text:00000001400AD9AF                 and     rcx, rax               
    50. .text:00000001400AD9B2                 mov     rax, -0FFFFF68000000000h
    51. .text:00000001400AD9BC                 sub     rcx, rax
    52. .text:00000001400AD9BF                 mov     rax, [rcx]       
    53.                                                    ;此时RCX指向PT的的某一项
    54. .text:00000001400AD9C2                 test    al, 1
    55. .text:00000001400AD9C4                 jz      short loc_1400AD9D3
    56.                                                    ; 检测PT项的P位
    57. .text:00000001400AD9C6                 mov     r8b, 80h
    58. .text:00000001400AD9C9                 and     al, r8b
    59. .text:00000001400AD9CC                 cmp     al, r8b
    60. .text:00000001400AD9CF                 setnz   al            
    61.                                                   ; 检测PT项的PAT位是否存在,不存在返回真
    62. .text:00000001400AD9D2                 retn
    63. .text:00000001400AD9D3 ; ---------------------------------------------------------------------------
    64. .text:00000001400AD9D3
    65. .text:00000001400AD9D3 loc_1400AD9D3:                          
    66. .text:00000001400AD9D3                                         
    67. .text:00000001400AD9D3                 xor     al, al
    68. .text:00000001400AD9D5                 retn
    69. .text:00000001400AD9D6 ; ---------------------------------------------------------------------------
    70. .text:00000001400AD9D6
    71. .text:00000001400AD9D6 loc_1400AD9D6:                         _
    72. .text:00000001400AD9D6                 mov     al, 1
    73. .text:00000001400AD9D8                 retn
    74. .text:00000001400AD9D8 _MmIsAddressValid endp

    在win10 1607版本以后,微软更改了策略,将页目录基址更改为了随机地址,那么我们之前在win7里面直接定位PTE_Base的方法就不可用,那么我们就可以使用提取特征码的方式去定位内核模块的地址

    首先在WinDbg中定位内核模块的地址

    image-20220420102547386.png

    然后在内核模块中搜索与当前页表基址相同的值出现的位置,当前页表基址为0xFFFF800000000000

    image-20220420102559314.png

    接着,在IDA中定位到数据所在的位置,可以看到是某行代码引用了这个值的硬编码

    image-20220420102611892.png

    在WinDbg中查看这段代码,能够识别到位于CcUnpinFileDataEx函数。那么,由于系统每次启动时基址是不固定的,因此这些值也不可能是固定的硬编码,肯定对这些值进行了修改,在需要使用时,可以通过固定的偏移量提取硬编码,从而得到页表基址,但要注意不同版本的内核文件的偏移量可能是不同的

    image-20220420102628929.png

    代码实现

    那么这里我们首先编写4个函数分别定位PTEPDEPPEPXE,这里g_PTE_BASE就分为两种情况

    1. PULONG64 GetPteAddress(PVOID addr)
    2. {
    3.     return (PULONG64)(((((ULONG64)addr & 0xffffffffffff) >> 12) << 3) + g_PTE_BASE);
    4. }
    5. PULONG64 GetPdeAddress(PVOID addr)
    6. {
    7.     return (PULONG64)(((((ULONG64)addr & 0xffffffffffff) >> 21) << 3) + g_PDE_BASE);
    8. }
    9. PULONG64 GetPpeAddress(PVOID addr)
    10. {
    11.     return (PULONG64)(((((ULONG64)addr & 0xffffffffffff) >> 30) << 3) + g_PPE_BASE);
    12. }
    13. PULONG64 GetPxeAddress(PVOID addr)
    14. {
    15.     return (PULONG64)(((((ULONG64)addr & 0xffffffffffff) >> 39) << 3) + g_PXE_BASE);
    16. }

    当系统为win7或者win10 1607以下版本的时候就可以直接将g_PTE_BASE定义成固定的地址

    1.     if (versionNumber == 7600 || versionNumber < 14393)
    2.     {
    3.         g_PTE_BASE = 0xFFFFF68000000000ull;
    4.         return g_PTE_BASE;
    5.     }

    如果为win10 1607以上的版本就需要自己通过逆向的方式提取硬编码进行定位,这里我通过MmGetVirtualForPhysical函数加偏移的方式进行定位

    1.         UNICODE_STRING Name = { 0 };
    2.         RtlInitUnicodeString(&Name, L"MmGetVirtualForPhysical");
    3.         PUCHAR func = (PUCHAR)MmGetSystemRoutineAddress(&unName);
    4.         pte_base = *(PULONG64)(func + 0x22);
    5.         return pte_base;

    那么这里要得到系统版本,就需要用到RtlGetVersion进行判断,这里注意,在win7以后如果直接使用GetVersion会失败,必须调用更底层的RtlGetVersion才能得到具体的版本

    image-20220504214628464.png

    NTSTATUS status = RtlGetVersion(&version);

    这里我们明确一下思路,我们想要隐藏可执行内存,那么就可以首先申请一块可读可写的内存,然后通过修改PXE的最高位为0即可达到可执行的效果

    例如下面的程序,PXE的最高位为8,则内存是没有可执行权限的

    image-20220424202559150.png

    那么这里我们找到目标进程,然后通过KeStackAttachProcess函数实现进程挂靠,即把自己的cr3换成目标进程的cr3

    1.     NTSTATUS status = PsLookupProcessByProcessId(pid, &Process);
    2.     KAPC_STATE kapc_state = { 0 };
    3.     KeStackAttachProcess(Process, &kapc_state);

    我们将cr3切换为目标进程的cr3之后就可以使用ZwAllocateVirtualMemory先分配一块可读可写的内存

    status = ZwAllocateVirtualMemory(NtCurrentProcess(), &BaseAddress, 0&size, MEM_COMMITPAGE_READWRITE);

    通过RtlMoveMemory写入shellcode并修改内存为可执行权限,这里我们直接定位到pte和pde修改即可将pxe的最高位置0

    首先将前3位符号位去掉得到内存的起始地址和结束地址

    1.     ULONG64 startAddress = VirtualAddress & (~0xFFF); 
    2.     ULONG64 endAddress = (VirtualAddress + size& (~0xFFF); 

    这里写一个循环判断,必须每一块内存都需要修改

    for (ULONG64 i = startAddress; i <= endAddress; i += PAGE_SIZE)

    结合MmIsAddressValid并判断valid是否为1,这里如果valid为0则该块内存无效,然后将no_execute置0即可获得可执行权限

    1. PHardwarePte pde = GetPdeAddress(i);
    2. PHardwarePte pte = GetPdeAddress(i);
    3. if (MmIsAddressValid(pde) && pde->valid == 1)
    4. {
    5.     pde->no_execute = 0;
    6.     pde->write = 1;
    7. }
    8. if (MmIsAddressValid(pte) && pte->valid == 1)
    9. {
    10.     pte->no_execute = 0;
    11.     pte->write = 1;
    12. }

    那么这里我们调用SetExecute函数将我们之前分配的可读可写内存修改为可读可写可执行权限

    SetExecute(BaseAddress, size);

    然后使用KeUnstackDetachProcess还原cr3

    KeUnstackDetachProcess(&kapc_state);

    实现效果

    在64位下VadRoot位于EPROCESS结构体的7d8偏移处

    image-20220504221446105.png

    起一个notepad.exe进程定位到vad

    image-20220504221800112.png

    然后这里可以看到有97块内存

    image-20220504221852378.png

    我们加载一下驱动,可以看到修改了pte的值,将最高位的8改为了0,分配的这块内存地址为222F5EB0000

    image-20220504223225749.png

    我们看下没有加载驱动之前vad树里面是没有这块内存的

    image-20220504223334000.png

    加载驱动之后可以看到这是一块READWRITE内存

    image-20220504223354777.png

    这里定位到地址可以看到shellcode执行成功,证明这块内存已经修改为可执行内存,但是在vad树里面仍然显示为可读可写内存

  • 相关阅读:
    Arial.tff字体,YOLO中下载。已解决
    docker笔记14--docker-nerdctl-crictl-ctr使用对比
    java--02
    .Net 7 轻松上手Dapr之服务调用
    Channel扇出模式
    工具类app变现难?工具类产品广告变现策略实用指南
    linux上安装qt creator
    C++ Balanced Braces
    【附源码】计算机毕业设计JAVA资源循环利用
    闲聊四种旅游方式
  • 原文地址:https://blog.csdn.net/hongduilanjun/article/details/126850927