Трассировка описателей. Вторая серия

Краткое содержание первой части:

Не получается продолжить выполнение кода после того, как было сгенерировано исключение STATUS_INVALID_HANDLE - почему-то портится сохраненный контекст процессора. В частности, не сохраняются non-volatile регистры esi и edi.

Некто Indy засомневался в том, что происходит именно это. Следует сказать, что засомневался он не без оснований. На самом деле, способы возбудить исключение пользовательского режима из кода в ядре можно пересчитать по пальцам одной руки и все они формируют конекст процессора одинаково. Получается, что если бы регистры не сохранялись, то не работали бы все исключения, а не избирательно STATUS_INVALID_HANDLE.

Покопавшись в отладчике я понял в чем дело.

Начну издалека. Системные вызовы из 32-х битных приложений перехватываются Wow64. Wow64 транслирует 32-х битные параметры в 64-х битные, а затем выполняет системный вызов. Когда, например, приложение вызывает функцию ReleaseMutex() получается такая цепочка вызовов:

0:000:x86> uf ntdll32!NtReleaseMutant
ntdll32!ZwReleaseMutant:
77c1fb6c b81d000000      mov     eax,1Dh
77c1fb71 b907000000      mov     ecx,7
77c1fb76 8d542404        lea     edx,[esp+4]
77c1fb7a 64ff15c0000000  call    dword ptr fs:[0C0h]
77c1fb81 83c404          add     esp,4
77c1fb84 c20800          ret     8

Инструкция call выше выполняет переход на точку входа внутри Wow64. Для сравнения в 64-х разрядной версии используется инструкция syscall, которая выполняет переход в ядро:

0:000> uf ntdll!NtReleaseMutant
ntdll!NtReleaseMutant:
00000000`77a71510 4c8bd1          mov     r10,rcx
00000000`77a71513 b81d000000      mov     eax,1Dh
00000000`77a71518 0f05            syscall
00000000`77a7151a c3              ret

При переключении между 32-х и 64-х битным кодом, Wow64 первым делом сохраняет 32-х разрядный контекст процессора (включая esi и edi) в специальной структуре CPUCONTEXT, на которую указывает TlsSlots[1] в 64-х битной версии TEB (Thread Environment Block). Когда ядро генерирует исключение пользовательского режима, первым дело управление получает Wow64. Wow64 формирует 32-х битный контекст, используя значения, сохраненные в CPUCONTEXT, и генерирует 32-х разрядное исключение с этим контекстом. В том числе, восстанавливаются и прежние значения esi и edi.

Почему же это не работает с исключением STATUS_INVALID_HANDLE? Дело в том, что Wow64 использует упрощенный (и более быстрый) вариант трансляции 32-х битных параметров в 64-х для некоторых системных вызовов. В том числе, для NtReleaseMutant. Смотрите:

Breakpoint 0 hit
eax=0000001d ebx=6a4f5584 ecx=00000007 edx=003ef764 esi=00000900 edi=00000000
eip=77c1fb7a esp=003ef760 ebp=003ef76c iopl=0         nv up ei pl nz ac pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000216
ntdll32!ZwReleaseMutant+0xe:
77c1fb7a 64ff15c0000000  call    dword ptr fs:[0C0h]  fs:0053:000000c0=00000000
0:000:x86> t
wow64cpu!X86SwitchTo64BitMode:
74ae2320 ea1e27ae743300  jmp     0033:74AE271E
0:000:x86>
wow64cpu!CpupReturnFromSimulatedCode:
00000000`74ae271e 67448b0424      mov     r8d,dword ptr [esp]

Функция wow64cpu!CpupReturnFromSimulatedCode - это точка возврата из x86 кода в Wow64:

0:000> u wow64cpu!CpupReturnFromSimulatedCode
wow64cpu!CpupReturnFromSimulatedCode:
00000000`74ae271e 67448b0424      mov     r8d,dword ptr [esp]
00000000`74ae2723 458985bc000000  mov     dword ptr [r13+0BCh],r8d
00000000`74ae272a 4189a5c8000000  mov     dword ptr [r13+0C8h],esp
00000000`74ae2731 498ba42480140000 mov     rsp,qword ptr [r12+1480h]
00000000`74ae2739 4983a4248014000000 and   qword ptr [r12+1480h],0
00000000`74ae2742 448bda          mov     r11d,edx
wow64cpu!TurboDispatchJumpAddressStart:
00000000`74ae2745 41ff24cf        jmp     qword ptr [r15+rcx*8]
wow64cpu!TurboDispatchJumpAddressEnd:
00000000`74ae2749 4189b5a4000000  mov     dword ptr [r13+0A4h],esi
00000000`74ae2750 4189bda0000000  mov     dword ptr [r13+0A0h],edi
00000000`74ae2757 41899da8000000  mov     dword ptr [r13+0A8h],ebx
00000000`74ae275e 4189adb8000000  mov     dword ptr [r13+0B8h],ebp

