父进程似乎是一个很简单的概念:如果进程a创建了进程b,那么进程a就是进程b的父进程,反之进程b就是进程a的子进程。最近在研究UAC的时候,发现事情并非这样简单,进程在创建新进程时,可以指定另一个进程作为被创建进程的父进程。
我们先来看看《深入解析Windows操作系统》中关于UAC提权的描述:
WhenauseragreestoanelevationbyeitherenteringadministratorcredentialsorclickingContinue,AIScallsCreateProcessAsUsertolaunchtheprocesswiththeappropriateadministrativeidentity.
AlthoughAISistechnicallytheparentoftheelevatedprocess,AISusesnewsupportinthe
CreateProcessAsUserAPIthatsetstheprocess’sparentprocessIDtothatoftheprocessthatoriginallylaunchedit.That’swhyelevatedprocessesdon’tappearaschildrenoftheAISservice-hostingprocessintoolssuchasProcessExplorerthatshowprocesstrees.
这段话明确表明,当用户允许一次UAC提权时,AIS服务(AppInfoService)调用CreateProcessAsUser创建进程并赋予恰当的管理员权限。从理论上讲,AIS服务(所在的进程)是提权后的进程的父进程,但当我们用ProcessExplorer等工具检查进程树时,会发现提权的进程的父进程是创建它的进程。这是因为AIS利用了CreateProcessAsUserAPI中的“新功能”,将提权进程的父进程设置成为创建该进程的进程。如果我们能够利用这种功能,就可以将想要启动的进程的父进程设为任意可信进程,进而躲过查杀;对于检测父进程是否为explorer.exe的反调试技术,也可以轻易破解。不过书中没有就这种“新功能”做详细介绍,只有查阅MSDN。
根据MSDN的介绍,如果CreateProcessAsUser的dwCreationFlags参数被设置为EXTENDED_STARTUPINFO_PRESENT,则表示存在扩展启动信息。此时lpStartupInfo参数需要填入STARTUPINFOEX结构。该结构非常简单,由STARTUPINFO结构和PROC_THREAD_ATTRIBUTE_LIST指针组成。
typedefstruct_STARTUPINFOEX{
STARTUPINFO
StartupInfo;
PPROC_THREAD_ATTRIBUTE_LIST
lpAttributeList;
}STARTUPINFOEX,*LPSTARTUPINFOEX;
PROC_THREAD_ATTRIBUTE_LIST
是未公开的结构,需要通过InitializeProcThreadAttributeList函数初始化,并通过UpdateProcThreadAttribute函数添加和设置属性。我们只需添加PROC_THREAD_ATTRIBUTE_PARENT_PROCESS属性,并提供一个(有足够权限的)进程句柄,即可设置被创建进程的父进程。部分代码如下:
DWORDpid=0; GetProcessIdByName(L"explorer.exe",&pid)) HANDLEhandle=OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid); cout<<"PID:"<<pid<<endl<<"Handle:"<<handle<<endl; STARTUPINFOEXAsi; ZeroMemory(&si,sizeof(si)); si.StartupInfo.cb=sizeof(si); SIZE_Tlpsize=0; InitializeProcThreadAttributeList(NULL,1,0,&lpsize); char*temp=newchar[lpsize]; LPPROC_THREAD_ATTRIBUTE_LIST AttributeList=(LPPROC_THREAD_ATTRIBUTE_LIST)temp; InitializeProcThreadAttributeList(AttributeList,1,0,&lpsize); if(!UpdateProcThreadAttribute(AttributeList,0,PROC_THREAD_ATTRIB UTE_PARENT_PROCESS,&handle,sizeof(HANDLE),NULL,NULL)){ cout<<"Failtoupdateattributes"<<endl; } si.lpAttributeList=AttributeList; PROCESS_INFORMATIONpi; ZeroMemory(&pi,sizeof(pi)); #ifdefADMIN HANDLEToken; OpenProcessToken(GetCurrentProcess(),TOKEN_ALL_ACCESS,&Token); if(CreateProcessAsUserA(Token,0,"regedit.exe",0,0,0,EXTENDED_STA RTUPINFO_PRESENT,0,0,(LPSTARTUPINFOA)&si,&pi)) #else if(CreateProcessAsUserA(NULL,0,"calc.exe",0,0,0,EXTENDED_STARTUP INFO_PRESENT,0,0,(LPSTARTUPINFOA)&si,&pi)) #endif { cout<<"Processstarted"<<endl; }else{ cout<<"Errorcode:"<<GetLastError()<<endl; } DeleteProcThreadAttributeList(AttributeList); deletetemp;
以上代码会用explorer.exe作为父进程启动calc.exe。利用ProcessExplorer可以验证这一点,如图1和图2为伪造父进程与直接启动calc.exe的进程树对比。如果需要创建有管理员权限的进程(如regedit),只需以管理员权限运行本程序,并将CreateProcessAsUser的第一个参数设为当前进程的令牌句柄(对应于上述代码中#defineADMIN的情况)。
如果我们想要调试的程序会检测父进程,则只需用上述方法启动它,并将其父进程设为允许的进程即可。当然,启动时需要设置CREATE_SUSPEND并在调试器挂载后用ResumeThread恢复其执行。
综上,进程的父进程不一定是进程的创建者,所以杀软并不能简单的根据父进程(进程树)决定进程是否可信。实验发现,上述方法伪造父进程不能绕过360的检测,它是怎么做到的呢?查阅MSDN发现,通过PsSetCreateProcessNotifyRoutine设置的监控回调会接受一个指向PS_CREATE_NOTIFY_INFO结构的指针。通常我们认为ParentProcessId成员为父进程的PID;其实该结构有一个成员为CreatingThreadId,其中的CreatingThreadId->UniqueProcess为进程的创建者(即CreateProcess*的调用者),这样便可以确定谁是真正的“父进程”了。