You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pwndbg/pwndbg/aglib/nearpc.py

370 lines
15 KiB
Python

from __future__ import annotations
from typing import List
from capstone import * # noqa: F403
import pwndbg
import pwndbg.aglib.disasm.disassembly
import pwndbg.aglib.regs
import pwndbg.aglib.strings
import pwndbg.aglib.symbol
import pwndbg.aglib.vmmap
import pwndbg.color
import pwndbg.color.context as C
import pwndbg.color.disasm as D
import pwndbg.color.theme
import pwndbg.commands.comments
import pwndbg.integration
import pwndbg.lib.config
import pwndbg.lib.functions
import pwndbg.ui
from pwndbg.aglib.disasm.instruction import SplitType
from pwndbg.color import ColorConfig
from pwndbg.color import ColorParamSpec
from pwndbg.color import message
def ljust_padding(lst):
longest_len = max(map(len, lst)) if lst else 0
return [s.ljust(longest_len) for s in lst]
c = ColorConfig(
"nearpc",
[
ColorParamSpec("symbol", "normal", "color for nearpc command (symbol)"),
ColorParamSpec("address", "normal", "color for nearpc command (address)"),
ColorParamSpec("prefix", "none", "color for nearpc command (prefix marker)"),
ColorParamSpec("breakpoint", "red", "color for nearpc command (breakpoint marker)"),
ColorParamSpec("syscall-name", "red", "color for nearpc command (resolved syscall name)"),
ColorParamSpec("argument", "bold", "color for nearpc command (target argument)"),
ColorParamSpec(
"integration-comments", "bold", "color for nearpc command (integration comments)"
),
ColorParamSpec("branch-marker", "normal", "color for nearpc command (branch marker line)"),
],
)
# `pwndbg.arguments` imports `c` from this module.
import pwndbg.arguments
nearpc_branch_marker = pwndbg.color.theme.add_param(
"nearpc-branch-marker", "", "branch marker line for nearpc command"
)
nearpc_branch_marker_contiguous = pwndbg.color.theme.add_param(
"nearpc-branch-marker-contiguous",
" ",
"contiguous branch marker line for nearpc command",
)
pwndbg.color.theme.add_param("highlight-pc", True, "whether to highlight the current instruction")
pwndbg.color.theme.add_param("highlight-breakpoints", True, "whether to highlight breakpoints")
pwndbg.color.theme.add_param("nearpc-prefix", "", "prefix marker for nearpc command")
pwndbg.color.theme.add_param(
"nearpc-breakpoint-prefix", "b+", "breakpoint marker for nearpc command"
)
pwndbg.config.add_param("left-pad-disasm", True, "whether to left-pad disassembly")
nearpc_lines = pwndbg.config.add_param(
"nearpc-lines", 10, "number of additional lines to print for the nearpc command"
)
show_args = pwndbg.config.add_param(
"nearpc-show-args", True, "whether to show call arguments below instruction"
)
show_comments = pwndbg.config.add_param(
"nearpc-integration-comments",
True,
"whether to show comments from integration provider",
)
show_opcode_bytes = pwndbg.config.add_param(
"nearpc-num-opcode-bytes",
0,
"number of opcode bytes to print for each instruction",
param_class=pwndbg.lib.config.PARAM_ZUINTEGER,
)
opcode_separator_bytes = pwndbg.config.add_param(
"nearpc-opcode-separator-bytes",
1,
"number of spaces between opcode bytes",
param_class=pwndbg.lib.config.PARAM_ZUINTEGER,
)
def nearpc(
pc: int = None, lines: int = None, emulate=False, repeat=False, use_cache=False, linear=False
) -> List[str]:
"""
Disassemble near a specified address.
The `linear` argument specifies if we should disassemble linearly in memory, or take jumps into account
"""
# Repeating nearpc (pressing enter) makes it show next addresses
# (writing nearpc explicitly again will reset its state)
if repeat:
# TODO: It would be better to do this in the nearpc command itself, but
# that would require a larger refactor
pc = nearpc.next_pc
result: List[str] = []
if pc is not None:
pc = pwndbg.dbg.selected_inferior().create_value(pc).cast(pwndbg.aglib.typeinfo.pvoid)
# Fix the case where we only have one argument, and
# it's a small value.
if lines is None and (pc is None or int(pc) < 0x100):
lines = pc
pc = None
if pc is None:
pc = pwndbg.aglib.regs.pc
if lines is None:
lines = nearpc_lines // 2
pc = int(pc)
lines = int(lines)
# Check whether we can even read this address
if not pwndbg.aglib.memory.peek(pc):
result.append(message.error("Invalid address %#x" % pc))
# # Load source data if it's available
# pc_to_linenos = collections.defaultdict(lambda: [])
# lineno_to_src = {}
# frame = gdb.selected_frame()
# if frame:
# sal = frame.find_sal()
# if sal:
# symtab = sal.symtab
# objfile = symtab.objfile
# sourcefilename = symtab.filename
# with open(sourcefilename, 'r') as sourcefile:
# lineno_to_src = {i:l for i,l in enumerate(sourcefile.readlines())}
# for line in symtab.linetable():
# pc_to_linenos[line.pc].append(line.line)
instructions, index_of_pc = pwndbg.aglib.disasm.disassembly.near(
pc, lines, emulate=emulate, show_prev_insns=not repeat, use_cache=use_cache, linear=linear
)
if pwndbg.aglib.memory.peek(pc) and not instructions:
result.append(message.error("Invalid instructions at %#x" % pc))
# In case $pc is in a new map we don't know about,
# this will trigger an exploratory search.
pwndbg.aglib.vmmap.find(pc)
# Gather all addresses and symbols for each instruction
# Ex: <main+43>
symbols = [pwndbg.aglib.symbol.resolve_addr(i.address) for i in instructions]
addresses: List[str] = ["%#x" % i.address for i in instructions]
nearpc.next_pc = instructions[-1].address + instructions[-1].size if instructions else 0
# Format the symbol name for each instruction
symbols = [f"<{sym}> " if sym else "" for sym in symbols]
# Pad out all of the symbols and addresses
if pwndbg.config.left_pad_disasm and not repeat:
symbols = ljust_padding(symbols)
addresses = ljust_padding(addresses)
assembly_strings = D.instructions_and_padding(instructions)
breakpoint_locations = pwndbg.dbg.breakpoint_locations()
prefix_sign = pwndbg.config.nearpc_prefix
current_insn_prefix = f" {prefix_sign}"
current_insn_prefix = c.prefix(current_insn_prefix)
default_prefix = " " * (len(prefix_sign) + 1)
default_prefix = c.prefix(default_prefix)
breakpoint_sign = pwndbg.config.nearpc_breakpoint_prefix
breakpoint_prefix = breakpoint_sign.ljust(len(prefix_sign) + 1)
breakpoint_prefix = c.breakpoint(breakpoint_prefix)
# Print out each instruction
for i, (address_str, symbol, instr, asm) in enumerate(
zip(addresses, symbols, instructions, assembly_strings)
):
# Show prefix only on the specified address and don't show it while in repeat-mode
# or when showing current instruction for the second time
show_prefix = instr.address == pc and not repeat and i == index_of_pc
is_breakpoint = False
if show_prefix:
prefix = current_insn_prefix
elif instr.address in breakpoint_locations:
# If the instruction is not the current instruction and a breakpoint,
# show the breakpoint sign
prefix = breakpoint_prefix
is_breakpoint = True
else:
prefix = default_prefix
# If this instruction is a breakpoint and not the current pc, highlight it.
if is_breakpoint and pwndbg.config.highlight_breakpoints:
address_str = c.breakpoint(address_str)
symbol = c.breakpoint(symbol)
# Colorize address and symbol if not highlighted
# symbol is fetched from gdb and it can be e.g. '<main+8>'
# In case there are duplicate instances of an instruction (tight loop),
# ones that the instruction pointer is not at stick out a little, to indicate the repetition
elif not pwndbg.config.highlight_pc or instr.address != pc or repeat:
address_str = c.address(address_str)
symbol = c.symbol(symbol)
elif pwndbg.config.highlight_pc and i == index_of_pc:
# If this instruction is the one the PC is at.
# In case of tight loops, with emulation we may display the same instruction multiple times.
# Only highlight current instance, not past or future times.
address_str = C.highlight(address_str)
symbol = C.highlight(symbol)
# If this instruction performs a memory access operation, we should tell
# the user anything we can figure out about the memory it's trying to
# access.
# mem_access = ""
if instr.address == pc and False:
accesses = []
for operand in instr.operands:
if operand.type != CS_OP_MEM:
continue
address = operand.mem.disp
base = operand.mem.base
if base > 0:
address += pwndbg.aglib.regs[instr.reg_name(base)]
vmmap = pwndbg.aglib.vmmap.get()
page = next((page for page in vmmap if address in page), None)
if page is None:
# This is definetly invalid. Don't even bother checking
# any other conditions.
accesses.append(f"[X] {address:#x}")
continue
if operand.access == CS_AC_READ and not page.read:
# Tried to read from a page we can't read.
accesses.append(f"[X] {address:#x}")
continue
if operand.access == CS_AC_WRITE and not page.write:
# Tried to write to a page we can't write.
accesses.append(f"[X] {address:#x}")
continue
# At this point, we know the operation is legal, but we don't
# know where it's going yet. It could be going to either memory
# managed by libc or memory managed by the program itself.
if not pwndbg.aglib.heap.current.is_initialized():
# The libc heap hasn't been initialized yet. There's not a
# lot that we can say beyond this point.
continue
allocator = pwndbg.aglib.heap.current
heap = pwndbg.aglib.heap.ptmalloc.Heap(address)
chunk = None
for ch in heap:
# Find the chunk in this heap the corresponds to the address
# we're trying to access.
offset = address - ch.address
if offset >= 0 and offset < ch.real_size:
chunk = ch
break
if chunk is None:
# The memory for this chunk is not managed by libc. We can't
# reason about it.
accesses.append(f"[?] {address:#x}")
continue
# Scavenge through all of the bins in the current allocator.
# Bins track free chunks, so, whether or not we can find the
# chunk we're trying to access in a bin will tells us whether
# this access is a UAF.
bins_list = [
allocator.fastbins(chunk.arena.address),
allocator.smallbins(chunk.arena.address),
allocator.largebins(chunk.arena.address),
allocator.unsortedbin(chunk.arena.address),
]
if allocator.has_tcache():
bins_list.append(allocator.tcachebins(None))
bins_list = [x for x in bins_list if x is not None]
for bins in bins_list:
if bins.contains_chunk(chunk.real_size, chunk.address):
# This chunk is free. This is a UAF.
accesses.append(f"[UAF] {address:#x}")
continue
# mem_access = " ".join(accesses)
opcodes = ""
if show_opcode_bytes > 0:
opcodes = (opcode_separator_bytes * " ").join(
f"{c:02x}" for c in instr.bytes[: int(show_opcode_bytes)]
)
# Must add +3 at minimum, due to truncated instructions adding "..."
align = show_opcode_bytes * 2 + 3
if opcode_separator_bytes > 0:
# add the length of the maximum number of separators to the alignment
align += (show_opcode_bytes - 1) * opcode_separator_bytes # type: ignore[operator]
if len(instr.bytes) > show_opcode_bytes:
opcodes += pwndbg.color.gray("...")
# the length of gray("...") is 12, so we need to add extra 9 (12-3) alignment length for the invisible characters
align += 9 # len(pwndbg.color.gray(""))
opcodes = opcodes.ljust(align)
if pwndbg.config.highlight_pc and i == index_of_pc:
opcodes = C.highlight(opcodes)
# Example line:
# ► 0x7ffff7f1aeb6 0f bd c0 <__strrchr_avx2+70> bsr eax, eax
# prefix = ►
# address_str = 0x555555556030
# opcodes = 0f bd c0 Opcodes are enabled with the 'nearpc-num-opcode-bytes' setting
# symbol = <__strrchr_avx2+70>
# asm = bsr eax, eax (jump target/annotation would go here too)
# mem_access was on this list, but not used due to the `and False` in the code that sets it above
line = " ".join(filter(None, (prefix, address_str, opcodes, symbol, asm)))
if show_comments:
# Pull comments from integration if possible
result += [
" "
* (len(pwndbg.color.unstylize(line)) - len(pwndbg.color.unstylize(asm).lstrip()))
+ c.integration_comments(x)
for x in pwndbg.integration.provider.get_comment_lines(instr.address)
]
# For Comment Function
try:
line += " " * 10 + C.comment(
pwndbg.commands.comments.file_lists[pwndbg.aglib.proc.exe][hex(instr.address)]
)
except Exception:
pass
result.append(line)
# For call instructions, attempt to resolve the target and
# determine the number of arguments.
if show_args:
result.extend(
"%8s%s" % ("", arg) for arg in pwndbg.arguments.format_args(instruction=instr)
)
# If this instruction deserves a down arrow to indicate a taken branch
if instr.split == SplitType.BRANCH_TAKEN:
result.append(c.branch_marker(f"{nearpc_branch_marker}"))
# Otherwise if it's a branch and it *is* contiguous, just put an empty line.
elif instr.split == SplitType.BRANCH_NOT_TAKEN:
if nearpc_branch_marker_contiguous:
result.append("%s" % nearpc_branch_marker_contiguous)
return result
nearpc.next_pc = 0