Port kernel vmmap to aglib (#2634)

* Port kernel vmmap to aglib

* fix

* add comment

* add comment

* fix page

* fix page

* lint

* lint

* Update pwndbg/aglib/kernel/vmmap.py

* Update pwndbg/aglib/kernel/__init__.py

* Update pwndbg/aglib/kernel/vmmap.py

---------

Co-authored-by: Disconnect3d <dominik.b.czarnota@gmail.com>
pull/2670/head
patryk4815 12 months ago committed by GitHub
parent 880f986dbd
commit 9f1753f4d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -12,6 +12,7 @@ from typing import TypeVar
from typing_extensions import ParamSpec
import pwndbg.aglib.arch
import pwndbg.aglib.memory
import pwndbg.aglib.regs
import pwndbg.aglib.symbol
@ -649,6 +650,11 @@ def paging_enabled() -> bool:
return x86_64Ops.paging_enabled()
elif arch_name == "aarch64":
return Aarch64Ops.paging_enabled()
elif arch_name == "rv64":
# https://starfivetech.com/uploads/u74_core_complex_manual_21G1.pdf
# page 41, satp.MODE, bits: 60,61,62,63
# "When satp.MODE=0x0, supervisor virtual addresses are equal to supervisor physical addresses"
return int(pwndbg.aglib.regs.satp) & (BIT(60) | BIT(61) | BIT(62) | BIT(63)) != 0
else:
raise NotImplementedError()

@ -0,0 +1,257 @@
from __future__ import annotations
import os
import random
import string
import subprocess
import tempfile
from typing import List
from typing import Tuple
from pt.machine import Machine
from pt.pt import PageTableDump
from pt.pt_aarch64_parse import PT_Aarch64_Backend
from pt.pt_riscv64_parse import PT_RiscV64_Backend
from pt.pt_x86_64_parse import PT_x86_64_Backend
import pwndbg
import pwndbg.aglib.arch
import pwndbg.aglib.kernel
import pwndbg.aglib.qemu
import pwndbg.aglib.regs
import pwndbg.color.message as M
import pwndbg.lib.cache
import pwndbg.lib.memory
# 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
class QemuMachine(Machine):
def __init__(self):
super().__init__()
self.pid = QemuMachine.get_qemu_pid()
self.file = None
self.file = os.open(f"/proc/{self.pid}/mem", os.O_RDONLY)
def __del__(self):
if self.file:
os.close(self.file)
@staticmethod
def search_pids_for_file(pids: List[str], filename: str) -> str | None:
for pid in pids:
fd_dir = f"/proc/{pid}/fd"
try:
for fd in os.listdir(fd_dir):
if os.readlink(f"{fd_dir}/{fd}") == filename:
return pid
except FileNotFoundError:
# Either the process has gone or fds are changing, not our pid
pass
except PermissionError:
# Evade processes owned by other users
pass
return None
@staticmethod
def get_qemu_pid():
out = subprocess.check_output(["pgrep", "qemu-system"], encoding="utf8")
pids = out.strip().split("\n")
if len(pids) == 1:
return int(pids[0], 10)
# We add a chardev file backend (we dont add a fronted, so it doesn't affect
# the guest). We can then look through proc to find which process has the file
# open. This approach is agnostic to namespaces (pid, network and mount).
chardev_id = "gdb-pt-dump" + "-" + "".join(random.choices(string.ascii_letters, k=16))
with tempfile.NamedTemporaryFile() as tmpf:
pwndbg.dbg.selected_inferior().send_monitor(
f"chardev-add file,id={chardev_id},path={tmpf.name}"
)
pid_found = QemuMachine.search_pids_for_file(pids, tmpf.name)
pwndbg.dbg.selected_inferior().send_monitor(f"chardev-remove {chardev_id}")
if not pid_found:
raise Exception("Could not find qemu pid")
return int(pid_found, 10)
def read_physical_memory(self, physical_address: int, length: int) -> bytes:
res = pwndbg.dbg.selected_inferior().send_monitor(f"gpa2hva {hex(physical_address)}")
# It's not possible to pread large sizes, so let's break the request
# into a few smaller ones.
max_block_size = 1024 * 1024 * 256
try:
hva = int(res.split(" ")[-1], 16)
data = b""
for offset in range(0, length, max_block_size):
length_to_read = min(length - offset, max_block_size)
block = os.pread(self.file, length_to_read, hva + offset)
data += block
return data
except Exception as e:
msg = f"Physical address ({hex(physical_address)}, +{hex(length)}) is not accessible. Reason: {e}. gpa2hva result: {res}"
raise OSError(msg)
def read_register(self, register_name: str) -> int:
if register_name.startswith("$"):
register_name = register_name[1:]
return int(getattr(pwndbg.aglib.regs, register_name))
@pwndbg.lib.cache.cache_until("stop")
def kernel_vmmap_via_page_tables() -> Tuple[pwndbg.lib.memory.Page, ...]:
if not pwndbg.aglib.qemu.is_qemu_kernel():
return ()
try:
machine_backend = QemuMachine()
except PermissionError:
print(
M.error(
"Permission error when attempting to parse page tables with gdb-pt-dump.\n"
"Either change the kernel-vmmap setting, re-run GDB as root, or disable "
"`ptrace_scope` (`echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope`)"
)
)
return ()
arch = pwndbg.aglib.arch.current
if arch == "aarch64":
arch_backend = PT_Aarch64_Backend(machine_backend)
elif arch == "i386" in arch:
arch_backend = PT_x86_64_Backend(machine_backend)
elif arch == "x86-64" in arch:
arch_backend = PT_x86_64_Backend(machine_backend)
elif arch == "rv64":
arch_backend = PT_RiscV64_Backend(machine_backend)
else:
print(
M.error(
f"The {pwndbg.aglib.arch.name} architecture does"
" not support the `vmmap_via_page_tables`.\n"
"Run `help show kernel-vmmap` for other options."
)
)
return ()
# If paging is not enabled, we shouldn't attempt to parse page tables
if not pwndbg.aglib.kernel.paging_enabled():
return ()
p = PageTableDump(machine_backend, arch_backend)
pages = p.arch_backend.parse_tables(p.cache, p.parser.parse_args(""))
retpages: List[pwndbg.lib.memory.Page] = []
for page in pages:
start = page.va
size = page.page_size
flags = 4 # IMPLY ALWAYS READ
if page.pwndbg_is_writeable():
flags |= 2
if page.pwndbg_is_executable():
flags |= 1
objfile = f"[pt_{hex(start)[2:-3]}]"
retpages.append(pwndbg.lib.memory.Page(start, size, flags, 0, objfile))
return tuple(retpages)
monitor_info_mem_not_warned = True
@pwndbg.lib.cache.cache_until("stop")
def kernel_vmmap_via_monitor_info_mem() -> Tuple[pwndbg.lib.memory.Page, ...]:
"""
Returns Linux memory maps information by parsing `monitor info mem` output
from QEMU kernel GDB stub.
Works only on X86/X64/RISC-V as this is what QEMU supports.
Consider using the `kernel_vmmap_via_page_tables` method
as it is probably more reliable/better.
See also: https://github.com/pwndbg/pwndbg/pull/685
(TODO: revisit with future QEMU versions)
# Example output from the command:
# pwndbg> monitor info mem
# ffff903580000000-ffff903580099000 0000000000099000 -rw
# ffff903580099000-ffff90358009b000 0000000000002000 -r-
# ffff90358009b000-ffff903582200000 0000000002165000 -rw
# ffff903582200000-ffff903582803000 0000000000603000 -r-
"""
if not pwndbg.aglib.qemu.is_qemu_kernel():
return ()
try:
monitor_info_mem = pwndbg.dbg.selected_inferior().send_monitor("info mem")
except pwndbg.dbg_mod.Error:
# Exception should not happen in new qemu, can we clean up it?
monitor_info_mem = None
is_error = monitor_info_mem is None or "unknown command" in monitor_info_mem
if is_error:
# Older versions of QEMU/GDB may throw `gdb.error: "monitor" command
# not supported by this target`. Newer versions will not throw, but will
# return a string starting with 'unknown command:'. We handle both of
# these cases in a `finally` block instead of an `except` block.
# TODO: Find out which other architectures don't support this command
if pwndbg.aglib.arch.name == "aarch64":
print(
M.error(
f"The {pwndbg.aglib.arch.name} architecture does"
" not support the `monitor info mem` command.\n"
"Run `help show kernel-vmmap` for other options."
)
)
return ()
lines = monitor_info_mem.splitlines()
# Handle disabled PG
# This will prevent a crash on abstract architectures
if len(lines) == 1 and lines[0] == "PG disabled":
return ()
global monitor_info_mem_not_warned
pages: List[pwndbg.lib.memory.Page] = []
for line in lines:
dash_idx = line.index("-")
space_idx = line.index(" ")
rspace_idx = line.rindex(" ")
start = int(line[:dash_idx], 16)
end = int(line[dash_idx + 1 : space_idx], 16)
size = int(line[space_idx + 1 : rspace_idx], 16)
if end - start != size and monitor_info_mem_not_warned:
print(
M.warn(
(
"The vmmap output may be incorrect as `monitor info mem` output assertion/assumption\n"
"that end-start==size failed. The values are:\n"
"end=%#x; start=%#x; size=%#x; end-start=%#x\n"
"Note that this warning will not show up again in this Pwndbg/GDB session."
)
% (end, start, size, end - start)
)
)
monitor_info_mem_not_warned = False
perm = line[rspace_idx + 1 :]
flags = 0
if "r" in perm:
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
pages.append(pwndbg.lib.memory.Page(start, size, flags, 0, "<qemu>"))
return tuple(pages)

@ -20,9 +20,6 @@ if pwndbg.dbg.is_gdblib_available():
@pwndbg.lib.cache.cache_until("start", "stop")
def get() -> Tuple[pwndbg.lib.memory.Page, ...]:
if pwndbg.dbg.is_gdblib_available():
return pwndbg.gdblib.vmmap.get()
return tuple(pwndbg.dbg.selected_inferior().vmmap().ranges())

@ -1087,8 +1087,14 @@ class LLDBProcess(pwndbg.dbg_mod.Process):
if not self._is_gdb_remote:
raise RuntimeError("Called send_monitor() on a local process")
# Same as `send_remote()`.
return self.dbg._execute_lldb_command(f"process plugin packet monitor {cmd}")
# `process plugin packet monitor {cmd}` command is returned in an ugly way, eg:
# "Host virtual address for 0x1000 (virt.flash0) is 0xe2780fc01000\r\n packet: qRcmd,6770613268766120307831303030\nresponse: OK\n"
# We need to cut the string, so it matches the same format we have in GDB.
res = self.dbg._execute_lldb_command(f"process plugin packet monitor {cmd}")
if (idx := res.rindex(" packet: ")) > -1:
return res[:idx]
return res
@override
def download_remote_file(self, remote_path: str, local_path: str) -> None:
@ -1129,7 +1135,7 @@ class LLDBProcess(pwndbg.dbg_mod.Process):
if not remote.IsValid():
raise pwndbg.dbg_mod.Error(f"LLDB considers the path '{remote_path}' invalid")
if local.IsValid():
if not local.IsValid():
raise pwndbg.dbg_mod.Error(f"LLDB considers the path '{local_path} invalid'")
error = platform.Get(remote, local)

@ -32,6 +32,8 @@ import pwndbg.gdblib.info
import pwndbg.lib.cache
import pwndbg.lib.config
import pwndbg.lib.memory
from pwndbg.aglib.kernel.vmmap import kernel_vmmap_via_monitor_info_mem
from pwndbg.aglib.kernel.vmmap import kernel_vmmap_via_page_tables
# List of manually-explored pages which were discovered
# by analyzing the stack or register context.
@ -582,132 +584,6 @@ def proc_tid_maps() -> Tuple[pwndbg.lib.memory.Page, ...] | None:
return tuple(pages)
@pwndbg.lib.cache.cache_until("stop")
def kernel_vmmap_via_page_tables() -> Tuple[pwndbg.lib.memory.Page, ...]:
import pt_gdb as pt
retpages: List[pwndbg.lib.memory.Page] = []
p = pt.PageTableDumpGdbFrontend()
try:
p.lazy_init()
except Exception:
print(
M.error(
"Permission error when attempting to parse page tables with gdb-pt-dump.\n"
"Either change the kernel-vmmap setting, re-run GDB as root, or disable "
"`ptrace_scope` (`echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope`)"
)
)
return tuple(retpages)
# If paging is not enabled, we shouldn't attempt to parse page tables
if not pwndbg.aglib.kernel.paging_enabled():
return tuple(retpages)
pages = p.pt.arch_backend.parse_tables(p.pt.cache, p.pt.parser.parse_args(""))
for page in pages:
start = page.va
size = page.page_size
flags = 4 # IMPLY ALWAYS READ
if page.pwndbg_is_writeable():
flags |= 2
if page.pwndbg_is_executable():
flags |= 1
objfile = f"[pt_{hex(start)[2:-3]}]"
retpages.append(pwndbg.lib.memory.Page(start, size, flags, 0, objfile))
return tuple(retpages)
monitor_info_mem_not_warned = True
def kernel_vmmap_via_monitor_info_mem() -> Tuple[pwndbg.lib.memory.Page, ...]:
"""
Returns Linux memory maps information by parsing `monitor info mem` output
from QEMU kernel GDB stub.
Works only on X86/X64/RISC-V as this is what QEMU supports.
Consider using the `kernel_vmmap_via_page_tables` method
as it is probably more reliable/better.
See also: https://github.com/pwndbg/pwndbg/pull/685
(TODO: revisit with future QEMU versions)
# Example output from the command:
# pwndbg> monitor info mem
# ffff903580000000-ffff903580099000 0000000000099000 -rw
# ffff903580099000-ffff90358009b000 0000000000002000 -r-
# ffff90358009b000-ffff903582200000 0000000002165000 -rw
# ffff903582200000-ffff903582803000 0000000000603000 -r-
"""
global monitor_info_mem_not_warned
monitor_info_mem = None
try:
monitor_info_mem = gdb.execute("monitor info mem", to_string=True)
finally:
# Older versions of QEMU/GDB may throw `gdb.error: "monitor" command
# not supported by this target`. Newer versions will not throw, but will
# return a string starting with 'unknown command:'. We handle both of
# these cases in a `finally` block instead of an `except` block.
if monitor_info_mem is None or "unknown command" in monitor_info_mem:
# TODO: Find out which other architectures don't support this command
if pwndbg.aglib.arch.name == "aarch64":
print(
M.error(
f"The {pwndbg.aglib.arch.name} architecture does"
" not support the `monitor info mem` command. Run "
"`help show kernel-vmmap` for other options."
)
)
return () # pylint: disable=lost-exception
lines = monitor_info_mem.splitlines()
# Handle disabled PG
# This will prevent a crash on abstract architectures
if len(lines) == 1 and lines[0] == "PG disabled":
return ()
pages: List[pwndbg.lib.memory.Page] = []
for line in lines:
dash_idx = line.index("-")
space_idx = line.index(" ")
rspace_idx = line.rindex(" ")
start = int(line[:dash_idx], 16)
end = int(line[dash_idx + 1 : space_idx], 16)
size = int(line[space_idx + 1 : rspace_idx], 16)
if end - start != size and monitor_info_mem_not_warned:
print(
M.warn(
(
"The vmmap output may be incorrect as `monitor info mem` output assertion/assumption\n"
"that end-start==size failed. The values are:\n"
"end=%#x; start=%#x; size=%#x; end-start=%#x\n"
"Note that this warning will not show up again in this Pwndbg/GDB session."
)
% (end, start, size, end - start)
)
)
monitor_info_mem_not_warned = False
perm = line[rspace_idx + 1 :]
flags = 0
if "r" in perm:
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
pages.append(pwndbg.lib.memory.Page(start, size, flags, 0, "<qemu>"))
return tuple(pages)
@pwndbg.lib.cache.cache_until("stop")
def info_sharedlibrary() -> Tuple[pwndbg.lib.memory.Page, ...]:
"""

@ -116,7 +116,7 @@ module = [
"r2pipe",
"rzpipe",
"rich.*",
"pt_gdb",
"pt.*",
"lldb.*",
"gnureadline",
]

Loading…
Cancel
Save