欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Windows内核对象(2) -- 内核对象跨进程访问

程序员文章站 2022-06-03 18:25:49
...

虽然内核对象位于独立于进程之外的内核区域,我们在开发中却只能通过调用Win32 API传入HANDLE参数来操作内核对象(如SetEvent等)。然而HANDLE句柄只对当前进程有效,离开了当前进程该句柄就无效了(具体原因参考:Windows内核对象(1) – 内核对象与句柄)。所以说,跨进程访问内核对象的关键在于我们怎么跨进程访问句柄HANDLE

下面介绍几种方法来实现跨进程共享内核对象。

一、使用句柄继承的方式

只有进程之间有父子关系时,才可以使用句柄继承的方式。在这种情况下,父进程可以生成一个子进程,并允许子进程访问父进程的内核对象。为了使这种继承生效,父进程必须执行几个步骤:
(1). 父进程在创建一个内核对象时,父进程必须向系统指定它希望这个内核对象的句柄是可以继承的。为了创建一个可继承的内核对象,必须分配并初始化一个SECURITY_ATTRIBUTES结构,如:

SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE;  // 可继承的
sa.lpSecurityDescriptor = NULL;

HANDLE h = CreateEvent(&sa, TRUE, FALSE, NULL); 

(2). 父进程通过CreateProcess生成子进程,且指定bInheritHandles为TRUE,从而允许子进程来继承父进程的那些“可继承的句柄”。

// 启动子进程TestB.exe,将句柄h作为启动参数传给进程TestB
//
TCHAR cmd_buf[MAX_PATH];
StringCchPrintf(cmd_buf, MAX_PATH, TEXT("TestB.exe %ld"), (long)h);

STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
BOOL ret = CreateProcess(
    NULL, 
    cmd_buf, 
    NULL, 
    NULL, 
    TRUE,  // 指定子进程可以继承父进程的“可继承句柄”
    0, 
    NULL, 
    NULL, 
    &si, 
    &pi
);


CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);

由于我们传给bInheritHandles参数的值是TRUE,所以系统在创建子进程时会多做一件事情:它会遍历父进程的句柄表,对它的每一项进行检查,凡是包含一个有效的“可继承的句柄”的项,都会将该项完整的复制到子进程的句柄表。在子进程的句柄表中,复制项的位置与它在父进程句柄表中的位置完全一样(包含索引),这个就意味着:在父进程和子进程中,对一个内核对象进行标识的句柄值也是完全一样的。所以我们只需要通过某种方式(如上面示例中的启动参数的方式,或者环境变量的方式等任何进程间通讯的方式)将这个值告诉子进程,子进程就可以将该值转成HANDLE,然后使用这个HANDLE来调用系统API。

二、使用DuplicateHandle方式

明白DuplicateHandle的工作原理,需要先了解进程句柄表,可以参考Windows内核对象(1) – 内核对象与句柄

2.1 DuplicateHandle功能

DuplicateHandle函数可以将指定“源进程的句柄表”中的某一项复制到“目的进程句柄表”中(除了索引),并且返回该项在目的进程句柄表中的索引(即HADNLE)。
可以在任何时候调用DuplicateHandle函数,DuplicateHandle对源句柄是否是可继承的没有要求。

函数声明如下:

BOOL DuplicateHandle(
  HANDLE hSourceProcessHandle,
  HANDLE hSourceHandle,
  HANDLE hTargetProcessHandle,
  LPHANDLE lpTargetHandle,
  DWORD dwDesiredAccess,
  BOOL bInheritHandle,
  DWORD dwOptions
);

DuplicateHandle详细介绍可以参考MSDN:https://msdn.microsoft.com/en-us/library/windows/desktop/ms724251(v=vs.85).aspx

2.2 支持的句柄类型

DuplicateHandle函数不能复制所有类型的句柄,只能复制如下类型的句柄(从MSDN复制而来):

