I have the handle to a file; how can I get the file name from the debugger?


Suppose you're debugging and you find yourself with the handle to a file, and you would like to know what file that handle corresponds to.

Well, the way you would do this from code is to call Get­Final­Path­Name­By­Handle, but you're not in code; you're in the debugger.

But you can simulate what code did from the debugger.

Now, if Get­Final­Path­Name­By­Handle were a function you had written, you could just use the .call command to ask the debugger to build a call frame and call the function and then clean up. But since you didn't write that function, you'll have to build the call frame manually.

Let's say the handle you are interested is 0x5CC.

0:000> !handle 5cc f
Handle 5cc
  Type          File
  Attributes    0
  GrantedAccess 0x120089:
         ReadControl,Synch
         Read/List,ReadEA,ReadAttr
  HandleCount   2
  PointerCount  262145
  No Object Specific Information available

Yup, it's a file. Let's see what file it is. First, the x86 way. We start by recording the contents of the volatile registers so that we can restore them afterwards.

0:000> r
eax=00000925 ebx=00000003 ecx=267ad2be edx=00000000 esi=00000000 edi=00b300e0
eip=00b36e1d esp=0089f7cc ebp=0089f838 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200246
contoso!CContoso::OnCommand+0x1a5:
00b36e1d 8bd1            mov     edx,ecx

Next, we allocate some memory to hold the result.

0:000> .dvalloc 1000
Allocated 1000 bytes starting at 05740000

Next, we build the function call. Since this is x86, all parameters go on the stack, and the stdcall calling convention pushes the last parameter first.

; simulate "push dwFlags"
0:000> r esp=esp-4;ed esp 0
; simulate "push cchFilePath"
0:000> r esp=esp-4;ed esp 0x800
; simulate "push lpszFilePath"
0:000> r esp=esp-4;ed esp 05740000
; simulate "push hFile"
0:000> r esp=esp-4;ed esp 5cc
; simulate "call kernelbase!GetFinalPathNameByHandleW"
0:000> r esp=esp-4;ed esp eip
0:000> r eip=kernelbase!GetFinalPathNameByHandleW
; execute until the function returns
0:000> g poi esp
eax=00000025 ebx=00000003 ecx=7715fb62 edx=00000046 esi=00000000 edi=00b300e0
eip=00b36e1d esp=0089f7cc ebp=0089f838 iopl=0         nv up ei pl zr na pe cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200247
contoso!CContoso::OnCommand+0x1a5:
00b36e1d 8bd1            mov     edx,ecx

Okay, we've returned. We see that eax is nonzero, so the call succeeded. Let's look at the output buffer.

0:000> du 05740000
05740000  "\\?\C:\Users\Public\Documents\lo"
05740040  "g.txt"

There's our path to the file, which looks like a log file.

Now we have to play Boy Scout and leave things the way we found them.

; do not need to restore stack pointer since stdcall is callee-clean
0:000> r eax=925
0:000> r ecx=267ad2be
0:000> r edx=0
0:000> r efl=200246
0:000> r
eax=00000925 ebx=00000003 ecx=267ad2be edx=00000000 esi=00000000 edi=00b300e0
eip=00b36e1d esp=0089f7cc ebp=0089f838 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200246
contoso!CContoso::OnCommand+0x1a5:
00b36e1d 8bd1            mov     edx,ecx

Most of the registers are preserved by the function call, but we need to restore the volatile registers eax, ecx, edx, and flags.

In practice, you may be able to get away with not restoring everything. For example, flags may not be meaningful at the point in the code you broke in. And if you broke in immediately after a function call or immediately on entry to a function, the values in the nonvolatile registers are already trashed or trashable, so you don't need to restore them either.

Next up is x64. This is harder because of the shadow space and many more registers. Again, we start by recording the contents of the volatile registers so that we can restore them afterwards.

0:000> r
rax=0000000000000a0c rbx=0000000000000000 rcx=230fe38816f90000
rdx=0000000000000000 rsi=0000000000000000 rdi=0000000080004005
rip=000007f709e471fc rsp=0000002e45a9f880 rbp=0000002e45a9f949
 r8=0000000000000000  r9=000007fbab8aabd0 r10=0000000000000003
