Adding an extra x64 register set for qemu-system kernel pwning (#2932)

* added/modified registers for kernel pwning

* added a RegisterContext class for more complex register context handling

* cleaned up register context selection and flag bits

* further cleaned up register context selection

* fixing None deref issue

* handling NoneType registers

* linting

* removed most of the extra register classes

* fully removed extra register classes in commands/context.py

* renamed var so that the linter doesn't confuse the var name with dataclass type name

* some comments on newly added classes

* fixed issues based on suggestions

* fixed issues when debug symbols are not present in x64 kernel

* Apply suggestions from code review

Co-authored-by: OBarronCS <55004530+OBarronCS@users.noreply.github.com>

* reduced performance overhead & added some examples for arm biflags & error handling

---------

Co-authored-by: OBarronCS <55004530+OBarronCS@users.noreply.github.com>
pull/2966/head
jxuanli 7 months ago committed by GitHub
parent 205b0fd791
commit 7e41119045
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -28,6 +28,7 @@ import pwndbg.aglib.typeinfo
import pwndbg.lib.cache
from pwndbg.dbg import EventType
from pwndbg.lib.regs import BitFlags
from pwndbg.lib.regs import KernelRegisterSet
from pwndbg.lib.regs import RegisterSet
from pwndbg.lib.regs import reg_sets
@ -167,6 +168,10 @@ class module(ModuleType):
def retaddr(self) -> Tuple[str, ...]:
return reg_sets[pwndbg.aglib.arch.name].retaddr
@property
def kernel(self) -> KernelRegisterSet:
return reg_sets[pwndbg.aglib.arch.name].kernel
@property
def flags(self) -> Dict[str, BitFlags]:
return reg_sets[pwndbg.aglib.arch.name].flags

@ -23,6 +23,7 @@ import pwndbg
import pwndbg.aglib.arch
import pwndbg.aglib.disasm.disassembly
import pwndbg.aglib.nearpc
import pwndbg.aglib.qemu
import pwndbg.aglib.regs
import pwndbg.aglib.symbol
import pwndbg.arguments
@ -895,8 +896,86 @@ pwndbg.config.add_param("show-flags", False, "whether to show flags registers")
pwndbg.config.add_param("show-retaddr-reg", True, "whether to show return address register")
class RegisterContext:
changed: List[str]
def __init__(self):
self.changed = pwndbg.aglib.regs.changed
def get_prefix(self, reg):
# Make the register stand out and give a color if changed
regname = C.register(reg.ljust(4).upper())
if reg in self.changed:
regname = C.register_changed(regname)
# Show a marker next to the register if it changed
change_marker = f"{C.config_register_changed_marker}"
m = (
" " * len(change_marker)
if reg not in self.changed
else C.register_changed(change_marker)
)
return f"{m}{regname}"
def get_register_value(self, reg):
val = pwndbg.aglib.regs[reg]
if val is None:
print(message.warn(f"Unknown register: {reg!r}"))
return None
return val
def flag_register_context(self, reg, bit_flags):
val = self.get_register_value(reg)
if val is None:
return None
desc = C.format_flags(val, bit_flags, pwndbg.aglib.regs.last.get(reg, 0))
prefix = self.get_prefix(reg)
return f"{prefix} {desc}"
def segment_registers_context(self, regs):
result = ""
for reg in regs:
val = self.get_register_value(reg)
if val is None:
continue
prefix = self.get_prefix(reg)
result += f"{prefix} {hex(val)} "
return result
def addressing_register_context(self, reg, is_virtual):
if is_virtual:
return self.register_context_default(reg)
val = self.get_register_value(reg)
if val is None:
return None
prefix = self.get_prefix(reg)
desc = hex(val)
if pwndbg.aglib.kernel.has_debug_syms():
# TODO: phys_to_virt is bugged when kaslr is enabled, ptrace_scope is enabled, or if symbols are not present
try:
virtual = pwndbg.aglib.kernel.phys_to_virt(val)
desc += f" [virtual: {pwndbg.chain.format(virtual)}]"
except Exception:
print(
message.error(
"error when running phys_to_virt, try running `echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope`"
)
)
return f"{prefix} {desc}"
def register_context_default(self, reg):
val = self.get_register_value(reg)
if val is None:
return None
desc = pwndbg.chain.format(val)
prefix = self.get_prefix(reg)
return f"{prefix} {desc}"
def get_regs(regs: List[str] = None):
regs: List[Any] = regs
result = []
rc = RegisterContext()
if regs is None:
regs = []
@ -912,42 +991,39 @@ def get_regs(regs: List[str] = None):
regs.append(pwndbg.aglib.regs.current.pc)
if pwndbg.aglib.qemu.is_qemu_kernel() and pwndbg.aglib.regs.kernel is not None:
controls = pwndbg.aglib.regs.kernel.controls
if controls is not None:
for regname, control in controls.items():
control.update(regname)
regs.append(control)
msrs = pwndbg.aglib.regs.kernel.msrs
if msrs is not None:
for regname, msr in msrs.items():
msr.update(regname)
regs.append(msr)
if pwndbg.config.show_flags:
regs += pwndbg.aglib.regs.flags.keys()
changed = pwndbg.aglib.regs.changed
flags = pwndbg.aglib.regs.flags
if flags is not None:
for regname, flag in flags.items():
flag.update(regname)
regs.append(flag)
if pwndbg.aglib.qemu.is_qemu_kernel() and pwndbg.aglib.regs.kernel is not None:
if pwndbg.aglib.regs.kernel.segments is not None:
regs.append(pwndbg.aglib.regs.kernel.segments)
for reg in regs:
if reg is None:
continue
if not isinstance(reg, str):
desc = reg.context(rc)
if desc is not None:
result.append(desc)
continue
desc = rc.register_context_default(reg)
if desc is not None:
result.append(desc)
value = pwndbg.aglib.regs[reg]
if value is None:
print(message.warn(f"Unknown register: {reg!r}"))
continue
# Make the register stand out and give a color if changed
regname = C.register(reg.ljust(4).upper())
if reg in changed:
regname = C.register_changed(regname)
# Show a dot next to the register if it changed
change_marker = f"{C.config_register_changed_marker}"
m = " " * len(change_marker) if reg not in changed else C.register_changed(change_marker)
bit_flags = None
if reg in pwndbg.aglib.regs.flags:
bit_flags = pwndbg.aglib.regs.flags[reg]
elif reg in pwndbg.aglib.regs.extra_flags:
bit_flags = pwndbg.aglib.regs.extra_flags[reg]
if bit_flags:
desc = C.format_flags(value, bit_flags, pwndbg.aglib.regs.last.get(reg, 0))
else:
desc = pwndbg.chain.format(value)
result.append(f"{m}{regname} {desc}")
return result

@ -19,7 +19,110 @@ from typing import Union
import pwndbg.lib.disasm.helpers as bit_math
from pwndbg.lib.arch import PWNDBG_SUPPORTED_ARCHITECTURES_TYPE
BitFlags = OrderedDict[str, Union[int, Tuple[int, int]]]
class BitFlags:
# this is intentionally uninitialized -- arm uses the same self.flags structuture for different registers
# for example
# - aarch64_cpsr_flags is used for "cpsr", "spsr_el1", "spsr_el2", "spsr_el3"
# - aarch64_sctlr_flags is used for "sctlr", "sctlr_el2", "sctlr_el3"
regname: str
flags: OrderedDict[str, Union[int, Tuple[int, int]]]
def __init__(self, flags: List[Tuple[str, Union[int, Tuple[int, int]]]] = []):
self.regname = ""
self.flags = {}
for name, bits in flags:
self.flags[name] = bits
def __getattr__(self, name):
if name in {"regname"}:
return self.__dict__[name]
return getattr(self.flags, name)
def __getitem__(self, key):
return self.flags[key]
def __setitem__(self, key, value):
self.flags[key] = value
def __delitem__(self, key):
del self.flags[key]
def __iter__(self):
return iter(self.flags)
def __len__(self):
return len(self.flags)
def __repr__(self):
return f"BitFlags({self.flags})"
def update(self, regname: str):
self.regname = regname
def context(self, rc):
return rc.flag_register_context(self.regname, self)
class AddressingRegister:
"""
Represents a register that is used to store an address, e.g. cr3, gsbase, fsbase
"""
reg: str
value: int
is_virtual: bool
def __init__(self, reg: str, is_virtual: bool):
self.reg = reg
self.value = 0
self.is_virtual = is_virtual
def update(self, regname: str):
pass
def context(self, rc):
return rc.addressing_register_context(self.reg, self.is_virtual)
class SegmentRegisters:
"""
Represents the x86 segment register set
"""
regs: List[str]
def __init__(self, regs: List[str]):
self.regs = regs
def context(self, rc):
return rc.segment_registers_context(self.regs)
class KernelRegisterSet:
"""
additional registers that are useful when pwning kernels
used only for x86-64 for now
"""
# Segment registers (CS, DS, ES, FS, GS, SS)
segments: SegmentRegisters
# Control registers (cr0, cr3, cr4)
controls: Dict[str, BitFlags | AddressingRegister]
# Model specific registers
msrs: Dict[str, BitFlags | AddressingRegister]
def __init__(
self,
segments: SegmentRegisters | None,
controls: Dict[str, BitFlags | AddressingRegister] = {},
msrs: Dict[str, BitFlags | AddressingRegister] = {},
):
self.segments = segments
self.controls = controls
self.msrs = msrs
@dataclass
@ -75,6 +178,9 @@ class RegisterSet:
#: Common registers which should be displayed in the register context
common: List[str] = []
#: Extra registers for kernel debugging
kernel: KernelRegisterSet | None
#: All valid registers
all: Set[str]
@ -95,6 +201,7 @@ class RegisterSet:
gpr: Tuple[Reg, ...] = (),
misc: Tuple[str, ...] = (),
args: Tuple[str, ...] = (),
kernel: KernelRegisterSet | None = None,
retval: str | None = None,
) -> None:
self.pc = pc.name
@ -107,6 +214,7 @@ class RegisterSet:
self.misc = misc
self.args = args
self.retval = retval
self.kernel = kernel
all_subregisters: List[str] = []
@ -129,6 +237,14 @@ class RegisterSet:
if regname and regname not in self.common:
self.common.append(regname)
if self.kernel is not None:
controls = self.kernel.controls
segments = self.kernel.segments
msrs = self.kernel.msrs
for regname in itertools.chain(controls, segments.regs, msrs):
if regname and regname not in self.common:
self.common.append(regname)
# The specific order of this list is very important:
# Due to the behavior of Arm in the Unicorn engine,
# we must write the flags register after PC, and the stack pointer after the flags register.
@ -522,10 +638,45 @@ aarch64 = RegisterSet(
x86flags = {
"eflags": BitFlags(
[("CF", 0), ("PF", 2), ("AF", 4), ("ZF", 6), ("SF", 7), ("IF", 9), ("DF", 10), ("OF", 11)]
[
("CF", 0),
("PF", 2),
("AF", 4),
("ZF", 6),
("SF", 7),
("IF", 9),
("DF", 10),
("OF", 11),
("AC", 18),
]
)
}
amd64_kernel = KernelRegisterSet(
segments=SegmentRegisters(["cs", "ss", "ds", "es", "fs", "gs"]),
controls={
# only displays the security related bits, otherwise it can be too clustered
"cr0": BitFlags([("PE", 0), ("WP", 16), ("PG", 31)]),
"cr3": AddressingRegister("cr3", False),
"cr4": BitFlags(
[
("UMIP", 11),
("FSGSBASE", 16),
("SMEP", 20),
("SMAP", 21),
("PKE", 22),
("CET", 23),
("PKS", 24),
]
),
},
msrs={
"efer": BitFlags([("NXE", 11)]),
"gs_base": AddressingRegister("gs_base", True),
"fs_base": AddressingRegister("fs_base", True),
},
)
amd64 = RegisterSet(
pc=Reg("rip"),
stack=Reg(
@ -678,10 +829,11 @@ amd64 = RegisterSet(
"es",
"fs",
"gs",
"fsbase",
"gsbase",
"fs_base",
"gs_base",
"ip",
),
kernel=amd64_kernel,
args=("rdi", "rsi", "rdx", "rcx", "r8", "r9"),
retval="rax",
)
@ -730,8 +882,8 @@ i386 = RegisterSet(
"es",
"fs",
"gs",
"fsbase",
"gsbase",
"fs_base",
"gs_base",
"ip",
),
retval="eax",

@ -73,6 +73,19 @@ def test_command_slab_contains():
assert f"{addr} @ {slab_cache}" in res
def test_x64_extra_registers_under_kernel_mode():
res = gdb.execute("context", to_string=True)
if "RAX" not in res or "RSP" not in res:
# we are not debugging x64
# there's probably a better way to check this but good enough
return
for reg in ["cr0", "cr3", "cr4", "fs_base", "gs_base", "efer", "ss", "cs"]:
assert reg.upper() in res
# those are the most important ones, and their presence should indicate it's working as intended
for flag in ["smep", "smap", "wp"]:
assert flag in res or flag.upper() in res
def get_slab_object_address():
"""helper function to get the address of some kmalloc slab object
and the associated slab cache name"""

Loading…
Cancel
Save