One: Background
1. Tell stories
In the last article, we talked about why the Console is stuck. Friends who have read that article believe that they have a general understanding of conhost.exe. This article goes further and talks about the special events of the window Ctrl + C
What is the underlying flow like? To facilitate the description, let chagtgpt generate a Ctrl + C business code for me.
class Program { static void Main(string[] args) { Console.CancelKeyPress + = new ConsoleCancelEventHandler(CancelKeyPressHandler); Console.WriteLine("Press Ctrl + C and try it!"); while(true) { //Write your code here } } static void CancelKeyPressHandler(object sender, ConsoleCancelEventArgs e) { Console.WriteLine("You pressed Ctrl + C!"); //Here you can add the code you want to execute e.Cancel = true; } }
Then simply run it to confirm that there is nothing wrong with the code.
Two: Publish and subscribe model
1. Where are the subscription events
I believe many friends have read this code and know that this is a publish-subscribe model. The code subscribes to the Console.CancelKeyPress
event. Once there is Ctrl + C
or Ctrl + Break
will automatically execute the corresponding subscription logic when it occurs, but there is a problem here. Ctrl + C is a window event, so Win32 API must be involved. With this idea, you can decompile and take a lookConsole.CancelKeyPress
Does the bottom layer use the Win32 API? The reference code is as follows:
private unsafe static PosixSignalRegistration Register(PosixSignal signal, Action<PosixSignalContext> handler) { Token token = new Token(signal, handler); PosixSignalRegistration result = new PosixSignalRegistration(token); lock (s_registrations) { if (s_registrations.Count == 0 & amp; & amp; !Interop.Kernel32.SetConsoleCtrlHandler((delegate* unmanaged<int, Interop.BOOL>)(delegate*<int, Interop.BOOL>)( & amp;HandlerRoutine) , Add: true)) { throw Win32Marshal.GetExceptionForLastWin32Error(); } s_registrations.Add(token); return result; } }
Judging from the hexagram, the handler is registered through the Kernel32.SetConsoleCtrlHandler method. If you don’t believe it, you can also use windbg to verify it.
0:000> bp KERNEL32!SetConsoleCtrlHandler breakpoint 0 redefined 0:000> g 0:000> k 6 # Child-SP RetAddr Call Site 00 00000064`e7b7e948 00007ff9`8162b797 KERNEL32!SetConsoleCtrlHandler 01 00000064`e7b7e950 00007ff9`817ac3cd System_Private_CoreLib + 0x1ab797 02 00000064`e7b7ea20 00007ff9`817ac270 System_Private_CoreLib!System.Runtime.InteropServices.PosixSignalRegistration.Register + 0xdd 03 00000064`e7b7ea90 00007ffa`144f8437 System_Private_CoreLib!System.Runtime.InteropServices.PosixSignalRegistration.Create + 0x10 04 00000064`e7b7eac0 00007ff9`226f29a1 System_Console!System.Console.add_CancelKeyPress + 0x97 05 00000064`e7b7eb20 00007ff9`8229af33 ConsoleApp1!ConsoleApp1.Program.Main + 0x61 ...
2. Where to publish events
Friends who are familiar with Win32 programming
believe that they already understand it very well. It turns out that C#’s Ctrl + C is a set of gameplay based on win32api encapsulation, that is, use KERNEL32!SetConsoleCtrlHandler
to register, use KERNELBASE!CtrlRoutine
to publish. To verify it is very simple, you can add a Debugger.Break();
in the callback function.
static void CancelKeyPressHandler(object sender, ConsoleCancelEventArgs e) { Console.WriteLine("You pressed Ctrl + C!"); //Here you can add the code you want to execute e.Cancel = true; Debugger.Break(); }
After running the program, use the Ctrl + C
shortcut key on the console. One thing to note here is that this event will trigger an interrupt. We use gn (Go with Exception Not Handled) to handle it, otherwise windbg will swallow this exception notification.
0:000> g (224c.51c8): Control-C exception - code 40010005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. KERNELBASE!CtrlRoutine + 0x369ff: 00007ffa`4ab9169f 0f1f440000 nop dword ptr [rax + rax] 0:008> dp rax + rax L1 00000456`73806260 ?`? 0:008>gn (224c.51c8): Break instruction exception - code 80000003 (first chance) KERNELBASE!wil::details::DebugBreak + 0x2: 00007ffa`4ab9d962 cc int 3 0:008> k # Child-SP RetAddr Call Site 00 0000003f`63b7f848 00007ff9`82343ed9 KERNELBASE!wil::details::DebugBreak + 0x2 01 0000003f`63b7f850 00007ff9`8184a66a coreclr!DebugDebugger::Break + 0x149 [D:\a\_work\1\s\src\coreclr\vm\debugdebugger.cpp @ 148] 02 0000003f`63b7f9d0 00007ff9`227198ca System_Private_CoreLib!System.Diagnostics.Debugger.Break + 0xa [/_/src/coreclr/System.Private.CoreLib/src/System/Diagnostics/Debugger.cs @ 18] 03 0000003f`63b7fa00 00007ffa`1448922d ConsoleApp1!ConsoleApp1.Program.CancelKeyPressHandler + 0x4a [D:\code\MyApplication\ConsoleApp1\Program.cs @ 32] 04 0000003f`63b7fa30 00007ff9`817ac6c3 System_Console!System.Console.HandlePosixSignal + 0x7d [/_/src/libraries/System.Console/src/System/Console.cs @ 952] 05 0000003f`63b7fa80 00007ffa`4ab91704 System_Private_CoreLib!System.Runtime.InteropServices.PosixSignalRegistration.HandlerRoutine + 0x193 [/_/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/PosixSignalRe gistration.Windows.cs@106] 06 0000003f`63b7fb20 00007ffa`4b667614 KERNELBASE!CtrlRoutine + 0x36a64 07 0000003f`63b7fc10 00007ffa`4cdc26a1 KERNEL32!BaseThreadInitThunk + 0x14 08 0000003f`63b7fc40 00000000`00000000 ntdll!RtlUserThreadStart + 0x21
Judging from the hexagram information above, it turns out that the KERNELBASE!CtrlRoutine
logic triggered our custom callback function. I believe that careful friends will definitely have a question? How did this thread get new? It was not created in my code!
3. How does the CtrlRoutine thread come from
To answer this question, you need to have an understanding of some important processes in the Windows operating system. For example, what does csrss.exe do? You can consult chatgpt.
Its words have implicitly told us that the thread may be remotely injected by the csrss process. Is that so? You can set a breakpoint to observe.
0: kd> !process 0 0 csrss.exe PROCESS ffffe001d350f300 SessionId: 0 Cid: 0180 Peb: 7ff755c16000 ParentCid: 0178 DirBase: 1037cd000 ObjectTable: ffffc000a88e0480 HandleCount: <Data Not Accessible> Image: csrss.exe PROCESS ffffe001d351c080 SessionId: 1 Cid: 01d8 Peb: 7ff75628d000 ParentCid: 01c8 DirBase: 10471f000 ObjectTable: ffffc000a907a140 HandleCount: <Data Not Accessible> Image: csrss.exe 0: kd> bp /p ffffe001d351c080 ntdll!NtCreateThreadEx 0: kd> g 3: kd> .reload /user 3: kd> k # Child-SP RetAddr Call Site 00 000000ff`917ff238 00007fff`9e9d6720 ntdll!NtCreateThreadEx 01 000000ff`917ff240 00007fff`9e9d6602 ntdll!RtlpCreateUserThreadEx + 0x110 02 000000ff`917ff380 00007fff`9b310290 ntdll!RtlCreateUserThread + 0x62 03 000000ff`917ff3f0 00007fff`9b30efb9 winsrv!InternalCreateCallbackThread + 0x114 04 000000ff`917ff4b0 00007fff`9b3109e9 winsrv!CreateCtrlThread + 0xe9 05 000000ff`917ff610 00007fff`9b365837 winsrv!SrvEndTask + 0xa9 06 000000ff`917ff880 00007fff`9e969f75 CSRSRV!CsrApiRequestThread + 0x4d7 07 000000ff`917ffd10 00000000`00000000 ntdll!RtlUserThreadStart + 0x45
Judging from the CreateCtrlThread
and ntdll!NtCreateThreadEx
in the hexagram, it seems that a remote thread related to Ctrl + C is really created. Is that true? The method signature is as follows:
NTSTATUS NtCreateThreadEx( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, HANDLE ProcessHandle, PVOID StartRoutine, PVOID Argument, ULONGCreateFlags, ULONG_PTR ZeroBits, SIZE_T StackSize, SIZE_T MaximumStackSize, PVOID AttributeList );
Then use windbg to extract the fifth parameter (StartRoutine) and verify it. The screenshot is as follows:
From the information in the hexagram, it can be seen that kernelbase!CtrlRoutine
is indeed used as the thread entry point, which has been verified.
At this point, some friends may have raised a new question. In the previous article, didn’t you say that GetMessageW of conhost.exe is used to receive window events? For example, this Ctrl + C
event, how does it notify csrss.exe
to inject a remote thread into ConsoleApp1.exe
?
4. How conhost notifies csrss
This question sounds quite confusing. In fact, friends who are familiar with Windows programming know that there is a mechanism called ALPC
in the Windows kernel that is specifically used to implement inter-process communication. The implication is conhost
and csrss
have an ALPC connection
, which can be observed with windbg’s !alpc command.
1: kd> !process 0n4028 0 Searching for Process with Cid == fbc PROCESS ffffe001d26a3080 SessionId: 1 Cid: 0fbc Peb: 7ff680afc000 ParentCid: 05e4 DirBase: 8b8de000 ObjectTable: ffffc000ab252cc0 HandleCount: <Data Not Accessible> Image: conhost.exe 1: kd> !alpc /lpp ffffe001d26a3080 Ports the process ffffe001d26a3080 is connected to: ffffe001d28a25c0 0 -> ffffe001d1749090 ('ApiPort') 1 ffffe001d351c080 ('csrss.exe') ... 1: kd> !process ffffe001d26a3080 ... THREAD ffffe001d2250080 Cid 0fbc.0f94 Teb: 00007ff680afe000 Win32Thread: ffffe001d4559fa0 WAIT: (WrLpcReply) UserMode Non-Alertable ffffe001d22506b8 Semaphore Limit 0x1 Waiting for reply to ALPC Message ffffc000b1c6a7a0 : queued at port ffffe001d1749090 : owned by process ffffe001d351c080 Not impersonating DeviceMap ffffc000a9ea6f50 Owning Process ffffe001d26a3080 Image: conhost.exe Attached Process N/A Image: N/A Wait Start TickCount 48619 Ticks: 1 (0:00:00:00.015) Context Switch Count 1406 IdealProcessor: 1 UserTime 00:00:00.031 KernelTime 00:00:00.687 Win32 Start Address 0x00007fff8e601c90 Stack Init ffffd00030df4c90 Current ffffd00030df4500 Base ffffd00030df5000 Limit ffffd00030def000 Call 0000000000000000 Priority 12 BasePriority 8 PriorityDecrement 2 IoPriority 2 PagePriority 5 Child-SP RetAddr Call Site ffffd000`30df4540 fffff802`792c0090 nt!KiSwapContext + 0x76 ffffd000`30df4680 fffff802`792bfaa8 nt!KiSwapThread + 0x160 ffffd000`30df4730 fffff802`792b86e5 nt!KiCommitThreadWait + 0x148 ffffd000`30df47c0 fffff802`792bc643 nt!KeWaitForSingleObject + 0x385 ffffd000`30df4870 fffff802`7969e70b nt!AlpcpSignalAndWait + 0x213 ffffd000`30df4910 fffff802`7969f32a nt!AlpcpReceiveSynchronousReply + 0x5b ffffd000`30df4970 fffff802`7978f1fd nt!AlpcpProcessSynchronousRequest + 0x34a ffffd000`30df4a60 fffff802`7978f142 nt!LpcpRequestWaitReplyPort + 0x95 ffffd000`30df4ac0 fffff802`793cdb63 nt!NtRequestWaitReplyPort + 0x6e ffffd000`30df4b00 00007fff`9e9f3a4a nt!KiSystemServiceCopyEnd + 0x13 (TrapFrame @ ffffd000`30df4b00) 00000062`3d2bf5d8 00000000`00000000 ntdll!NtRequestWaitReplyPort + 0xa
If you carefully observe the information in the hexagram, you can see that there is indeed an ALPC connection communicating with csrss.exe
, and conhost is waiting for the csrss message to return, that is, Waiting for reply to ALPC Message
.
At this point, I feel that all the questions have been cleared up. Drawing a picture probably looks like this.
Three: Summary
This article explains in detail the underlying gameplay of Ctrl + C
. I think the more important thing is how to use windbg. Seeing is believing. This is the value of debugging. I hope it can help friends who have doubts about this issue. Bring a little value.