`LoadLibraryExW` 触发来自 `NtMapViewOfSection` 的异常 `0xC0000023`

`LoadLibraryExW` triggers exception `0xC0000023` from `NtMapViewOfSection`

缩小这个问题的范围真的很难,但我们开始吧。

上下文

我处于加载到主机 (TstCon.exe) 的 32 位 ActiveX 控件的上下文中。卸载并重新加载控件后,我从 NtMapViewOfSection 收到一连串错误,第一个错误发生在 odbc32.dll 使用 LoadLibraryExW 加载 C:\Windows\system32\odbcint.dll 时。此时,SEH 异常从 NtMapViewOfSection 内部某处发出,代码为 0xC0000023(根据调试器又称为 STATUS_BUFFER_TOO_SMALL)。

后果

这是调试器拦截异常时调用堆栈的样子:

ntdll.dll!_NtMapViewOfSection@40()
KernelBase.dll!BasepLoadLibraryAsDataFileInternal()
KernelBase.dll!BasepLoadLibraryAsDataFile()
KernelBase.dll!LoadLibraryExW()
odbc32.dll!_InitializeDll@0()
odbc32.dll!_SQLAllocEnv@4()
<OurDll>.dll!<OurFunction>()
...

那时,我已经使用完全正常技术通过以下this documentation检索调用NtMapViewOfSection的参数:

*(void**)(ESP + 4 + 0)           /*SectionHandle*/      0x000003b0              void *
*(void**)(ESP + 4 + 4)           /*ProcessHandle*/      0xffffffff              void *
*(void**)(ESP + 4 + 8)           /*BaseAddress*/        0x00daae30              void *
*(unsigned long*)(ESP + 4 + 12)  /*ZeroBits*/           0x00000000              unsigned long
*(unsigned long*)(ESP + 4 + 16)  /*CommitSize*/         0x00000000              unsigned long
*(long long**)(ESP + 4 + 20)     /*SectionOffset*/      0x00000000 {???}        __int64 *
*(unsigned long**)(ESP + 4 + 24) /*ViewSize*/           0x00daae28 {0x00000000} unsigned long *
*(int*)(ESP + 4 + 28)            /*InheritDisposition*/ 0x00000001              int
*(unsigned long*)(ESP + 4 + 32)  /*AllocationType*/     0x00800000              unsigned long
*(unsigned long*)(ESP + 4 + 36)  /*Protect*/            0x00000002              unsigned long

程序集演练

我最初是通过在 VS 的调试器中启用 break-on-throw 来捕获异常的,然后我就能够查明第一个失败的调用并在前面放置一个断点。这是我从反汇编内部调试中看到的内容(> 标记当前指令):

  _NtMapViewOfSection@40:
  76F2EF60  mov         eax,28h  
  76F2EF65  mov         edx,offset _Wow64SystemServiceCall@0 (76F43430h)  
> 76F2EF6A  call        edx  
  76F2EF6C  ret         28h  
  76F2EF6F  nop  

...步入:

  _Wow64SystemServiceCall@0:
> 76F43430  jmp         dword ptr [_Wow64Transition (76FD2218h)]  

...步入:

> 74A37000  jmp         0033:74A37009  
  74A37007  add         byte ptr [eax],al  
  74A37009  inc         ecx  
  74A3700A  jmp         dword ptr [edi+0F8h]  

...步入:

  _NtQueryObject@20:
  76F2EDC0  mov         eax,10h  
  76F2EDC5  mov         edx,offset _Wow64SystemServiceCall@0 (76F43430h)  
  76F2EDCA  call        edx  
> 76F2EDCC  ret         14h  
  76F2EDCF  nop  

而下一个 step into 触发异常。


对程序环境的干扰,例如:

... 更改对 NtMapViewOfSection 的调用将成功或失败,似乎是随机的:并非所有调用都失败,但有相当一部分会失败。事实上,错误的第一次出现可能并不能说明问题的真正根源,因为我很少能够让它更早地崩溃(在卸载控件时),甚至在 none 我们的代码在调用堆栈中(通过直接退出 TstCon.exe)。

我找不到任何文档(官方或其他)提及 STATUS_BUFFER_TOO_SMALL 或在此上下文中的 0xC0000023 代码。我一直无法在失败的调用中找到模式,并且没有看到来自 Dr. Memory 运行.

的相关访问错误

所以...这个过程中可能发生了什么导致出现这种症状?

在对 ActiveX 控件的执行进行了长时间的研究之后,使用调试器跳过了特定的代码部分,我将错误的可能位置缩小到单个函数。事实证明,问题的根源几乎无法猜测。

有一天,有人希望能够为线程命名。为此,他们使用了记录在案的 0x406D1388 技术(事实上,代码几乎是从链接文档中复制粘贴的)。这本身就很好。但是后来他们想要(或者我收集到)以编程方式检索名称,自定义异常方法不支持。这都是在 SetThreadDescription/GetThreadDescription 存在之前,所以他们寻找另一种方法。 man他们有创造力吗

除了调用抛出自定义异常的函数外,还有以下行:

// Grab the TIB.
P_T_TIB pTib = GetTIB();

if (pTib == NULL)
    return false;

// If someone has already written to the arbitrary field, I don't
// want to be overwriting it.
if (pTib->pvArbitrary == NULL)
{
    // Nothing's there. Set the name.
    pTib->pvArbitrary = (void *)pszName;
}

有什么,当然肯定没有"nothing"。 GetTIB定义如下:

// A static function to get the TIB.
static P_T_TIB GetTIB()
{
    P_T_TIB pTib = NULL;

    _asm
    {
        MOV  EAX, FS:[18h]
        MOV  pTib, EAX
    }

    return pTib;
}

据我所知,这段高度可疑的程序集检索指向当前线程的 Thread Information Block 的指针。 T_TIB 的定义遵循该页面上的描述,并允许线程命名函数将指向名称的指针存储到任意数据槽中。同样的技术被用于名称获取函数来检索指针和 return 名称。

当然,难点在于"arbitrary"不等于"user data",而NtMapViewOfSection和小伙伴们是靠字段来存储信息的,数据断点放在被 LdrpMapViewOfSection 击中的字段,等等。在他们路径的某个地方,遇到具有异常非零值(即我们的名称指针)的字段导致他们执行一些非常错误的操作,最终触发了那个奇怪的异常。

出于好奇,我完全删除了所有这些,因为它没有在其他任何地方使用,并且只是使用 thread_local 变量来存储名称。结案!