How to view the stack of threads that were terminated as part of process teardown from user mode


Last time we saw how to view the stack of threads that were terminated as part of process teardown from the kernel debugger. You can do the same thing from a user-mode debugger, and it's actually a bit easier there. (The user-mode debugger I'm using is the one that comes with the Debugging Tools for Windows, the debugging engine that goes by a number of different front-ends, such as ntsd, cdb, and windbg.)

A direct translation of the kernel-mode technique from last time would involve using the !vadump command and picking through for the memory blocks with candidate size and attributes. But there's an easier way.

Now would be a good point for me to remind you that this information is for debugging purposes only. The structures and offsets are all implementation details which can change from release to release.

Recall that the TEB begins with some pointers which bound the stack, and the seventh pointer is a self-pointer. What's even more useful is the thirteenth pointer (offset 0x30 for 32-bit TEBs, offset 0x60 for 64-bit TEBs), because that is where the PEB is stored.

Each process has a single global PEB, so all the TEBs will have the same PEB value at offset 0x30/0x60. And you can figure out the address of the current process's PEB either by using the !peb command or by simply looking at the TEB you already have.

0:000> dd fs:30 l1
0053:00000030  7efde000

Now you can search through memory looking for that value. If you see any hits at offset 0x30/0x60, then that's a candidate TEB.

The debugger normally limits memory scans to 256MB.

0:001> s 00000000 L 80000000 00 e0 fd 7e
                           ^ Range error in 's 00000000 l 80000000 00 e0 fd 7e'

Therefore, you have to issue the search eight times (for 32-bit processes) to cover the 2GB user-mode address space.

