温馨提示

本文的内容均在Windows 11 Enterprise(22000.466)版本下测试

不同版本的部分内容可能存在差异,但万变不离其中

[Upadate 20220803]经测试,本文内容目前向下兼容

正文

Part1.理论知识

PspCidTable是一个指向类型为_HANDLE_TABLE的指针

0: kd> dp Pspcidtable
fffff801`6bf195d0  ffffb10c`bd635180 ffffd087`1bec6da0
fffff801`6bf195e0  00000000`00000000 00010000`00000000
fffff801`6bf195f0  00000000`00001000 00000000`00000000
fffff801`6bf19600  00000000`00000000 0000a503`00000000
fffff801`6bf19610  00000000`00000000 00000000`00000000
fffff801`6bf19620  00000000`00000000 00000000`00000000
fffff801`6bf19630  00000000`00000000 00000000`00000000
fffff801`6bf19640  ffffd087`1bef4bc0 fffff801`6c263000

我们可以通过WinDbg查看该结构

0: kd> dt _handle_table ffffb10c`bd635180
nt!_HANDLE_TABLE
   +0x000 NextHandleNeedingPool : 0x1c00
   +0x004 ExtraInfoPages   : 0n0
   +0x008 TableCode        : 0xffffb10c`c118e001
   +0x010 QuotaProcess     : (null) 
   +0x018 HandleTableList  : _LIST_ENTRY [ 0xffffb10c`bd635198 - 0xffffb10c`bd635198 ]
   +0x028 UniqueProcessId  : 0
   +0x02c Flags            : 1
   +0x02c StrictFIFO       : 0y1
   +0x02c EnableHandleExceptions : 0y0
   +0x02c Rundown          : 0y0
   +0x02c Duplicated       : 0y0
   +0x02c RaiseUMExceptionOnInvalidHandleClose : 0y0
   +0x030 HandleContentionEvent : _EX_PUSH_LOCK
   +0x038 HandleTableLock  : _EX_PUSH_LOCK
   +0x040 FreeLists        : [1] _HANDLE_TABLE_FREE_LIST
   +0x040 ActualEntry      : [32]  ""
   +0x060 DebugInfo        : (null) 

其中,+0x8 TableCode就是我们所需的全局句柄表的地址

关于该表,有如下命名规则

满足条件含义翻译成人话
(TableCode & 3) == 0该指针所指句柄表为一级句柄表TableCode的二进制低两位都是0,遍历此表可以直接得到我们要的信息
(TableCode & 3) == 1该指针所指句柄表为二级句柄表TableCode的二进制低两位为01,此表中表项指向一级句柄表
(TableCode & 3) == 2该指针所指句柄表为三级句柄表TableCode的二进制低两位为10,此表中的表项指向二级句柄表

关于各级句柄表的关系,如图所示:

5cc3423efeac575354904e8d2eb22c2d.png

!!!更正上图!由于忘记保存EXCEL文件,口头补充说明,对于一级表每16bytes才有一次数据、有八字节空;二三级表则每隔8bytes都有一次数据!!!

关于这个句柄表,微软这个糟老头子在Win7 x32还是没有加密的

但是到Win7x64以后开始就开始加密了,我们可以通过反汇编查看PsLookUpProcessByProcessId => PspReferenceCidTableEntry函数的实现过程来解决

以22000.466为例,如下为IDA反汇编结果

