public int FillPageQueue(bool OnlyLarge = false) { PageQueue = new ConcurrentQueue <PFN>(); VIRTUAL_ADDRESS VA; VA.Address = 0; if (DP.PT == null) { PageTable.AddProcess(DP, DP.MemAccess); } Parallel.ForEach(DP.TopPageTablePage, (kvp) => //foreach (var kvp in DP.TopPageTablePage) { // were at the top level (4th) VA.PML4 = kvp.Key; var pfn = new PFN { PTE = kvp.Value, VA = new VIRTUAL_ADDRESS(VA.PML4 << 39) }; foreach (var DirectoryPointerOffset in DP.PT.ExtractNextLevel(pfn, true, 3)) { if (DirectoryPointerOffset == null) { continue; } foreach (var DirectoryOffset in DP.PT.ExtractNextLevel(DirectoryPointerOffset, KernelSpace, 2)) { if (DirectoryOffset == null) { continue; } // if we have a large page we add it now if (DirectoryOffset.PTE.LargePage) { PageQueue.Enqueue(DirectoryOffset); } // otherwise were scanning lower level entries else if (!OnlyLarge) { foreach (var TableOffset in DP.PT.ExtractNextLevel(DirectoryOffset, KernelSpace, 1)) { if (TableOffset == null) { continue; } PageQueue.Enqueue(TableOffset); } } } } //} }); return(PageQueue.Count()); }
long FillTable(VIRTUAL_ADDRESS VA, int PageIndex, long CR3, bool OnlyUserSpace = false) { var entries = 0L; var PageTable = new Dictionary <VIRTUAL_ADDRESS, PFN>(); // We can just pick up the top level page to speed things up // we've already visited this page so were not going to waste time looking up the VA->PA //var page = new long[512]; //mem.GetPageFromFileOffset(FileOffset, ref page); // clear out the VA for the other indexes since were looking at the top level VA.Address = 0; foreach (var kvp in DP.TopPageTablePage) { // Only extract user portion, kernel will be mostly redundant if (OnlyUserSpace && kvp.Key >= 256) { continue; } // were at the top level (4th) VA.PML4 = kvp.Key; var pfn = new PFN(kvp.Value, VA.Address, CR3, DP.vmcs == null ? 0 : DP.vmcs.EPTP); PageTable.Add(VA, pfn); entries++; } // simulated top entry RootPageTable = new PFN(DP.TopPageTablePage[PageIndex], VA.Address, CR3, DP.vmcs == null ? 0 : DP.vmcs.EPTP) { SubTables = PageTable }; // descend the remaining levels // if we find nothing, we can be sure the value were using for EPTP or CR3 is bogus var new_entries = InlineExtract(RootPageTable, 3); if (new_entries == 0) { return(0); } entries += new_entries; // a hint for the full count of entries extracted RootPageTable.PFNCount = entries; return(entries); }
/// <summary> /// An Inline extraction for the page table hierarchy. /// Why not let the compiler do it? I have code clones here? /// /// I guess so, but we can see/deal with the subtle differences at each level here as we implement them. /// e.g. some levels have LargePage bits and we may also lay over other CPU modes here like 32 in 64 etc.. /// </summary> /// <param name="top"></param> /// <param name="Level"></param> /// <returns></returns> long InlineExtract(PFN top, int Level) { if (Level == 0) { return(0); } var entries = 0L; var VA = new VIRTUAL_ADDRESS(top.VA); //WriteLine($"4: Scanning {top.PageTable:X16}"); var hPA = HARDWARE_ADDRESS_ENTRY.MinAddr; var SLAT = top.SLAT; var CR3 = top.PageTable; // pull level 4 entries attach level 3 foreach (var top_sub in top.SubTables) { // scan each page for the level 4 entry var PTE = top_sub.Value.PTE; // we don't need to | in the PML4 AO (address offset) since were pulling down the whole page not just the one value // and were going to brute force our way through the entire table so this was just confusing things. var l3HW_Addr = PTE.NextTableAddress; // | top_sub.Key.PML4; // if we have an EPTP use it and request resolution of the HW_Addr if (SLAT != 0) { var hl3HW_Addr = HARDWARE_ADDRESS_ENTRY.MaxAddr; try { hl3HW_Addr = mem.VirtualToPhysical(SLAT, l3HW_Addr); } catch (Exception) { if (Vtero.DiagOutput) { WriteLine($"level3: Failed lookup {l3HW_Addr:X16}"); } } l3HW_Addr = hl3HW_Addr; } if (SLAT != 0 && (l3HW_Addr == long.MaxValue || l3HW_Addr == long.MaxValue - 1)) { continue; } // copy VA since were going to be making changes var s3va = new VIRTUAL_ADDRESS(top_sub.Key.Address); //WriteLine($"3: Scanning {s3va.Address:X16}"); top_sub.Value.hostPTE = l3HW_Addr; // cache translated value var lvl3_page = new long[512]; // extract the l3 page for each PTEEntry we had in l4 try { mem.GetPageForPhysAddr(l3HW_Addr, ref lvl3_page); } catch (Exception) { if (Vtero.DiagOutput) { WriteLine($"level3: Failed lookup {l3HW_Addr:X16}"); } } if (lvl3_page == null) { continue; } for (uint i3 = 0; i3 < 512; i3++) { if (lvl3_page[i3] == 0) { continue; } var l3PTE = new HARDWARE_ADDRESS_ENTRY(lvl3_page[i3]); // adjust VA to match extracted page entries s3va.DirectoryPointerOffset = i3; // save 'PFN' entry into sub-table I should really revisit all these names var l3PFN = new PFN(l3PTE, s3va.Address, CR3, SLAT); top_sub.Value.SubTables.Add(s3va, l3PFN); entries++; /// TODO: Double check if this is a real bit... /// I added it to help weed out some failure cases if (!l3PTE.LargePage) { // get the page that the current l3PFN describes var l2HW_Addr = l3PTE.NextTableAddress; if (SLAT != 0) { var hl2HW_Addr = HARDWARE_ADDRESS_ENTRY.MaxAddr; try { hl2HW_Addr = mem.VirtualToPhysical(SLAT, l2HW_Addr); } catch (Exception ex) { if (Vtero.DiagOutput) { WriteLine($"level2: Unable to V2P {l3PTE}"); } } l2HW_Addr = hl2HW_Addr; } // TODO: more error handling of exceptions & bad returns // TODO: support software PTE types if (l2HW_Addr == HARDWARE_ADDRESS_ENTRY.MaxAddr) { continue; } l3PFN.hostPTE = l2HW_Addr; var lvl2_page = new long[512]; try { mem.GetPageForPhysAddr(l2HW_Addr, ref lvl2_page); } catch (Exception ex) { if (Vtero.DiagOutput) { WriteLine($"level2: Failed lookup {l2HW_Addr:X16}"); } } if (lvl2_page == null) { continue; } // copy VA var s2va = new VIRTUAL_ADDRESS(s3va.Address); //WriteLine($"2: Scanning {s2va.Address:X16}"); // extract PTE's for each set entry for (uint i2 = 0; i2 < 512; i2++) { if (lvl2_page[i2] == 0) { continue; } var l2PTE = new HARDWARE_ADDRESS_ENTRY(lvl2_page[i2]); s2va.DirectoryOffset = i2; var l2PFN = new PFN(l2PTE, s2va.Address, CR3, SLAT) { Type = PFNType.Data }; l3PFN.SubTables.Add(s2va, l2PFN); entries++; if (!l2PTE.LargePage) { var l1HW_Addr = l2PTE.NextTableAddress; if (SLAT != 0) { var hl1HW_Addr = HARDWARE_ADDRESS_ENTRY.MaxAddr; try { hl1HW_Addr = mem.VirtualToPhysical(SLAT, l1HW_Addr); } catch (Exception ex) { if (Vtero.DiagOutput) { WriteLine($"level1: Unable to V2P {l2PTE}"); } } l1HW_Addr = hl1HW_Addr; } if (l1HW_Addr == HARDWARE_ADDRESS_ENTRY.MaxAddr) { continue; } l2PFN.hostPTE = l1HW_Addr; var lvl1_page = new long[512]; try { mem.GetPageForPhysAddr(l1HW_Addr, ref lvl1_page); } catch (Exception ex) { if (Vtero.DiagOutput) { WriteLine($"level1: Failed lookup {l1HW_Addr:X16}"); } } if (lvl1_page == null) { continue; } var s1va = new VIRTUAL_ADDRESS(s2va.Address); //WriteLine($"1: Scanning {s1va.Address:X16}"); for (uint i1 = 0; i1 < 512; i1++) { if (lvl1_page[i1] == 0) { continue; } var l1PTE = new HARDWARE_ADDRESS_ENTRY(lvl1_page[i1]); s1va.TableOffset = i1; var l1PFN = new PFN(l1PTE, s1va.Address, CR3, SLAT) { Type = PFNType.Data }; l2PFN.SubTables.Add(s1va, l1PFN); entries++; } } } } } } //top.PFNCount += entries; return(entries); }
public IEnumerable<PFN> ExtractNextLevel(PFN PageContext, bool RedundantKernelSpaces, int Level = 4) { if (PageContext == null) yield break; RedundantKernelSpaces = true; var SLAT = Root.SLAT; var CR3 = Root.CR3; var top = Root.Entries; VIRTUAL_ADDRESS SubVA = PageContext.VA; HARDWARE_ADDRESS_ENTRY PA = PageContext.PTE; // get the page that the current PFN describes var HW_Addr = PA.NextTableAddress; if (SLAT != 0) { var hHW_Addr = HARDWARE_ADDRESS_ENTRY.MaxAddr; try { hHW_Addr = mem.VirtualToPhysical(SLAT, HW_Addr); } catch (Exception ex) { if (Vtero.DiagOutput) WriteLine($"level{Level}: Unable to V2P {HW_Addr}"); } HW_Addr = hHW_Addr; if (HW_Addr == long.MaxValue || HW_Addr == long.MaxValue - 1) yield break; } if (PageContext.PTE.LargePage && Level <= 1) { // cyclic PageContext.SubTables.Add(PageContext.VA, PageContext); yield break; } long[] page = new long[512]; bool ReadData = false; // copy VA since were going to be making changes var valueRead = mem.GetPageForPhysAddr(HW_Addr, ref page, ref ReadData); if (!ReadData || page == null) yield break; var dupVA = new VIRTUAL_ADDRESS(SubVA.Address); for (int i = 0; i < 512; i++) { if (!RedundantKernelSpaces && i >= MagicNumbers.KERNEL_PT_INDEX_START_USUALLY) continue; if (page[i] == 0) continue; switch (Level) { case 4: dupVA.PML4 = i; break; case 3: dupVA.DirectoryPointerOffset = i; break; case 2: dupVA.DirectoryOffset = i; break; case 1: dupVA.TableOffset = i; break; default: break; } var pfn = new PFN { VA = new VIRTUAL_ADDRESS(dupVA.Address), PTE = new HARDWARE_ADDRESS_ENTRY(page[i]) }; PageContext.SubTables.Add( pfn.VA, pfn); EntriesParsed++; yield return pfn; } yield break; }
long FillTable(VIRTUAL_ADDRESS VA, int PageIndex, long CR3, bool OnlyUserSpace = false) { var entries = 0L; var PageTable = new Dictionary<VIRTUAL_ADDRESS, PFN>(); // We can just pick up the top level page to speed things up // we've already visited this page so were not going to waste time looking up the VA->PA //var page = new long[512]; //mem.GetPageFromFileOffset(FileOffset, ref page); // clear out the VA for the other indexes since were looking at the top level VA.Address = 0; foreach (var kvp in DP.TopPageTablePage) { // Only extract user portion, kernel will be mostly redundant if(OnlyUserSpace && kvp.Key >= 256) continue; // were at the top level (4th) VA.PML4 = kvp.Key; var pfn = new PFN(kvp.Value, VA.Address, CR3, DP.vmcs == null ? 0 : DP.vmcs.EPTP); PageTable.Add(VA, pfn); entries++; } // simulated top entry RootPageTable = new PFN(DP.TopPageTablePage[PageIndex], VA.Address, CR3, DP.vmcs == null ? 0 : DP.vmcs.EPTP) { SubTables = PageTable }; // descend the remaining levels // if we find nothing, we can be sure the value were using for EPTP or CR3 is bogus var new_entries = InlineExtract(RootPageTable, 3); if (new_entries == 0) return 0; entries += new_entries; // a hint for the full count of entries extracted RootPageTable.PFNCount = entries; return entries; }
/// <summary> /// Scan and return Extract objects which represent detected PE's /// /// TODO:simplify/rewrite this /// </summary> /// <param name="Start"></param> /// <param name="Stop">We just truncate VA's at 48 bit's</param> /// <returns>count of new detections since last Run</returns> public List <Extract> Run(long Start = 0, long Stop = 0xFFFFffffFFFF, PFN entry = null, ParallelLoopState pState = null, Mem Instance = null) { bool GotData = false; var memAxss = Instance == null ? BackingBlocks : Instance; long i = Start, Curr = 0; byte[] block; var rv = new List <Extract>(); // large page read if (entry != null && entry.PTE.LargePage) { block = new byte[MagicNumbers.LARGE_PAGE_SIZE]; memAxss.GetPageForPhysAddr(entry.PTE, ref block, ref GotData); if (GotData) { rv = FastPE(Start, block); } return(rv); } else // use supplied page sized physical entry if (entry != null && Stop - Start == MagicNumbers.PAGE_SIZE) { block = new byte[MagicNumbers.PAGE_SIZE]; memAxss.GetPageForPhysAddr(entry.PTE, ref block, ref GotData); if (GotData) { rv = FastPE(Start, block); } // we only header scan when asked and if the page read is 1 from an alignment if (!HeaderScan) { return(rv); } if ((Start & 0xF000) != MagicNumbers.PAGE_SIZE) { return(rv); } // if were doing a header scan back up i so we do the previous page i -= 0x1000; // back up Stop also so we just scan this current page one time Stop -= 0x1000; } // just use the virtual addresses and attempt to locate phys from page walk // this is a really slow way to enumerate memory // convert index to an address // then add start to it block = new byte[MagicNumbers.PAGE_SIZE]; // 0x200 * 8 = 4k while (i < Stop) { if (pState != null && pState.IsStopped) { return(rv); } HARDWARE_ADDRESS_ENTRY locPhys = HARDWARE_ADDRESS_ENTRY.MinAddr; if (DPContext.vmcs != null) { locPhys = memAxss.VirtualToPhysical(DPContext.vmcs.EPTP, DPContext.CR3Value, i); } else { locPhys = memAxss.VirtualToPhysical(DPContext.CR3Value, i); } Curr = i; i += MagicNumbers.PAGE_SIZE; if (HARDWARE_ADDRESS_ENTRY.IsBadEntry(locPhys) || !locPhys.Valid) { continue; } memAxss.GetPageForPhysAddr(locPhys, ref block, ref GotData); if (!GotData) { continue; } var new_pe = FastPE(Curr, block); rv.AddRange(new_pe); if (Vtero.VerboseOutput && new_pe.Count > 0) { Console.WriteLine($"Detected PE @ VA {Curr:X}"); } } return(rv); }
// TODO: Remove this call, only seen called by legacy stuff public long FillTable(bool RedundantKernelSpaces, int depth = 4) { var entries = 0L; var PageTables = new Dictionary <VIRTUAL_ADDRESS, PFN>(); KernelSpace = RedundantKernelSpaces; // clear out the VA for the other indexes since were looking at the top level VIRTUAL_ADDRESS VA; VA.Address = 0; // making use of the cached top level foreach (var kvp in DP.TopPageTablePage) { // Only extract user portion, kernel will be mostly redundant if (!RedundantKernelSpaces && kvp.Key >= (MagicNumbers.KERNEL_PT_INDEX_START_USUALLY - 1)) { continue; } // were at the top level (4th) VA.PML4 = kvp.Key; var pfn = new PFN { PTE = kvp.Value, VA = new VIRTUAL_ADDRESS(VA.PML4 << 39) }; // Top level for page table if (MemorizeTables) { PageTables.Add(VA, pfn); } // We will only do one level if were not buffering if (depth > 1) { foreach (var DirectoryPointerOffset in ExtractNextLevel(pfn, 3)) { if (DirectoryPointerOffset == null) { continue; } if (depth > 2 /* && !DirectoryPointerOffset.PTE.LargePage */) { foreach (var DirectoryOffset in ExtractNextLevel(DirectoryPointerOffset, 2)) { if (DirectoryOffset == null) { continue; } if (depth > 3 /* && !DirectoryOffset.PTE.LargePage && EnvLimits.MAX_PageTableEntriesToScan > entries */) { foreach (var TableOffset in ExtractNextLevel(DirectoryOffset, 1)) { if (TableOffset == null) { continue; } entries++; } } entries++; } } entries++; } } entries++; } if (MemorizeTables) { Root.Entries = new PFN() { SubTables = PageTables } } ; // InlineExtract may be faster but it's memory requirement is higher which was a problem // when analyzing 64GB+ dumps (yes InVtero.net handles very big memory)++ #if FALSE // descend the remaining levelsPageTableEntries // if we find nothing, we can be sure the value were using for EPTP or CR3 is bogus var new_entries = InlineExtract(Root); if (new_entries == 0) { return(0); } entries += new_entries; #endif EntriesParsed += entries; DepthParsed = depth; // a hint for the full count of entries extracted Root.Count = entries; return(entries); }
/// <summary> /// An Inline extraction for the page table hierarchy. /// Why not let the compiler do it? I have code clones here? /// /// I guess so, but we can see/deal with the subtle differences at each level here as we implement them. /// e.g. some levels have LargePage bits and we may also lay over other CPU modes here like 32 in 64 etc.. /// </summary> /// <param name="top"></param> /// <param name="Level"></param> /// <returns></returns> long InlineExtract(PFN top, int Level) { if (Level == 0) return 0; var entries = 0L; var VA = new VIRTUAL_ADDRESS(top.VA); //WriteLine($"4: Scanning {top.PageTable:X16}"); var hPA = HARDWARE_ADDRESS_ENTRY.MinAddr; var SLAT = top.SLAT; var CR3 = top.PageTable; // pull level 4 entries attach level 3 foreach (var top_sub in top.SubTables) { // scan each page for the level 4 entry var PTE = top_sub.Value.PTE; // we don't need to | in the PML4 AO (address offset) since were pulling down the whole page not just the one value // and were going to brute force our way through the entire table so this was just confusing things. var l3HW_Addr = PTE.NextTableAddress ;// | top_sub.Key.PML4; // if we have an EPTP use it and request resolution of the HW_Addr if (SLAT != 0) { var hl3HW_Addr = HARDWARE_ADDRESS_ENTRY.MaxAddr; try { hl3HW_Addr = mem.VirtualToPhysical(SLAT, l3HW_Addr); } catch (Exception) { if (Vtero.DiagOutput) WriteLine($"level3: Failed lookup {l3HW_Addr:X16}"); } l3HW_Addr = hl3HW_Addr; } if (SLAT != 0 && (l3HW_Addr == long.MaxValue || l3HW_Addr == long.MaxValue-1)) continue; // copy VA since were going to be making changes var s3va = new VIRTUAL_ADDRESS(top_sub.Key.Address); //WriteLine($"3: Scanning {s3va.Address:X16}"); top_sub.Value.hostPTE = l3HW_Addr; // cache translated value var lvl3_page = new long[512]; // extract the l3 page for each PTEEntry we had in l4 try { mem.GetPageForPhysAddr(l3HW_Addr, ref lvl3_page); } catch (Exception) { if (Vtero.DiagOutput) WriteLine($"level3: Failed lookup {l3HW_Addr:X16}"); } if (lvl3_page == null) continue; for (uint i3 = 0; i3 < 512; i3++) { if (lvl3_page[i3] == 0) continue; var l3PTE = new HARDWARE_ADDRESS_ENTRY(lvl3_page[i3]); // adjust VA to match extracted page entries s3va.DirectoryPointerOffset = i3; // save 'PFN' entry into sub-table I should really revisit all these names var l3PFN = new PFN(l3PTE, s3va.Address, CR3, SLAT); top_sub.Value.SubTables.Add(s3va, l3PFN); entries++; /// TODO: Double check if this is a real bit... /// I added it to help weed out some failure cases if (!l3PTE.LargePage) { // get the page that the current l3PFN describes var l2HW_Addr = l3PTE.NextTableAddress; if (SLAT != 0) { var hl2HW_Addr = HARDWARE_ADDRESS_ENTRY.MaxAddr; try { hl2HW_Addr = mem.VirtualToPhysical(SLAT, l2HW_Addr); } catch (Exception ex) { if (Vtero.DiagOutput) WriteLine($"level2: Unable to V2P {l3PTE}"); } l2HW_Addr = hl2HW_Addr; } // TODO: more error handling of exceptions & bad returns // TODO: support software PTE types if (l2HW_Addr == HARDWARE_ADDRESS_ENTRY.MaxAddr) continue; l3PFN.hostPTE = l2HW_Addr; var lvl2_page = new long[512]; try { mem.GetPageForPhysAddr(l2HW_Addr, ref lvl2_page); } catch (Exception ex) { if (Vtero.DiagOutput) WriteLine($"level2: Failed lookup {l2HW_Addr:X16}"); } if (lvl2_page == null) continue; // copy VA var s2va = new VIRTUAL_ADDRESS(s3va.Address); //WriteLine($"2: Scanning {s2va.Address:X16}"); // extract PTE's for each set entry for (uint i2 = 0; i2 < 512; i2++) { if (lvl2_page[i2] == 0) continue; var l2PTE = new HARDWARE_ADDRESS_ENTRY(lvl2_page[i2]); s2va.DirectoryOffset = i2; var l2PFN = new PFN(l2PTE, s2va.Address, CR3, SLAT) { Type = PFNType.Data }; l3PFN.SubTables.Add(s2va, l2PFN); entries++; if (!l2PTE.LargePage) { var l1HW_Addr = l2PTE.NextTableAddress; if (SLAT != 0) { var hl1HW_Addr = HARDWARE_ADDRESS_ENTRY.MaxAddr; try { hl1HW_Addr = mem.VirtualToPhysical(SLAT, l1HW_Addr); } catch (Exception ex) { if (Vtero.DiagOutput) WriteLine($"level1: Unable to V2P {l2PTE}"); } l1HW_Addr = hl1HW_Addr; } if (l1HW_Addr == HARDWARE_ADDRESS_ENTRY.MaxAddr) continue; l2PFN.hostPTE = l1HW_Addr; var lvl1_page = new long[512]; try { mem.GetPageForPhysAddr(l1HW_Addr, ref lvl1_page); } catch (Exception ex) { if(Vtero.DiagOutput) WriteLine($"level1: Failed lookup {l1HW_Addr:X16}"); } if (lvl1_page == null) continue; var s1va = new VIRTUAL_ADDRESS(s2va.Address); //WriteLine($"1: Scanning {s1va.Address:X16}"); for (uint i1 = 0; i1 < 512; i1++) { if (lvl1_page[i1] == 0) continue; var l1PTE = new HARDWARE_ADDRESS_ENTRY(lvl1_page[i1]); s1va.TableOffset = i1; var l1PFN = new PFN(l1PTE, s1va.Address, CR3, SLAT) { Type = PFNType.Data }; l2PFN.SubTables.Add(s1va, l1PFN); entries++; } } } } } } //top.PFNCount += entries; return entries; }
public IEnumerable <PFN> ExtractNextLevel(PFN PageContext, int Level = 4, bool RedundantKernelSpaces = false) { if (PageContext == null) { yield break; } var SLAT = Root.SLAT; var CR3 = Root.CR3; var top = Root.Entries; VIRTUAL_ADDRESS SubVA = PageContext.VA; HARDWARE_ADDRESS_ENTRY PA = PageContext.PTE; // get the page that the current PFN describes var HW_Addr = PA.NextTableAddress; if (SLAT != 0) { var hHW_Addr = HARDWARE_ADDRESS_ENTRY.MaxAddr; try { hHW_Addr = mem.VirtualToPhysical(SLAT, HW_Addr); } catch (Exception ex) { if (Vtero.DiagOutput) { WriteLine($"level{Level}: Unable to V2P {HW_Addr}"); } } HW_Addr = hHW_Addr; if (HW_Addr == long.MaxValue || HW_Addr == long.MaxValue - 1) { yield break; } } if (PageContext.PTE.LargePage && Level <= 1) { // cyclic if (MemorizeTables) { PageContext.SubTables.Add(PageContext.VA, PageContext); } yield break; } long[] page = new long[512]; bool ReadData = false; // copy VA since were going to be making changes var valueRead = mem.GetPageForPhysAddr(HW_Addr, ref page, ref ReadData); if (!ReadData || page == null) { yield break; } // if every entry is exactially the same bail out var check1 = page[0]; if (Array.TrueForAll <long>(page, (entry) => entry == check1)) { yield break; } var dupVA = new VIRTUAL_ADDRESS(SubVA.Address); for (int i = 0; i < 512; i++) { // kernel indexes are only relevant on the top level if (Level == 4 && (!RedundantKernelSpaces && i >= MagicNumbers.KERNEL_PT_INDEX_START_USUALLY)) { continue; } if (page[i] == 0) { continue; } switch (Level) { case 4: dupVA.PML4 = i; break; case 3: dupVA.DirectoryPointerOffset = i; break; case 2: dupVA.DirectoryOffset = i; break; case 1: dupVA.TableOffset = i; break; default: break; } var pfn = new PFN { VA = new VIRTUAL_ADDRESS(dupVA.Address), PTE = new HARDWARE_ADDRESS_ENTRY(page[i]) }; if (MemorizeTables) { PageContext.SubTables.Add( pfn.VA, pfn); } EntriesParsed++; yield return(pfn); } yield break; }
//[ProtoIgnore] //public List<PFN> PageQueue; //TODO: this should really take a PFN with various bit's set we can test with a .Match //TODO: fix all callers of this to use a callback also public IEnumerable <PFN> FillPageQueue(bool OnlyLarge = false, bool RedundantKernelSpaces = false, bool OnlyValid = true, bool OnlyExec = true) { KernelSpace = RedundantKernelSpaces; //PageQueue = new List<PFN>(); VIRTUAL_ADDRESS VA; VA.Address = 0; if (DP.PT == null) { PageTable.AddProcess(DP, DP.MemAccess); } //Parallel.ForEach(DP.TopPageTablePage, (kvp) => foreach (var kvp in DP.TopPageTablePage) { // were at the top level (4th) VA.PML4 = kvp.Key; var pfn = new PFN { PTE = kvp.Value, VA = new VIRTUAL_ADDRESS(VA.PML4 << 39) }; // do redundant check here if (!RedundantKernelSpaces && (kvp.Key >= MagicNumbers.KERNEL_PT_INDEX_START_USUALLY)) { continue; } if (OnlyExec && pfn.PTE.NoExecute) { continue; } foreach (var DirectoryPointerOffset in DP.PT.ExtractNextLevel(pfn, 3)) { if (DirectoryPointerOffset == null) { continue; } if (OnlyExec && DirectoryPointerOffset.PTE.NoExecute) { continue; } foreach (var DirectoryOffset in DP.PT.ExtractNextLevel(DirectoryPointerOffset, 2)) { if (DirectoryOffset == null) { continue; } // if we have a large page we add it now if (DirectoryOffset.PTE.LargePage || (OnlyValid && !DirectoryOffset.PTE.Valid)) { if (OnlyExec && DirectoryOffset.PTE.NoExecute) { continue; } yield return(DirectoryOffset); continue; } // otherwise were scanning lower level entries // unless we are only large page scanning. else if (!OnlyLarge) { foreach (var TableOffset in DP.PT.ExtractNextLevel(DirectoryOffset, 1)) { if (OnlyExec && TableOffset.PTE.NoExecute) { continue; } if (TableOffset == null || (OnlyValid && !TableOffset.PTE.Valid)) { continue; } yield return(TableOffset); } } } } } //}); yield break; }
long InlineExtract(PFN top, int Level) { if (Level == 0) return 0; var entries = 0L; var VA = new VIRTUAL_ADDRESS(top.VA); var hPA = HARDWARE_ADDRESS_ENTRY.MinAddr; var CR3 = top.PageTable; var SLAT = top.SLAT; // pull level 4 entries attach level 3 foreach (var top_sub in top.SubTables) { // scan each page for the level 4 entry var PTE = top_sub.Value.PTE; var l3HW_Addr = PTE.NextTableAddress | top_sub.Key.PML4; // if we have an EPTP use it and request resolution of the HW_Addr if (SLAT != 0) { var hl3HW_Addr = HARDWARE_ADDRESS_ENTRY.MaxAddr; hl3HW_Addr = mem.VirtualToPhysical(SLAT, l3HW_Addr); l3HW_Addr = hl3HW_Addr; } if (l3HW_Addr == long.MaxValue) continue; // copy VA since were going to be making changes var s3va = new VIRTUAL_ADDRESS(top_sub.Key.Address); top_sub.Value.hostPTE = l3HW_Addr; // cache translated value var lvl3_page = new long[512]; // extract the l3 page for each PTEEntry we had in l4 try { mem.GetPageForPhysAddr(l3HW_Addr, ref lvl3_page); } catch (Exception) { WriteLine($"level3: Failed lookup {l3HW_Addr:X16}"); } if (lvl3_page == null) continue; for (uint i3 = 0; i3 < 512; i3++) { if (lvl3_page[i3] == 0) continue; var l3PTE = new HARDWARE_ADDRESS_ENTRY(lvl3_page[i3]); // adjust VA to match extracted page entries s3va.DirectoryPointerOffset = i3; // save 'PFN' entry into sub-table I should really revisit all these names var l3PFN = new PFN(l3PTE, s3va.Address, CR3, SLAT); top_sub.Value.SubTables.Add(s3va, l3PFN); entries++; // get the page that the current l3PFN describes var l2HW_Addr = l3PTE.NextTableAddress; if (SLAT != 0) { var hl2HW_Addr = HARDWARE_ADDRESS_ENTRY.MaxAddr; try { hl2HW_Addr = mem.VirtualToPhysical(SLAT, l2HW_Addr); } catch(Exception ex) { WriteLine($"level2: Unable to V2P {l3PTE}"); } l2HW_Addr = hl2HW_Addr; } // TODO: more error handlng of exceptions & bad return's // TODO: support software PTE types if (l2HW_Addr == HARDWARE_ADDRESS_ENTRY.MaxAddr) continue; l3PFN.hostPTE = l2HW_Addr; var lvl2_page = new long[512]; try { mem.GetPageForPhysAddr(l2HW_Addr, ref lvl2_page); } catch (Exception ex) { WriteLine($"level2: Failed lookup {l2HW_Addr:X16}"); } if (lvl2_page == null) continue; // copy VA var s2va = new VIRTUAL_ADDRESS(s3va.Address); // extract PTE's for each set entry for (uint i2 = 0; i2 < 512; i2++) { if (lvl2_page[i2] == 0) continue; var l2PTE = new HARDWARE_ADDRESS_ENTRY(lvl2_page[i2]); s2va.DirectoryOffset = i2; var l2PFN = new PFN(l2PTE, s2va.Address, CR3, SLAT); l3PFN.SubTables.Add(s2va, l2PFN); entries++; if (!l2PTE.LargePage) { var l1HW_Addr = l2PTE.NextTableAddress; if (SLAT != 0) { var hl1HW_Addr = HARDWARE_ADDRESS_ENTRY.MaxAddr; try { hl1HW_Addr = mem.VirtualToPhysical(SLAT, l1HW_Addr); } catch (Exception ex) { WriteLine($"level1: Unable to V2P {l2PTE}"); } l1HW_Addr = hl1HW_Addr; } if (l1HW_Addr == HARDWARE_ADDRESS_ENTRY.MaxAddr) continue; l2PFN.hostPTE = l1HW_Addr; var lvl1_page = new long[512]; try { mem.GetPageForPhysAddr(l1HW_Addr, ref lvl1_page); } catch (Exception ex) { WriteLine($"level1: Failed lookup {l1HW_Addr:X16}"); } if (lvl1_page == null) continue; var s1va = new VIRTUAL_ADDRESS(s2va.Address); for (uint i1 = 0; i1 < 512; i1++) { if (lvl1_page[i1] == 0) continue; var l1PTE = new HARDWARE_ADDRESS_ENTRY(lvl1_page[i1]); s1va.TableOffset = i1; var l1PFN = new PFN(l1PTE, s1va.Address, CR3, SLAT); l2PFN.SubTables.Add(s1va, l1PFN); entries++; } } } } } top.PFNCount += entries; return entries; }
public int FillPageQueue(bool OnlyLarge = false) { PageQueue = new ConcurrentQueue<PFN>(); VIRTUAL_ADDRESS VA; VA.Address = 0; if (DP.PT == null) PageTable.AddProcess(DP, DP.MemAccess); //Parallel.ForEach(DP.TopPageTablePage, (kvp) => foreach (var kvp in DP.TopPageTablePage) { // were at the top level (4th) VA.PML4 = kvp.Key; var pfn = new PFN { PTE = kvp.Value, VA = new VIRTUAL_ADDRESS(VA.PML4 << 39) }; foreach (var DirectoryPointerOffset in DP.PT.ExtractNextLevel(pfn, true, 3)) { if (DirectoryPointerOffset == null) continue; foreach (var DirectoryOffset in DP.PT.ExtractNextLevel(DirectoryPointerOffset, KernelSpace, 2)) { if (DirectoryOffset == null) continue; // if we have a large page we add it now if (DirectoryOffset.PTE.LargePage) PageQueue.Enqueue(DirectoryOffset); // otherwise were scanning lower level entries else if(!OnlyLarge) { foreach (var TableOffset in DP.PT.ExtractNextLevel(DirectoryOffset, KernelSpace, 1)) { if (TableOffset == null) continue; PageQueue.Enqueue(TableOffset); } } } } } //}); return PageQueue.Count(); }
public long DumpProc(string Folder, DetectedProc Proc, bool IncludeData = false, bool KernelSpace = true) { //var entries = PageTable.Flatten(Proc.PT.Root.Entries.SubTables, 4); VIRTUAL_ADDRESS VA; VA.Address = 0; var PageTables = new Dictionary<VIRTUAL_ADDRESS, PFN>(); int level = 3; long entries = 0; if (Proc.PT == null) PageTable.AddProcess(Proc, MemAccess); foreach (var kvp in Proc.TopPageTablePage) { // were at the top level (4th) VA.PML4 = kvp.Key; var pfn = new PFN { PTE = kvp.Value, VA = new VIRTUAL_ADDRESS(VA.PML4 << 39) }; // Top level for page table PageTables.Add(VA, pfn); foreach (var DirectoryPointerOffset in Proc.PT.ExtractNextLevel(pfn, true, level)) { if (DirectoryPointerOffset == null) continue; foreach (var DirectoryOffset in Proc.PT.ExtractNextLevel(DirectoryPointerOffset, KernelSpace, level - 1)) { if (DirectoryOffset == null) continue; foreach (var TableOffset in Proc.PT.ExtractNextLevel(DirectoryOffset, KernelSpace, level - 2)) { if (TableOffset == null) continue; entries++; if (IncludeData == TableOffset.PTE.NoExecute) WriteRange(TableOffset.VA, TableOffset, Folder, MemAccess); if (!TableOffset.PTE.NoExecute) WriteRange(TableOffset.VA, TableOffset, Folder, MemAccess); } entries++; if (IncludeData == DirectoryOffset.PTE.NoExecute) WriteRange(DirectoryOffset.VA, DirectoryOffset, Folder, MemAccess); if (!DirectoryOffset.PTE.NoExecute) WriteRange(DirectoryOffset.VA, DirectoryOffset, Folder, MemAccess); } entries++; } } return entries; }
public byte[] HashRange(VIRTUAL_ADDRESS KEY, PFN VALUE) { var rv = new byte[1]; var block = new long[0x200]; // 0x200 * 8 = 4k var bpage = new byte[0x1000]; var tigger = new Tiger(); tigger.Initialize(); //fixed (void* lp = block, bp = bpage) //{ if (DiagOutput) WriteColor(VALUE.PTE.Valid ? ConsoleColor.Cyan : ConsoleColor.Red, $"VA: {KEY:X12} \t PFN: {VALUE.PTE}"); // if we have invalid (software managed) page table entries // the data may be present, or a prototype or actually in swap. // for the moment were only going to dump hardware managed data // or feel free to patch this up ;) if (!VALUE.PTE.Valid) return rv; if (VALUE.PTE.LargePage) { bool GoodRead = false; // 0x200 * 4kb = 2MB for (int i = 0; i < 0x200; i++) { MemAccess.GetPageForPhysAddr(VALUE.PTE, ref block, ref GoodRead); VALUE.PTE.PTE += 0x1000; if (!GoodRead) block = new long[0x200]; Buffer.BlockCopy(block, 0, bpage, 0, 4096); rv = tigger.ComputeHash(bpage); } return rv; } else { try { MemAccess.GetPageForPhysAddr(VALUE.PTE, ref block); } catch (Exception ex) { } if (block != null) { Buffer.BlockCopy(block, 0, bpage, 0, 4096); rv = tigger.ComputeHash(bpage); } } return rv; }
// TODO: Figure out if MemoryCopy or BlockCopy ... public string WriteRange(VIRTUAL_ADDRESS KEY, PFN VALUE, string BaseFileName, Mem PhysMemReader = null, bool SinglePFNStore = false, bool DumpNULL = false) { if (SinglePFNStore && SISmap == null) SISmap = new WAHBitArray(); if(SinglePFNStore) { if (SISmap.Get((int)VALUE.PTE.PFN)) return string.Empty; SISmap.Set((int)VALUE.PTE.PFN, true); } bool canAppend = false; var saveLoc = BaseFileName + KEY.Address.ToString("X") + ".bin"; var lastLoc = BaseFileName + (KEY.Address - ContigSize).ToString("X") + ".bin"; if (File.Exists(lastLoc)) { canAppend = true; ContigSize += 0x1000; saveLoc = lastLoc; } else ContigSize = 0x1000; //unsafe //{ var block = new long[0x200]; // 0x200 * 8 = 4k var bpage = new byte[0x1000]; //fixed (void* lp = block, bp = bpage) //{ if (DiagOutput) WriteColor(VALUE.PTE.Valid ? ConsoleColor.Cyan : ConsoleColor.Red, $"VA: {KEY:X12} \t PFN: {VALUE.PTE}"); // if we have invalid (software managed) page table entries // the data may be present, or a prototype or actually in swap. // for the moment were only going to dump hardware managed data // or feel free to patch this up ;) if (!VALUE.PTE.Valid) return string.Empty; if (VALUE.PTE.LargePage) { bool GoodRead = false; using (var lsavefile = File.OpenWrite(saveLoc)) { // 0x200 * 4kb = 2MB // TODO: Large pages properly? // TODO: PageCache is still broken in some cases... disable for now here for (int i = 0; i < 0x200; i++) { PhysMemReader.GetPageForPhysAddr(VALUE.PTE, ref block, ref GoodRead); VALUE.PTE.PTE += 0x1000; if(!GoodRead) block = new long[0x200]; Buffer.BlockCopy(block, 0, bpage, 0, 4096); //Buffer.MemoryCopy(lp, bp, 4096, 4096); lsavefile.Write(bpage, 0, 4096); //lsavefile.Write(bpage, 0, 4096); } return lastLoc; } } else { try { PhysMemReader.GetPageForPhysAddr(VALUE.PTE, ref block); } catch (Exception ex) { } if (block != null) { if (DumpNULL || !UnsafeHelp.IsZero(block)) { Buffer.BlockCopy(block, 0, bpage, 0, 4096); //Buffer.MemoryCopy(lp, bp, 4096, 4096); using (var savefile = (canAppend ? File.Open(lastLoc, FileMode.Append, FileAccess.Write, FileShare.ReadWrite) : File.OpenWrite(saveLoc))) savefile.Write(bpage, 0, 4096); //savefile.Write(bpage, 0, 4096); return lastLoc; } } } ContigSize = 0; return string.Empty; //} //} }
// TODO: Move this to Dumper.cs /// <summary> /// Memory Dump routines /// </summary> /// <param name="AS_ToDump"></param> public void DumpASToFile(IDictionary<int, List<DetectedProc>> AS_ToDump = null) { var DumpList = AS_ToDump; if(DumpList == null) { DumpList = new Dictionary<int, List <DetectedProc>> (); foreach(var g in ASGroups) { DumpList[g.Key] = new List<DetectedProc>(); var p = from px in g.Value orderby px.CR3Value select px; foreach (var pp in p) DumpList[pp.AddressSpaceID].Add(pp); } } List <KeyValuePair <VIRTUAL_ADDRESS, PFN>> MemRanges = null; List<string> DumpedToDisk = new List<string>(); Stack<PFN> PFNStack = new Stack<PFN>(); // instance member ContigSize = -1; ForegroundColor = ConsoleColor.Gray; string LastDumped = string.Empty; int cntDumped = 0; WriteLine($"{Environment.NewLine} Address spaces resolved. Dump method starting. {Environment.NewLine}"); //using (var memAxs = new Mem(MemFile, null, DetectedDesc)) var memAxs = MemAccess; { memAxs.OverrideBufferLoadInput = true; TripleBreak: int asID=0; foreach(var AS in DumpList) if(DumpList[AS.Key] != null && DumpList[AS.Key].Count() > 0) if(DumpList[AS.Key].Count() > 1) WriteColor(ConsoleColor.Green, $"[{AS.Key}] Contains {DumpList[AS.Key].Count()} entries EPTP/Kernels shared {DumpList[AS.Key][0]}"); else WriteColor(ConsoleColor.Yellow, $"[{AS.Key}] Contains {DumpList[AS.Key].Count()} entries EPTP/Kernels shared {DumpList[AS.Key][0]}"); bool validInput = false; do { ForegroundColor = ConsoleColor.White; Write("Select an address space: "); var ASselect = ReadLine(); validInput = int.TryParse(ASselect, out asID); if (!validInput) WriteLine("just enter the number that coincides with the address space you want to investigate."); if (!DumpList.ContainsKey(asID)) validInput = false; } while (!validInput); WriteColor(ConsoleColor.Green, $"Loading address space entries based on {DumpList[asID][0]}"); var ToDump = DumpList[asID]; // sort for convince ToDump.Sort((x, y) => { if (x.CR3Value < y.CR3Value) return -1; else if (x.CR3Value > y.CR3Value) return 1; else return 0; }); while (true) { DoubleBreak: // prompt user for (int i = 0; i < ToDump.Count; i++) { var vmcs = ToDump[i].vmcs == null ? 0 : ToDump[i].vmcs.EPTP; if (ToDump[i].PT == null) PageTable.AddProcess(ToDump[i], memAxs, true, 1); WriteColor(ConsoleColor.Magenta, $"{i} VMCS:{vmcs:X} Process:{ToDump[i].CR3Value:X} (top level) {ToDump[i].PT.Root.Count} type {ToDump[i].PageTableType} group {ToDump[i].Group}"); } validInput = false; int procId = 0; do { ForegroundColor = ConsoleColor.White; Write("Select a process to dump: "); var selection = ReadLine(); validInput = int.TryParse(selection, out procId); if (!validInput) WriteLine("just enter the number 0 or 1 or 2 or ... that coincides with the process you want to investigate."); } while (!validInput); WriteColor(ConsoleColor.Gray, $"Selected process {procId} {ToDump[procId]}"); var tdp = ToDump[procId]; var saveLoc = Path.Combine(Path.GetDirectoryName(MemFile), Path.GetFileName(MemFile) + "."); var table = tdp.PT.Root.Entries; bool fKeepGoing = true; while (fKeepGoing) { WriteColor(ConsoleColor.Gray, $"{Environment.NewLine}Listing ranges for {tdp}, {table.PFNCount} entries scanned."); int parse = -1, level = 4; PFN next_table = new PFN(); Dictionary<VIRTUAL_ADDRESS, PFN> TableEntries; Dictionary<VIRTUAL_ADDRESS, PFN> LastTableEntries = null; do { TableEntries = table.SubTables; // If we have 0 entries, ensure there really are none and we did // not optimize out pre-buffering everything if (TableEntries.Count() == 0) foreach (var pfn in tdp.PT.ExtractNextLevel(table, true, level)) ; if (TableEntries.Count() == 0) { WriteColor(ConsoleColor.Yellow, $"Entry {parse}:{table.VA}{table.PTE} contains no in-memory pages addressable to this process."); if(LastTableEntries != null) TableEntries = LastTableEntries; if (level < 4) level++; if (PFNStack.Count() > 0) table = PFNStack.Pop(); else { table = tdp.PT.Root.Entries; level = 4; TableEntries = table.SubTables; } } var dict_keys = TableEntries.Keys.ToArray(); for (int r = 0; r < TableEntries.Count(); r++) { var dict_Val = TableEntries[dict_keys[r]]; WriteColor((level & 1) == 1 ? ConsoleColor.Cyan : ConsoleColor.Green, $"{r} Virtual: {dict_keys[r]} \t Physical: {dict_Val.PTE}"); } ForegroundColor = ConsoleColor.White; Write($"command ({level}): "); var userSelect = ReadLine().ToLower(); if (string.IsNullOrWhiteSpace(userSelect)) parse = -1; else if(char.IsLetter(userSelect[0])) switch(userSelect) { case "u": if (PFNStack.Count() > 0) { table = PFNStack.Pop(); level++; } else { WriteColor(ConsoleColor.Yellow, "Can not go any higher"); table = tdp.PT.Root.Entries; level = 4; } continue; case "l": PrintLastDumped(DumpedToDisk); continue; case "x": Environment.Exit(0); break; case "p": goto DoubleBreak; case "s": goto TripleBreak; case "h": case "r": ReScanNextLevel(tdp); break; case "d": ReScanNextLevel(tdp, true); break; case "a": AddProcessPageTable(tdp, memAxs); break; default: REPLhelp(); continue; } else int.TryParse(userSelect, out parse); // extract the key that the user index is referring to and reassign table if (parse >= 0) { PFNStack.Push(table); try { next_table = TableEntries[TableEntries.Keys.ToArray()[parse]]; } catch (Exception ex) { WriteColor(ConsoleColor.Red, $"Exception accessing page table, try again... {ex.ToString()}"); continue; } table = next_table; } if (parse < 0) break; level--; LastTableEntries = TableEntries; } while (level > 0); WriteColor(ConsoleColor.Gray, $"Writing out data into the same folder as the input: {Path.GetDirectoryName(MemFile)}"); if (parse < 0) { switch (level) { case 4: MemRanges = TableEntries.SelectMany(x => x.Value.SubTables).SelectMany(y => y.Value.SubTables).SelectMany(z => z.Value.SubTables).ToList(); break; case 3: MemRanges = TableEntries.SelectMany(x => x.Value.SubTables).SelectMany(y => y.Value.SubTables).ToList(); break; case 2: MemRanges = TableEntries.SelectMany(x => x.Value.SubTables).ToList(); break; case 1: default: MemRanges = TableEntries.ToList(); break; } foreach (var mr in MemRanges) { LastDumped = WriteRange(mr.Key, mr.Value, saveLoc, memAxs); DumpedToDisk.Add(LastDumped); cntDumped++; } } else { var a_range = new KeyValuePair<VIRTUAL_ADDRESS, PFN>(next_table.VA, next_table); LastDumped = WriteRange(a_range.Key, a_range.Value, saveLoc, memAxs); DumpedToDisk.Add(LastDumped); cntDumped++; } Write($"All done, last written file {LastDumped} of {cntDumped} so far. KeepGoing? (y)"); var answer = ReadKey(); if (answer.Key == ConsoleKey.N) fKeepGoing = false; } } } }
public long FillTable(bool RedundantKernelSpaces, int depth = 4) { var entries = 0L; var PageTables = new Dictionary<VIRTUAL_ADDRESS, PFN>(); KernelSpace = RedundantKernelSpaces; // clear out the VA for the other indexes since were looking at the top level VIRTUAL_ADDRESS VA; VA.Address = 0; int level = 3; foreach (var kvp in DP.TopPageTablePage) { // Only extract user portion, kernel will be mostly redundant if(!RedundantKernelSpaces && kvp.Key >= 255) continue; // were at the top level (4th) VA.PML4 = kvp.Key; var pfn = new PFN { PTE = kvp.Value, VA = new VIRTUAL_ADDRESS(VA.PML4 << 39) }; // Top level for page table PageTables.Add(VA, pfn); // We will only do one level if were not buffering if(depth > 1) foreach(var DirectoryPointerOffset in ExtractNextLevel(pfn, KernelSpace, level)) { if (DirectoryPointerOffset == null) continue; if(depth > 2 /* && !DirectoryPointerOffset.PTE.LargePage */) foreach (var DirectoryOffset in ExtractNextLevel(DirectoryPointerOffset, KernelSpace, level-1)) { if (DirectoryOffset == null) continue; if(depth > 3 /* && !DirectoryOffset.PTE.LargePage && EnvLimits.MAX_PageTableEntriesToScan > entries */) foreach (var TableOffset in ExtractNextLevel(DirectoryOffset, KernelSpace, level - 2)) { if (TableOffset == null) continue; entries++; } entries++; } entries++; } entries++; } Root.Entries = new PFN() { SubTables = PageTables }; // InlineExtract may be faster but it's memory requirement is higher which was a problem // when analyzing 64GB+ dumps (yes InVtero.net handles very big memory)++ #if FALSE // descend the remaining levelsPageTableEntries // if we find nothing, we can be sure the value were using for EPTP or CR3 is bogus var new_entries = InlineExtract(Root); if (new_entries == 0) return 0; entries += new_entries; #endif EntriesParsed += entries; DepthParsed = depth; // a hint for the full count of entries extracted Root.Count = entries; return entries; }