r11=0000002e45a9f2a8 r12=0000000000000000 r13=0000000000000001
r14=ffffffffffffffff r15=0000000000000000
iopl=0         nv up ei pl nz na po cy
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000207
contoso!CContoso::OnCommand+0x3b9:
000007f7`09e471fc 33c0            xor     eax,eax

Next, we allocate some memory to hold the result.

0:000> .dvalloc 1000
Allocated 1000 bytes starting at 0000002e`475e0000

Next, we build the function call. On x64, the first four parameters go into registers rcx, rdx, r8, and r9, and corresponding shadow space is reserved on the stack.

; create shadow space
0:000> r rsp=@rsp-20
; first parameter is hFile
0:000> r rcx=5cc
; second parameter is lpszFilePath
0:000> r rdx=0000002e`475e0000
; third parameter is cchFilePath
0:000> r r8=800
; fourth parameter is dwFlags
0:000> r r9=0
; simulate "call kernelbase!GetFinalPathNameByHandleW"
0:000> r rsp=@rsp-8; ep rsp @rip
0:000> r rip=kernelbase!GetFinalPathNameByHandleW
; execute until the function returns
0:000> g poi @rsp
0:000> r
rax=0000000000000025 rbx=0000000000000000 rcx=00000000ffffffff
rdx=0000000000008000 rsi=0000000000000000 rdi=0000000080004005
rip=000007f709e471fc rsp=0000002e45a9f860 rbp=0000002e45a9f949
 r8=0000002e4a159390  r9=000007fbab91a3a0 r10=0000000000000003
r11=0000002e45a9f3a8 r12=0000000000000000 r13=0000000000000001
r14=ffffffffffffffff r15=0000000000000000
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
contoso!CContoso::OnCommand+0x3b9:
000007f7`09e471fc 33c0            xor     eax,eax

Again, we look at the result.

0:000> du 0000002e`475e0000
0000002e`475e0000  "\\?\C:\Users\raymondc\Desktop\lo"
0000002e`475e0040  "g.txt"

And again, we need to undo everything we did so the application doesn't freak out.

0:000> r rsp=@rsp+20
0:000> r rax=0a0c
0:000> r rcx=230fe38816f90000
0:000> r rdx=0
0:000> r r8=0
0:000> r r9=7fbab8aabd0
0:000> r r10=3
0:000> r r11=2e45a9f2a8
0:000> r efl=207
0:000> r
rax=0000000000000a0c rbx=0000000000000000 rcx=230fe38816f90000
rdx=0000000000000000 rsi=0000000000000000 rdi=0000000080004005
rip=000007f709e471fc rsp=0000002e45a9f880 rbp=0000002e45a9f949
 r8=0000000000000000  r9=000007fbab8aabd0 r10=0000000000000003
r11=0000002e45a9f2a8 r12=0000000000000000 r13=0000000000000001
r14=ffffffffffffffff r15=0000000000000000
iopl=0         nv up ei pl nz na po cy
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000207
contoso!CContoso::OnCommand+0x3b9:
000007f7`09e471fc 33c0            xor     eax,eax

Again, most of the registers are preserved by the function call, but we need to restore the volatile registers eax, ecx, edx, r8 through r11, and flags.

Exercise: I could've skipped restoring rax and flags. Why?