_BYTE *__fastcall PspReferenceCidTableEntry(__int64 a1, char a2)
{
  volatile signed __int64 *v3; // rax
  volatile signed __int64 *v4; // rsi
  __int64 v5; // r14
  signed __int64 v6; // rcx
  __int64 v7; // rdi
  unsigned __int128 v8; // rt0
  unsigned __int8 v9; // tt
  unsigned __int64 v10; // rax
  _BYTE *v11; // rdi
  int v13; // ebx
  bool v14; // zf
  signed __int64 v15; // rax
  signed __int64 v16; // rtt
  __int64 v17; // rcx
  _QWORD *v18; // rcx
  unsigned __int64 v19; // rax
  int v20[8]; // [rsp+0h] [rbp-48h] BYREF
  unsigned __int128 v21; // [rsp+20h] [rbp-28h]
  __int128 v22; // [rsp+30h] [rbp-18h]

  if ( (a1 & 0x3FC) == 0 )
    return 0i64;
  v3 = (volatile signed __int64 *)ExpLookupHandleTableEntry(PspCidTable, a1);
  v4 = v3;
  if ( !v3 )
    return 0i64;
  v5 = PspCidTable;
  _m_prefetchw((const void *)v3);
  *(_QWORD *)&v21 = *v3;
  v6 = *((_QWORD *)v3 + 1);
  *((_QWORD *)&v21 + 1) = v6;
  v7 = v21;
  if ( (v21 & 0x1FFFE) == 0 )
  {
LABEL_10:
    v13 = 0;
    if ( !(unsigned __int8)ExLockHandleTableEntry(PspCidTable, v4) )
      return 0i64;
    v11 = (_BYTE *)((*(__int64 *)v4 >> 16) & 0xFFFFFFFFFFFFFFF0ui64);
    if ( (*v11 & 0x7F) == a2 )
    {
      if ( a2 == 3 )
        v14 = (*(_DWORD *)(((*(__int64 *)v4 >> 16) & 0xFFFFFFFFFFFFFFF0ui64) + 0x464) & 0x400000C) == 0x4000000;
      else
        v14 = (*(_DWORD *)(((*(__int64 *)v4 >> 16) & 0xFFFFFFFFFFFFFFF0ui64) + 0x560) & 3) == 2;
      if ( v14 )
        v13 = ExSlowReplenishHandleTableEntry(v4);
      _m_prefetchw(v11 - 48);
      v15 = *((_QWORD *)v11 - 6);
      if ( v15 )
      {
        while ( 1 )
        {
          v16 = v15;
          v15 = _InterlockedCompareExchange64((volatile signed __int64 *)v11 - 6, (unsigned int)(v13 + 1) + v15, v15);
          if ( v16 == v15 )
            break;
          if ( !v15 )
            goto LABEL_25;
        }
        if ( ObpTraceFlags )
          ObpPushStackInfo((_DWORD)v11 - 48);
LABEL_20:
        v17 = PspCidTable;
        _InterlockedExchangeAdd64(v4, 1ui64);
        v18 = (_QWORD *)(v17 + 48);
        _InterlockedOr(v20, 0);
        if ( *v18 )
          ExfUnblockPushLock(v18, 0i64);
        return v11;
      }
LABEL_25:
      v19 = *v4 & 0xFFFFFFFFFFFE0001ui64;
      v22 = v19;
      *v4 = v19;
    }
    v11 = 0i64;
    goto LABEL_20;
  }
  while ( 1 )
  {
    if ( (v7 & 1) == 0 )
    {
      ExpBlockOnLockedHandleEntry(v5, v4, v7);
      _m_prefetchw((const void *)v4);
      v6 = *((_QWORD *)v4 + 1);
      *(_QWORD *)&v21 = *v4;
      v7 = v21;
      *((_QWORD *)&v21 + 1) = v6;
      goto LABEL_27;
    }
    *(_QWORD *)&v8 = v7;
    *((_QWORD *)&v8 + 1) = v6;
    v9 = _InterlockedCompareExchange128(v4, v6, v7 - 2, (signed __int64 *)&v8);
    v6 = v8 >> 64;
    v10 = v8;
    v7 = v8;
    v21 = v8;
    if ( v9 )
      break;
LABEL_27:
    if ( (v7 & 0x1FFFE) == 0 )
      goto LABEL_10;
  }
  if ( (unsigned __int16)(v10 >> 1) == 16 )
    v7 = ((unsigned int)v7 ^ (2 * (unsigned int)(v10 >> 1) - 2)) & 0x1FFFE ^ (unsigned __int64)v7;
  v11 = (_BYTE *)((v7 >> 16) & 0xFFFFFFFFFFFFFFF0ui64);
  if ( (*v11 & 0x7F) == a2 )
    return v11;
  ObfDereferenceObject(v11);
  return 0i64;
}

我们根据对原函数的分析,可以发现在成功的情况下应该是返回v11这个变量,通过分析代码可以分析出他的解密方式,其他系统的解密方法同理,这里在下面给出。

系统解密方式(v4是PEPROCESS*)
Win7(_BYTE *)(*(__int64 *)v4)& 0xFFFFFFFFFFFFFFF0ui64
Win8(_BYTE *)(*(__int64 *)v4 >> 19) & 0xFFFFFFFFFFFFFFF0ui64)
Win10 - Win11(截至20220803)(_BYTE *)(*(__int64 *)v4 >> 16) & 0xFFFFFFFFFFFFFFF0ui64)

为了保证正确,我们进入WinDbg选取一个表项进行测试,这里顺带说一声待会实现的时候要注意,在查询CidTableCode时低两位要抹掉零,不要问我怎么知道的,问就是看上面代码。

这一个系统的TableCode0xffffb10cc118e001,由于低两位为1可以确定为二级表,我们使用dp指令查看抹除低两位后指向内存信息

