1.进程的创建过程
步骤一:
当系统启动后,创建一个进程:Explorer.exe 也就是桌面进程.
步骤二:
当用户双击某一个EXE时,Explorer 进程使用CreateProcess函数创建被双击的EXE,也就是说,我们在桌面上双
击创建的进程都是Explorer进程的子进程.
可以用工具XueTr.exe,查看哪些进程是由Explorer创建的.
1)CreateProcess函数
BOOL CreateProcess( LPCTSTR lpApplicationName, // name of executable module LPTSTR lpCommandLine, // command line string LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD BOOL bInheritHandles, // handle inheritance option DWORD dwCreationFlags, // creation flags LPVOID lpEnvironment, // new environment block LPCTSTR lpCurrentDirectory, // current directory name LPSTARTUPINFO lpStartupInfo, // startup information LPPROCESS_INFORMATION lpProcessInformation // process information );
CreateProcess用来创建进程;
首先在内核层也就是高2G创建一块内存,也就是进程的内核对象;
内核对象都有一个计数器,创建时计数器的值为1,如果呗其它进程调用OpenXX计数器的值+1;当计数器值为0时内核对象会被释放;
进程的内核对象中会有一张表格,叫句柄表,用来保存进程中创建的内核对象的地址;
每当调用CreateXX创建内核对象时就会在句柄表中添加一条记录;
例如:CreateProcess、CreateMutex、CreateThread、CreateFile、CreateEvent、 CreateFileMapping;
但直接将内核对象的地址返回给3环程序不安全:
如果内核对象将内核对象地址传给3环,3环程序修改了该地址然后返回给内核,内核用这个修改后的地址找不到内核对象,然后就系统崩溃;
因此内核会将一个编号也就是内核对象的句柄传递给3环程序;
2)分配4GB空间
进程对象创建完后,接下来就创建一个4GB的虚拟空间;
32位的windows系统每个进程都有自己独立的虚拟4GB空间;(2的32次方=4GB)
这段4GB空间分为3个区域:
0~FFFF不可使用,用作NULL指针,当访问该段内存时,程序会报内存无法访问错误并退出;
10000~7FFFFFFF为用户区,3环程序主要操作该区域的内存;
剩下的高2G内存被内核使用;
3)创建进程的主线程
当进程的4GB空间分配后,首先会将程序exe文件拉伸为镜像,放入ImageBase的位置;
exe文件可能用到了其它dll,这些dll的信息在导入表中;通过遍历导入表,将需要用到的dll拉伸然后放入各自ImageBase的位置;
如果dll的ImageBase位置被占用,将放入其它位置,此时需要参考dll中的重定位表来修复器全局变量的得知;
dll也可能用到其它dll,处理方式和前一步一样;
IAT表中储存的是导入的dll函数的名称,为了让程序能正确定位到函数,需要将IAT表中的函数名转成函数地址;
当exe和dll贴入内存空间并修复完毕后程序达到了可运行的条件;
进程只是一个空间概念,程序真正想跑起来还需要至少有一个主线程;
每个线程都会有一个CONTEXT结构,用来记录线程运行的状态,比如寄存器eip的值;因此线程切换后再次运行时依然能接着跑;
程序需要从程序入口处开始跑,也就是main函数的地址;程序的入口地址=ImageBase+OEP;这两个值都可以在exe的pe结构的可选pe头中保存;
将程序入口的地址交给EIP,程序就能跑了;
当进程的空间创建完毕,EXE与导入表中的DLL都正确加载完毕后,会创建一个线程 ;
当线程得到CPU的时候,程序就正开始指向了,EIP的初始值设定为:ImageBase+OEP ;
HANDLE CreateThread( //创建线程 PSECURITY_ATTRIBUTES psa, DWORD cbStack, PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD fdwCreate, PDWORD pdwThreadID );
当进程创建成功后,会将进程句柄、主线程句柄、进程ID以及主线程ID存储在 下面的结构中:
typedef struct _PROCESS_INFORMATION { HANDLE hProcess; //进程句柄 HANDLE hThread; //主线程句柄 DWORD dwProcessId; //进程ID DWORD dwThreadId; //线程ID } PROCESS_INFORMATION;
到这里进程创建结束;
4)总结
每一个进程都需要由其它进程把它创建出来;
一般双击exe开启的进程都是由Explorer.exe调用CreateProcess来创建的;
CreateProcess所做的工作:
创建一个进程的内核对象;
给进程分配一个4GB的空间;
加载pe:包括exe、dll;修复dll;
创建主线程,把程序入口地址交给EIP;
既然Explorer.exe可以创建一个进程,我们也可以写程序模拟这种方式来创建一个进程;
程序加壳就是用这种思路;
2.创建进程
可以自己在程序中调用CreateProcess来创建一个进程;
BOOL CreateProcess(
PCTSTR pszApplicationName,
PTSTR pszCommandLine,
PSECURITY_ATTRIBUTES psaProcess,
PSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles,
DWORD fdwCreate,
PVOID pvEnvironment,
PCTSTR pszCurDir,
PSTARTUPINFO psiStartInfo,
PPROCESS_INFORMATION ppiProcInfo
);
pszApplicationName ->需要运行的程序名,该参数需要传一个字符串常量;
pszCommandLine ->相当于命令行运行程序时输入的内容;
psiStartInfo ->STARTUPINFO结构,用来设定进程的属性;
ppiProcInfo ->是一个out参数,传入PROCESS_INFORMATION结构,用来保存进程创建后的进程句柄、进程id、主线程句柄以及主线程id;
STARTUPINFO结构:
用来设定要创建的应用程序的属性,比如可以指定新创建的控制台程序的标题等待。
一般情况,只要为第一个成员赋值就可以了:si.cb = sizeof(si);
typedef struct _STARTUPINFO { DWORD cb; PSTR lpReserved; PSTR lpDesktop; PSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize; DWORD dwXCountChars; DWORD dwYCountChars; DWORD dwFillAttribute; DWORD dwFlags; WORD wShowWindow; WORD cbReserved2; PBYTE lpReserved2; HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError; } STARTUPINFO, *LPSTARTUPINFO;
1)main函数的参数
main的第二个参数是一个字符串数组,用来接收命令行参数;
代码:
#include<stdio.h> int main(int argc, char* argv[]){ printf("%s ", argv[0]); //第一个值为程序名 printf("%s ", argv[1]); //第二个值为第一个产数,依次类推 getchar(); return 0; }
在命令窗口运行程序:
可以看到通过main的第二个参数获取到了命令行中输入的字符串;
关于命令行运行程序:
如果在命令行中直接输入程序名不加.exe也能运行程序;
系统会自动给程序名后面加上.exe;
先从命令窗口的当前目录区找该exe文件,如果没有就去环境变量中找,没有就去操作系统中找;找不到就运行失败;
2)通过程序名创建进程
可以在程序中调用CreateProcess来创建一个进程:
#include<stdio.h> #include<windows.h> //通过程序名创建进程 VOID TestCreateProcessByAPPName() { STARTUPINFO si = {0}; PROCESS_INFORMATION pi; si.cb = sizeof(si); TCHAR szApplicationName[] =TEXT("c://program files//internet explorer//iexplore.exe"); BOOL res = CreateProcess( szApplicationName, //CreateProcess的第一个参数用来接收需要运行的程序名 NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi); } int main(int argc, char* argv[]){ TestCreateProcessByAPPName(); getchar(); return 0; }
结果:
ie浏览器被打开;
可以在任务管理器中看到ie的进程;程序结束后ie浏览器依然继续运行;
2)通过命令行创建进程
将命令行中的命令作为一个字符串传入CreateProcess的第二个参数;
//通过命令行创建进程 VOID TestCreateProcessByCmdline() { STARTUPINFO si = {0}; PROCESS_INFORMATION pi; si.cb = sizeof(si); TCHAR szCmdline[] =TEXT("c://program files//internet explorer//iexplore.exe http://www.4399.com"); //程序名和参数之间加空格 BOOL res = CreateProcess( NULL, szCmdline, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi); }
结果:ie浏览器被打开,并且打开了4399网址;
3)命令行和程序名组合使用
VOID TestCreateProcess() { STARTUPINFO si = {0}; PROCESS_INFORMATION pi; si.cb = sizeof(si); TCHAR szCmdline[] =TEXT(" http://www.4399.com"); BOOL res = CreateProcess( TEXT("c://program files//internet explorer//iexplore.exe"), //打开ie szCmdline, //通过命令行传入ie用到的网址 NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi); }
3.关于句柄和ID
内核对象创建后系统都会给一个句柄和一个ID;
大多数时候程序中使用的都是句柄;操作系统调度时使用的是ID;
1】都是系统分配的一个编号,句柄是客户程序使用 ID主要是系统调度时使用.
2】调用CloseHandle关闭进程或者线程句柄的时候,只是让内核计数器减少一个,
并不是终止进程或者线程.进程或线程将继续运行,直到它自己终止运行。
3】进程ID与线程ID 是不可能相同。
但不要通过进程或者线程的ID来操作进程或者线程,
因为,这个编号是会重复使用的,
也就是说,当你通过ID=100这个编号去访问一个进程的时候,它已经结束了,
而且系统将这个编号赋给了另外一个进程或者线程.
4.进程的终止
终止进程的三种方式:
VOID ExitProcess(UINT fuExitCode) //进程自己调用 BOOL TerminateProcess(HANDLE hProcess, UINT fuExitCode); //终止其他进程 ExitThread //终止进程中的所有线程,进程也会终止
获取进程终止时的退出码:
BOOL GetExitCodeProcess(HANDLE hProcess,PDWORD pdwExitCode);
终止线程时的相关操作:
1】进程中剩余的所有线程全部终止运行
2】进程指定的所有用户对象均被释放,所有内核对象均被关闭
3】进程内核对象的状态变成收到通知的状态
4】进程内核对象的使用计数递减1
5.句柄继承
一个进程创建时,会有一张句柄表;
子进程会复制一份父进程的句柄表;
在进程中用openXX可以得到内核对象的句柄,以此来实现进程间共享内核对象;
也可以通过句柄继承来实现内核对象共享;
1)思路
在一个进程中用CreateProcess创建一个子进程;
父进程中创建一个可继承的事件,事件是内核对象;
让内核对象可继承需要在调用CreateXX创建时传入的SECURITY_ATTRIBUTES结构中的第三个成员为TRUE;
子进程调用Wait函数等待该事件,如果事件变为已通知状态继续向下执行;
在子进程中不用OpenXX打开事件,如果能等到通知说明子进程的句柄表中已经有了事件的句柄,也就是继承成功;
子进程中等待事件通知需要知道是等待的哪个事件,不调用OPenXX无法直接得到事件句柄,可以通过命令行传参来将事件句柄告诉子进程;
2)子进程
代码:
#include<stdio.h> #include<windows.h> int main(int argc, char* argv[]){ char szBuffer[256] = {0}; memcpy(szBuffer,argv[1],8); DWORD dwHandle = 0; sscanf(szBuffer,"%x",&dwHandle); printf("%s ",argv[0]); printf("%x ",dwHandle); HANDLE g_hEvent = (HANDLE)dwHandle; printf("开始等待..... "); //当事件变成已通知时 WaitForSingleObject(g_hEvent, INFINITE); DWORD dwCode = GetLastError(); printf("等到消息.....%x ",dwCode); getchar(); }
编译成exe文件,放在桌面,等待父进程用OpenProcess打开;
3)父进程
调用OpenProcess创建一个子进程;
创建一个内核对象事件,来测试是否传递到子进程中;
代码:
#include<stdio.h> #include<windows.h> int main(int argc, char* argv[]){ char szBuffer[256] = {0}; char szHandle[8] = {0}; //若要创建能继承的句柄,父进程必须指定一个SECURITY_ATTRIBUTES结构并对它进行初始化 //三个成员的意义:大小、默认安全属性、是否可以继承 SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE; //创建一个可以被继承的内核对象 HANDLE g_hEvent = CreateEvent(&sa, TRUE, FALSE, NULL); //组织命令行参数 sprintf(szHandle,"%x",g_hEvent); sprintf(szBuffer,"C:\Users\Administrator\Desktop\jicheng_child.exe %s",szHandle); //定义创建进程需要用的结构体 STARTUPINFO si = {0}; PROCESS_INFORMATION pi; si.cb = sizeof(si); //创建子进程,用命令行参数创建 BOOL res = CreateProcess( NULL, szBuffer, NULL, NULL, TRUE, //TRUE的时候,允许把父进程句柄表中可继承的内核句柄继承到子进程的句柄表中 CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi); getchar(); //设置事件为已通知 SetEvent(g_hEvent); //关闭句柄 内核对象不会立刻销毁,因为继承一次后事件的计数器会加1;内核对象只有计数器为0才销毁 //CloseHandle调用后,操作系统会到进程的句柄表中找到目标句柄并移出,因此连续调2次CloseHandle没意义 CloseHandle(g_hEvent); return 0; }
结果:
子进程开始被阻塞;
当夫进程调用SetEvent将事件变为已通知后,子进程继续执行;
子进程中没有调用OpenXX函数打开事件的句柄也能等到通知说明子进程句柄表中已经有了事件的句柄;
也就是继承了父进程的句柄表;