关闭显示控制面板时无效函数指针的案例
摘要
这篇来自 The Old New Thing 的文章调查了 Windows 显示控制面板中一个常见的崩溃问题,该问题是由一个被截断为 32 位并进行符号扩展的无效函数指针引起的。作者通过分析崩溃转储来找出根本原因。
<p>显示控制面板中最常见的崩溃如下所示:</p>
<pre>rax=ffffffff924bbde0 rbx=0000000000000001 rcx=0000000000030440
rdx=0000000000000002 rsi=0000000000030440 rdi=0000000080006011
rip=00007ffac835cd1e rsp=000000155e48e3f8 rbp=000000155e48e749
r8=0000000000000000 r9=0000000000000000 r10=007fffffffe41b69
r11=00007df502390000 r12=0000000000000000 r13=0000000000000000
r14=0000000000000002 r15=0000000000000000
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
ntdll!LdrpDispatchUserCallTarget+0xe:
00007fff`924acd1e mov r11,qword ptr [r11+r10*8] ds:04007df5`0159db48=????????????????
0:000> k
Call Site
ntdll!LdrpDispatchUserCallTarget+0xe
user32!UserCallWinProcCheckWow+0x2bd
user32!DispatchClientMessage+0x9c
user32!__fnDWORD+0x33
ntdll!KiUserCallbackDispatcherContinue
win32u!ZwUserDestroyWindow+0x14
comctl32!_RealPropertySheet+0x36d
comctl32!_PropertySheet+0x47
Display!PropertySheetW+0x5d
Display!AdvancedSettingSheetHelper+0x3be
Display!ShowAdapterSettings+0x89
rundll32!CallRunDllFunction+0x1c
rundll32!wWinMain+0x2bf
rundll32!__wmainCRTStartup+0x1c9
kernel32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21
</pre>
<p>从堆栈中,我们看到我们有一个显示适配器设置属性表。我们正在销毁它,并在尝试验证窗口过程地址时崩溃。</p>
<p>我们之前看到过<a title="另一种控制流保护检查:组合验证与调用" href="https://devblogs.microsoft.com/oldnewthing/20260708-00/?p=112510">可以通过检查来提取错误地址</a>。</p>
<pre>0:000> u .-e .
ntdll!LdrpDispatchUserCallTarget:
00007fff`924acd10 mov r11,qword ptr [ntdll+0x001813a8]
00007fff`924acd17 mov r10,rax
00007fff`924acd1a shr r10,9
00007fff`924acd1e mov r11,qword ptr [r11+r10*8]
</pre>
<p>作为移位源的寄存器是 <code>rax</code>,所以那就是函数指针。从寄存器转储中,我们看到地址是</p>
<pre>rax=ffffffff924bbde0
</pre>
<p>是的,那个地址看起来不像一个有效的函数指针。</p>
<p>在 64 位系统上,用户模式指针具有低地址(以 <code>0000</code> 开头),而内核模式指针具有高地址(以 <code>ffff</code> 开头)。所以这个函数指针对于用户模式显然是无效的。</p>
<p>也许我们可以修复它使其再次有效。让我们看看这个进程中哪些代码地址是有效的。</p>
<pre>0:000> lm
start end module name
00000001`80000000 00000001`80043000 contoso
00007ff6`44570000 00007ff6`44587000 rundll32
00007fff`6a4f0000 00007fff`6a6b7000 d3d9
00007fff`6e600000 00007fff`6e6a9000 comctl32_7fff6e600000
00007fff`6f5d0000 00007fff`6f5e5000 pcacli
00007fff`753b0000 00007fff`753c1000 sfc_os
...
00007fff`91020000 00007fff`910f0000 comdlg32
00007fff`912b0000 00007fff`915e6000 combase
00007fff`91600000 00007fff`91794000 user32
00007fff`917a0000 00007fff`91852000 kernel32
00007fff`918e0000 00007fff`91989000 SHCore
00007fff`91990000 00007fff`91ae6000 ole32
00007fff`91af0000 00007fff`91b16000 gdi32
00007fff`91b20000 00007fff`91bc3000 advapi32
00007fff`91bd0000 00007fff`91c67000 sechost
00007fff`91c70000 00007fff`91cc2000 shlwapi
00007fff`91cd0000 00007fff`91ced000 imagehlp
00007fff`91d50000 00007fff`921c0000 setupapi
00007fff`92220000 00007fff`92355000 msctf
00007fff`92420000 00007fff`92610000 ntdll
...
</pre>
<p>我的怀疑是函数指针被截断为 32 位值,然后被符号扩展回 64 位值。所以我们正在寻找形式为 <code>xxxxxxxx`924bbde0</code> 的有效函数指针。在上面的有效代码地址列表中,唯一低位在 <code>92xxxxxx</code> 范围内的地址的高 32 位都是 <code>00007fff</code>,所以我们将其代入,看看是否能得到一个窗口过程。</p>
<pre>0:000> ln 7fff924bbde0
(00007fff`924bbde0) ntdll!NtdllButtonWndProc_A | (00007fff`924bbdf0) ntdll!NtdllButtonWndProc_W
</pre>
<p>中大奖了。</p>
<p>所以调用者可能对一个窗口进行了子类化,然后试图恢复原始窗口过程,但搞砸了,只恢复了低 32 位。</p>
<p>但会是谁呢?</p>
<pre>0:000> k
Call Site
ntdll!LdrpICallHandler+0xf
ntdll!RtlpExecuteHandlerForException+0xf
ntdll!RtlDispatchException+0x219
ntdll!KiUserExceptionDispatch+0x2e
ntdll!LdrpDispatchUserCallTarget+0xe
user32!UserCallWinProcCheckWow+0x2bd
user32!DispatchClientMessage+0x9c
user32!__fnDWORD+0x33
ntdll!KiUserCallbackDispatcherContinue
win32u!ZwUserDestroyWindow+0x14
comctl32!_RealPropertySheet+0x36d
comctl32!_PropertySheet+0x47
Display!PropertySheetW+0x5d
Display!AdvancedSettingSheetHelper+0x3be
Display!ShowAdapterSettings+0x89
rundll32!CallRunDllFunction+0x1c
rundll32!wWinMain+0x2bf
rundll32!__wmainCRTStartup+0x1c9
kernel32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21
</pre>
<p>这是一个属性表,所以我们应该能够提取属性表的页面。(注意:需要内部 Microsoft 符号,所以你在家里无法做到这一点。)</p>
<pre>0:000> .frame d
09 00000017`85a7e820 00007fff`86e60349 Display!AdvancedSettingSheetHelper+0x3be
0:000> dv
hwndParent = <value unavailable>
psh = struct _PROPSHEETHEADERW_V2
szMonitor = wchar_t [140] "Generic PnP Monitor"
rPages = struct _PSP *[100]
iResult = 0n0
</pre>
<p><a title="如何为控制面板应用程序注册和实现属性表处理程序" href="https://learn.microsoft.com/windows/win32/shell/how-to-register-and-implement-a-property-sheet-handler-for-a-control-panel-application">桌面背景控制面板是可扩展的</a>,插件向桌面背景控制面板添加页面的方式是处理 <a href="https://learn.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishellpropsheetext-addpages">IShellPropSheetExt::<wbr />AddPages</a> 方法,并使用 <code>HPROPSHEETPAGE</code> 调用提供的“页面添加函数”。该函数的作用是将 <code>HPROPSHEETPAGE</code> 添加到属性表的页面中。(我们可以看到 <code>rPages</code> 中容纳了 100 个这样的页面。)</p>
<p>而 <code>psh</code> 就是 <code>PROPSHEETHEADER</code>。</p>
<pre>0:000> ?? psh
struct _PROPSHEETHEADERW_V2
+0x000 dwSize : 0x60
+0x004 dwFlags : 0x2000001
+0x008 hwndParent : 0x00000000`000401aa HWND__
+0x010 hInstance : 0x00007fff`86e50000 HINSTANCE__
+0x018 hIcon : (null)
+0x020 pszCaption : 0x00000017`85a7f100 "Generic PnP Monitor and Contoso Chipset"
+0x028 nPages : <span style="border: solid 1px currentcolor;">4</span>
+0x030 nStartPage : 0
+0x038 ppsp : 0x00000017`85a7ec70 _PROPSHEETPAGEW
+0x038 phpage : 0x00000017`85a7ec70 -> 0x000001d5`4e1aac90 _PSP
</pre>
<p>我们看到有四个页面,所以我们可以检查 <code>rPages</code> 中的前四个 <code>HPROPSHEETPAGE</code>。</p>
<p>嘿,看,我们有一个 <code>HPROPSHEETPAGE</code> 结构数组</p>
<pre>0:000> ?? psh.phpage[0]
struct _PSP * 0x000001d5`4e1aac90
0:000> ?? psh.phpage[1]
struct _PSP * 0x000001d5`4e19e470
0:000> ?? psh.phpage[2]
struct _PSP * 0x000001d5`4e19e520
0:000> ?? psh.phpage[3]
struct _PSP * 0x000001d5`4e1d26d0
</pre>
<p><code>HPROPSHEETPAGE</code> 是一个不透明的结构,但我们可以转储它并寻找有趣的东西,仅供娱乐。</p>
<pre>0:000> dps 0x000001d5`4e1aac90 l4
000001d5`4e1aac90 000001d5`4e1aac60
000001d5`4e1aac98 00000000`00000000
000001d5`4e1aaca0 00004088`00000068
000001d5`4e1aaca8 00007fff`88d700
查看缓存全文
缓存时间: 2026/07/15 22:53
# 关闭显示控制面板时出现无效函数指针的案例 - The Old New Thing
来源:https://devblogs.microsoft.com/oldnewthing/20260715-00?p=112535
显示控制面板中的第一大崩溃如下所示:
```
rax=ffffffff924bbde0 rbx=0000000000000001 rcx=0000000000030440 rdx=0000000000000002 rsi=0000000000030440 rdi=0000000080006011 rip=00007ffac835cd1e rsp=000000155e48e3f8 rbp=000000155e48e749 r8=0000000000000000 r9=0000000000000000 r10=007fffffffe41b69 r11=00007df502390000 r12=0000000000000000 r13=0000000000000000 r14=0000000000000002 r15=0000000000000000 iopl=0 nv up ei pl nz na pe nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206 ntdll!LdrpDispatchUserCallTarget+0xe:
00007fff`924acd1e mov r11,qword ptr [r11+r10*8] ds:04007df5`0159db48=????????????????
0:000> k
Call Site
ntdll!LdrpDispatchUserCallTarget+0xe
user32!UserCallWinProcCheckWow+0x2bd
user32!DispatchClientMessage+0x9c
user32!__fnDWORD+0x33
ntdll!KiUserCallbackDispatcherContinue
win32u!ZwUserDestroyWindow+0x14
comctl32!_RealPropertySheet+0x36d
comctl32!_PropertySheet+0x47
Display!PropertySheetW+0x5d
Display!AdvancedSettingSheetHelper+0x3be
Display!ShowAdapterSettings+0x89
rundll32!CallRunDllFunction+0x1c
rundll32!wWinMain+0x2bf
rundll32!__wmainCRTStartup+0x1c9
kernel32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21
```
从堆栈中,我们看到有一个显示适配器设置属性表。我们正在销毁它,并且在尝试验证窗口过程地址时崩溃了。我们之前看到过(https://devblogs.microsoft.com/oldnewthing/20260708-00/?p=112510),可以通过检查找出错误的地址。
```
0:000> u .-e .
ntdll!LdrpDispatchUserCallTarget:
00007fff`924acd10 mov r11,qword ptr [ntdll+0x001813a8]
00007fff`924acd17 mov r10,rax
00007fff`924acd1a shr r10,9
00007fff`924acd1e mov r11,qword ptr [r11+r10*8]
```
进行移位的源寄存器是 `rax`,所以这就是函数指针。从寄存器转储中,我们看到地址是
```
rax=ffffffff924bbde0
```
没错,这个地址看起来不像一个有效的函数指针。在 64 位系统上,用户模式指针具有低地址(以 `0000` 开头),而内核模式指针具有高地址(以 `ffff` 开头)。所以这个函数指针在用户模式下显然是无效的。也许我们可以修复它,使其再次有效。让我们看看在这个进程中哪些代码地址是有效的。
```
0:000> lm
start end module name
00000001`80000000 00000001`80043000 contoso
00007ff6`44570000 00007ff6`44587000 rundll32
00007fff`6a4f0000 00007fff`6a6b7000 d3d9
00007fff`6e600000 00007fff`6e6a9000 comctl32_7fff6e600000
00007fff`6f5d0000 00007fff`6f5e5000 pcacli
00007fff`753b0000 00007fff`753c1000 sfc_os
...
00007fff`91020000 00007fff`910f0000 comdlg32
00007fff`912b0000 00007fff`915e6000 combase
00007fff`91600000 00007fff`91794000 user32
00007fff`917a0000 00007fff`91852000 kernel32
00007fff`918e0000 00007fff`91989000 SHCore
00007fff`91990000 00007fff`91ae6000 ole32
00007fff`91af0000 00007fff`91b16000 gdi32
00007fff`91b20000 00007fff`91bc3000 advapi32
00007fff`91bd0000 00007fff`91c67000 sechost
00007fff`91c70000 00007fff`91cc2000 shlwapi
00007fff`91cd0000 00007fff`91ced000 imagehlp
00007fff`91d50000 00007fff`921c0000 setupapi
00007fff`92220000 00007fff`92355000 msctf
00007fff`92420000 00007fff`92610000 ntdll
...
```
我怀疑函数指针被截断为 32 位值,然后被符号扩展回 64 位值。所以我们在寻找形式为 `xxxxxxxx\`924bbde0` 的有效函数指针。在上面的有效代码地址列表中,低位位于 `92xxxxxx` 范围内的地址,其高 32 位都是 `00007fff`,那么我们把这个代入,看看是否能得到一个窗口过程。
```
0:000> ln 7fff924bbde0
(00007fff`924bbde0) ntdll!NtdllButtonWndProc_A | (00007fff`924bbdf0) ntdll!NtdllButtonWndProc_W
```
中奖了。所以调用者可能对窗口进行了子类化,然后尝试恢复原始窗口过程,但搞砸了,只恢复了低 32 位。但这会是谁呢?
```
0:000> k
Call Site
ntdll!LdrpICallHandler+0xf
ntdll!RtlpExecuteHandlerForException+0xf
ntdll!RtlDispatchException+0x219
ntdll!KiUserExceptionDispatch+0x2e
ntdll!LdrpDispatchUserCallTarget+0xe
user32!UserCallWinProcCheckWow+0x2bd
user32!DispatchClientMessage+0x9c
user32!__fnDWORD+0x33
ntdll!KiUserCallbackDispatcherContinue
win32u!ZwUserDestroyWindow+0x14
comctl32!_RealPropertySheet+0x36d
comctl32!_PropertySheet+0x47
Display!PropertySheetW+0x5d
Display!AdvancedSettingSheetHelper+0x3be
Display!ShowAdapterSettings+0x89
rundll32!CallRunDllFunction+0x1c
rundll32!wWinMain+0x2bf
rundll32!__wmainCRTStartup+0x1c9
kernel32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21
```
这是一个属性表,所以我们应该能够提取属性表的页面。(注意:需要内部 Microsoft 符号,所以你在家无法做到。)
```
0:000> .frame d 09
00000017`85a7e820 00007fff`86e60349 Display!AdvancedSettingSheetHelper+0x3be
0:000> dv
hwndParent =
psh = struct _PROPSHEETHEADERW_V2
szMonitor = wchar_t [140] "Generic PnP Monitor"
rPages = struct _PSP *[100]
iResult = 0n0
```
桌面背景控制面板是可扩展的(https://learn.microsoft.com/windows/win32/shell/how-to-register-and-implement-a-property-sheet-handler-for-a-control-panel-application),插件添加页面到桌面背景控制面板的方式是处理 `IShellPropSheetExt::AddPages`(https://learn.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishellpropsheetext-addpages)方法,并调用所提供的“添加页面函数”,参数为一个 `HPROPSHEETPAGE`。该函数的作用是将这个 `HPROPSHEETPAGE` 添加到属性表的页面中。(我们可以看到 `rPages` 中有空间容纳 100 个。)而 `psh` 就是 `PROPSHEETHEADER`。
```
0:000> ?? psh
struct _PROPSHEETHEADERW_V2
+0x000 dwSize : 0x60
+0x004 dwFlags : 0x2000001
+0x008 hwndParent : 0x00000000`000401aa HWND__
+0x010 hInstance : 0x00007fff`86e50000 HINSTANCE__
+0x018 hIcon : (null)
+0x020 pszCaption : 0x00000017`85a7f100 "Generic PnP Monitor and Contoso Chipset"
+0x028 nPages : 4
+0x030 nStartPage : 0
+0x038 ppsp : 0x00000017`85a7ec70 _PROPSHEETPAGEW
+0x038 phpage : 0x00000017`85a7ec70 -> 0x000001d5`4e1aac90 _PSP
```
我们看到有四个页面,所以我们可以检查 `rPages` 中的前四个 `HPROPSHEETPAGE`。嘿,瞧,我们有一个 `HPROPSHEETPAGE` 结构数组
```
0:000> ?? psh.phpage[0]
struct _PSP * 0x000001d5`4e1aac90
0:000> ?? psh.phpage[1]
struct _PSP * 0x000001d5`4e19e470
0:000> ?? psh.phpage[2]
struct _PSP * 0x000001d5`4e19e520
0:000> ?? psh.phpage[3]
struct _PSP * 0x000001d5`4e1d26d0
```
`HPROPSHEETPAGE` 是一个不透明的结构,但我们可以将其转储出来,找点有趣的东西,仅供娱乐。
```
0:000> dps 0x000001d5`4e1aac90 l4
000001d5`4e1aac90 000001d5`4e1aac60
000001d5`4e1aac98 00000000`00000000
000001d5`4e1aaca0 00004088`00000068
000001d5`4e1aaca8 00007fff`88d70000 deskadp
0:000> dps 0x000001d5`4e19e470 l4
000001d5`4e19e470 000001d5`4e19e440
000001d5`4e19e478 00000000`00000000
000001d5`4e19e480 00004088`00000068
000001d5`4e19e488 00007fff`893e0000 deskmon
0:000> dps 0x000001d5`4e19e520 l4
000001d5`4e19e520 000001d5`4e19e4f0
000001d5`4e19e528 00000000`00000000
000001d5`4e19e530 000040c8`00000068
000001d5`4e19e538 00007fff`86e30000 colorui
0:000> dps 0x000001d5`4e1d26d0 l4
000001d5`4e1d26d0 000001d5`4e1bcb30
000001d5`4e1d26d8 000001d5`4e1d26a0
000001d5`4e1d26e0 0000008a`00000068
000001d5`4e1d26e8 00000001`80000000 contoso
```
这里有一些 `HMODULE`,很可能是属性表页面来自的模块。前三个随 Windows 附带。最后一个显然是 Contoso。让我们重点关注最后一个。在前两个值(看起来像指针)之后,我们有 `0x00000068`,这并非巧合地等于 `sizeof(PROPSHEETPAGE)`,所以我要猜测这是系统存储创建句柄所用的 `PROPSHEETPAGE` 的地方。**注意**:请注意,这是一个实现细节,仅应用于调试目的。请不要编写依赖于此的程序,因为它可能会发生变化。¹
```
0:000> dt comctl32!_PROPSHEETPAGEW 000001d5`4e1d26e0
+0x000 dwSize : 0x68
+0x004 dwFlags : 0x8a
+0x008 hInstance : 0x00000001`80000000 HINSTANCE__
+0x010 pszTemplate : 0x00000000`00000589 "--- memory read error at address 0x00000000`00000589 ---"
+0x010 pResource : 0x00000000`00000589 DLGTEMPLATE
+0x018 hIcon : 0x00000000`000503b9 HICON__
+0x018 pszIcon : 0x00000000`000503b9 "--- memory read error at address 0x00000000`000503b9 ---"
+0x020 pszTitle : 0x000001d5`4e19cde0 "?????"
+0x028 pfnDlgProc : 0x00000001`800047ac contoso+0x47ac
+0x030 lParam : 0n2015682301296
+0x038 pfnCallback : (null)
+0x040 pcRefParent : (null)
+0x048 pszHeaderTitle : (null)
+0x050 pszHeaderSubTitle : (null)
+0x058 hActCtx : (null)
+0x060 hbmHeader : (null)
+0x060 pszbmHeader : (null)
```
对话框过程是 `0x00000001\`800047ac`。我希望我能对其进行足够的逆向工程,以找到它错误地对按钮进行子类化的位置。
```
00000001`800047ac mov [rsp+8],rbx
00000001`800047b1 mov [rsp+10h],rbp
00000001`800047b6 mov [rsp+18h],rsi
00000001`800047bb push rdi
00000001`800047bc sub rsp,30h
00000001`800047c0 mov rdi,r9 ; rdi = r9 = lParam
00000001`800047c3 mov rbp,r8 ; rbp = r8 = wParam
00000001`800047c6 mov esi,edx ; esi = edx = message
00000001`800047c8 mov rbx,rcx ; rbx = rcx = hdlg
00000001`800047cb cmp edx,110h ; Q: WM_INITDIALOG?
00000001`800047d1 jne 00000001`800047e2 ; N: Skip
00000001`800047d3 mov r8,[r9+30h] ; Y: r8 = ((PROPSHEETPAGE*)r9)->lParam
00000001`800047d7 mov edx,0FFFFFFEBh ; edx = -21
; ecx = hdlg (unchanged)
00000001`800047dc call [00000001`8002b4a0] ; mystery function 1
00000001`800047e2 mov edx,0FFFFFFEBh ; edx = -21
00000001`800047e7 mov rcx,rbx ; rcx = hdlg
00000001`800047ea call [00000001`8002b480] ; mystery function 2
00000001`800047f0 test rax,rax ; Q: Failed?
00000001`800047f3 je 00000001`8000480b ; Y: Bail out
00000001`800047f5 mov r9,rbp ; param4 = wParam
00000001`800047f8 mov r8d,esi ; param3 = message
00000001`800047fb mov rdx,rbx ; param2 = hdlg
00000001`800047fe mov rcx,rax ; param1 = from mystery function 2
00000001`80004801 mov [rsp+20h],rdi ; param5 = lParam
00000001`80004806 call 00000001`800045fc ; mystery function 3
00000001`8000480b mov rbx,[rsp+40h] ; restore registers
00000001`80004810 mov rbp,[rsp+48h]
00000001`80004815 mov rsi,[rsp+50h]
00000001`8000481a add rsp,30h
00000001`8000481e pop rdi
00000001`8000481f ret ; done
```
我们知道 `WM_INITDIALOG` 消息的 `lParam` 参数是传递给 `CreateDialogParam` 等函数的“参数”,具体对于属性表来说,它是一个指向 `PROPSHEETPAGE` 的指针。我们从上面的结构转储中看到,偏移 `0x30` 是 `lParam`。从这个函数的结构来看,很明显魔数 `-21` 是 `GWLP_USERDATA`,神秘函数 1 是 `SetWindowLongPtr`,神秘函数 2 是 `GetWindowLongPtr`。这是对话框函数的标准模式,通常使用包装函数。真正的对话框过程是第三个神秘函数,让我们看看它。
```
00000001`800045fc mov [rsp+8],rbx
00000001`80004601 mov [rsp+10h],rbp
00000001`80004606 mov [rsp+18h],rsi
00000001`8000460b push rdi
00000001`8000460c push r12
00000001`8000460e push r13
00000001`80004610 sub rsp,20h
00000001`80004614 mov rsi,[rsp+60h] ; rsi = lParam
00000001`80004619 mov rbp,r9 ; rbp = wParam
00000001`8000461c mov ebx,r8d ; ebx = message
00000001`8000461f mov r13,rdx ; r13 = hdlg
00000001`80004622 mov rdi,rcx ; rdi = this
00000001`80004625 cmp r8d,2Bh ; Q: WM_DRAWITEM?
00000001`80004629 jne 00000001`80004685 ; N: Skip
```
在最初的寄存器溢出和保存之后,它检查消息是否为 `0x2B`:`WM_DRAWITEM`。这对我们来说并不特别有趣,所以我们假设它不是。
```
00000001`80004685 sub ebx,2 ; Q: WM_DESTROY?
00000001`80004688 je 00000001`8000470f
```
哦,`WM_DESTROY` 消息很有趣。它可能在其 `WM_DESTROY` 处理程序中恢复原始窗口过程,我们希望在那里找到截断问题。
```
00000001`8000470f mov rcx,[rdi+110h] ; rcx = something
00000001`80004716 movsxd rbx,dword ptr [00000001`80039c50] ; rbx = something
00000001`8000471d mov edx,668h ; ecx = some number
00000001`80004722 call [00000001`8002b4e0] ; mystery function 4
00000001`80004728 mov r8,rbx ; r8 = something
00000001`8000472b mov edx,0FFFFFFFCh ; edx = -12
00000001`80004730 mov rcx,rax ; rcx = function 4 retval
00000001`80004733 call [00000001`8002b4a0] ; mystery function 1 again
```
收到 `WM_DESTROY` 消息后,代码开始从 `this` 指针(我们在序言中看到保存在 `rdi` 中)获取某个东西,并从全局变量加载另一个东西。接下来,它以 `0x668` 作为第二个参数调用神秘函数 `00000001\`8002b4e0`。我不确定那是什么,但我们会记在心里。接下来,我们设置另一个函数调用,这次我们认出来了:`00000001\`8002b4a0` 是 `SetWindowLongPtr` 的导入地址表项。我们在静态对话框过程中看到过它。参数是从神秘函数 4 获得的窗口句柄、常量 `-12`,以及我们从 `00000001\`80039c50` 加载的 32 位值。神秘函数 4 可能是 `GetDlgItem`。并且由于我们已弄清楚被调用的函数是 `SetWindowLongPtr`,所以值 `-12` 是 `GWLP_WNDPROC`。正在设置的值是第三个参数,它是通过 `movsxd dword ptr` 加载的,这是一个 32 位到 64 位的符号扩展加载。这是一个问题,因为窗口过程是一个 64 位值。我打赌他们错误地加载了这个值。
```
0:000> dp 00000001`80039c50 l1
00000001`80039c50 00007fff`924bbde0
```
嘿,看,这正是我们应该使用的完整 64 位指针,只不过我们搞砸了,把指针截断了。C++ 源代码可能如下所示:
```cpp
SetWindowLongPtr(GetDlgItem(m_hdlg, 0x668), GWLP_WNDPROC, (LONG)g_originalWndProc);
```
转换为 `LONG` 就是导致截断和符号扩展的原因。应该转换为 `LONG_PTR`。我们可以在查看处理器指令编码文档后将其修补到二进制文件中。原始指令是
```
00000001`80004716 48631d33550300 movsxd rbx,dword ptr [00000001`80039c50]
```
文档说 `movxsd r64, r/m32` 的编码是 "REX.W + 63 /r"。我们想要的是 `mov rbx, [00000001\`80039c50]`,文档说 `mov r64, r/m64` 的编码是 "REX.W + 8B /r"。所以让我们将 `63` 修补为 `8b`。
```
0:000> eb 00000001`80004717 8b
0:000> u 00000001`80004716 l1
00000001`80004716 488b1d33550300 mov rbx,qword ptr [00000001`80039c50]
```
这实际上是一个字节的 bug 修复。下次,我们将推测这个 bug 是如何产生的。
**额外阅读**:诱饵控制面板(https://devblogs.microsoft.com/oldnewthing/20060410-17/?p=32703)。
¹ 早在 1990 年代末,我们发现了一个程序,它对 Windows 95 属性表管理器的内部数据结构进行了逆向工程,以至于它不再传递由 `CreatePropertySheetPage` 函数创建的 `HPROPSHEETPAGE`,而是创建了它在内存中手动构造的虚假 `HPROPSHEETPAGE`。这使得为 Unicode 属性表添加支持变得更加困难,因为 `HPROPSHEETPAGE` 的内部结构为了同时支持 ANSI 和 Unicode 属性表页面而发生了变化,而它们传递的是旧版本。属性表管理器必须识别出它被给出了一个虚假的 `HPROPSHEETPAGE`,并即时将其转换为真实的。
### 类别
### 主题
## 作者
Raymond Chen 在 Windows 的演进中工作了 30 多年。2003 年,他创建了一个名为 The Old New Thing 的网站。
相似文章
推测故障控制面板扩展如何截断了它面前的值
Raymond Chen 推测故障控制面板扩展如何因将64位指针截断为32位而导致崩溃,可能是由于64位移植过程中代码更新不完整。
为什么显示控制面板指针截断错误长期未被修复?
本文解释了显示控制面板中与指针截断相关的错误为何长期存在,尽管供应商已经修复,但受限于计算机制造商的驱动程序认证流程,用户只能使用过时的认证驱动程序。
一个DLL未正式卸载却从内存消失的案例,第一部分
本文调查了一个崩溃转储文件:进程关闭期间,shell32.dll中发生栈溢出,导致重复的异常处理循环。分析追踪了递归异常处理,并确定了涉及DLL卸载的根本原因。
一个未正式卸载却从内存中消失的DLL案例,第二部分
Ray Chen的一篇技术博文,探究一个内存损坏bug:单个字节0x01破坏HMODULE句柄,导致DLL被错误释放,进而在进程终止时崩溃。
关于线程从已卸载的第三方DLL中执行的问题
微软Raymond Chen的一则技术调试故事,讲述了Windows资源管理器崩溃由从已卸载第三方DLL中执行的线程引起,展示了开发者如何调查此类问题。