分类: WINDOWS
2009-06-23 01:14:31
In
our debug classes, I always invite students to bring in crash dumps for
us to analyze. This allows people to see that the analysis we do during
the course is not just canned, but rather really reflects skills that
can be applied to "real world" crash analysis. In one recent class, a
student brought in a dump in which their filter driver had crashed when
accessing a file. The question the student asked was "how can I tell
which file was being accessed?" Once we tracked that down, the student
asked "how can I see the contents of the file - surely it is in memory?"
Since this is an interesting exercise - and one that seems generally useful - I’ll walk through that process in this article. Thus, the next time you need to track down a file in memory, you can do so.
Before walking through the mechanism, it is important to understand the basic relationship of files to the data structures used to track them. The key data structures for this analysis are:
FILE_OBJECT - This is the object used by Windows to track a single open instance of a file. Thus, if you have multiple open instances of a file, you have multiple file objects.
SECTION_OBJECT_POINTERS - This is the data structure that connects the specific FILE_OBJECT to the virtual memory control structures that keep track of the file contents when they are in memory - and allow Windows to fetch those contents when they are not.
CONTROL_AREA - This is the data structure used by the Memory Manager to track the state information for a given section.
SHARED_CACHE_MAP - This is the data structure used by the Cache Manager to track the state of individual cached regions of the file. There can be many of these for a single file, with each one describing a cached region of the file.
VACB - This is the data structure used by the Cache Manager to describe the virtual address region in use within the cache to access data within a cached region.
All of these data structures are available in the public type information provided in current versions of Windows - and thus we have everything that we need to find the virtual addresses used for the cache. With those virtual addresses, it is trivial for us to find the actual file data itself.
In general, we either find the FILE_OBJECT by using the value in the I/O Request Packet (IRP) or by using a handle and looking up the value in the object handle table. Thus, we can either use the !irp command (to display an IRP and its stack locations) or we can use the !handle command. In either case, we use the resulting information. Here, I’ve chosen to use the handle and I’ve picked the "Microsoft Word" process that I was using to write the article itself. To do this I used the command "!handle 0 3 86304020". The first argument indicates I want to see all of the handles for the process (I am not sure which handle I want yet). The second is the flags field, which indicates I want information for each handle (the default is 1 and would have only provided me with rudimentary information. The additional flag means I am shown the name of the various objects). The third argument is the address of the process itself (so the handle command used the correct handle table). From this I am able to look through the table to find the handle for this file:
0cf0: Object: 85df1028 GrantedAccess: 0012019f
Object: 85df1028 Type: (86fdbe70) File
ObjectHeader: 85df1010
HandleCount: 1 PointerCount: 1
Directory Object: 00000000 Name: \Documents and Settings\mason\My Documents\Files in Memory.doc {HarddiskVolume1}
This gives me the address of the file object (85df1028) which I then use to display information about the specific file object:
lkd> dt nt!_FILE_OBJECT 85df1028
+0x000 Type : 5
+0x002 Size : 112
+0x004 DeviceObject : 0x86f22cc0
+0x008 Vpb : 0x86f22c38
+0x00c FsContext : 0xe3d92d90
+0x010 FsContext2 : 0xe16f35b0
+0x014 SectionObjectPointer : 0x85f50c6c
+0x018 PrivateCacheMap : (null)
+0x01c FinalStatus : 0
+0x020 RelatedFileObject : (null)
+0x024 LockOperation : 0x1 ''
+0x025 DeletePending : 0 ''
+0x026 ReadAccess : 0x1 ''
+0x027 WriteAccess : 0x1 ''
+0x028 DeleteAccess : 0 ''
+0x029 SharedRead : 0x1 ''
+0x02a SharedWrite : 0x1 ''
+0x02b SharedDelete : 0 ''
+0x02c Flags : 0x40042
+0x030 FileName : _UNICODE_STRING "\Documents and Settings\mason\My Documents\Files in Memory.doc"
+0x038 CurrentByteOffset : _LARGE_INTEGER 0x0
+0x040 Waiters : 0
+0x044 Busy : 0
+0x048 LastLock : 0x860c35a0
+0x04c Lock : _KEVENT
+0x05c Event : _KEVENT
+0x06c CompletionContext : (null)
From this I can find the SECTION_OBJECT_POINTERS structure (85f50c6c) which once again I can display using the debugger:
lkd> dt nt!_SECTION_OBJECT_POINTERS 0x85f50c6c
+0x000 DataSectionObject : 0x869ed9d8
+0x004 SharedCacheMap : (null)
+0x008 ImageSectionObject : (null)
This indicates to me that the file has a data section object but no corresponding shared cache map (which means this file instance has not been set up for caching) nor image section object (which means it is not an executable image). Now for the tricky part. The section object actually points to a CONTROL_AREA. I can take the address listed as the address of the DataSectionObject and see it in the debugger:
kd> dt nt!_CONTROL_AREA 0x869ed9d8
+0x000 Segment : 0xe34fd698
+0x004 DereferenceList : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x00c NumberOfSectionReferences : 1
+0x010 NumberOfPfnReferences : 7
+0x014 NumberOfMappedViews : 1
+0x018 NumberOfSubsections : 1
+0x01a FlushInProgressCount : 0
+0x01c NumberOfUserReferences : 2
+0x020 u : __unnamed
+0x024 FilePointer : 0x85f84478
+0x028 WaitingForDeletion : (null)
+0x02c ModifiedWriteCount : 0
+0x02e NumberOfSystemCacheViews : 0
Notice that the FilePointer does not point back to the same file object. Recall that a FILE_OBJECT is just an open instance, not the actual file. The Memory Manager, however, must use one specific FILE_OBJECT in order to perform paging I/O - and this is the file object it will use. If I display information about this object you will notice something interesting:
lkd> !object 0x85f84478
Object: 85f84478 Type: (86fdbe70) File
ObjectHeader: 85f84460
HandleCount: 0 PointerCount: 1
Directory Object: 00000000 Name: \Documents and Settings\mason\My Documents\~WRL2542.tmpory.doc {HarddiskVolume1}
Notice that the name of this file is different than the name of the original file. If I display this using the dt command I can see the full file structure:
lkd> dt nt!_FILE_OBJECT 0x85f84478
+0x000 Type : 5
+0x002 Size : 112
+0x004 DeviceObject : 0x86f22cc0
+0x008 Vpb : 0x86f22c38
+0x00c FsContext : 0xe3d92d90
+0x010 FsContext2 : 0xe1a706e8
+0x014 SectionObjectPointer : 0x85f50c6c
+0x018 PrivateCacheMap : (null)
+0x01c FinalStatus : 0
+0x020 RelatedFileObject : (null)
+0x024 LockOperation : 0 ''
+0x025 DeletePending : 0 ''
+0x026 ReadAccess : 0x1 ''
+0x027 WriteAccess : 0 ''
+0x028 DeleteAccess : 0 ''
+0x029 SharedRead : 0x1 ''
+0x02a SharedWrite : 0x1 ''
+0x02b SharedDelete : 0x1 ''
+0x02c Flags : 0x144042
+0x030 FileName : _UNICODE_STRING "\Documents and Settings\mason\My Documents\~WRL2542.tmpory.doc"
+0x038 CurrentByteOffset : _LARGE_INTEGER 0x200
+0x040 Waiters : 0
+0x044 Busy : 0
+0x048 LastLock : (null)
+0x04c Lock : _KEVENT
+0x05c Event : _KEVENT
+0x06c CompletionContext : (null)
Notice that the SectionObjectPointers field has the same address as the other file object - these really are two instances of the same file, albeit opened using different names! Since we do not have a cache map, this file is not stored in the Cache Manager at all (this indicates the file was memory mapped). Thus, we cannot walk through the cache structures to find the contents of this file because it is not in the file system data cache.
Let’s do this again, this time using a file from a web browser. I open up and look through the Internet Explorer process. From there I choose FILE_OBJECT 8606bea8:
lkd> dt nt!_FILE_OBJECT 8606bea8
+0x000 Type : 5
+0x002 Size : 112
+0x004 DeviceObject : 0x86f22cc0
+0x008 Vpb : 0x86f22c38
+0x00c FsContext : 0xe1d32710
+0x010 FsContext2 : 0xe18a25b8
+0x014 SectionObjectPointer : 0x86cae074
+0x018 PrivateCacheMap : (null)
+0x01c FinalStatus : 0
+0x020 RelatedFileObject : (null)
+0x024 LockOperation : 0 ''
+0x025 DeletePending : 0 ''
+0x026 ReadAccess : 0x1 ''
+0x027 WriteAccess : 0x1 ''
+0x028 DeleteAccess : 0 ''
+0x029 SharedRead : 0x1 ''
+0x02a SharedWrite : 0x1 ''
+0x02b SharedDelete : 0 ''
+0x02c Flags : 0x140042
+0x030 FileName : _UNICODE_STRING "\Documents and Settings\mason\Cookies\index.dat"
+0x038 CurrentByteOffset : _LARGE_INTEGER 0x0
+0x040 Waiters : 0
+0x044 Busy : 0
+0x048 LastLock : (null)
+0x04c Lock : _KEVENT
+0x05c Event : _KEVENT
+0x06c CompletionContext : (null)
And from this I look at the SectionObjectPointer field:
lkd> d nt!_SECTION_OBJECT_POINTERS 0x86cae074
+0x000 DataSectionObject : 0x86e3e1a8
+0x004 SharedCacheMap : 0x86413008
+0x008 ImageSectionObject : (null)
Notice that this time I do have a shared cache map. Let’s look at the data section object again:
lkd> dt nt!_CONTROL_AREA 0x86e3e1a8
+0x000 Segment : 0xe1bae688
+0x004 DereferenceList : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x00c NumberOfSectionReferences : 2
+0x010 NumberOfPfnReferences : 0x2a
+0x014 NumberOfMappedViews : 0xe
+0x018 NumberOfSubsections : 2
+0x01a FlushInProgressCount : 0
+0x01c NumberOfUserReferences : 0xd
+0x020 u : __unnamed
+0x024 FilePointer : 0x86cce290
+0x028 WaitingForDeletion : (null)
+0x02c ModifiedWriteCount : 0
+0x02e NumberOfSystemCacheViews : 2
Once again, this does not point back to the same file object:
lkd> dt nt!_FILE_OBJECT 0x86cce290
+0x000 Type : 5
+0x002 Size : 112
+0x004 DeviceObject : 0x86f22cc0
+0x008 Vpb : 0x86f22c38
+0x00c FsContext : 0xe1d32710
+0x010 FsContext2 : 0xe2ae4938
+0x014 SectionObjectPointer : 0x86cae074
+0x018 PrivateCacheMap : (null)
+0x01c FinalStatus : 0
+0x020 RelatedFileObject : (null)
+0x024 LockOperation : 0 ''
+0x025 DeletePending : 0 ''
+0x026 ReadAccess : 0x1 ''
+0x027 WriteAccess : 0 ''
+0x028 DeleteAccess : 0 ''
+0x029 SharedRead : 0x1 ''
+0x02a SharedWrite : 0x1 ''
+0x02b SharedDelete : 0x1 ''
+0x02c Flags : 0x144042
+0x030 FileName : _UNICODE_STRING "\Documents and Settings\mason\Cookies\index.dat"
+0x038 CurrentByteOffset : _LARGE_INTEGER 0x1000
+0x040 Waiters : 0
+0x044 Busy : 0
+0x048 LastLock : (null)
+0x04c Lock : _KEVENT
+0x05c Event : _KEVENT
+0x06c CompletionContext : (null)
This is the file object used for paging - notice that in this case the file name is the same for both file objects. Similar to the last example, and yet different. This time we also have the shared cache map, so we display that information:
lkd> dt nt!_SHARED_CACHE_MAP 0x86413008
+0x000 NodeTypeCode : 767
+0x002 NodeByteSize : 304
+0x004 OpenCount : 1
+0x008 FileSize : _LARGE_INTEGER 0x48000
+0x010 BcbList : _LIST_ENTRY [ 0x86413018 - 0x86413018 ]
+0x018 SectionSize : _LARGE_INTEGER 0x100000
+0x020 ValidDataLength : _LARGE_INTEGER 0x48000
+0x028 ValidDataGoal : _LARGE_INTEGER 0x48000
+0x030 InitialVacbs : [4] 0x86face00
+0x040 Vacbs : 0x86413038 -> 0x86face00
+0x044 FileObject : 0x866068d8
+0x048 ActiveVacb : 0x86face00
+0x04c NeedToZero : (null)
+0x050 ActivePage : 0
+0x054 NeedToZeroPage : 0
+0x058 ActiveVacbSpinLock : 0
+0x05c VacbActiveCount : 1
+0x060 DirtyPages : 0
+0x064 SharedCacheMapLinks : _LIST_ENTRY [ 0x8640b06c - 0x862f706c ]
+0x06c Flags : 0x1000
+0x070 Status : 0
+0x074 Mbcb : (null)
+0x078 Section : 0xe2d329f0
+0x07c CreateEvent : (null)
+0x080 WaitOnActiveCount : (null)
+0x084 PagesToWrite : 0
+0x088 BeyondLastFlush : 0
+0x090 Callbacks : 0xf7479c2c
+0x094 LazyWriteContext : 0xe1d32710
+0x098 PrivateList : _LIST_ENTRY [ 0x8641312c - 0x8641312c ]
+0x0a0 LogHandle : (null)
+0x0a4 FlushToLsnRoutine : (null)
+0x0a8 DirtyPageThreshold : 0
+0x0ac LazyWritePassCount : 0
+0x0b0 UninitializeEvent : (null)
+0x0b4 NeedToZeroVacb : (null)
+0x0b8 BcbSpinLock : 0
+0x0bc Reserved : (null)
+0x0c0 Event : _KEVENT
+0x0d0 VacbPushLock : _EX_PUSH_LOCK
+0x0d8 PrivateCacheMap : _PRIVATE_CACHE_MAP
Notice that there is a single VACB for this file: 0x86face00. We can use this to display the memory region used in the cache to map this file:
lkd> dt nt!_VACB 0x86face00
+0x000 BaseAddress : 0xd4680000
+0x004 SharedCacheMap : 0x86413008
+0x008 Overlay : __unnamed
+0x010 LruList : _LIST_ENTRY [ 0x86facfa8 - 0x86facfc0 ]
And this provides us with the memory location we can use to look at the actual data contents of the file itself:
lkd> db d4680000
d4680000 43 6c 69 65 6e 74 20 55-72 6c 43 61 63 68 65 20 Client UrlCache
d4680010 4d 4d 46 20 56 65 72 20-35 2e 32 00 00 80 04 00 MMF Ver 5.2.....
d4680020 00 40 00 00 80 08 00 00-22 03 00 00 00 00 00 00 .@......".......
d4680030 00 00 80 00 00 00 00 00-00 b0 14 00 00 00 00 00 ................
d4680040 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
d4680050 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
d4680060 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
d4680070 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
Once you have used this technique a few times, finding the actual data contents in memory is really rather straight-forward to do. Of course, there are some caveats of which you should be aware:
A file need not be stored in the Cache Manager. This is often going to be the case for files that are memory mapped (e.g., the files accessed by Microsoft Word or Notepad). Of course, mixed access could easily cause the file to be stored in cache.
A file need not be entirely mapped in the cache. In current versions of Windows the Cache Manager uses 256KB regions for mapping the file. Thus, if a file is larger than 256KB, only some portions may be mapped into the cache.
Virtual addresses need not be present in physical memory. Just because the cache region is defined there is no requirement that the region contain data. If there is data present, it is the file data (within the size constraints of the file, of course).
Happy Debugging!