win32 进程创建_句柄表


在之前的学习中,我们了解到了,程序、镜像(ImageBuffer)、进程。那么这三个有什么区别呢?

通俗来讲程序就是写好的.exe但是还没有运行,死的一个东西;镜像是将程序按照PE格式拉伸贴到4GB空间中的东西,而进程就是这个镜像跑起来后的东西。当eip”给到”这个ImageBufffer时这个就运行起来了

父进程创建子进程,父进程挂了子进程不会挂

一、进程创建的过程


image-20240916164833028

步骤一:

当系统启动后,会创建一个进程:Explorer.exe 也就是桌面进程。

步骤二:

当用户双击某一个exe时,Explorer 进程使用CreateProcess函数创建被双击的exe,也就是说:我们在桌面上双击创建的进程都是Explorer进程的子进程。

我们可以通过XueTr.exe ,查看那些进程是由Explorer创建的。

image-20240916173018779

二、CreateProcess函数做了什么

1
2
3
4
5
6
7
8
9
10
11
12
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
);

参数说明

现在先主要了解几个,其他的等用到再了解:

  1. lpApplicationName : 要执行的程序的名称,字符串可以指定要执行的模块的完整路径和文件名,也可以指定部分名称。如果只有程序名称没有路径,则系统会按照以下顺序解释:
1
2
3
4
c:\program.exe
c:\program files\sub.exe
c:\program files\sub dir\program.exe
c:\program files\sub dir\program name.exe
  1. [in, out, optional] lpCommandLine : 要执行的命令行

  2. [in] bInheritHandles : 如果这个参数为TRUE,句柄表可继承的句柄都会被由这个进程创建的进程继承。如:A进程句柄表中的存在0x1句柄,并且这个句柄可以被继承,如果A进程通过CreateProcess创建出进程B则进程B,就能继承这个0x1

  3. [in] lpStartupInfo : 指向STARTUPINFOSTARTUPINFOEX结构的指针,当不使用时,必须用CloseHandle关闭他们。

  4. [out] lpProcessInformation : 指向接收有关新进程的标识信息的 PROCESS_INFORMATION 结构的指针。

1、创建内核对象

image-20240916173423991

当CreateProcess后,就会创建这个句柄表,但是这个表刚刚创建的时候是空的。得做Create内核对象时计数器++,然后往表里面写东西。

句柄表:第一列就是句柄,也就是我们常用的hThread之类的,相当与是内核对象的编号;第二列是内核对象的真正地址,第三列表示能不能被继承

2、分配4GB的虚拟内存空间(Windows 32位)

image-20240916173854180

创建线程的过程:

  1. 将exe拉伸,存储到指定位置
  2. 遍历exe导入表,将需要用到的dll拉伸存储到指定位置,如果位置被占用,换地方,并通过DLL的重定位表,修复全局
  3. DLL如果引用了其他DLL,递归第二步
  4. 修复exe/dll中的IAT表
  5. 创建线程、设置线程CONTEXT开始执行

3、创建进程的主线程

image-20240916174230289

三、创建进程(示例代码)


代码一:

通过名字创建,也就是CreateProcess第一个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
VOID TestCreateProcessByAPPName()					
{
STARTUPINFO si = {0};
PROCESS_INFORMATION pi;

si.cb = sizeof(si);

TCHAR szApplicationName[] =TEXT(文件路径); // 这里是程序的绝对地址

BOOL res = CreateProcess(
szApplicationName,
NULL,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE,
NULL,
NULL, &si, &pi);

}

image-20240916175237084

执行后直接打开了pe查看器

代码二:

通过命令行打开(参数二):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
VOID TestCreateProcessByCmdline()					
{
STARTUPINFO si = {0};
PROCESS_INFORMATION pi;

si.cb = sizeof(si);

TCHAR szCmdline[] =TEXT("c://program files//internet explorer//iexplore.exe http://www.ifeng.com"); // 程序路径 如果是浏览器还可以空格后面加网址

BOOL res = CreateProcess(
NULL,
szCmdline,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE,
NULL,
NULL, &si, &pi);
}

代码三:

两个参数一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
VOID TestCreateProcess()					
{
STARTUPINFO si = {0};
PROCESS_INFORMATION pi;

si.cb = sizeof(si);

TCHAR szCmdline[] =TEXT(" http://www.ifeng.com");

BOOL res = CreateProcess(
TEXT("c://program files//internet explorer//iexplore.exe"),
szCmdline,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE,
NULL,
NULL, &si, &pi);
}

接下来看看最后两个结构体是什么东西:

STARTUPINFO:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
si.cb = sizeof(si);
PROCESS_INFORMATION:

也就是最后一个参数的结构体

