`vmmap` improvment/fix (#3086)

* page walks

* Improving kernel-vmmap

* added test for kernel vmmap

* improve userland heap handling

* improve kernelland sections handling

* fixed typo

* fixed test

* adding support for info mem

* changed array to tuple based on suggestions

* removing esp fixup stacks from display

* including call stacks

* implemented pagewalk

* added pagewalk test

* improved testing / output

* added docs

* renaming

* actually adding the remamed file

* adding decoration for cpu arch

* Revert "adding decoration for cpu arch"

This reverts commit 84aa120f68.

* added arch check for pagewalk

* adding req on symbols

* supporting mem info

* refactored pagewalk helpers

* added support for older versions of qemu-x86_64

* improved pagewalk helper function signature

* improved processing of vmmap

* refactored a bit more

* refactored a bit more

* removed changes to kernel/vmmap

* adding option to not process pages

* improving support to info mem

* changed to tuple

* changed to tuple

* changed to tuple

* added aarch64 mem mapping

* improved testing

* fixing for arm64

* a bit more amending

* fixing test

* fixed edge cases
pull/3089/head
jxuanli 6 months ago committed by GitHub
parent c19327a720
commit 0f97f0f762
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -202,33 +202,7 @@ def is_kaslr_enabled() -> bool:
@pwndbg.lib.cache.cache_until("start")
def kbase() -> int | None:
arch_name = pwndbg.aglib.arch.name
address = 0
if arch_name == "x86-64":
address = get_idt_entries()[0].offset
elif arch_name == "aarch64":
address = pwndbg.aglib.regs.vbar
else:
return None
mappings = pwndbg.aglib.vmmap.get()
for mapping in mappings:
# TODO: Check alignment
# only search in kernel mappings:
# https://www.kernel.org/doc/html/v5.3/arm64/memory.html
if mapping.vaddr & (0xFFFF << 48) == 0:
continue
if not mapping.execute:
continue
if address in mapping:
return mapping.vaddr
return None
return pwndbg.aglib.kernel.paging.kbase()
def get_idt_entries() -> List[pwndbg.lib.kernel.structs.IDTEntry]:

@ -15,7 +15,7 @@ ENTRYMASK = ~((1 << 12) - 1) & ((1 << 51) - 1)
@pwndbg.lib.cache.cache_until("start", "stop")
def get_memory_map_raw() -> Tuple[pwndbg.lib.memory.Page, ...]:
return pwndbg.aglib.kernel.vmmap.kernel_vmmap()
return pwndbg.aglib.kernel.vmmap.kernel_vmmap(False)
def find_kbase(pages) -> int | None:
@ -32,7 +32,7 @@ def find_kbase(pages) -> int | None:
mappings = pages
for mapping in mappings:
# TODO: Check alignment
# should be page aligned -- either from pt-dump or info mem
# only search in kernel mappings:
# https://www.kernel.org/doc/html/v5.3/arm64/memory.html
@ -68,17 +68,21 @@ guess_physmap = config.add_param(
def physmap_base() -> int:
if pwndbg.aglib.kernel.has_debug_syms():
result = pwndbg.aglib.symbol.lookup_symbol_value("page_offset_base")
if result is not None:
return result
if guess_physmap:
result = guess_physmap_base()
if pwndbg.aglib.kernel.has_debug_syms() and pwndbg.aglib.arch.name == "x86-64":
result = pwndbg.aglib.symbol.lookup_symbol_addr("page_offset_base")
if pwndbg.aglib.memory.peek(result):
result = pwndbg.aglib.memory.u64(result)
else:
return None
if result is not None:
return result
print(M.warn("physmap base cannot be guessed, resort to default"))
else:
print(M.warn("guess-physmap is set to false, not guessing physmap address"))
if guess_physmap or pwndbg.aglib.arch.name == "aarch64":
# this is mostly true
# https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt
for page in get_memory_map_raw():
if page.start & (1 << 63) > 0:
return page.start
print(M.warn("physmap base cannot be determined, resort to default"))
if uses_5lvl_paging():
return 0xFF11000000000000
return 0xFFFF888000000000
@ -122,12 +126,3 @@ def pagewalk(target, entry=None) -> List[Tuple[int | None, int | None]]:
result[i] = (entry, vaddr)
result[0] = (None, (entry & ENTRYMASK) + base + offset)
return result
def guess_physmap_base() -> int | None:
# this is mostly true
# https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt
for page in get_memory_map_raw():
if page.start & (1 << 63) > 0:
return page.start
return None

@ -18,13 +18,165 @@ from pt.pt_x86_64_parse import PT_x86_64_Backend
import pwndbg
import pwndbg.aglib.arch
import pwndbg.aglib.kernel
import pwndbg.aglib.kernel.paging
import pwndbg.aglib.qemu
import pwndbg.aglib.regs
import pwndbg.aglib.vmmap
import pwndbg.color.message as M
import pwndbg.lib.cache
import pwndbg.lib.memory
class KernelVmmap:
USERLAND = "userland"
KERNELLAND = "kernel [.text]"
KERNELRO = "kernel [.rodata]"
KERNELBSS = "kernel [.bss]"
KERNELDRIVER = "kernel [.driver .bpf]"
ESPSTACK = "%esp fixup"
def __init__(self, pages: Tuple[pwndbg.lib.memory.Page, ...]):
self.pages = pages
self.sections = None
if not pwndbg.aglib.kernel.has_debug_syms():
return
self.kbase = kbase = pwndbg.aglib.kernel.paging.find_kbase(pages)
if pwndbg.aglib.arch.name == "x86-64":
# https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt
# works for v5.x and v6.x
physmap = pwndbg.aglib.kernel.paging.physmap_base()
if physmap is None: # what??
return
vmalloc = pwndbg.aglib.symbol.lookup_symbol_value("vmalloc_base")
vmemmap = pwndbg.aglib.symbol.lookup_symbol_value("vmemmap_base")
self.sections = (
(self.USERLAND, 0),
(None, 0x8000000000000000),
("physmap", physmap),
("vmalloc", vmalloc),
("vmemmap", vmemmap),
# TODO: find better ways to handle the following constants
# I cound not find kernel symbols that reference their values
# the actual region base may differ but the region always falls within the below range
# even if KASLR is enabled
("cpu entry", 0xFFFFFE0000000000),
("rand cpu entry", 0xFFFFFE0000001000),
(self.ESPSTACK, 0xFFFFFF0000000000),
("EFI", 0xFFFFFFEF00000000),
(self.KERNELLAND, kbase),
("fixmap", 0xFFFFFFFFFF000000),
("legacy abi", 0xFFFFFFFFFF600000),
(None, 0xFFFFFFFFFFFFFFFF),
)
if pwndbg.aglib.arch.name == "aarch64":
# https://www.kernel.org/doc/html/v5.3/arm64/memory.html
# https://elixir.bootlin.com/linux/v6.15/source/arch/arm64/mm/ptdump.c#L351
# TODO: I don't think those are necessarily accurate when KASLR is enabled
# but I'm not familiar with ARM enough quite yet to find better ways
address_markers = pwndbg.aglib.symbol.lookup_symbol_addr("address_markers")
if address_markers is None:
return
sections = [(self.USERLAND, 0)]
value = 0
name = None
for i in range(20):
value = pwndbg.aglib.memory.u64(address_markers + i * 0x10)
if value > 0:
name_ptr = pwndbg.aglib.memory.u64(address_markers + i * 0x10 + 8)
name = None
if name_ptr > 0:
name = pwndbg.aglib.memory.string(name_ptr).decode()
name = name.split(" ")[0].lower()
if "end" in name:
name = None
if name == "linear":
name = "physmap"
sections.append((name, value))
if value == 0xFFFFFFFFFFFFFFFF:
break
self.sections = tuple(sections)
def get_name(self, addr: int) -> str:
if addr is None or self.sections is None:
return None
for i in range(len(self.sections) - 1):
name, cur = self.sections[i]
_, next = self.sections[i + 1]
if cur is None or next is None:
continue
if addr >= cur and addr < next:
return name
return None
def adjust(self):
for i, page in enumerate(self.pages):
name = self.get_name(page.start)
if name is not None:
page.objfile = name
user_idx, kernel_idx = None, None
for i, page in enumerate(self.pages):
if user_idx is None and page.objfile == self.USERLAND:
user_idx = i
if kernel_idx is None and page.objfile == self.KERNELLAND:
kernel_idx = i
self.handle_user_pages(user_idx)
self.handle_kernel_pages(kernel_idx)
self.handle_offsets()
def handle_user_pages(self, user_idx):
if user_idx is None:
return
base_offset = self.pages[user_idx].start
for i in range(user_idx, len(self.pages)):
page = self.pages[i]
if page.objfile != self.USERLAND:
break
diff = page.start - base_offset
if diff > 0x100000:
if diff > 0x100000000000:
if page.execute:
page.objfile = "userland [library]"
elif page.rw:
page.objfile = "userland [stack]"
else:
page.objfile = "userland [heap]"
else:
# page.objfile += f"_{hex(i)[2:]}"
base_offset = page.start
def handle_kernel_pages(self, kernel_idx):
if kernel_idx is None:
return
has_loadable_driver = False
for i in range(kernel_idx, len(self.pages)):
page = self.pages[i]
if page.objfile != self.KERNELLAND:
break
if not page.execute:
if page.write:
page.objfile = self.KERNELBSS
else:
page.objfile = self.KERNELRO
if has_loadable_driver:
page.objfile = self.KERNELDRIVER
if page.execute and page.start != self.kbase:
page.objfile = self.KERNELDRIVER
has_loadable_driver = True
if pwndbg.aglib.regs[pwndbg.aglib.regs.stack] in page:
page.objfile = "kernel [stack]"
def handle_offsets(self):
prev_objfile, base = "", 0
for page in self.pages:
# the check on KERNELRO is to make getting offsets for symbols such as `init_creds` more convinient
if page.objfile != self.KERNELRO and prev_objfile != page.objfile:
prev_objfile = page.objfile
base = page.start
page.offset = page.start - base
if len(hex(page.offset)) > 9:
page.offset = 0
# Most of QemuMachine code was inherited from gdb-pt-dump thanks to Martin Radev (@martinradev)
# on the MIT license, see:
# https://github.com/martinradev/gdb-pt-dump/blob/21158ac3f9b36d0e5e0c86193e0ef018fc628e74/pt_gdb/pt_gdb.py#L11-L80
@ -177,7 +329,6 @@ def kernel_vmmap_via_page_tables() -> Tuple[pwndbg.lib.memory.Page, ...]:
flags |= 1
objfile = f"[pt_{hex(start)[2:-3]}]"
retpages.append(pwndbg.lib.memory.Page(start, size, flags, 0, objfile))
return tuple(retpages)
@ -267,10 +418,9 @@ def kernel_vmmap_via_monitor_info_mem() -> Tuple[pwndbg.lib.memory.Page, ...]:
flags |= 4
if "w" in perm:
flags |= 2
# QEMU does not expose X/NX bit, see #685
# if 'x' in perm: flags |= 1
flags |= 1
if len(perm) == 4: # if the qemu version displays if the page is executable
if "x" in perm:
flags |= 1
pages.append(pwndbg.lib.memory.Page(start, size, flags, 0, "<qemu>"))
return tuple(pages)
@ -294,7 +444,7 @@ Note that the page-tables method will require the QEMU kernel process to be on t
)
def kernel_vmmap() -> Tuple[pwndbg.lib.memory.Page, ...]:
def kernel_vmmap(process_pages=True) -> Tuple[pwndbg.lib.memory.Page, ...]:
if not pwndbg.aglib.qemu.is_qemu_kernel():
return ()
@ -307,9 +457,22 @@ def kernel_vmmap() -> Tuple[pwndbg.lib.memory.Page, ...]:
):
return ()
pages = None
if kernel_vmmap_mode == "page-tables":
return kernel_vmmap_via_page_tables()
pages = kernel_vmmap_via_page_tables()
elif kernel_vmmap_mode == "monitor":
return kernel_vmmap_via_monitor_info_mem()
pages = kernel_vmmap_via_monitor_info_mem()
if process_pages and pwndbg.aglib.arch.name == "x86-64":
# TODO: check version here when QEMU displays the x bit for x64
for page in pages:
pgwalk_res = pwndbg.aglib.kernel.paging.pagewalk(page.start)
entry, vaddr = pgwalk_res[0]
if entry and entry >> 63 == 0:
page.flags |= 1
if pages is None:
return ()
if process_pages:
kv = KernelVmmap(pages)
kv.adjust()
return ()
return tuple(pages)