0:001> s 00000000 L 10000000 00 e0 fd 7e
0009e01c  00 e0 fd 7e 00 d0 fd 7e-44 e0 09 00 7b ef 17 77  ...~...~D...{..w
0009fdc0  00 e0 fd 7e 44 00 00 00-f0 ee 3a 00 10 ef 3a 00  ...~D.....:...:.
0009fe34  00 e0 fd 7e 78 fe 09 00-02 9f 18 77 00 e0 fd 7e  ...~x......w...~
0:001> s 10000000 L 10000000 00 e0 fd 7e
0:001> s 20000000 L 10000000 00 e0 fd 7e
0:001> s 30000000 L 10000000 00 e0 fd 7e
0:001> s 40000000 L 10000000 00 e0 fd 7e
0:001> s 50000000 L 10000000 00 e0 fd 7e
0:001> s 60000000 L 10000000 00 e0 fd 7e
0:001> s 70000000 L 10000000 00 e0 fd 7e
7486af70  00 e0 fd 7e 00 00 00 00-b8 00 16 77 28 00 16 77  ...~.......w(..w
7efda030  00 e0 fd 7e 00 00 00 00-00 00 00 00 00 00 00 00  ...~............
7efdd030  00 e0 fd 7e 00 00 00 00-00 00 00 00 00 00 00 00  ...~............

Alternatively, you can use the "length sanity check override" by inserting a question mark after the L:

0:001> s 00000000 L?80000000 00 e0 fd 7e
0009e01c  00 e0 fd 7e 00 d0 fd 7e-44 e0 09 00 7b ef 17 77  ...~...~D...{..w
0009fdc0  00 e0 fd 7e 44 00 00 00-f0 ee 3a 00 10 ef 3a 00  ...~D.....:...:.
0009fe34  00 e0 fd 7e 78 fe 09 00-02 9f 18 77 00 e0 fd 7e  ...~x......w...~
7486af70  00 e0 fd 7e 00 00 00 00-b8 00 16 77 28 00 16 77  ...~.......w(..w
7efda030  00 e0 fd 7e 00 00 00 00-00 00 00 00 00 00 00 00  ...~............
7efdd030  00 e0 fd 7e 00 00 00 00-00 00 00 00 00 00 00 00  ...~............

From the above output, we see that we can quickly reject all but the last two entries because the offset within the page is not the magic value 0x30. (This is a 32-bit process.) Hooray, two debugger commands reduce the search space to just two pages!

At this point, you can continue with the debugging technique from last time, looking at each candidate TEB to see if there's a valid stack in there.

Comments (25)
  1. Joshua says:

    [ Now would be a good point for me to remind you that this information is for debugging purposes only. The structures and offsets are all implementation details which can change from release to release. ]

    Given the fact the Wine team had to go out of their way to implement these structures due to massive dependencies on them, you can't change them without massive breakage.

  2. Matt Tait says:

    A slightly more supported way of getting the PEB than "dd fs:30 l1" is "? $peb"

  3. Ken W says:

    @Joshua: [Given the fact the Wine team had to go out of their way to implement these structures due to massive dependencies on them, you can't change them without massive breakage.]

    This has nothing to do with anything. The Wine developers are using implementation-specific details that are internal, and if those internals change the Wine team will need to make appropriate changes as well. MS isn't bound to not change anything just because Wine depends on it the way it is now. :)

  4. Joshua says:

    @Ken W: I think you missed the point. The Wine team did it because they found out what happened when they didn't do it. If MS repeats the experiment they too will find out just how many people depended on it.

  5. Ens says:

    Wish I had this trick about a year and a half ago.  This is one of those techniques that is probably pure gold once in a blue moon.

    As for Wine:  the fact that the structures exist and are necessary does not imply that every detail of every member is necessary, so it could change.

  6. @Joshua:

    Actually Microsoft don't have to bother too much with compatability if they don't want to. They have the advantage of msdn.microsoft.com/…/ms686708(v=vs.85).aspx on their side. In the public Windows headers TEB is virtually undefined and marked as this structure could change. So anyone relying on TEB is playing with fire.

    I can understand Wine emulating it, since it is a Windows environment emulator, so their priority is to get existing applications working, but Microsoft are not constrained by that. All Microsoft are bound by is the promise of keeping documented interfaces and structures as compatable as possible. Microsoft have broken people using undocumented interfaces and structures, or people using reserved things in the past. So TEB is no exception to this.

  7. Neil says:

    @Crescens2k: but Wine Is Not an Emulator…

    I once tried to submit a patch to Wine, but it was refused because it was too accurate. Instead they accepted a patch which happened to suffice to allow some application or other to run correctly.

  8. voo says:

    @Crescens2k The point is that MS quite rarely breaks backwards compatibility without a really good reason, even if people are using undocumented structures or just rely on bugs to work. Considering that you obviously know this blog here, I don't have to point to some examples of this ;)

    So changing a structure in a way that would break lots of programs (by admission of Joshua, I've no idea why people would need this structure for something other than debugging) is at least unlikely or would result in poor Raymond having to write some interesting shims.

  9. @Niel:

    That is why I was so specific about Wine's role. It is an environment emulator/compatability layer. It is not an "Emulator" as in it doesn't do emulation of processors or things like that. But while the term "Emulator" is often seen as software to mimic the functionality of a piece of hardware in software, the word emulate has a much looser meaning. The base meaning is "match or surpass by imitation", and the computing related meaning is "reproduce the function or action of ". This was taken from the Oxford English Dictionary.

    So Wine reproduces the Win32 API -> Wine emulates the Win32 API -> Wine is a Win32 environment emulator isn't that wrong, is it?

    The acronym itself was really used to try and stop people thinking of it as an actual hardware emulator (like console emulation or the x86 emulation on ia64). As taken from the Debunking Wine Myths page

    "Wine is slow because it is an emulator"

    Wine's not that kind of emulator

    When users think of emulators, they think of programs like Dosbox or zsnes. These applications run as virtual machines and are slow, having to emulate each processor instruction. Wine does not do any CPU emulation – hence the name "Wine Is Not an Emulator."

  10. @voo:

    The thing is, the backwards compatability is centered around the public interfaces. Yes, Microsoft breaking compatability on public functions is rare. The thing is, these are internal interfaces. The compatability stories on here are usually about people getting information on public interfaces and assuming that it will always be true. For example, the stories on shell extensions.

    But then we also have things like blogs.msdn.com/…/44425.aspx which is then followed up later by blogs.msdn.com/…/434648.aspx when Microsoft just changed the meaning of the reserved fields in a structure. So if something is marked as internal, then Microsoft can and will change things.

    But I would be very surprised if changing the meaning of an internal structure would have mass breakage anyway. But then again, considering what some programmers are like, you never know.

  11. Joshua says:

    @Crescens2k: Microsoft seems often more likely to change documented interfaces than undocumented interfaces. Like say ntdll was documented to be loaded at 0x7c900000. It's not often there anymore.

  12. @Joshua:

    That is a curious thing to say, since undocumented interfaces are undocumented, how would you know that they have changed? Also, for ntdll.dll, the base address of that is also implementation specific. As far as I remember, the only requirement for the base address of that DLL is that kernel mode knows where it is. Anyway, is it documented anywhere officially by Microsoft that ntdll would load in at that address only?

    Anyway, think about the truely undocumented interfaces and structures. How much is actually written about those? If these interfaces are truely undocumented, why would Microsoft need to tell you about changes? We have had some rather big, not very well documented things over the last few years. The change in exception handling schemes on x64, the change in process termination and more. But since all of these are internal functions, how do you know that none of the interfaces have changed.

  13. Joshua says:

    @Crescens2k: Read the NT4 documentation. The exact base address of that dll was listed in the documentation as a fundamental constant.

  14. But did that documentation say that it would always load in at that same address even after major version updates? The fact that it is Windows NT4 documentation heavily implies that it was only guaranteed during the NT4 lifetime. When you also take into consideration that when Windows 2000 shipped, the base address of ntdll.dll had moved to 0x77F80000, then it does really show that 0x7c900000 was NT4 only.

    Since both the Windows kernel and executive, and ntdll.dll are controlled by Microsoft, it does make a lot of sense to think that the base can be moved. The only reason why ntdll.dll is fixed is because of user mode/kernel mode callbacks and perhaps the switches between WoW64 and native x64 (I'm not sure if wntdll.dll goes through ntdll.dll or one of the wow DLLs). Either way, all that needs to occur is that everything agrees on where, and that is completely under Microsoft's control.

  15. voo says:

    @Crescens You have a point there, although I can think of exceptions to that rule, the pattern does make sense.

  16. Killer{R} says:

    Aother wa to do it with !vadump. Typically stack consists 3 parts: a reserved then guadpage and then finally – commited part with actual stack data. For example:

    BaseAddress:       00030000

    RegionSize:        000e9000

    State:             00002000  MEM_RESERVE

    Type:              00020000  MEM_PRIVATE

    BaseAddress:       00119000

    RegionSize:        00001000

    State:             00001000  MEM_COMMIT

    Protect:           00000104  PAGE_READWRITE + PAGE_GUARD

    Type:              00020000  MEM_PRIVATE

    BaseAddress:       0011a000

    RegionSize:        00016000

    State:             00001000  MEM_COMMIT

    Protect:           00000004  PAGE_READWRITE

    Type:              00020000  MEM_PRIVATE

  17. Joshua says:

    But did that documentation say that it would always load in at that same address even after major version updates?

    Neither does it say any such thing about CreateWindow. Without the benefit of hindsight it would have been impossible to tell the difference between the two.

  18. Matt T says:

    NTDLL loads at a different address every time you reboot as of Vista. It's called ASLR.

  19. voo says:

    @Matt Yep and I think security better trump backwards compatibility every time.. and MS seems to agree.

    Also maybe I'm missing something (I've had not much sleep), but why exactly do we need to know where the OS will load some library? I can't think of a single thing where this would be necessary that couldn't be achieved in a simpler manner (heck if that wasn't true ASLR would be quite problematic). Well except if we're writing viruses, but I doubt MS is quite as worried about those developers ;)

  20. Ken W says:

    @Joshua – Ah, got it. You're right – I did miss your point. I stand corrected. :)

  21. alegr1 says:

    @Joshua:

    What was NTDLL address in NT 3.1?

  22. Myria says:

    It's somewhat annoying that there's no documented way to get the TEB for a target thread.  NtQueryInformationThread works, but isn't documented.  The only fully-documented way only works on x86: GetThreadContext to get the value of FS, GetThreadSelectorEntry to get the base address of the FS segment, ReadProcessMemory that address into an NT_TIB32 structure (defined in WinNT.h), then look at the Self pointer, which in NT will equal the base address of the FS segment.

    It's too bad that GetThreadSelectorEntry isn't allowed on x86-64 threads.  There's no reason it couldn't be implemented.

  23. Joshua says:

    @Myria: AFAIK GetThreadSelectorEntry works on x86-64 when operating on the thread that called it.

  24. Killer{R} says:

    2Myria actually you can to get FS (GS fo x64) by GetThreadContext, then launch additional thread in same process and that thread must load passed to it selector's value into say es and then read TEB::Self ptr into eax (rax) and return. The caller will able to get pointer as thread's exit code or whatever else.

    PS But NtQueryInformationThread simplier ad faster :)

  25. Myria says:

    @Joshua: Nope, GetThreadSelectorEntry on x86-64 just calls BaseSetLastNTError(STATUS_NOT_SUPPORTED) and returns FALSE.  Wow64GetThreadSelectorEntry only understands the segments used in 32-bit mode: 0x0020, 0x0028, and 0x0050 (ignoring RPL bits), so won't accept 64-bit.

    @Killer{R}: All threads have the same GS selector value – it's the segment's base that differs.  Attempting to do that from a new thread will return the new thread's TEB.

    The fact that GetThreadSelectorEntry doesn't work on x86-64 means that you could make some really obfuscated code by using user-mode thread scheduling, which creates LDT entries for the TEBs.  Debuggers wouldn't be able to tell what address is being read.

Comments are closed.