`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 触发异常。
对程序环境的干扰,例如:
- 更新编译器和 运行 次(在 MSVC90 和 MSVC141 之间),这首先揭示了错误;
- 在发布和调试配置之间切换;
- 通过
/base
链接器标志强制 OCX 的基地址;
- 运行 附加调试器;
- 使用
drstrace.exe
监控系统调用;
... 更改对 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
变量来存储名称。结案!
缩小这个问题的范围真的很难,但我们开始吧。
上下文
我处于加载到主机 (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 触发异常。
对程序环境的干扰,例如:
- 更新编译器和 运行 次(在 MSVC90 和 MSVC141 之间),这首先揭示了错误;
- 在发布和调试配置之间切换;
- 通过
/base
链接器标志强制 OCX 的基地址; - 运行 附加调试器;
- 使用
drstrace.exe
监控系统调用;
... 更改对 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
变量来存储名称。结案!