Introduction
In order to interpret the cdb.exe debugger prompt, we have to enter commands to figure out some details concerning the debugger output. Moreover, when we set the symbol path, we must remember that the downloaded symbols are for the Microsoft device drivers and system key components alone. This means if we are to use to the cdb.exe (or graphical WinDbg.exe) debugger on our own code, we must use a compiler switch that includes debugging information with our compiled executable. On that premise, we can write a small program in the C language in order to interpret the debugger. Below is a simple program that writes a global function that is called by the main
function in order to produce a string
in the standard console output:
c:\Program Files\Microsoft Visual Studio 9.0\VC\bin>type con > hello.c
#include <stdio.h> <stdio.h />
#include <string.h><string.h />
salute(char *temp1,char *temp2){
char name[400];
strcpy(name, temp2);
printf("Hello %s %s\n", temp1, name);
}
main(int argc, char * argv[]){
salute(argv[1], argv[2]);
printf("Bye %s %s\n", argv[1], argv[2]);
}
We now compile this code with Microsoft Visual Studio C++ Express Edition’s cl.exe compiler, with the /Zi switch to obtain debugging information.
C:\PROGRA~1\MICROS~1.0\VC\bin>cl /Zi hello.c
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86
Copyright (C) Microsoft Corporation. All rights reserved.
hello.c
Microsoft (R) Incremental Linker Version 9.00.30729.01
Copyright (C) Microsoft Corporation. All rights reserved.
/out:hello.exe
/debug
hello.obj
We run the program: C:\....\bin>hello Silly Willy
c:\Program Files\Microsoft Visual Studio 9.0\VC\bin>hello Silly Willy
Hello Silly Willy
Bye Silly Willy
Having run that program, we have to find an easy way to transfer this code and its debugging information, to the C:\Program files\Debugging Tools for Windows directory. Of course it is possible to set an environmental path:
set PATH=%PATH%;.;C:\Program Files\Microsoft Visual Studio 9.0\VC\bin
Using the more basic approach, we copy and paste those files to directory containing the debuggers, where we will set the symbol path to download the symbols and cache them locally in a directory called C:\symbols.
Symbols and the Symbol Server
Symbols connect function names and arguments to offsets in a compiled executable. A method to obtain symbols is to use Microsoft’s symbol server and to fetch symbols as you need them. Windows debuggers make this easy to do by providing symsrv.dll, which you can use to set up a local cache of symbols and specify the location to get new symbols as you need them. This is done through the environment variable _NT_SYMBOL_PATH
. You’ll need to set this environment variable so the debugger knows where to look for symbols. If you already have all the symbols you need locally, you can simply set the variable to that directory like this:
C:\Program Files\Debugging Tools For Windows>set _NT_SYMBOL_PATH=c:\symbols
If you prefer to use the symbol server, the syntax goes as follows:
C:\Program Files\Debugging Tools For Windows>
set_NT_SYMBOL_PATH=symsrv*symsrv.dll*c:\symbols*
http://msdl.microsoft.com/download/symbols
User Mode Debugger Output
C:\PROGRA~1\Debugging Tools for Windows>cdb.exe Hello Silly Willy
Microsoft (R) Windows Debugger Version 6.8.0004.0 X86
Copyright (c) Microsoft Corporation. All rights reserved.
CommandLine: Hello Silly Willy
Symbol search path is: srv*c:\Symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00400000 00427000 hello.exe
ModLoad: 76ea0000 76fc7000 ntdll.dll
ModLoad: 75a90000 75b6b000 C:\Windows\system32\kernel32.dll
(27c.ff4): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=0012fb08 edx=76ef9a94 esi=fffffffe edi=76efb6f8
eip=76ee7dfe esp=0012fb20 ebp=0012fb50 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!DbgBreakPoint:
76ee7dfe cc int 3
Notice that the first line contains the process and thread identifier that generates the last debugger event. For an excellent documentation on debugging as a whole, it is strongly suggested that the reader research “Advanced Windows Debugging” by Mario Hewardt and Daniel Pravat. The last debugger event is displayed identifier (27c.ff4) along with the event description, a break instruction and exception code 80000003. The debugger handled the event on the first chance, before the normal exception handling in the user code. These next two commands are self-explanatory:
0:000> vertarget
Windows Version 6001 (Service Pack 1) MP (2 procs) Free x86 compatible
Product: WinNt, suite: SingleUserTS
kernel32.dll version: 6.0.6001.18000 (longhorn_rtm.080118-1840)
Debug session time: Tue Dec 23 16:53:26.860 2008 (GMT-5)
System Uptime: 0 days 1:48:23.592
Process Uptime: 0 days 0:00:06.037
Kernel time: 0 days 0:00:00.000
User time: 0 days 0:00:00.015
0:000> .lastevent
Last event: 6f8.5a0: Break instruction exception - code 80000003 (first chance)
debugger time: Tue Dec 23 16:53:20.854 2008 (GMT-5)
As we can see, the debugger stopped at a breakpoint. A stack trace will show us the reason why:
0:000> k
ChildEBP RetAddr
0012fb1c 76f2e214 ntdll!DbgBreakPoint
0012fb50 76f12ef5 ntdll!LdrpDoDebuggerBreak+0x31
0012fc94 76ed1235 ntdll!LdrpInitializeProcess+0x1132
0012fd00 76ede2b7 ntdll!_LdrpInitialize+0xf2
0012fd10 00000000 ntdll!LdrInitializeThunk+0x10
Looking at the top instruction, we can see that Windows debugger breaks after initializing before execution begins. This is actually a convenient breakpoint, as program is loaded; now we can set any breakpoints that we’d like on our program before execution begins. Let’s set a breakpoint on main
:
0:000> bm hello!main
*** WARNING: Unable to verify checksum for hello.exe
1: 00401070 @!"hello!main"
0:000> bl
1 e 00401070 0001 (0001) 0:**** hello!main
The thing to do now is run our program past the ntdll.dll initialization on to our main
function:
0:000> g
Breakpoint 1 hit
eax=008f1d78 ebx=7ffda000 ecx=00000001 edx=76ef9a94 esi=00000000 edi=00000000
eip=00401070 esp=0012ff44 ebp=0012ff88 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
hello!main:
00401070 55 push ebp
0:000> k
ChildEBP RetAddr
0012ff40 004014b2 hello!main
0012ff88 75ad4911 hello!__tmainCRTStartup+0xfb
0012ff94 76ede4b6 kernel32!BaseThreadInitThunk+0xe
0012ffd4 76ede489 ntdll!__RtlUserThreadStart+0x23
0012ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
During any disassembly, the number 55 normally means program entry. That is, the opcode 55 indicates where the source code execution begins. If you have ever dealt with Linux or assembly, then you would recognize that the instruction push ebp
is the beginning of what is called the function prologue. At this point, the Debugging Tools for Windows should be noted. If you read the section of “Debugging in Source Mode” in the debugging.chm, you will find that the .lines
command will modify the stack trace to display the line that is currently being executed:
0:000> .lines
ne number information will be loaded
0:000> k
ChildEBP RetAddr
0012ff40 004014b2 hello!main [c:\program files\Microsoft visual studio 9.0\vc\bi
n\hello.c @ 8]
0012ff88 75ad4911 hello!__tmainCRTStartup+0xfb [f:\dd\vctools\crt_bld\self_x86\c
rt\src\crt0.c @ 266]
0012ff94 76ede4b6 kernel32!BaseThreadInitThunk+0xe
0012ffd4 76ede489 ntdll!__RtlUserThreadStart+0x23
0012ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> g
To continue past this breakpoint, our program will finish executing:
0:000> g
Hello Silly Willy
Bye Silly Willy
eax=00000000 ebx=00000001 ecx=00000000 edx=000000fe esi=008f1a34 edi=008f1a38
eip=76ef9a94 esp=0012fec0 ebp=0012fed0 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!KiFastSystemCallRet:
76ef9a94 c3 ret
0:000> k
ChildEBP RetAddr
0012febc 76ef9134 ntdll!KiFastSystemCallRet
0012fec0 76eca869 ntdll!NtTerminateProcess+0xc
0012fed0 75ab3b68 ntdll!RtlExitUserProcess+0x7a
0012fee4 00402cd8 kernel32!ExitProcess+0x12
0012fee4 00402cd8 hello!__crtExitProcess+0x17 [f:\dd\vctools\crt_bld\self_x86\cr
t\src\crt0dat.c @ 731]
0012fef0 00402f3c hello!__crtExitProcess+0x17 [f:\dd\vctools\crt_bld\self_x86\cr
t\src\crt0dat.c @ 731]
0012ff34 00402f66 hello!doexit+0x113 [f:\dd\vctools\crt_bld\self_x86\crt\src\crt
0dat.c @ 644]
0012ff48 004014c4 hello!exit+0x11 [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0da
t.c @ 412]
0012ff88 75ad4911 hello!__tmainCRTStartup+0x10d [f:\dd\vctools\crt_bld\self_x86\
crt\src\crt0.c @ 272]
0012ff94 76ede4b6 kernel32!BaseThreadInitThunk+0xe
0012ffd4 76ede489 ntdll!__RtlUserThreadStart+0x23
0012ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> q
quit:
Examining the Debugger
A good exercise at this point is to locate the data the debugged application is using. To do this, we will launch the debugger and set breakpoints on both the main
function and the salute
function:
C:\PROGRA~1\Debugging Tools for Windows>cdb.exe Hello Silly Willy
Microsoft (R) Windows Debugger Version 6.8.0004.0 X86
Copyright (c) Microsoft Corporation. All rights reserved.
CommandLine: Hello Silly Willy
Symbol search path is: srv*c:\Symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00400000 00427000 hello.exe
ModLoad: 76ea0000 76fc7000 ntdll.dll
ModLoad: 75a90000 75b6b000 C:\Windows\system32\kernel32.dll
(564.e0c): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=0012fb08 edx=76ef9a94 esi=fffffffe edi=76efb6f8
eip=76ee7dfe esp=0012fb20 ebp=0012fb50 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!DbgBreakPoint:
76ee7dfe cc int 3
0:000> k
ChildEBP RetAddr
0012fb1c 76f2e214 ntdll!DbgBreakPoint
0012fb50 76f12ef5 ntdll!LdrpDoDebuggerBreak+0x31
0012fc94 76ed1235 ntdll!LdrpInitializeProcess+0x1132
0012fd00 76ede2b7 ntdll!_LdrpInitialize+0xf2
0012fd10 00000000 ntdll!LdrInitializeThunk+0x10
0:000> bm hello!main
*** WARNING: Unable to verify checksum for hello.exe
1: 00401070 @!"hello!main"
0:000> bm hello!*salute*
2: 00401020 @!"hello!salute"
In the C language, (int argc, char *argv[]
) are the command line parameters that act as a prototype to pass to the main
to launch the program. Int argc
is the number of arguments, and argv[]
is a vector of pointers to those arguments. That is, rather than invoking the environment to launch the program, we pass these variables to main
to launch our program. Argv[0]
would therefore be the name of the program, hello.exe.
0:000> g
Breakpoint 1 hit
eax=001c1d78 ebx=7ffdb000 ecx=00000001 edx=76ef9a94 esi=00000000 edi=00000000
eip=00401070 esp=0012ff44 ebp=0012ff88 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
hello!main:
00401070 55 push ebp
Looking at the source, we can ascertain that the main
should have been passed the command line used to launch the program via the argc
command string counter and argv
, this points to the array of strings. To verify that, we’ll use dv
to list the local variables, and then poke around in memory with dt
and db
to find the value of those variables.
0:000> dv /V
0012ff48 @ebp+0x08 argc = 3
0012ff4c @ebp+0x0c argv = 0x001c1d38
0:000> dt argv
Local var @ 0x12ff4c Type char**
0x001c1d38
-> 0x001c1d48 "Hello"
From the dv
output, we see that argc
and argv
are, indeed, local variables with argc
stored 8 bytes past the local ebp
, and argv
stored at ebp+0xc
. The dt
command shows the data type of argv
to be a pointer to a character pointer. The address 0x001c1d48
holds that pointer to 0x001c1d48
where the data actually lives.
0:000> db 0x001c1d48
001c1d48 48 65 6c 6c 6f 00 53 69-6c 6c 79 00 57 69 6c 6c 79 Hello.Silly.Willy
So we go some more until we ge to the salute
function:
0:000> g
Breakpoint 2 hit
eax=001c1d4e ebx=7ffdb000 ecx=001c1d54 edx=001c1d38 esi=00000000 edi=00000000
eip=00401020 esp=0012ff34 ebp=0012ff40 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
hello!salute:
00401020 55 push ebp
0:000> kp
ChildEBP RetAddr
0012ff30 00401086 hello!salute(char * temp1 = 0x001c1d4e "Silly", char * temp2 =
0x001c1d54 "Willy")
0012ff40 004014b2 hello!main(int argc = 3, char ** argv = 0x001c1d38)+0x16
0012ff88 75ad4911 hello!__tmainCRTStartup(void)+0xfb
0012ff94 76ede4b6 kernel32!BaseThreadInitThunk+0xe
0012ffd4 76ede489 ntdll!__RtlUserThreadStart+0x23
0012ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
Looking at the stack trace (or code) that the salute
function is passed two arguments, Silly
and Willy
. So if a function is passed two arguments, then they should appear on the stack, as when is called, it must push its arguments onto the stack. This means we take a look at the local variables and map them out:
0:000> dv /V
0012ff38 @ebp+0x08 temp1 = 0x001c1d4e "Silly"
0012ff3c @ebp+0x0c temp2 = 0x001c1d54 "Willy"
0012fd98 @ebp-0x198 name = char [400] " s"
The variable name is 0x198
above ebp
. To convert this hexadecimal, use the .formats
command:
.0:000> .formats 198
Evaluate expression:
Hex: 00000198
Decimal: 408
Octal: 00000000630
Binary: 00000000 00000000 00000001 10011000
Chars: ....
Time: Wed Dec 31 19:06:48 1969
Float: low 5.7173e-043 high 0
Double: 2.01579e-321
0:000> pr
hello!salute+0xe:
0040102e 33c5 mov ebp,esp
0:000> p
hello!salute+0x10:
00401030 8945fc sub esp, 198h
0:000> pr
eax=0020201e ebx=7ffdf000 ecx=00202024 edx=00202008 esi=00000000 edi=00000000
eip=00401029 esp=0012fd98 ebp=0012ff30 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
hello!salute+0x9:
00401029 a120204200 mov eax,dword ptr [hello!__security_cookie (0042202
0)] ds:0023:00422020=ec0c1ced
We see that at the top of the stack (esp
, or address 0012fd98
, as noted above where the variable “name
” is in the prior ‘dv /V
’ command) we find the function variable name, which goes on for the next 408 bytes. So let’s add those bytes to the stack pointer:
0:000> .formats esp+198
Evaluate expression:
Hex: 0012ff30
Decimal: 1244976
Octal: 00004577460
Binary: 00000000 00010010 11111111 00110000
Chars: ...0
Time: Thu Jan 15 04:49:36 1970
Float: low 1.74458e-039 high 0
Double: 6.151e-318
Notice the value 0012ff30
. This should look familiar, as it is the value of the ebp
, which was saved initially by “push”ing it onto stack. We know that register ebp
is a 32 bit or 4 byte pointer. So let’s examine what comes after that:
0:000> dd esp+198+4 l1
0012ff34 00401086
The command l1
stands for length of one. Now if we do a stack trace, the return address should match: The return address 00401086
matches to have rebuilt the stack.
ChildEBP RetAddr
0012ff30 00401086 hello!salute+0x13
0012ff40 004014b2 hello!main+0x16
0012ff88 75ad4911 hello!__tmainCRTStartup+0xfb
0012ff94 76ede4b6 kernel32!BaseThreadInitThunk+0xe
0012ffd4 76ede489 ntdll!__RtlUserThreadStart+0x23
0012ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> dd esp+198+4+4 l1
0012ff38 001c1d4e
0:000> db 001c1d4e
001c1d4e 53 69 6c 6c 79 00 57 69-6c 6c 79 00 ab ab ab ab Silly.Willy.....
Disassembling with Cdb.exe
To disassemble, just use the uf
instruction at one of the breakpoints, like the global function:
0:000> uf hello!salute
hello!salute:
00401020 55 push ebp
00401021 8bec mov ebp,esp
00401023 81ec98010000 sub esp,198h
00401029 a120204200 mov eax,dword ptr [hello!__security_cookie (0042202
0)]
0040102e 33c5 xor eax,ebp
00401030 8945fc mov dword ptr [ebp-4],eax
00401033 8b450c mov eax,dword ptr [ebp+0Ch]
00401036 50 push eax
00401037 8d8d68feffff lea ecx,[ebp-198h]
0040103d 51 push ecx
0040103e e8fd010000 call hello!strcpy (00401240)
00401043 83c408 add esp,8
00401046 8d9568feffff lea edx,[ebp-198h]
0040104c 52 push edx
0040104d 8b4508 mov eax,dword ptr [ebp+8]
00401050 50 push eax
00401051 6800204200 push offset hello!__rtc_tzz <perf /> (hello+0x22000) (
00422000)
00401056 e86f000000 call hello!printf (004010ca)
0040105b 83c40c add esp,0Ch
0040105e 8b4dfc mov ecx,dword ptr [ebp-4]
00401061 33cd xor ecx,ebp
00401063 e8d0020000 call hello!__security_check_cookie (00401338)
00401068 8be5 mov esp,ebp
0040106a 5d pop ebp
0040106b c3 ret
Hopefully this article will help those who struggle with debugging and the commands involved in order to obtain the data the debugged compiled source code application uses.
References
- Advanced Windows Debugging, by Mario Hewardt and Daniel Pravat
- Ethical Hacking by Shon Harris, Allen Harper, Chris Eagle, and Jonathan Ness
History
- 23rd December, 2008: Initial post