PPID Spoofing(父进程欺骗)

主要参考文章: 奇安信攻防社区-Parent Process ID (PPID) Spoofing

PPID Spoofing介绍

PPID Spoofing(父进程ID欺骗)的原理是通过操控新进程的父进程ID,使其看起来是由另一个指定的进程创建的,而不是实际创建它的进程。这种技术可以用来隐藏恶意行为或混淆分析,避免安全软件的检测,因为某些安全工具可能通过监控父子进程关系来检测恶意软件。

PPID Spoofing 的操作方式: 在PPID欺骗中,通过使用扩展启动信息(STARTUPINFOEXA)结构,并修改其属性列表(ProcThreadAttributeList),可以在进程创建时指定一个自定义的父进程,而不是默认的创建进程。这通过在创建新进程时,将另一个合法进程的句柄传递给CreateProcess函数来实现。

PPID Spoofing实现代码

先看一个代码:
这个代码的主要功能是在一个指定的父进程下创建一个notepad子进程,通过CreateProcessA来实现,并使用扩展启动信息STARTUPINFOEXA来指定父进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//PPID Spoofing(父进程id欺骗)。
#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <TlHelp32.h>
#include <iostream>
int main()
{
STARTUPINFOEXA si;
PROCESS_INFORMATION pi;
SIZE_T attributeSize;
ZeroMemory(&si, sizeof(STARTUPINFOEXA));
// 要修改这里的 pid
HANDLE parentProcessHandle = OpenProcess(MAXIMUM_ALLOWED, false, 12896);//MAXIMUM_ALLOWED表示获取最大允许权限,false表示不需要继承句柄。
InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);//初始化用于创建进程和线程的指定属性列表。
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);
UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentProcessHandle, sizeof(HANDLE), NULL, NULL);
si.StartupInfo.cb = sizeof(STARTUPINFOEXA);

CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi);

return 0;
}

这里解释下这些代码的含义:

1
InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);

这是调用InitializeProcThreadAttributeList函数的第一步,目的是计算所需的属性列表大小

参数解释

  • NULL:因为这次调用是为了获取需要的内存大小,而不是初始化实际的列表,所以传递NULL
  • 1:指定列表中的属性数量。在这个例子中,我们只需要一个属性(父进程属性),因此传递1
  • 0:这个标志(dwFlags)一般设置为0,表示没有特殊要求。
  • &attributeSize:用于存储计算出来的属性列表所需的大小。在第一次调用时,InitializeProcThreadAttributeList不会创建列表,而是通过这个参数告诉你需要多大的内存来存储属性列表。
1
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);

功能:这行代码为属性列表分配内存,大小为上一步中得到的attributeSize

参数解释

  • GetProcessHeap():获取当前进程的默认堆句柄,用于分配内存。
  • 0:分配内存的标志,一般设置为0,表示没有特殊的分配要求。
  • attributeSize:上一步计算出来的内存大小,这是属性列表所需的大小。

最终,HeapAlloc函数为属性列表分配一块堆内存,并将其赋值给si.lpAttributeList,这是一个指向属性列表的指针。

1
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);

功能:在分配了合适大小的内存之后,真正初始化属性列表。

参数解释

  • si.lpAttributeList:这是前面分配好的内存指针,InitializeProcThreadAttributeList将在此指针指向的内存块中初始化属性列表。
  • 1:指定属性列表中包含的属性数量。此处为1,因为我们只需要一个属性(父进程属性)。
  • 0:这个标志(dwFlags)一般设置为0,表示没有特殊要求。
  • &attributeSize:在第一次调用时,它用于计算大小。第二次调用时,它已经被分配了内存并传递给该函数。
1
UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentProcessHandle, sizeof(HANDLE), NULL, NULL);
  • 功能:这行代码将父进程属性更新到启动信息的属性列表中。换句话说,它将告诉CreateProcessA函数,在启动新的进程时要将parentProcessHandle作为新进程的父进程。
  • 参数解释
    • si.lpAttributeList:指向我们之前通过HeapAlloc分配并初始化好的属性列表,这里更新父进程信息到这个列表中。
    • 0:保留参数,通常设置为0。
    • PROC_THREAD_ATTRIBUTE_PARENT_PROCESS:这是一个常量,表示父进程属性。通过这个参数告诉系统,你要在新创建的进程中设置一个特定的父进程。
    • &parentProcessHandle:指向父进程的句柄。我们之前通过OpenProcess函数获取了这个父进程句柄,将它传递给新进程作为其父进程。
    • sizeof(HANDLE):表示传递的parentProcessHandle的大小,HANDLE在Windows系统中通常是一个32位或64位的值,所以需要传递它的字节大小。
    • NULL, NULL:这两个参数分别用于额外的输入缓冲区和返回缓冲区,这里不需要,因此传递NULL

总结:通过这行代码,si.lpAttributeList这个属性列表中被更新了父进程信息。当后续使用CreateProcessA创建进程时,这个新进程的父进程将是parentProcessHandle指定的进程。

1
2
si.StartupInfo.cb = sizeof(STARTUPINFOEXA);
CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi);
  • 功能:这行代码的作用是使用扩展的启动信息结构STARTUPINFOEXA来创建一个新的进程,并且传递我们刚刚设置的父进程属性。具体来说,这个进程是notepad.exe,它的父进程将是之前通过parentProcessHandle指定的进程。

参数解析:

  1. **si.StartupInfo.cb = sizeof(STARTUPINFOEXA);**:
    • 设置STARTUPINFOEXA结构的大小(cb字段),这是标准步骤,CreateProcessA需要知道这个结构的大小。
    • 这里是通过扩展启动信息结构STARTUPINFOEXA来实现父进程继承,而不仅仅是普通的STARTUPINFO

CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi);

    • NULL:应用程序的路径,这里传递NULL,表示通过命令行参数"notepad"来启动程序。
    • (LPSTR)"notepad":命令行参数,指定要启动的应用程序是notepad.exe
    • NULL:进程安全属性,表示使用默认安全属性。
    • NULL:线程安全属性,表示使用默认安全属性。
    • FALSE:不继承父进程的句柄。
    • EXTENDED_STARTUPINFO_PRESENT:这是关键的标志,表示你使用了扩展启动信息(即STARTUPINFOEXA)。这个标志告诉系统要检查并应用si.lpAttributeList中的属性(如父进程属性)。
    • NULL:新进程的环境变量,使用当前进程的默认环境变量。
    • NULL:新进程的工作目录,使用当前进程的默认工作目录。
    • &si.StartupInfo:传递启动信息结构,实际上是STARTUPINFOEXA结构。
    • &pi:进程信息结构,CreateProcessA调用成功后将返回新进程的句柄和ID等信息。

总结:通过CreateProcessA函数,使用了我们之前设置的扩展启动信息(包含父进程属性),启动了notepad.exe进程,并将其父进程设置为我们指定的进程。

效果展示:

我们指定一个Notepad.exe的父进程为一个Notepad.exe

1729598225906

利用ETW监控PPID

复习一下之前说的ETW

  • Providers(提供程序):可以产生事件日志的程序;
  • Consumers:订阅和监听 Providers 发出的事件的程序
  • Keywords(关键字):Providers 提供给 Consumer 的事件类型;
  • Tracing session(跟踪会话):记录来自一个或多个 Providers 的事件;
  • Contollers:可以启动 Tracing session 的程序。

步骤

1.选择Providers

我们可以用 Windows 自带的工具 Logman.exe启动跟踪会话

这里的 Logman.exe对应着上面的Controllers角色。

首先列出有哪些 Providers

1
logman query providers

1729605074865

这样一下会列出超级多的项,但是我们只需要关注一个就行

也就是Microsoft-Windows-Kernel-Process这个providers

具体来说,有两种方式可以查询

1
2
3
4
# 通过 provider 名字查询
logman query providers Microsoft-Windows-Kernel-Process
# 通过 GUID 查询
logman query providers "{22FB2CD6-0E7B-422B-A0C7-2FAD1FD0E716}"

查询出来长这样
1729605239352

可以看到,这个 Providers 有一些 Keywords(关键字),代表这个 Providers 可以提供一些进程、线程等事件。对我们的 PPID欺骗来说,只要看进程就行,所以这里选择的关键字是WINEVENT_KEYWORD_PROCESS,对应值为0x10

如果我想要同时知道 WINEVENT_KEYWORD_PROCESSWINEVENT_KEYWORD_THREAD 怎么办,那么只需要把他们相加,也就是0x10+0x20=0x30

2.创建跟踪会话并指定 Provider

选定好Providers之后,我们就需要启用Tracing session,也就是跟踪会话。这里我们指定一个名字,例如叫ppid-spoof,同时指定Providers为我们之前找的Microsoft-Windows-Kernel-Process,keyword指定为0x10,也就是WINEVENT_KEYWORD_PROCESS

1
2
logman create trace ppid-spoofing -p Microsoft-Windows-Kernel-Process 0x10 -ets

指令详解

  • **logman**:这是 Windows 的命令行工具,用于管理事件跟踪会话,能够创建、启动、停止和删除跟踪会话。
  • **create trace**:这是指令的动作,表示创建一个新的追踪会话。
  • **ppid-spoof**:这是新创建的追踪会话的名称。你可以自定义这个名称,以便于后续识别和管理这个会话。
  • **-p Microsoft-Windows-Kernel-Process**:这个参数指定要注册的 ETW 提供程序(Provider)。在这里,Microsoft-Windows-Kernel-Process 是一个系统提供程序,负责跟踪与进程相关的事件,如进程的创建、终止等。
  • **0x10**:这是一个事件关键字(Keyword),用于过滤事件类型。0x10 对应于 WINEVENT_KEYWORD_PROCESS,它指定了要监控的事件类别,即与进程活动相关的事件。具体来说,0x10 表示仅记录与进程创建相关的事件。
  • **-ets**:这是一个选项,表示使用 ETW(事件跟踪)会话。在这种模式下,创建的追踪会话将立即开始记录事件,而不需要后续手动启动。

1729606518855

然后可以查一下这个跟踪会话的内容,确保它在运行。

1
2
logman query ppid-spoofing -ets

我们主要观察三个点

1.状态 2.输出位置 3.KeywordsAny

1729606946865

3. 分析日志

我们打开之前我们收集到的etl文件

1729607456845

然后打开事件查看器

选择打开保存的日志,就把我们之前生成的etl文件导入进去1729607621852

这里选择否

1729607687114

进来之后去对比俩,一个是Execution的PID,另一个是EventData的PID

CreateProcess可以改变的是

1729608927119

真正的 ParentProcessID 应该是Execution下的ProcessID

可以看到,如果是伪造的,这俩是不一样的

1729608966881

Execution 中的 ProcessID:

  • 这是与事件的执行上下文相关的进程ID。它通常指的是正在执行该事件的线程所属的进程。换句话说,它表示触发该事件的执行上下文。

因此得出结论,Execution下的ProcessIDData下的ParentProcessID的值要一致,否则可能是 PPID 欺骗,而且真正的 PPID 应该以Execution下的ProcessID为准。

4.移除会话

想要移除这个会话

1
2
logman stop ppid-spoof -ets

1729606824828