Intro to kernel debugging 3
Topic: Probing, Altering User Mode Memory
This is part 3 of the intro to kernel debugging series. Other posts:
- Intro to kernel debugging 1
- KD setup
- Intro to kernel debugging 2
- Debugger context
In this post, we will explore the following:
- Probe memory of a user mode process
- Alter user mode process memory
Reminders about how this tutorial is authored:
- The author is a user mode developer, and tailors the conversation to that audience
- Always make sure you have symbols loaded!
- We are using a kd connection to a VM, as explained in a previous tutorial
Setup Test Apparatus
Let's create a simple user mode app to test with. For simplicity, we want a test app that runs for a long time, making it easier to break into the app and inspect its state. Here is the app we will be using:
#include <windows.h> #include <iostream> void main(void) { std::wcout << L"Hello, world" << std::endl; for (int i = 0; ; i++) { std::wcout << L"Loop iteration: " << i << std::endl; Sleep(1000); } }
Having the test app wake up from time to time and inspect state will help illustrate the ability to modify state.
In a previous tutorial, we used a kd connection to a local VM. To set up the test apparatus, compile the above code and run the app on the VM OS.
Inspecting User Mode Process
Start by breaking into the debugger with control+break (windbg) or control+c (kd). As always, make sure you have symbols loaded:
Microsoft (R) Windows Debugger Version 10.0.10586.567 AMD64 Copyright (c) Microsoft Corporation. All rights reserved. Opened \\.\pipe\kd Waiting to reconnect... Connected to Windows 10 10240 x64 target at (Wed Jun 29 18:03:54.125 2016 (UTC - 7:00)), ptr64 TRUE Kernel Debugger connection established. Symbol search path is: srv* Executable search path is: Windows 10 Kernel Version 10240 MP (4 procs) Free x64 Product: WinNt, suite: TerminalServer SingleUserTS Built by: 10240.16841.amd64fre.th1_st1.160408-1853 Machine Name: Kernel base = 0xfffff800`8f685000 PsLoadedModuleList = 0xfffff800`8f9aa070 Debug session time: Wed Jun 29 18:03:52.560 2016 (UTC - 7:00) System Uptime: 0 days 4:34:42.939 Break instruction exception - code 80000003 (first chance) ******************************************************************************* * * * You are seeing this message because you pressed either * * CTRL+C (if you run console kernel debugger) or, * * CTRL+BREAK (if you run GUI kernel debugger), * * on your debugger machine's keyboard. * * * * THIS IS NOT A BUG OR A SYSTEM CRASH * * * * If you did not intend to break into the debugger, press the "g" key, then * * press the "Enter" key now. This message might immediately reappear. If it * * does, press "g" and "Enter" again. * * * ******************************************************************************* nt!DbgBreakPointWithStatus: fffff800`8f7d9bb0 cc int 3 0: kd> .sympath cache*d:\sym;srv* Symbol search path is: cache*d:\sym;srv* Expanded Symbol search path is: cache*d:\sym;SRV*https://msdl.microsoft.com/download/symbols ************* Symbol Path validation summary ************** Response Time (ms) Location Deferred cache*d:\sym Deferred srv* 0: kd> .reload /f *.* *** WARNING: Unable to verify timestamp for msrpc.sys *** ERROR: Module load completed but symbols could not be loaded for msrpc.sys *** ERROR: Symbol file could not be found. Defaulted to export symbols for clipsp.sys - Press ctrl-c (cdb, kd, ntsd) or ctrl-break (windbg) to abort symbol loads that take too long. Run !sym noisy before .reload to track down problems loading symbols. *** WARNING: Unable to verify timestamp for spaceport.sys *** ERROR: Module load completed but symbols could not be loaded for spaceport.sys *** WARNING: Unable to verify timestamp for volmgrx.sys *** ERROR: Module load completed but symbols could not be loaded for volmgrx.sys *** WARNING: Unable to verify timestamp for Fs_Rec.sys *** ERROR: Module load completed but symbols could not be loaded for Fs_Rec.sys *** WARNING: Unable to verify timestamp for Null.SYS *** ERROR: Module load completed but symbols could not be loaded for Null.SYS *** ERROR: Module load completed but symbols could not be loaded for peauth.sys
I gave a name to my test app, and I will be searching for that name in the process list: meason_test.exe. The following command does the search:
0: kd> !process 0 0 meason_test.exe PROCESS ffffe0016723c840 SessionId: 1 Cid: 1078 Peb: 7ff7e1ed5000 ParentCid: 0eec DirBase: 4a4ea000 ObjectTable: ffffc00063ac0c80 HandleCount: <Data Not Accessible> Image: meason_test.exe
Let's take a peek at what the process is doing. We can do that by looking at the call stacks of all threads in the process, which can be done by passing the 0x17 value as the second argument to !process. However, before we can do that, we must make sure the debugger can find the user mode symbols:
0: kd> .sympath+ D:\_inc\test\amd64 Symbol search path is: cache*d:\sym;srv*;D:\_inc\test\amd64 Expanded Symbol search path is: cache*d:\sym;SRV*https://msdl.microsoft.com/download/symbols;d:\_inc\test\amd64
You may wish to optimize the symbol search by putting the private symbol path before the public symbol path, but we won't do that here.
On to the process inspection with the 0x17 option. Use the nt!_EPROCESS address that was output in the above !process search (it has the word "PROCESS" next to it in all caps).
0: kd> !process ffffe0016723c840 17 PROCESS ffffe0016723c840 SessionId: 1 Cid: 1078 Peb: 7ff7e1ed5000 ParentCid: 0eec DirBase: 4a4ea000 ObjectTable: ffffc00063ac0c80 HandleCount: <Data Not Accessible> Image: meason_test.exe VadRoot ffffe001667c91e0 Vads 20 Clone 0 Private 88. Modified 0. Locked 0. DeviceMap ffffc0006a2b8870 Token ffffc00063c58060 ElapsedTime 00:00:31.376 UserTime 00:00:00.000 KernelTime 00:00:00.000 QuotaPoolUsage[PagedPool] 18696 QuotaPoolUsage[NonPagedPool] 2712 Working Set Sizes (now,min,max) (512, 50, 345) (2048KB, 200KB, 1380KB) PeakWorkingSetSize 486 VirtualSize 2097160 Mb PeakVirtualSize 2097161 Mb PageFaultCount 512 MemoryPriority BACKGROUND BasePriority 8 CommitCharge 84 Setting context for this process... ffffffffffffffff NotificationEvent Not impersonating DeviceMap ffffc0006a2b8870 Owning Process ffffe0016723c840 Image: meason_test.exe Attached Process N/A Image: N/A Wait Start TickCount 1078268 Ticks: 4 (0:00:00:00.062) Context Switch Count 1265 IdealProcessor: 1 UserTime 00:00:00.000 KernelTime 00:00:00.015 Win32 Start Address meason_test!mainCRTStartup (0x00007ff7e281b9f0) Stack Init ffffd0010e766c90 Current ffffd0010e766790 Base ffffd0010e767000 Limit ffffd0010e761000 Call 0 Priority 8 BasePriority 8 UnusualBoost 0 ForegroundBoost 0 IoPriority 2 PagePriority 5 Child-SP RetAddr : Args to Child : Call Site ffffd001`0e7667d0 fffff800`8f6d92a0 : ffffe001`00000000 00000000`00000001 00000000`00000000 ffffe001`63b4f440 : nt!KiSwapContext+0x76 ffffd001`0e766910 fffff800`8f6d8cb8 : ffffe001`63b4f340 fffff800`8f9eb780 000000b6`b5522ee7 ffffe001`670bbf20 : nt!KiSwapThread+0x160 ffffd001`0e7669c0 fffff800`8f709d89 : 00000027`3ac0b8cd 00000000`00000001 00000000`000000b0 000000fe`a11bf910 : nt!KiCommitThreadWait+0x148 ffffd001`0e766a50 fffff800`8fb2ecac : ffffe001`63b4f340 00000000`00000000 00000000`00000000 000000fe`a11bf900 : nt!KeDelayExecutionThread+0x229 ffffd001`0e766ad0 fffff800`8f7deb63 : ffffe001`63b4f340 00007ff7`e1ed5000 ffffffff`ff676980 ffffe001`6427c350 : nt!NtDelayExecution+0x5c ffffd001`0e766b00 00007fff`c5803b6a : 00007fff`c2be3757 000000fe`a11bfbd8 000000fe`a11bfb01 ffffffff`00000000 : nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ ffffd001`0e766b00) 000000fe`a11bfb18 00007fff`c2be3757 : 000000fe`a11bfbd8 000000fe`a11bfb01 ffffffff`00000000 00007ff7`e2821440 : ntdll!NtDelayExecution+0xa 000000fe`a11bfb20 00007ff7`e2818323 : 00000000`00000001 00007ff7`00000000 00000000`00000000 00000000`00000000 : KERNELBASE!SleepEx+0xa7 000000fe`a11bfbc0 00007ff7`e281b96d : 00000000`00000001 00000000`00000000 00000000`00000000 00007ff7`e281d3d0 : meason_test!main+0x73 [d:\test\meason_test\main.cpp @ 13] 000000fe`a11bfc00 00007fff`c3c22d92 : 00007ff7`e281b9f0 00007ff7`e1ed5000 00007ff7`e1ed5000 00000000`00000000 : meason_test!__mainCRTStartup+0x14d [d:\(omitted) @ 697] 000000fe`a11bfc40 00007fff`c5779f64 : 00007fff`c3c22d70 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x22 000000fe`a11bfc70 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x34
Probe User Mode Memory
At this point, we could attempt to query user mode memory with the 'u' command (unassemble). However, it likely won't work as expected. Let's inspect a return address from the call stack above (chosen carefully to ensure we are looking at our own app):
0: kd> u 00007ff7`e2818323 00007ff7`e2818323 ?? ??? ^ Memory access error in 'u 00007ff7`e2818323'
As we learned in the last tutorial, when you first break in with the kernel debugger, your debugger context may not necessarily be set to where you want it to be. That's why we are unable to read this address: the debugger is using some other process context, and it is one in which this address does not translate to a valid page.
Let's set the right debugger context for our test app. Use the nt!_EPROCESS address that was output in the !process command above.
0: kd> .process /p /r ffffe0016723c840 Implicit process is now ffffe001`6723c840 .cache forcedecodeuser done Loading User Symbols ..... ************* Symbol Loading Error Summary ************** Module name Error msrpc The system cannot find the file specified clipsp The system cannot find the file specified spaceport The system cannot find the file specified volmgrx The system cannot find the file specified Fs_Rec The system cannot find the file specified Null The system cannot find the file specified peauth The system cannot find the file specified You can troubleshoot most symbol related issues by turning on symbol loading diagnostics (!sym noisy) and repeating the command that caused symbols to be loaded. You should also verify that your symbol search path (.sympath) is correct.
You should now be able to unassemble the user mode call stack address:
0: kd> ub 00007ff7`e2818323 meason_test!main+0x4d [d:\test\meason_test\main.cpp @ 11]: 00007ff7`e28182fd 8b542420 mov edx,dword ptr [rsp+20h] 00007ff7`e2818301 488bc8 mov rcx,rax 00007ff7`e2818304 e887a6ffff call meason_test!std::basic_ostream<wchar_t,std::char_traits<wchar_t> >::operator<< (00007ff7`e2812990) 00007ff7`e2818309 488d15a0f1ffff lea rdx,[meason_test!std::endl (00007ff7`e28174b0)] 00007ff7`e2818310 488bc8 mov rcx,rax 00007ff7`e2818313 e8e8a8ffff call meason_test!std::basic_ostream<wchar_t,std::char_traits<wchar_t> >::operator<< (00007ff7`e2812c00) 00007ff7`e2818318 b9e8030000 mov ecx,3E8h 00007ff7`e281831d ff159d4d0000 call qword ptr [meason_test!_imp_Sleep (00007ff7`e281d0c0)] 0: kd> u meason_test!main+0x73 [d:\test\meason_test\main.cpp @ 13]: 00007ff7`e2818323 ebbb jmp meason_test!main+0x30 (00007ff7`e28182e0) 00007ff7`e2818325 4883c438 add rsp,38h 00007ff7`e2818329 c3 ret 00007ff7`e281832a cc int 3 00007ff7`e281832b cc int 3 00007ff7`e281832c cc int 3 00007ff7`e281832d cc int 3 00007ff7`e281832e cc int 3
Observe that we have user mode code showing up in the kernel debugger! The use of the 'ub' unassembled command was chosen in order to show the assembly code leading up to our Sleep() call.
Alter User Memory
Let's start messing around with the memory in our app! Recall that our test app from above had a running counter. Let's edit its value. While the app is running (and the kernel debugger is in run mode, not break mode), you will see the loop iteration keeps increasing:
c:\test\amd64>meason_test.exe
Hello, world
Loop iteration: 0
Loop iteration: 1
Loop iteration: 2
Loop iteration: 3
Loop iteration: 4
Loop iteration: 5
Break into the debugger, find our test process, and set the debugger context to that process:
0: kd> !process 0 0 meason_test.exe
PROCESS ffffe00138301840
SessionId: 1 Cid: 0450 Peb: 7ff7a568f000 ParentCid: 0cb0
DirBase: 21cb6000 ObjectTable: ffffc001ae412840 HandleCount: <Data Not Accessible>
Image: meason_test.exe
0: kd> .process /p /r ffffe00138301840
Implicit process is now ffffe001`38301840
.cache forcedecodeuser done
Loading User Symbols
.....
Using !process, let's iterate over all threads in the process:
0: kd> !process ffffe00138301840 f
PROCESS ffffe00138301840
SessionId: 1 Cid: 0450 Peb: 7ff7a568f000 ParentCid: 0cb0
DirBase: 21cb6000 ObjectTable: ffffc001ae412840 HandleCount: <Data Not Accessible>
Image: meason_test.exe
VadRoot ffffe00138d16010 Vads 20 Clone 0 Private 89. Modified 0. Locked 0.
DeviceMap ffffc001a6c4c710
Token ffffc001a7633060
ElapsedTime 00:01:28.220
UserTime 00:00:00.000
KernelTime 00:00:00.000
QuotaPoolUsage[PagedPool] 18696
QuotaPoolUsage[NonPagedPool] 2712
Working Set Sizes (now,min,max) (518, 50, 345) (2072KB, 200KB, 1380KB)
PeakWorkingSetSize 491
VirtualSize 2097160 Mb
PeakVirtualSize 2097162 Mb
PageFaultCount 519
MemoryPriority BACKGROUND
BasePriority 8
CommitCharge 86
THREAD ffffe00138d9a640 Cid 0450.0e2c Teb: 00007ff7a568d000 Win32Thread: 0000000000000000 WAIT: (DelayExecution) UserMode Non-Alertable
ffffffffffffffff NotificationEvent
Not impersonating
DeviceMap ffffc001a6c4c710
Owning Process ffffe00138301840 Image: meason_test.exe
Attached Process N/A Image: N/A
Wait Start TickCount 11054 Ticks: 64 (0:00:00:01.000)
Context Switch Count 1114 IdealProcessor: 1
UserTime 00:00:00.000
KernelTime 00:00:00.031
Win32 Start Address meason_test!mainCRTStartup (0x00007ff7a5b9b9f0)
Stack Init ffffd0015490dc90 Current ffffd0015490d790
Base ffffd0015490e000 Limit ffffd00154908000 Call 0
Priority 8 BasePriority 8 UnusualBoost 0 ForegroundBoost 0 IoPriority 2 PagePriority
Child-SP RetAddr Call Site
ffffd001`5490d7d0 fffff801`8f4592a0 nt!KiSwapContext+0x76
ffffd001`5490d910 fffff801`8f458cb8 nt!KiSwapThread+0x160
ffffd001`5490d9c0 fffff801`8f489d89 nt!KiCommitThreadWait+0x148
ffffd001`5490da50 fffff801`8f8aecac nt!KeDelayExecutionThread+0x229
ffffd001`5490dad0 fffff801`8f55eb63 nt!NtDelayExecution+0x5c
ffffd001`5490db00 00007ffa`5a313b6a nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ ffffd001`5490db00)
000000c5`afc5f7d8 00007ffa`57743757 ntdll!NtDelayExecution+0xa
000000c5`afc5f7e0 00007ff7`a5b98323 KERNELBASE!SleepEx+0xa7
000000c5`afc5f880 00007ff7`a5b9b96d meason_test!main+0x73 [d:\test\meason_test\main.cpp @ 13]
000000c5`afc5f8c0 00007ffa`57f42d92 meason_test!__mainCRTStartup+0x14d [(omitted) @ 697]
000000c5`afc5f900 00007ffa`5a289f64 KERNEL32!BaseThreadInitThunk+0x22
000000c5`afc5f930 00000000`00000000 ntdll!RtlUserThreadStart+0x34
Our process only has one thread, so choosing the relevant thread here is trivial. Set the debugger context to that thread, so we can pull up its call stack:
0: kd> .thread /p /r ffffe00138d9a640
Implicit thread is now ffffe001`38d9a640
Implicit process is now ffffe001`38301840
.cache forcedecodeuser done
Loading User Symbols
.....
0: kd> k
*** Stack trace for last set context - .thread/.cxr resets it
# Child-SP RetAddr Call Site
00 ffffd001`5490d7d0 fffff801`8f4592a0 nt!KiSwapContext+0x76
01 ffffd001`5490d910 fffff801`8f458cb8 nt!KiSwapThread+0x160
02 ffffd001`5490d9c0 fffff801`8f489d89 nt!KiCommitThreadWait+0x148
03 ffffd001`5490da50 fffff801`8f8aecac nt!KeDelayExecutionThread+0x229
04 ffffd001`5490dad0 fffff801`8f55eb63 nt!NtDelayExecution+0x5c
05 ffffd001`5490db00 00007ffa`5a313b6a nt!KiSystemServiceCopyEnd+0x13
06 000000c5`afc5f7d8 00007ffa`57743757 ntdll!NtDelayExecution+0xa
07 000000c5`afc5f7e0 00007ff7`a5b98323 KERNELBASE!SleepEx+0xa7
08 000000c5`afc5f880 00007ff7`a5b9b96d meason_test!main+0x73 [d:\test\meason_test\main.cpp @ 13]
09 000000c5`afc5f8c0 00007ffa`57f42d92 meason_test!__mainCRTStartup+0x14d [(omitted) @ 697]
0a 000000c5`afc5f900 00007ffa`5a289f64 KERNEL32!BaseThreadInitThunk+0x22
0b 000000c5`afc5f930 00000000`00000000 ntdll!RtlUserThreadStart+0x34
We are going to alter the loop iteration variable, which is a variable stored on the stack. It is located in the main() function, so let's switch debugger context to that frame of the call stack. The frame number is 8, which we can see as the first column in the 'k' output above.
0: kd> .frame 8
08 000000c5`afc5f880 00007ff7`a5b9b96d meason_test!main+0x73 [d:\test\meason_test\main.cpp @ 13]
0: kd> dv
i = 0n27
How do you find the address of this stack variable? There are many ways, but we'll use the most clunky method: Look at disassembly. Let's look for the variable being incremented in the main function:
0: kd> u meason_test!main
meason_test!main [d:\test\meason_test\main.cpp @ 6]:
00007ff7`a5b982b0 4883ec38 sub rsp,38h
00007ff7`a5b982b4 488d1515580000 lea rdx,[meason_test!`string' (00007ff7`a5b9dad0)]
00007ff7`a5b982bb 488d0d7e8d0000 lea rcx,[meason_test!std::wcout (00007ff7`a5ba1040)]
00007ff7`a5b982c2 e8d98effff call meason_test!std::operator<<<wchar_t,std::char_traits<wchar_t> > (00007ff7`a5b911a0)
00007ff7`a5b982c7 488d15e2f1ffff lea rdx,[meason_test!std::endl (00007ff7`a5b974b0)]
00007ff7`a5b982ce 488bc8 mov rcx,rax
00007ff7`a5b982d1 e82aa9ffff call meason_test!std::basic_ostream<wchar_t,std::char_traits<wchar_t> >::operator<< (00007ff7`a5b92c00)
00007ff7`a5b982d6 c744242000000000 mov dword ptr [rsp+20h],0
0: kd> u
meason_test!main+0x2e [d:\test\meason_test\main.cpp @ 9]:
00007ff7`a5b982de eb0a jmp meason_test!main+0x3a (00007ff7`a5b982ea)
00007ff7`a5b982e0 8b442420 mov eax,dword ptr [rsp+20h]
00007ff7`a5b982e4 ffc0 inc eax
00007ff7`a5b982e6 89442420 mov dword ptr [rsp+20h],eax
00007ff7`a5b982ea 488d15ff570000 lea rdx,[meason_test!`string' (00007ff7`a5b9daf0)]
00007ff7`a5b982f1 488d0d488d0000 lea rcx,[meason_test!std::wcout (00007ff7`a5ba1040)]
00007ff7`a5b982f8 e8a38effff call meason_test!std::operator<<<wchar_t,std::char_traits<wchar_t> > (00007ff7`a5b911a0)
00007ff7`a5b982fd 8b542420 mov edx,dword ptr [rsp+20h]
The 'inc eax' instruction is the one we're looking for. As we can see in the instruction above the 'inc eax' instruction, the EAX register gets loaded from whatever is at address [rsp+20h]. That is, 0x20 offset from the RSP register for the frame. We can't rely on the actual value in the RSP register anymore, as it is now loaded with data that is relative to frame 0 of our call stack. Using the 'k' output from above, we see the stack pointer for this frame is 000000c5`afc5f880. Thus, we retrieve data at 0x20 offset from that address:
0: kd> dd 000000c5`afc5f880+0x20
000000c5`afc5f8a0 0000001b 00000000 00000000 00000000
000000c5`afc5f8b0 00000000 00000000 a5b9b96d 00007ff7
000000c5`afc5f8c0 00000001 00000000 00000000 00000000
000000c5`afc5f8d0 00000000 00000000 a5b9d3d0 00007ff7
000000c5`afc5f8e0 00000000 00000000 a5b9d3d0 00007ff7
000000c5`afc5f8f0 00000000 00000000 57f42d92 00007ffa
000000c5`afc5f900 a5b9b9f0 00007ff7 a568f000 00007ff7
000000c5`afc5f910 a568f000 00007ff7 00000000 00000000
0: kd> ? 0000001b
Evaluate expression: 27 = 00000000`0000001b
Notice how we used the 'dd' debugger command to dump 32-bit values, as our code is using a 32-bit data type. As we can see, the loop iteration is at number 27 in our program's execution.
Let's alter the value to something wacky, just to be sure we edited the value we want. Use the 'ed' debugger command to edit the value at the address of this stack variable. Notice that this only works when the debugger context is set to this process, so the debugger knows how to translate this address properly.
0: kd> ed 000000c5`afc5f880+0x20 0x12345678
0: kd> dd 000000c5`afc5f880+0x20
000000c5`afc5f8a0 12345678 00000000 00000000 00000000
000000c5`afc5f8b0 00000000 00000000 a5b9b96d 00007ff7
000000c5`afc5f8c0 00000001 00000000 00000000 00000000
000000c5`afc5f8d0 00000000 00000000 a5b9d3d0 00007ff7
000000c5`afc5f8e0 00000000 00000000 a5b9d3d0 00007ff7
000000c5`afc5f8f0 00000000 00000000 57f42d92 00007ffa
000000c5`afc5f900 a5b9b9f0 00007ff7 a568f000 00007ff7
000000c5`afc5f910 a568f000 00007ff7 00000000 00000000
Now, let's run the app and see what the result looks like. Make sure to resume the debugger so the OS can start executing again.
Loop iteration: 25
Loop iteration: 26
Loop iteration: 27
Loop iteration: 305419897
Loop iteration: 305419898
Loop iteration: 305419899
That first large value corresponds to 0x12345679, which is what we would expect after coming out of the Sleep() point of the app and starting on the next loop iteration.
Summary
In this tutorial, we probed user memory with a sample app. We then altered the memory and saw the result. In all cases, the debugger context had to be set appropriately, else nothing would happen.