0: kd> dp 0xffffb10c`c118e000
ffffb10c`c118e000  ffffb10c`bd6b1000 ffffb10c`c118f000
ffffb10c`c118e010  ffffb10c`c1a4b000 ffffb10c`c25fe000
ffffb10c`c118e020  ffffb10c`c2fff000 ffffb10c`c16de000
ffffb10c`c118e030  ffffb10c`c40fb000 00000000`00000000
ffffb10c`c118e040  00000000`00000000 00000000`00000000
ffffb10c`c118e050  00000000`00000000 00000000`00000000
ffffb10c`c118e060  00000000`00000000 00000000`00000000
ffffb10c`c118e070  00000000`00000000 00000000`00000000

上面每八个字节的指针都指向一个一级句柄表,我们选取ffffb10cbd6b1000做测试

0: kd> dp ffffb10c`bd6b1000
ffffb10c`bd6b1000  00000000`00000000 00000000`00000000
ffffb10c`bd6b1010  d0871be5`f040f6a5 00000000`00000000
ffffb10c`bd6b1020  d0871bf9`3080ffef 00000000`00000000
ffffb10c`bd6b1030  d0871be7`f4800001 00000000`00000000
ffffb10c`bd6b1040  d0871bf7`90800001 00000000`00000000
ffffb10c`bd6b1050  d0871bf4`60800001 00000000`00000000
ffffb10c`bd6b1060  d0871bf9`b0800001 00000000`00000000
ffffb10c`bd6b1070  d0871bfc`91400001 00000000`00000000

这个便是一级表,可以发现每$16bytes$才有一次数据,按照上文提及的解密方法进行解密(这里偷懒直接扔程序算)

在这里插入图片描述

带入WinDbg进行检验

0: kd> dt _eprocess ffffd0871be5f040
nt!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x438 ProcessLock      : _EX_PUSH_LOCK
   +0x440 UniqueProcessId  : 0x00000000`00000004 Void
   +0x448 ActiveProcessLinks : _LIST_ENTRY [ 0xffffd087`1bed74c8 - 0xfffff801`6be3af60 ]
   +0x458 RundownProtect   : _EX_RUNDOWN_REF
   +0x460 Flags2           : 0xd000
   +0x460 JobNotReallyActive : 0y0
   +0x460 AccountingFolded : 0y0
   +0x460 NewProcessReported : 0y0
   +0x460 ExitProcessReported : 0y0
   +0x460 ReportCommitChanges : 0y0
   +0x460 LastReportMemory : 0y0
       ...

可以发现解密是正确的,此地址正是System进程的ERPCOESS对象

Part2.实现

Part2.1 查找PspCidTable表

众所周知,微软为提升系统稳定性,对于这种内核API的基函数、变量都是不导出的,但是我们通过刚才的反编译可以看到系统在PspReferenceCidTableEntry函数中引用了PspCidTable但是很遗憾他也不导出,但是PsLookupProcessByProcessId他导出!所以我们可以枚举该函数的内存,跟随第一个call访问到PspReferenceCidTableEntry中再寻找PspCidTable即可

Part2.2实现代码
#include"stdafx.h"
#include"Proc.h"
DWORD TargetPID;
ULONG64 ret = 0;
ULONG64 cidTableAddr = 0; 
BOOLEAN found;
BOOLEAN GetPspCidTable(ULONG64* tableAddr)
{
    UNICODE_STRING uc_funcName;
    RtlInitUnicodeString(&uc_funcName, L"PsLookupProcessByProcessId");
    ULONG64 func = MmGetSystemRoutineAddress(&uc_funcName);
    if (func == NULL)
        return FALSE;
    ULONG64 Psp_func = 0;
    for (int i = 0; i < 47; i++) //The origin jmp address in 22000.466 is PsLook...+ 27
        if (*(PUCHAR)(func + i) == 0xE8) { // The First Call
            Psp_func = func + i;
            break;
        }
    if (Psp_func != 0)
    {
        int i_callCode = *(int*)(Psp_func + 1);
        ULONG64 ul_callJmp = Psp_func + i_callCode + 5;
        for (int i = 0; i < 0x3A; i++) // The origin jmp code in 22000.466 is 0x1A 
        {
            if (*(PUCHAR)(ul_callJmp + i) == 0x48 &&
                *(PUCHAR)(ul_callJmp + i + 1) == 0x8b &&
                *(PUCHAR)(ul_callJmp + i + 2) == 0x05)
            {
                int i_movCode = *(int*)(ul_callJmp + i + 3);
                ULONG64 ul_movJmp = ul_callJmp + i + i_movCode + 7;
                *tableAddr = ul_movJmp;
                return TRUE;
            }
        }
    }
    return FALSE;
}
//枚举表的操作很简单自己打吧
}

Part3.应用

可以用来遍历部分病毒/勒索程序 隐藏的进程来实现ARK
当然还有更多其他的功能

Last modification:November 9, 2025
如果觉得我的文章对你有用,请随意赞赏