@ -88,10 +88,12 @@ def get(
color = normal
elif "[stack" in page.objfile:
color = c.stack
elif "[heap" in page.objfile:
color = c.heap
elif page.execute:
color = c.code
elif not page.write:
color = c.rodata
elif any(keyword in page.objfile for keyword in ("[heap", "physmap", "vmalloc")):
color = c.heap
elif page.rw:
color = c.data
elif page.is_guard:

@ -14,6 +14,7 @@ from elftools.elf.elffile import ELFFile
import pwndbg.aglib.arch
import pwndbg.aglib.elf
import pwndbg.aglib.file
import pwndbg.aglib.qemu
import pwndbg.aglib.vmmap
import pwndbg.aglib.vmmap_custom
import pwndbg.color.memory as M
@ -49,7 +50,7 @@ def print_vmmap_table_header() -> None:
prefer_relpaths = "on" if pwndbg.config.vmmap_prefer_relpaths else "off"
width = 2 + 2 * pwndbg.aglib.arch.ptrsize
print(
f"{'Start':>{width}} {'End':>{width}} {'Perm'} {'Size':>8} {'Offset':>6} "
f"{'Start':>{width}} {'End':>{width}} {'Perm'} {'Size':>8} {'Offset':>7} "
f"{'File'} (set vmmap-prefer-relpaths {prefer_relpaths})"
)

@ -148,7 +148,7 @@ class Page:
else:
objfile = self.objfile
width = 2 + 2 * pwndbg.aglib.arch.ptrsize
return f"{self.vaddr:#{width}x} {self.vaddr + self.memsz:#{width}x} {self.permstr} {self.memsz:8x} {self.offset:6x} {objfile or ''}"
return f"{self.vaddr:#{width}x} {self.vaddr + self.memsz:#{width}x} {self.permstr} {self.memsz:8x} {self.offset:7x} {objfile or ''}"
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.__str__()!r})"

@ -129,6 +129,32 @@ def test_command_msr_write():
gdb.execute(f"msr MSR_LSTAR -w {prev_msr_lstar}")
@pytest.mark.skipif(not pwndbg.aglib.kernel.has_debug_syms(), reason="test requires debug symbols")
def test_command_kernel_vmmap():
res = gdb.execute("vmmap", to_string=True)
assert all(
key in res
for key in (
"vmalloc",
"fixmap",
"physmap",
"vmemmap",
)
)
if pwndbg.aglib.arch.name == "x86-64":
assert any(
key in res
# this needs to be `any` because kernel is not fully initialized
# when the test is run (qemu-system takes >3 seconds to fully setup for linux)
for key in (
"kernel [.text]",
"kernel [.rodata]",
"kernel [.bss]",
"kernel [stack]",
)
)
@pytest.mark.skipif(not pwndbg.aglib.kernel.has_debug_syms(), reason="test requires debug symbols")
@pytest.mark.skipif(
pwndbg.aglib.arch.name not in ["i386", "x86-64"],

Loading…
Cancel
Save