PPID的攻与防
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 | //PPID Spoofing(父进程id欺骗)。 |
这里解释下这些代码的含义:
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 | si.StartupInfo.cb = sizeof(STARTUPINFOEXA); |
- 功能:这行代码的作用是使用扩展的启动信息结构
STARTUPINFOEXA来创建一个新的进程,并且传递我们刚刚设置的父进程属性。具体来说,这个进程是notepad.exe,它的父进程将是之前通过parentProcessHandle指定的进程。
参数解析:
- **
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

利用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 |

这样一下会列出超级多的项,但是我们只需要关注一个就行
也就是Microsoft-Windows-Kernel-Process这个providers
具体来说,有两种方式可以查询
1 | # 通过 provider 名字查询 |
查询出来长这样
可以看到,这个 Providers 有一些 Keywords(关键字),代表这个 Providers 可以提供一些进程、线程等事件。对我们的 PPID欺骗来说,只要看进程就行,所以这里选择的关键字是WINEVENT_KEYWORD_PROCESS,对应值为0x10
如果我想要同时知道 WINEVENT_KEYWORD_PROCESS 和 WINEVENT_KEYWORD_THREAD 怎么办,那么只需要把他们相加,也就是0x10+0x20=0x30
2.创建跟踪会话并指定 Provider
选定好Providers之后,我们就需要启用Tracing session,也就是跟踪会话。这里我们指定一个名字,例如叫ppid-spoof,同时指定Providers为我们之前找的Microsoft-Windows-Kernel-Process,keyword指定为0x10,也就是WINEVENT_KEYWORD_PROCESS
1 | 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(事件跟踪)会话。在这种模式下,创建的追踪会话将立即开始记录事件,而不需要后续手动启动。

然后可以查一下这个跟踪会话的内容,确保它在运行。
1 | logman query ppid-spoofing -ets |
我们主要观察三个点
1.状态 2.输出位置 3.KeywordsAny

3. 分析日志
我们打开之前我们收集到的etl文件

然后打开事件查看器
选择打开保存的日志,就把我们之前生成的etl文件导入进去
这里选择否

进来之后去对比俩,一个是Execution的PID,另一个是EventData的PID
CreateProcess可以改变的是

真正的 ParentProcessID 应该是Execution下的ProcessID
可以看到,如果是伪造的,这俩是不一样的

Execution 中的 ProcessID:
- 这是与事件的执行上下文相关的进程ID。它通常指的是正在执行该事件的线程所属的进程。换句话说,它表示触发该事件的执行上下文。
因此得出结论,Execution下的ProcessID和Data下的ParentProcessID的值要一致,否则可能是 PPID 欺骗,而且真正的 PPID 应该以Execution下的ProcessID为准。
4.移除会话
想要移除这个会话
1 | logman stop ppid-spoof -ets |