Object Description
Access token The handle is returned by the CreateRestrictedToken, DuplicateToken, DuplicateTokenEx, OpenProcessToken, or OpenThreadToken function.
Change notification The handle is returned by the FindFirstChangeNotification function.
Communications device The handle is returned by the CreateFile function.
Console input The handle is returned by the CreateFile function when CONIN$ is specified, or by the GetStdHandle function when STD_INPUT_HANDLE is specified. Console handles can be duplicated for use only in the same process.
Console screen buffer The handle is returned by the CreateFile function when CONOUT$ is specified, or by the GetStdHandle function when STD_OUTPUT_HANDLE is specified. Console handles can be duplicated for use only in the same process.
Desktop The handle is returned by the GetThreadDesktop function.
Event The handle is returned by the CreateEvent or OpenEvent function.
File The handle is returned by the CreateFile function.
File mapping The handle is returned by the CreateFileMapping function.
Job The handle is returned by the CreateJobObject function.
Mailslot The handle is returned by the CreateMailslot function.
Mutex The handle is returned by the CreateMutex or OpenMutex function.
Pipe A named pipe handle is returned by the CreateNamedPipe or CreateFile function. An anonymous pipe handle is returned by the CreatePipe function.
Process The handle is returned by the CreateProcess, GetCurrentProcess, or OpenProcess function.
Registry key The handle is returned by the RegCreateKey, RegCreateKeyEx, RegOpenKey, or RegOpenKeyEx function. Note that registry key handles returned by the RegConnectRegistry function cannot be used in a call to DuplicateHandle.
Semaphore The handle is returned by the CreateSemaphore or OpenSemaphore function.
Thread The handle is returned by the CreateProcess, CreateThread, CreateRemoteThread, or GetCurrentThread function
Timer The handle is returned by the CreateWaitableTimer or OpenWaitableTimer function.
Transaction The handle is returned by the CreateTransaction function.
Window station The handle is returned by the GetProcessWindowStation function.

不同的事件类型对应的dwDesiredAccess参数不同,具体参考MSDN

2.3 使用示例

进程TestA源码

int main(int argc, char** argv) {
    HANDLE h = CreateEvent(NULL, TRUE, FALSE, NULL);

    // 启动子进程TestB.exe
    //
    TCHAR cmd_buf[MAX_PATH];
    StringCchPrintf(cmd_buf, MAX_PATH, TEXT("D:\\TestB.exe"), (long)h);

    STARTUPINFO si = { sizeof(si) };
    PROCESS_INFORMATION pi;
    BOOL ret = CreateProcess(NULL, cmd_buf, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
    assert(ret);
    assert(pi.hProcess);

    HANDLE duplicated_h = NULL;
    ret = DuplicateHandle(GetCurrentProcess(), h, pi.hProcess, &duplicated_h, 0, FALSE, DUPLICATE_SAME_ACCESS);


    WaitForSingleObject(pi.hProcess, INFINITE);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    bool has_signal = WaitForSingleObject(h, 0) == WAIT_OBJECT_0;
    assert(has_signal == true);

    return 0;
}

子进程TestB源码

int main(int argc, char** argv)
{
    long l = 0;
    printf("Input Handle:");
    scanf("%ld", &l);

    HANDLE h = (HANDLE)l;

    bool has_signal = WaitForSingleObject(h, 0) == WAIT_OBJECT_0;
    assert(has_signal == false);

    SetEvent(h);

    return 0;
}

在父进程TestA中创建一个不可继承的事件 -> 然后启动子进程TestB -> 调用DuplicateHandle复制句柄项到TestB进程句柄表 -> 并向TestB输入句柄值 -> TestB访问该事件句柄,将事件置为有信号状态。

三、使用命名的内核对象的方式

3.1 实现原理

这种方式严格的说已经不是文章开头说到的跨进程访问句柄了,有点类似跨进程直接访问内核对象了。
该方式实现起来比较简单,就是在调用创建内核对象的Create***函数时,通过pszName参数为内核对象取一个名字。
如创建事件Event的函数CreateEvent

HANDLE WINAPI CreateEvent(
  LPSECURITY_ATTRIBUTES lpEventAttributes,
  BOOL bManualReset,
  BOOL bInitialState,
  LPCTSTR lpName  // 指定名称
);
HANDLE h = CreateEvent(NULL, TRUE, FALSE, TEXT("TestA_Obj"));

若在其他进程中要访问这个内核对象,只需要使用打开函数Open***打开该内核对象,系统就会在进程的句柄表中插入一条记录,并返回这条记录的索引,也就是句柄。需要注意的是,在打开内核对象时需要留意返回值GetLastError函数的返回值。由于内核对象是有访问权限的,有时候虽然这个名字的内核对象存在,但该进程却不见得有权限可以打开它,这个时候GetLastError函数会返回失败的原因。

以打开事件的函数OpenEvent为例:

HANDLE h = OpenEvent(READ_CONTROL, FALSE, TEXT("TestA_Obj"));
if (h == NULL) {
    if (GetLastError() == ERROR_ACCESS_DENIED) { // 没有READ_CONTROL权限

    }
}

3.2 全局命令空间

不同的会话(Session)有不同的内核对象命名空间(如windows服务程序位于Session 0,而普通的用户进程位于Session 1),要通过名称访问其他会话中的内核对象,需要在名称前面加上Session\<当前会话ID>。Windows提供了一个全局的内核对象命名空间,处于任何会话中的进程都可以访问该命名空间,将内核对象放入全局命令空间的方式很简单:只需要在内核对象名称前加入Global\即可。

如:

HANDLE h = CreateEvent(NULL, TRUE, FALSE, TEXT("Global\\TestA_Obj"));