Comments (20)
  1. Joshua says:

    That's the hard way. OpenProcess() DuplicateHandleEx() GetFinalPathNabeByHandle() MessageBox()

  2. Henke37 says:

    Of course, by the time the world sees this post, the debugger team has already added the name resolving to the debugger. But it's the spirit of the problem that matters.

  3. Mason Wheeler says:

    What debugger is this?  Why doesn't it have the ability to call functions as a standard feature, and why are we using it when there are plenty of debuggers that do? (Including the one in Visual Studio.)

  4. Ian says:

    @Mason Raymond is teaching us how stuff works. You are free to do it the easy way without needing to understand what's really going on if you want to.

  5. Falcon says:

    Exercise answer: The code being debugged is about to execute the "xor eax,eax" instruction, which will trash rax and flags.

  6. @Mason Wheeler: This is windbg, which is pretty much the only debugger you can use to debug in kernel space (drivers, minifilters, etc.).  I think most sane application developers would avoid windbg as much as possible, though it is definitely a powerful tool.

  7. pm100 says:

    "Now, if Get­Final­Path­Name­By­Handle were a function you had written,"

    Why does it matter who wrote the function? What the net technical difference between a function in my code vs something in a loaded DLL

  8. Pietro Gagliardi (andlabs) says:

    @Joshua: sure, if you're writing code. Sometimes you want to call functions in the debugger, to do quick tests for things. I've done it a number of times (not on Windows, but the point is the same).

    @Mason, @pm100 – the debugger described *can* call functions. That's what Raymond told us: "you could just use the .call command". But the debugger only knows how to set up a call frame for the functions that it has debugging information about, and most of the time that means "your code only". And the debugger needs to know how to set up a stack frame, because all the world is not stdcall.

  9. Lars says:

    @Pietro: Aha, so the situation would be different if one downloaded the symbols from the MS symbol server? That's not what I (and several others, it seems) took away from Raymond's exposition. I thought the core difference was the function living in a DLL vs. in the main executable. Otherwise Raymond would've mentioned the symbol server.

  10. Darek says:

    @Lars: The MS symbol server only has the public symbols, which do not contain enough information for .call to work (no info about function parameters, just the FPO).

  11. Josh Bowman says:

    It's worth pointing out that any IT professional worth his salt should know his way around WinDbg, in order to be able to solve seemingly intractable problems directly on a customer's machine. This is yet another tool I'll stick in my bag. It's not just developers that need debuggers, though I did come to IT from development, so it's not as much of a mystery.

  12. Don't Go Trashing My Flags says:

    This trick only works on code that conforms to the ABI. The ABI is stupid. In order for a function to return a simple true/false – which, of course, the FLAGS register could do in a single bit – the ABI demands we use an entire 64-bit register… and, to add insult to injury, the compiler will use only the low 8 bits! That's wasting over 80% of the bits right there… but, since you really only need one bit, that's a 200% increase in bit-usage – PER YEAR – over the last three decades!! STOP THE MADNESS.  RESPECT THE FLAGS.

    [There's also 64 bits of unused space in the RCX, RDX, R8, R9, R10, R11, XMM0, XMM1, XMM2, XMM3, XMM4, and XMM5 registers, Plus the home space on the stack that could be used to return additional values. That's a total of 1472 bits wasted. WASTED! Plus in the flags register, you have C, P, A, Z, S, D, O flags available. Why not use them all? -Raymond]
  13. Martin Bonner says:

    @Don't Go Trashing My Flags:  How would you call such a function from a language that doesn't _have_ a single bit type?  In particular, how would you call such a function from C89 (which was pretty much the dominant language for this sort of thing when the x64 ABI was defined).

  14. Carl Mess says:

    I think SoftIce, WinIce did this too. Anyways way to go Raymond I always find very interesting this kinds of analysis about functions, calling conventions and stack preparation.

  15. Medinoc says:

    Won't the first solution run into the desynch stack problem?

    blogs.msdn.com/…/317157.aspx

    [The context switch out of the app into the debugger already broke the return address predictor. -Raymond]
  16. AlexShalimov says:

    How does this method (and Get­Final­Path­Name­By­Handle) work with hardlinks? It basically means that a file has several equal names; which one is returned?

  17. Medinoc says:

    @AlexShalimov: If it's done well, it will be the one that was used to open it, I'd guess.

  18. skSdnW says:

    @Medinoc: You can also open files by their file id so in some instances it has to just pick one…

  19. Nick says:

    Now I want to know how to do it in Itanium. ;)

  20. GWO says:

    ls -l /proc/<PID>/fd/<handle>  

    Ta Da!

Comments are closed.

Skip to main content