In July of 2010, I discovered a bug in Windows XP that allowed me to reliably crash a command shell. I reported the details to Microsoft's Security Response Center (any time you can force unexpected behavior in an application, there is at least a possibility that you can force your own arbitrary behavior). Microsoft's response was that while I was able to force cmd.exe to exit ungracefully, it did not indicate a security concern. That may well be true, but my curiosity brought it back to mind this week, and I was quite surprised to find that the bug still exists in Windows 7 with all current patches.
A little background lesson first. In computer science there is a concept of a standard stream, the predefined input and output methods known as stdin, stdout, and stderr. At a high level, stdin is how a program receives data or commands; stdout is the data or text it produces after running its logic, and stderr is where it sends error messages. Practically speaking, stdout and stderr appear the same when working from a command shell - both are printed to the screen, but programmatically they are two different streams that can be dealt with independently.
Another basic computer science concept is that of redirection. I can take the output of one program, and redirect it to a file or another output buffer. The simplest example is redirecting the output of a "dir" command to a file:
Instead of displaying the output on the screen, the output was written to the file "c:\temp\output.txt." However, if I enter a command that generates an error (such as asking for a directory listing of a folder that does not exist), the output goes to the screen instead of to the file:
This is because " > " by default redirects only stdout - the normal output of the command. It does not redirect stderr, the "File Not Found" error message. I can accomplish the same thing by explicitly specifying the stdout stream (or stream 1): C:\>dir 1> c:\temp\file
If stream 1 is stdout, what might stream 2 be? stderr! To capture the stderr stream and send it to a file, I would use "dir 2> c:\temp\file"
Knowing the identifiers for both streams, now we can do some interesting things. What if i want to capture BOTH normal and error output and send both to a file? I can't do "dir 1> file.txt 2> filename.txt" because the operations step on one another - each redirection tries to open an exclusive lock on the file. But, Windows and Unix offer a solution: redirect the error stream into the output stream, then handle the newly combined stream:
I captured stderr (2), and redirected it into stdout (1) - note that as the target of redirection I use &1 instead of just 1 - otherwise the system doesn't know whether I am talking about a file named "1", or the stdout stream. I can take it one step farther and redirect the combined stream to a file (logically, I am redirecting standard out to the file, then adding stderr to that same stream): C:\>dir xyzzy > c:\temp\output.txt 2>&1
And here is where our story really begins. 3 years ago I was writing a script to automate a task, and in this script I needed to delete some temporary files. Since this was automation, and there was a known error case (deleting a non-existent file), I wanted to suppress error messages from that particular command. I mistakenly thought stream 0 was the equivalent of "nul," or nowhere (i.e. if I redirected input to nul, it simply goes away). In actuality, stream 0 is stdin, the standard INPUT stream.
I tried redirecting stderr to stream 0 (mistakenly believing I was redirecting to the nul stream). In many cases, it worked as I thought it would:
But, when I did so with the delete command, cmd.exe exits:
C:\>Del <anything> 2>&0
The same happens with "erase." I then started experimenting, and could reproduce this behavior with an invalid command as well:
C:\>Xyzzy 2>&0
Again, cmd.exe exits. It seemed as though redirecting stderr to null, when there was any error output by cmd.exe or a built-in command, causes cmd.exe to exit.
I ultimately discovered my mistake - redirecting stderr to stream 0 (stdin for the current process) makes no logical sense, and cmd.exe did not have a handler for that scenario, so it just bailed out. The correct syntax was:
del <anything> 2> nul (if I only wanted to redirect the error)
or
del <anything> 2>nul 1>&2 (if I also wanted to redirect successful output).
I solved my original quest, but found my curiosity piqued. If I can crash a command shell, what else happens? Can I gain access to memory, or force code execution, or can I force a failure in a more critical service and create a denial of service condition?
I have used the Windows Debugger in the past to analyze OS crash dumps, so I used the same to analyze the cmd.exe crash. I launched Debugging Tools for Windows (version 6.2.9200.16384 64-bit edition, for reference), then chose the menu option to open an executable, in this case, c:\windows\system32\cmd.exe. I set a breakpoint, ran to the point that I had a prompt to enter commands at, then stepped through the subsequent operations. First I did a correctly-formatted command, c:\>xyzzy >nul 2>&1; the output was verbose, but the pieces of relevance are:
0:001> t
ntdll!LdrShutdownThread+0x1f1:
00000000`76d86c09 4d85ed test r13,r13
0:001> t
ntdll!LdrShutdownThread+0x1f4:
00000000`76d86c0c 0f844ae70200 je ntdll! ?? ::FNODOBFM::`string'+0x1bfcb (00000000`76db535c) [br=1]
0:001> t
ntdll! ?? ::FNODOBFM::`string'+0x1bfcb:
00000000`76db535c 33db xor ebx,ebx
0:001> t
ntdll! ?? ::FNODOBFM::`string'+0x1bfcd:
00000000`76db535e e9d318fdff jmp ntdll!LdrShutdownThread+0x21e (00000000`76d86c36)
0:001> t
ntdll!LdrShutdownThread+0x21e:
00000000`76d86c36 498bbe80170000 mov rdi,qword ptr [r14+1780h] ds:000007ff`fffdc780=0000000000000000
0:001> t
ntdll!LdrShutdownThread+0x225:
00000000`76d86c3d 4885ff test rdi,rdi
Next I repeated the sequence, using an improperly formatted command c:\>xyzzy >nul 2>&0. The output was identical up to the point where the second command caused cmd.exe to exit:
0:001> t
ntdll! ?? ::FNODOBFM::`string'+0x1bfb9:
00000000`76db534a f680ee17000020 test byte ptr [rax+17EEh],20h ds:000007ff`fffdc7ee=08
0:001> t
ntdll! ?? ::FNODOBFM::`string'+0x1bfc0:
00000000`76db5351 0f84b218fdff je ntdll!LdrShutdownThread+0x1f1 (00000000`76d86c09) [br=1]
0:001> t
ntdll!LdrShutdownThread+0x1f1:
00000000`76d86c09 4d85ed test r13,r13
0:001> t
ntdll!LdrShutdownThread+0x1f4:
00000000`76d86c0c 0f844ae70200 je ntdll! ?? ::FNODOBFM::`string'+0x1bfcb (00000000`76db535c) [br=1]
0:001> t
ntdll! ?? ::FNODOBFM::`string'+0x1bfcb:
00000000`76db535c 33db xor ebx,ebx
0:001> t
ntdll! ?? ::FNODOBFM::`string'+0x1bfcd:
00000000`76db535e e9d318fdff jmp ntdll!LdrShutdownThread+0x21e (00000000`76d86c36)
0:001> t
ntdll! ?? ::FNODOBFM::`string'+0x1bfcd:
00000000`76db535e e9d318fdff jmp ntdll!LdrShutdownThread+0x21e (00000000`76d86c36)
The shell is trying to execute the LdrShutdownThread routine in NTDLL.DLL, one of the core function libraries in the operating system. Since this is a library that provides kernel-level functions for other programs to use, it begs the question, how else could this function be misused? Alas, this is the limit to my expertise in assembly-level debugging, so I am left to document my findings to date and hope someone with more advanced debugging skills can pick up where I leave off. Any takers? :-)