跨进程边界共享内核对象

内核对象

内核对象实际就是一块内存,其包含一些数据结构被操作系统使用用来控制和管理对应的内核对象。因为是内核对象,所以此内存的地址空间位于内核中(也就是高地址中)。因为对于每一个进程来说其内核部分的地址空间每次映射的都一样,所以一个进程中创建的内核对象在其他进程的内核地址空间中也是存在的,因此说内核对象是属于操作系统内核的,而不属于进程。

句柄

为了能够在进程中对内核对象进行一系列操作需要一个标识符来标识内核对象,此标识符就是句柄。因为进程中可能会创建多个内核对象,所以进程维护了一张句柄表(注意此句柄表只针对内核对象,GDI对象和普通对象并没有句柄表)。句柄表中每一项都标识一个内核对象,且内核对象句柄是属于进程的,不同的进程可能会使用不同的句柄访问同一个内核对象。

跨进程共享内核对象

句柄继承

所谓的句柄继承就是指父进程通过创建子进程将父进程句柄表中可继承的句柄复制到子进程的对应的句柄表的相同位置,这样在使用同一个句柄值来表示父进程与子进程中相同的句柄项,并通过此句柄项来访问内核中的同一个内核对象。

实现句柄继承的过程

typedef struct _SECURITY_ATTRIBUTES {
  DWORD  nLength;                            //结构大小                    
  LPVOID lpSecurityDescriptor;               //安全属性(一般为NULL采用默认属性)
  BOOL   bInheritHandle;                     //句柄是否可继承
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;

在利用匿名管道实现进程间通讯时就用到了句柄继承,第一步就是需要在创建对应的内核对象时指定其安全描述符bInheritHandle字段为TRUE表示此内核对象的句柄可继承。

CreateProcess(szExePath,
		NULL,
		NULL,
		NULL,
		TRUE,											//句柄可继承
		CREATE_NO_WINDOW,
		NULL,
		NULL,
		&si,
		&pi);

CreateProcess创建子进程时其第五个参数bInheritHandles要为TRUE,表示此进程创建的子进程可以继承本进程的句柄。这样就可以在子进程中使用相同的句柄值来操作同一个内核对象,那如何让子进程知道对应内核对象的句柄值为多少呢,通常会利用命令行或者其他进程间通讯机制(IPC)将父进程中的句柄值传递到子进程中。

内核对象命名

如果在一个进程中创建一个命名的内核对象,那么在其他进程中可以通过打开此特定名称的内核对象来重新生成一个标识此内核对象的句柄,然后通过此句柄在操作这个内核对象。但是在命名内核对象的时候存在一个命名空间的问题。

终端服务命名空间


Windows是支持多用户登录的操作系统,每登录一个用户相当于创建了一个会话。一般终端服务是运行在会话0,而接着其他用户运行在其他会话中。其中每一个会话都有其对应的命名空间,会话0也就是终端服务的命名空间是全局的,在其他会话中也可以访问到。而除了会话0之外的其他会话的命名空间都是局部的,也就是不同会话的命名空间是相互隔离的,无法互相访问。

//将内核对象放在全局命名空间
HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("Global\1234567"));

//将内核对象放在局部命名空间,即当前会话的命名空间中
HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("Local\1234567"));

我们可以通过使用特定的名称前缀对内核对象命名,如果前缀为"Global"则会强制将内核对象放在全局命名空间中,如果前缀为"Local"则会强制将内核对象放在局部命名空间中。

HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("1234567"));

如果我们在创建命名内核对象时不给出上述两种名称前缀则系统会默认将创建的内核对象放在局部命名空间中,也就是当前会话的命名空间中。因为不同的会话的命名空间是相互隔离的(不算会话0),所以可以在不同的会话中创建名称相同的命名内核对象。

私有命名空间

有时我们为了实现应用程序运行单一实例会使用命名内核对象实现,如果有恶意攻击者利用自写程序创建相同名称的内核对象并运行程序的话,我们的程序会在运行时误认为已经存在一个实例而直接结束运行。为了防止这种事情的发生,我们可以创建一个私有的命名空间,为私有命名空间提供一个名称前缀就像“Local”和“Global”一样。

HANDLE hBoundary = CreateBoundaryDescriptor(TEXT("1234321"), 0);				//创建边界描述符


BYTE localAdminSID[SECURITY_MAX_SID_SIZE];							//创建一个SID(NULL表示创建本机的SID)
DWORD cbSID = sizeof(localAdminSID);
CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, &localAdminSID, &cbSID);


AddSIDToBoundaryDescriptor(&hBoundary, &localAdminSID);						//将SID与边界描述符关联起来


SECURITY_ATTRIBUTES sa;										//构造安全描述符
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = FALSE;
ConvertStringSecurityDescriptorToSecurityDescriptor(TEXT("D:(A;;GA;;;BA)"), SDDL_REVISION_1, &sa.lpSecurityDescriptor, NULL);


HANDLE hNameSpace = CreatePrivateNamespace(&sa, hBoundary, TEXT("SpecialNameSpace"));		//创建私有命名空间

//可以创建以私有命名空间名称为前缀的命名内核对象


LocalFree(sa.lpSecurityDescriptor);								//释放安全描述符所占用的内存
ClosePrivateNamespace(hNameSpace, PRIVATE_NAMESPACE_FLAG_DESTROY);		                //关闭私有命名空间(第二个参数表明其关闭后不可见,为0表示可见)
DeleteBoundaryDescriptor(hBoundary);								//释放边界描述结构的内存

以上就是创建私有命名空间的步骤,通过创建一个边界描述符,并将此边界描述符与一个会话SDI关联。接着就可以创建一个私有命名空间并给其指定一个名称。这样Windows会在当此SDI会话标识的用户创建或打开此私有命名空间时才会成功,其他会话都会失败。

句柄复制

所谓的句柄复制和句柄非常相似,都是将一个进程的句柄表中的句柄项复制到另一个进程的句柄表中。只不过句柄继承是将父进程的句柄复制到子进程的句柄表的相同位置,所以其句柄值相等。而句柄复制并不一定将句柄复制到另一个进程的相同位置处,所以其两个进程所使用的句柄值一般是不同的。

HANDLE hObjectInProcessA = CreateMutex(NULL, FALSE, NULL);			//A进程创建一个内核对象
HANDLE hProcessB = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID_A);	//通过B进程的PID获得B进程句柄

HANDLE hObjectInProcessB;	      						//将A进程中刚刚创建的内核对象句柄复制到B进程中,返回内核对象在B进程的句柄值
DuplicateHandle(GetCurrentProcess(), hObjectInProcessA, hProcessB, &hObjectInProcessB, 0, FALSE, DUPLICATE_SAME_ACCESS);

CloseHandle(hObjectInProcessA);
CloseHandle(hProcessB);

上述代码就实现了将A进程中的句柄复制到B进程中,返回对应内核对象在B进程中的句柄值。我们需要利用其它进程间通讯机制(IPC)将hObjectInProcessB句柄值传给B进程。

参考:《Windows核心编程》(第五版)

原文地址:https://www.cnblogs.com/revercc/p/14111543.html