句柄与句柄表(数据结构,源码分析)
0x01 句柄,句柄表概念
任意进程,只要每打开一个对象,就会获得一个句柄,这个句柄用来标志对某个对象的一次打开,通过句柄,可以直接找到对应的内核对象。句柄本身是进程的句柄表中的一个结构体,用来描述一次打开操作。句柄值则可以简单看做句柄表中的索引,并不影响理解。HANDLE的值可以简单的看做一个整形索引值。
每个进程都有一个句柄表,用来记录本进程打开的所有内核对象。句柄表可以简单看做为一个一维数组,每个表项就是一个句柄,一个结构体,一个句柄描述符,其结构体定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | typedef struct _HANDLE_TABLE_ENTRY //句柄描述符 { union { PVOID Object; //关键字段。该句柄指向的内核对象(注意是其头部) ULONG_PTR ObAttributes; //关键字段。该句柄的属性 PHANDLE_TABLE_ENTRY_INFO InfoTable; ULONG_PTR Value; //值(可见值本身是一个复合体),最低3位表示该句柄的属性(Value= Object | ObAttributes) }; union { ULONG GrantedAccess; //关键字段。该句柄的访问权限 struct { USHORT GrantedAccessIndex; USHORT CreatorBackTraceIndex; }; LONG NextFreeTableEntry; //当本句柄是一个空闲表项时,用来链接到句柄表中下一个空闲表项 }; } HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY; |
一 个 进 程 可 能 同 时 打 开 许 多 对 象 ,跟 许 多 对 象 建 立 连 接 , 还 可 以 跟 同 一 个 对 象 建 立 起 多 个 连 接 。 所 以 每 个 程 都 需 要 有 个 句 柄 表 用 来 记 录 , 维 持 这 些 连 接 。 所 以 , 句 柄 表 最 基 本 的 作
用 就 是 一 张 句 柄 与 目 标 对 象 之 间 的 对 照 表 , 而 句 柄 表 中 的 每 个 表 項 , 则 代 表 着 一 个 具 体 的 连 接 。 所 以 , 在 "进 程 控 制 控 制 *"EPROCESS#“中有 指 针 ObjectTable, 用 来指 向 本 进 程 向 柄 表。
句柄表结构体定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | typedef struct _HANDLE_TABLE //句柄表描述符 { ULONG TableCode; //表的地址|表的层数(该字段的最后两位表示表的层数) PHANDLE_TABLE_ENTRY **Table; PEPROCESS QuotaProcess; //所属进程 PVOID UniqueProcessId; //所属进程的PID EX_PUSH_LOCK HandleTableLock[4]; LIST_ENTRY HandleTableList; //用来挂入全局的句柄表链表(间接给出了系统中的进程列表) EX_PUSH_LOCK HandleContentionEvent; ERESOURCE HandleLock; LIST_ENTRY HandleTableList; KEVENT HandleContentionEvent; PHANDLE_TRACE_DEBUG_INFO DebugInfo; LONG ExtraInfoPages; ULONG FirstFree; //第一个空闲表项的索引位置 ULONG LastFree; //最后一个空闲表项的索引位置 ULONG NextHandleNeedingPool; //本句柄表本身占用的内存页数 LONG HandleCount; //表中的有效句柄总数 union { ULONG Flags; UCHAR StrictFIFO:1; }; } HANDLE_TABLE, *PHANDLE_TABLE; |
每当创建或打开了一个对象,要为之创建句柄并插入句柄表的时候,就为其准备一个临时的HANDLE_TABLE_ENTRY结构,使其指向这个对象的头部,然后通过EXCreateHandle将其“安装”句柄表中。而EXCreateHandle返回句柄,句柄的值表明了安装的位置。
ExCreateHandle函数原型(关键代码):
1 2 3 4 5 6 7 | HANDLE ExCreateHandle(PHANDLE_TABLE HandleTable, PHANDLE_TABLE_ENTRY HandleTableEntry) { EXHANDLE Handle; NewEntry = ExpAllocateHandleTableEntry(HandleTable,&Handle); //在句柄表中找到一个空闲表项<br> ... *NewEntry = *HandleTableEntry; //复制句柄表项<br> ... return Handle.GenericHandleOverlay; //返回句柄值(也即空闲表项的索引位置) } |
ObpCreateHandle函数打开对象,获得句柄:
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 46 47 48 49 50 51 52 53 54 55 56 57 58 | NTSTATUS ObpCreateHandle(IN OB_OPEN_REASON OpenReason, //4种打开时机 IN PVOID Object, //要打开的对象 IN PACCESS_STATE AccessState, //句柄的访问权限 IN ULONG HandleAttributes, //句柄的属性 IN KPROCESSOR_MODE AccessMode, OUT PHANDLE ReturnedHandle) //返回的句柄值 { BOOLEAN AttachedToProcess = FALSE, KernelHandle = FALSE; NewEntry.Object = ObjectHeader; //关键。将该句柄指向对应的对象头 if (HandleAttributes & OBJ_KERNEL_HANDLE) //如果用户要求创建一个全局型的内核句柄 { HandleTable = ObpKernelHandleTable; //改用内核句柄表 KernelHandle = TRUE; //将当前线程挂靠到system进程,也即修改当前的CR3,将页表换成system进程的页表 if (PsGetCurrentProcess() != PsInitialSystemProcess) { KeStackAttachProcess(&PsInitialSystemProcess->Pcb, &ApcState); AttachedToProcess = TRUE; } } else HandleTable = PsGetCurrentProcess()->ObjectTable; //使用当前进程的句柄表 //检查是否可以独占打开,检查权限,若各项检查通过才打开对象,递增句柄计数,调用对象的OpenProcedure等等工作 Status = ObpIncrementHandleCount(Object, AccessState, AccessMode, HandleAttributes, PsGetCurrentProcess(), OpenReason); if (!NT_SUCCESS(Status)) return Status; NewEntry.ObAttributes |= (HandleAttributes & OBJ_HANDLE_ATTRIBUTES); //填上句柄的属性 DesiredAccess =AccessState->RemainingDesiredAccess|AccessState->PreviouslyGrantedAccess; GrantedAccess = DesiredAccess &(ObjectType->TypeInfo.ValidAccessMask); NewEntry.GrantedAccess = GrantedAccess; //填上句柄的属性 Handle = ExCreateHandle(HandleTable, &NewEntry); //将句柄插入到句柄表中 if (Handle) //if 插入成功 { if (KernelHandle) Handle = ObMarkHandleAsKernelHandle(Handle); //将句柄值的最高位设为1,标记为内核句柄 *ReturnedHandle = Handle; if (AttachedToProcess) KeUnstackDetachProcess(&ApcState); //撤销挂靠 return STATUS_SUCCESS; } Else { … return STATUS_INSUFFICIENT_RESOURCES; } } |
打开对象,以得到一个访问句柄。有四种打开时机:
1、 创建对象时就打开,如CreateFile在创建一个新文件时,就同时打开了那个文件对象
2、 显式打开,如OpenFile,OpenMutex,OpenProcess显式打开某个对象
3、 DuplicateHandle这个API间接打开对象,获得句柄
4、 子进程继承父进程句柄表中的句柄,也可看做是一种打开
在这四种情况下,都会调用这个函数来打开对象,得到一个句柄。OpenReason参数就是指打开原因、时机
注意句柄值的最高位为1,就表示这是一个内核全局句柄,可以在各个进程中通用。否则,一般的句柄,只能在对应的进程中有意义。
另外有两个特殊的伪句柄,他们并不表示‘索引’,而是一个简单的代号值
GetCurrentProcessHandle 返回的句柄值是-1
GetCurrentThreadHandle 返回的句柄值是-2
对这两个句柄要特殊处理。
句柄不光含有指向对象的指针,每个句柄都还有自己的访问权限与属性,这也是非常重要的。访问权限表示本次打开操作要求的、申请的并且最终得到的权限。句柄属性则表示本句柄是否可以继承,是否是独占打开的,是否是一个内核句柄等属性。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