Closed SpriteOvO closed 3 years ago
I have a hunch here, so please try adding /d2FH4-
to your compiler flags in the project settings (Configuration Properties -> C/C++ -> Command Line) to see if this fixes it.
@gsuberland Thanks for reply, but it still shows BAD.
Sorry, that should've been -d2FH4-
in the C/C++ command line options, and -d2:-FH4-
in the linker command line options.
Sorry, that should've been
-d2FH4-
in the C/C++ command line options, and-d2:-FH4-
in the linker command line options.
@gsuberland I added them both, but it still shows BAD.
I can't reproduce the issue on my system. The only difference I can see between your binary and the one I compiled straight out of the repo is that yours imports __CxxFrameHandler4
from VCRUNTIME140_1.DLL
, and mine doesn't. This could be related to a new compiler feature described here.
Can you post the full set of compiler flags for the x64 build target, from the command line page?
Can you post the full set of compiler flags for the x64 build target, from the command line page?
@gsuberland MSBuildDetailedOutput.txt It is these contents, right?
Not quite what I was looking for, but that works. Those logs show that you are passing /d2FH4
to the compiler, not /d2FH4-
.
Line 1453:
1> D:\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.28.29333\bin\HostX86\x64\CL.exe /c /Zi /nologo /W3 /WX- /diagnostics:column /sdl /O2 /Oi /GL /D NDEBUG /D _CONSOLE /D _UNICODE /D UNICODE /Gm- /EHsc /MD /GS /Gy /fp:precise /Qspectre /permissive- /Zc:wchar_t /Zc:forScope /Zc:inline /Yc"pch.h" /Fp"x64\Release\al-khaser.pch" /Fo"x64\Release\\" /Fd"x64\Release\vc142.pdb" /Gd /TP /FC /errorReport:prompt /d2FH4 pch.cpp
Not quite what I was looking for, but that works. Those logs show that you are passing
/d2FH4
to the compiler, not/d2FH4-
.
Yes, I found this command without dash from the link you just gave me. /d2FH4
and /d2FH4-
I tried both, did not work. :(
1> D:\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.28.29333\bin\HostX86\x64\CL.exe /c /Zi /nologo /W3 /WX- /diagnostics:column /sdl /O2 /Oi /GL /D NDEBUG /D _CONSOLE /D _UNICODE /D UNICODE /Gm- /EHsc /MD /GS /Gy /fp:precise /Qspectre /permissive- /Zc:wchar_t /Zc:forScope /Zc:inline /Yc"pch.h" /Fp"x64\Release\al-khaser.pch" /Fo"x64\Release\\" /Fd"x64\Release\vc142.pdb" /Gd /TP /FC /errorReport:prompt /d2FH4- pch.cpp
Oh, I took a look at the article comments, without the dash is to enable it, I just noticed that I misunderstood the article content.
I have replicated this issue in the latest version of VS2019 and confirmed that this is unrelated to FH4 exception handling. Turning the feature off completely does not change the behaviour. I will continue to investigate the new features to see what might have caused this issue.
I've identified the issue.
The behaviour of the 0x2D interrupt is dependent on the value of the rax
register at the time of the interrupt. In the unaffected builds, the code around the interrupt call looks something like this:
call AddVectoredExceptionHandler
mov rbx, rax
mov dword ptr [SwallowedException], 0x1
call __int2d
mov rcx, rbx
call RemoveVectoredExceptionHandler
In the affected builds, it instead looks like this:
call AddVectoredExceptionHandler
mov rbx, rax
mov eax, 1
mov [SwallowedException], eax
call __int2d
mov rcx, rbx
call RemoveVectoredExceptionHandler
Notice that in the unaffected builds, rax
contains the return value of AddVectoredExceptionHandler
. This is a pointer to a descriptor. By contrast, in the affected builds, the SwallowedException
value of 1 is loaded through eax, which means that rax
is 1 in these builds.
The value of rax
affects the behaviour of the int 0x2d
interrupt.
I noticed that this might be the culprit when I put a printf
in the code and it started working, but only with certain text and only when I put it in a certain place. The return value of printf
was overwriting the value in rax
, and when I changed the text it changed the return value (based on the length of the printed string). When I moved the call, it changed how the compiler ordered the operations and how it assigned registers.
The behaviour of VEH doesn't appear to have changed since the VS2019 update. The only difference is that the updated compiler made different decisions about how to order the operations and how to allocate registers. Our original code only worked by sheer luck because the return value of the previous operation happened to not be a small number, so it didn't do anything special when put into rax
. Your fixes happened to work for the same reason - they moved the calls around so that the value in rax
just happened to not cause problems.
The actual code path taken that causes this issue is quite interesting. For reference, here's the assembly for __int2d
, shown with opcode bytes:
0: cd 2d int 0x2d
2: 90 nop
3: c3 ret
When the int 0x2d
instruction is hit, it causes the associated dispatch function registered in the IDT on to be executed. On Windows this is in KiDebugServiceTrap
, which looks like this:
PUBLIC KiDebugServiceTrap
FUNC KiDebugServiceTrap
/* No error code */
EnterTrap TF_SAVE_ALL
/* Increase Rip to skip the int3 */
inc qword ptr [rbp + KTRAP_FRAME_Rip]
/* Dispatch the exception (Params = service, buffer, legth) */
DispatchException STATUS_BREAKPOINT, 3, [rbp+KTRAP_FRAME_Rax], [rbp+KTRAP_FRAME_Rcx], [rbp+KTRAP_FRAME_Rdx]
/* Return */
ExitTrap TF_SAVE_ALL
ENDFUNC
We can see here that it increments the rip
register value in the trap frame, by one. Because we're triggering this from an int 0x2d
instruction, this actually moves the instruction pointer to half way into the instruction instead. It then calls DispatchException
, passing the values of rax
, rcx
, and rdx
as three parameters. The code then constructs an EXCEPTION_RECORD
structure that looks like this:
EXCEPTION_RECORD.ExceptionCode = STATUS_BREAKPOINT;
EXCEPTION_RECORD.ExceptionFlags = 0;
EXCEPTION_RECORD.ExceptionRecord = 0;
EXCEPTION_RECORD.ExceptionAddress = KTRAP_FRAME.rip; // address of int 0x2d plus 1
EXCEPTION_RECORD.NumberParameters = 3;
EXCEPTION_RECORD.ExceptionInformation[0] = KTRAP_FRAME.rax;
EXCEPTION_RECORD.ExceptionInformation[1] = KTRAP_FRAME.rcx;
EXCEPTION_RECORD.ExceptionInformation[2] = KTRAP_FRAME.rdx;
The KEXCEPTION_FRAME
structure is then populated with all of the remaining registers and these structures are dispatched via KiDispatchException
. The KiDispatchException
implementation checks if the ExceptionCode
is STATUS_BREAKPOINT
and, if so, it undoes the increment that the KiDebugServiceTrap
handler performed, by decrementing rip
in the captured context structure. At this point rip
is back where it started.
At this point the control flow ends up in KdpTrap
, which implements debug services. When the exception record is STATUS_BREAKPOINT
, as it is here, it uses the value in ExceptionInformation[0]
as the debug service identifier. This is for things like debug printing and prompting. The defined values are:
#define BREAKPOINT_BREAK 0
#define BREAKPOINT_PRINT 1
#define BREAKPOINT_PROMPT 2
#define BREAKPOINT_LOAD_SYMBOLS 3
#define BREAKPOINT_UNLOAD_SYMBOLS 4
#define BREAKPOINT_COMMAND_STRING 5
This is what the different rax
values do when we hit an int 0x2d
. The code stores the program counter from the context record and allows each of these service calls to modify it.
rax=0
just drops straight into the debugger trap handler. Doesn't increment the instruction pointer.rax=1
calls BREAKPOINT_PRINT
service, which is implemented by KdpPrint
, increments the instruction pointer.rax=2
calls BREAKPOINT_PROMPT
service, which is implemented by KdpPrompt
, doesn't increment the instruction pointer.rax=3
calls BREAKPOINT_LOAD_SYMBOLS
, which is implemented by KdpSymbol
, increments the instruction pointer.rax=4
calls BREAKPOINT_UNLOAD_SYMBOLS
, which is also implemented by KdpSymbol
, increments the instruction pointer.rax=5
calls BREAKPOINT_COMMAND_STRING
, which is implemented in KdpCommandString
, increments the instruction pointer.This is why many blogs mention setting rax
to 1, 3, 4, or 5 in relation to this trick. In our case we don't care about the instruction pointer incrementing behaviour (in fact it gets incremented twice - once in KdpStub
and once in KdpTrap
), but rather the fact that it raises an exception when the debug trap handler is called. That's why we want rax
to be 0
, or some other value that isn't associated with a kernel debug service (e.g. rax=0x69
works just fine too).
If we set rax
to 0 explicitly, at the start of int2d_x64.asm
, the code suddenly starts working! This is because we are specifying the breakpoint command.
This is such a tiny fix that I'll apply it directly in the main branch.
Fix applied and I've verified that it's working as intended now. Thanks for submitting this one, I don't think I'd have ever found a fix without a sample executable to look at!
@gsuberland Thanks for your detailed explanation and fix too!
I compiled this project myself and tested it on multiple machines, the
Checking Interupt 0x2d
result was always BAD.I did the following checks:
Added a debug printing to
VectoredHandler
function, but there is no output, the exception handler function is not called.The return value of
AddVectoredExceptionHandler
was notNULL
, so the API call should be successful.Checked through IDA, the
SwallowedException
variable was not removed by the optimizer and theVectoredHandler
function andint 2Dh
instruction is still there.Then I found any of the following workarounds that could make it work:
Add
#pragma optimize("g", off)
and#pragma optimize("g", on)
to the head and end of theInterrupt_0x2d.cpp
file respectively.Add
__declspec(noinline)
attribute to theInterrupt_0x2d
function.Move the position of the
Interrupt_0x2d
function in themain
function (order of detections) or remove some other detections.But I can't figure out why the exception handler doesn't work, and why these workarounds make it work. Looking at these workarounds, it seems like the optimizer screwed something up, but I didn't see any issues from IDA. If someone knows and is willing to explain, that would be great.
Im using Visual Studio 2019, compiled it with Release-x64 configuration. And here is the binary file I compiled: Release.zip
Thanks in advance!