Первые три инструкции сохраняют eip и esp в CPUCONTEXT; две следующие - переключаются на 64-х битный стек. Инструкция по адресу wow64cpu!TurboDispatchJumpAddressStart выбирает обработчик, который будет конвертировать параметры в 64-х битный формат и выполнит системный вызов. Большинство системных вызовов обслуживаются обработчиком под индексом 0 (Индекс обработчика указывается в регистре rcx). Он начинается с метки wow64cpu!TurboDispatchJumpAddressEnd. Первым делом он как раз и сохраняет non-volatile регстры в CPUCONTEXT.

NtReleaseMutant использует обработчик с индексом 7, он же - wow64cpu!Thunk2ArgSpNSp:

0:000> u wow64cpu!Thunk2ArgSpNSp
wow64cpu!Thunk2ArgSpNSp:
00000000`74ae2d84 4d6313          movsxd  r10,dword ptr [r11]
00000000`74ae2d87 418b5304        mov     edx,dword ptr [r11+4]
00000000`74ae2d8b eb2d            jmp     wow64cpu!Thunk0Arg (00000000`74ae2dba)
0:000> u wow64cpu!Thunk0Arg
wow64cpu!Thunk0Arg:
00000000`74ae2dba e841000000      call    wow64cpu!CpupSyscallStub (00000000`74ae2e00)
0:000> u wow64cpu!CpupSyscallStub
wow64cpu!CpupSyscallStub:
00000000`74ae2e00 4189adb8000000  mov     dword ptr [r13+0B8h],ebp
00000000`74ae2e07 0f05            syscall
00000000`74ae2e09 c3              ret

Видно, что non-volatile регистры не сохраняются в CPUCONTEXT и системный вызов выполняется из wow64cpu!CpupSyscallStub, а не ntdll!NtReleaseMutant.

Хотя esi и edi не сохраняются в CPUCONTEXT, они не используются в wow64cpu!Thunk2ArgSpNSp и, соответственно, ядро их корректно восстанавливает. В этом можно убедится посмотрев на конекст исключения в 64-х разрядном диспетчере:

Breakpoint 1 hit
ntdll!KiUserExceptionDispatcher:
00000000`77a7124a fc              cld
0:000> .cxr @rsp
rax=0000000049af579a rbx=000000006a4f5584 rcx=000000000017ddb0
rdx=0000000074ae2dbf rsi=0000000000000900 rdi=0000000000000000
rip=0000000077a712f7 rsp=000000000017e3c0 rbp=00000000003ef76c
 r8=000000000017e488  r9=00000000003ef76c r10=0000000000000000
r11=0000000000000246 r12=000000007efdb000 r13=000000000017fd20
r14=000000000017e500 r15=0000000074ae2450
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000204
ntdll!KiRaiseUserExceptionDispatcher+0x3a:
00000000`77a712f7 8b8424c0000000  mov     eax,dword ptr [rsp+0C0h] ss:00000000`0017e480=c0000008

Значения регистров портятся позже, когда Wow64 транслирует 64-х битное исключение в 32-х битное. Значения регистров при этом, как я уже упоминал, берутся из структуры CPUCONTEXT, содержимое которой к этому моменту устарело:

0:000> dt poi(@$teb+1488)+4 ntdll32!_CONTEXT
   +0x000 ContextFlags     : 0x1002f
   ...
   +0x09c Edi              : 0x40
   +0x0a0 Esi              : 0x28
   +0x0a4 Ebx              : 0x6a327f
   +0x0a8 Edx              : 0
   ...

Пару слов о том, почему wow64cpu!Thunk2ArgSpNSp и другие “быстрые” обработчики не сохраняются эти регистры. Системные вызовы, как правило, не генерируют исключений. Вместо этого возвращаются код ошибки, тот же STATUS_ACCESS_DENIED. В случае же, когда исключение все-же случается, то либо оно и так фатально (скажем страница, хранящая CPUCONTEXT стала вдруг недоступна), либо используется полновестный обработчик (тот, что с нулевым индексом). Про трассировку описателей и исключение STATUS_INVALID_HANDLE, скороее всего, просто забыли. Что и не удивительно, если учесть, что все, в принципе, работает, если не пытаться продолжить выполниение после исключения STATUS_INVALID_HANDLE.

Самой простой вариант избежать этой проблемы - запретить “быстрые” обработчики ключём (недокументированным, вестимо) в реестре. Можно, затереть nop-ами код между wow64cpu!TurboDispatchJumpAddressStart и wow64cpu!TurboDispatchJumpAddressEnd. Еще вериант - перехварить ntdll!KiUserExceptionDispatcher. Или даже ntdll!KiRaiseUserExceptionDispatcher. Способы один другого краше.

Вот така х…ня, малята! ;-)

comments powered by Disqus