1
2
3
4
5
6
7
8
typedef struct _PROCESS_INFORMATION					
{
HANDLE hProcess; //进程句柄
HANDLE hThread; //主线程句柄
DWORD dwProcessId; //进程ID
DWORD dwThreadId; //线程ID
} PROCESS_INFORMATION;

在CreateProcess函数参数说明中,这个参数前面有一个[out]的东西,说明这个是CreateProcess函数输出的详细。这个结构是用来存放 进程和主线程的句柄和ID的,我们可以用以下代码来查看:

1
2
printf("dwProcessId: %x dwThreadId: %x\nhProcess: %x hThread: %x\n", pi.dwProcessId, pi.dwThreadId, pi.hProcess, pi.hThread);

输出:

image-20240916180342294

关于句柄和ID

  1. 都是系统分配的一个编号,句柄是客户程序使用,ID主要是系统调度时使用
  2. 调用CloseHandle关闭进程或者线程句柄的时候,只是让内核计数器减少一,并不是终止进程或者线程。进程猴子线程将继续运行,直到它自己终止运行
  3. 在进程执行过程中,进程Id与线程id 是不可能相同的。但是不要通过进程或者线程ID来操作进程或者线程,因为当进程关闭或意外中断再打开后id就不是原来的ID了,因为系统会把这个ID给了其他进程或者线程

四、进程终止


进程终止的三种方式:

1
2
3
VOID ExitProcess(UINT fuExitCode)								// 进程会自己调用
BOOL TerminateProcess(HANDLE hProcess, UINT fuExitCode); // 终止其他进程
ExitThread // 终止所有线程,系统就会把进程杀掉

获取进程的退出码:

1
BOOL GetExitCodeProcess(HANDLE hProcess, PDWORD pdwExitCode);

进程终止时相关操作:

  1. 进程中剩余的所有线程全部停止运行
  2. 进程指定的所有用户对象均被释放,所有内核对象均被关闭
  3. 进程内核对象的状态变成收到通知的状态
  4. 进程内核对象的使用计数递减1

五、句柄的继承


让不同进程间拥有相同的内核对象

首先先了解一下需要用到的知识

1、命令行参数的使用

1
2
3
4
5
6
7
char szBuffer[256] = {0};
memcpy(szBuffer,argv[1],8);
DWORD dwHandle = 0;
sscanf(szBuffer,"%x",&dwHandle);
printf("%s\n",argv[0]);
printf("%x\n",dwHandle);
getchar();

2、 句柄的继承

我们可以通过创建一个事件内核对象来验证这个句柄的继承:

进程A中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
VOID TestExtendHandle()
{
CHAR szBuffer[256] = { 0 };
memset(szBuffer, 0, 256);
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_s(szHandle, 8, "%d", g_hEvent);
sprintf_s(szBuffer, 256, "C:\\Users\\lys05\\Desktop\\test3.exe %s", szHandle);

// 定义创建进程需要用到的结构体
STARTUPINFOA si = { 0 };
si.cb = sizeof(si);

PROCESS_INFORMATION pi;

// 创建子进程
BOOL ret = CreateProcessA(
NULL,
szBuffer,
NULL,
NULL,
TRUE,
CREATE_NEW_CONSOLE,
NULL,
NULL,
&si, &pi
);

// 设置事件为已通知
SetEvent(g_hEvent);

// 关闭句柄 内核对象是否会被销毁?
CloseHandle(g_hEvent);
}

进程B中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(int argc,char* argv[])
{

CHAR szBuffer[256] = { 0 };
memcpy(szBuffer, argv[1],8);
DWORD dwHandle = 0;
sscanf_s(szBuffer, "%d", &dwHandle);
printf_s("%s\n", argv[0]);
printf_s("%d\n", dwHandle);

HANDLE g_hEvent = (HANDLE)dwHandle;
printf_s(".....开始等待\n");
WaitForSingleObject(g_hEvent, INFINITE);
DWORD dwCode = GetLastError();
printf_s("%d\n", dwCode);
printf_s("等到消息.....\n");
getchar();
}

思路就是进程A创建出一个可继承的事件,然后进程A创建出进程B,并且让进程B继承进程A的句柄表,此时B的句柄表中就就有A创建的事件的句柄,A创建B时又是通过命令行传参,此时B就可以通过参数获得事件的句柄,然后存到g_hEvent当中,接下来就是等待线程A将事件设置成已通知了。然后B就可以进行往下执行了

image-20240916224020671

可以看到已经输出了等待到消息,可以说明进程B继承了A的句柄表

注意: 这里参数传递有个坑,当我们要用sscanf将g_hEvent传到szBuffer中,里面如果用”%d”,则线程B中获取sprintf中的占位符也要用”%d”

形象化:

image-20240916224324742

image-20240916224335478

image-20240916224345497