Fetch stacks from vmmap if they exist (also stacks.update -> stacks.get) (#1959)

Hopefully fixes #1947 by fetching stacks only when they are used instead
of doing it on each stop event. It will also first try to compute stacks
dictionary based on vmmap and if it fallbacks to exploring stacks if
vmmap is not present.
pull/1960/head
Disconnect3d 2 years ago committed by GitHub
parent 03e97e9ea8
commit 9d0621c4d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -42,7 +42,7 @@ def canary() -> None:
stack_canaries = list(
pwndbg.search.search(
pwndbg.gdblib.arch.pack(global_canary), mappings=pwndbg.gdblib.stack.stacks.values()
pwndbg.gdblib.arch.pack(global_canary), mappings=pwndbg.gdblib.stack.get().values()
)
)

@ -45,7 +45,7 @@ def xinfo_stack(page, addr) -> None:
if canary_value is not None:
all_canaries = list(
pwndbg.search.search(
pwndbg.gdblib.arch.pack(canary_value), mappings=pwndbg.gdblib.stack.stacks.values()
pwndbg.gdblib.arch.pack(canary_value), mappings=pwndbg.gdblib.stack.get().values()
)
)
follow_canaries = sorted(filter(lambda a: a > addr, all_canaries))

@ -1,6 +1,5 @@
"""
Helpers for finding address mappings which are used as
a stack.
Helpers for finding address mappings which are used as a stack.
Generally not needed, except under qemu-user and for when
binaries do things to remap the stack (e.g. pwnies' postit).
@ -16,28 +15,18 @@ import pwndbg.gdblib.events
import pwndbg.gdblib.memory
import pwndbg.lib.cache
# Dictionary of stack ranges.
# Key is the gdb thread ptid
# Value is a pwndbg.lib.memory.Page object
stacks: dict[tuple[int, int, int], pwndbg.lib.memory.Page] = {}
# Whether the stack is protected by NX.
# This is updated automatically by is_executable.
nx = False
def find(address: int):
"""
Returns a pwndbg.lib.memory.Page object which corresponds to the
currently-loaded stack.
Returns a pwndbg.lib.memory.Page object which corresponds to given address stack
or None if it does not exist
"""
if not stacks:
update()
for stack in stacks.values():
for stack in get().values():
if address in stack:
return stack
return None
def find_upper_stack_boundary(stack_ptr: int, max_pages: int = 1024) -> int:
stack_ptr = pwndbg.lib.memory.page_align(int(stack_ptr))
@ -50,55 +39,19 @@ def find_upper_stack_boundary(stack_ptr: int, max_pages: int = 1024) -> int:
return pwndbg.gdblib.memory.find_upper_boundary(stack_ptr, max_pages)
@pwndbg.gdblib.events.stop
@pwndbg.lib.cache.cache_until("stop")
def update() -> None:
def get() -> dict[int, pwndbg.lib.memory.Page]:
"""
For each running thread, updates the known address range
for its stack.
For each running thread, return the known address range for its stack
Returns a dict which should never be modified (since its cached)
"""
curr_thread = gdb.selected_thread()
try:
for thread in gdb.selected_inferior().threads():
thread.switch()
pwndbg.gdblib.regs.__getattr__.cache.clear()
sp = pwndbg.gdblib.regs.sp
# Skip if sp is None or 0
# (it might be 0 if we debug a qemu kernel)
if not sp:
continue
sp_low = sp & ~(0xFFF)
sp_low -= 0x1000
# If we don't already know about this thread, create
# a new Page mapping for it.
page = stacks.get(thread.ptid, None)
if page is None:
start = sp_low
stop = find_upper_stack_boundary(sp)
page = pwndbg.lib.memory.Page(
start, stop - start, 6 if not is_executable() else 7, 0, "[stack]"
)
stacks[thread.ptid] = page
continue
elif page.objfile is None:
pid, tid, _ = thread.ptid
if pid == tid:
page.objfile = "[stack]"
else:
page.objfile = "[stack:%i]" % tid
# If we *DO* already know about this thread, just
# update the lower boundary if it got any lower.
low = min(page.vaddr, sp_low)
if low != page.vaddr:
page.memsz += page.vaddr - low
page.vaddr = low
finally:
if curr_thread:
curr_thread.switch()
stacks = _fetch_via_vmmap()
# This is slow :(
if not stacks:
_fetch_via_exploration()
return stacks
@pwndbg.lib.cache.cache_until("stop")
@ -109,22 +62,9 @@ def current():
return find(pwndbg.gdblib.regs.sp)
@pwndbg.gdblib.events.exit
def clear() -> None:
"""
Clears everything we know about any stack memory ranges.
Called when the target process exits.
"""
stacks.clear()
global nx
nx = False
@pwndbg.gdblib.events.stop
@pwndbg.lib.cache.cache_until("exit")
def is_executable() -> bool:
global nx
nx = False
PT_GNU_STACK = 0x6474E551
@ -135,3 +75,81 @@ def is_executable() -> bool:
nx = True
return not nx
def _fetch_via_vmmap() -> dict[int, pwndbg.lib.memory.Page]:
stacks: dict[int, pwndbg.lib.memory.Page] = {}
pages = pwndbg.gdblib.vmmap.get()
curr_thread = gdb.selected_thread()
for thread in gdb.selected_inferior().threads():
thread.switch()
# Need to clear regs values cache after switching thread
# So we get proper value of the SP register
pwndbg.gdblib.regs.__getattr__.cache.clear()
sp = pwndbg.gdblib.regs.sp
# Skip if sp is 0 (it might be 0 if we debug a qemu kernel)
if not sp:
continue
page = None
# Find the given SP in pages
for p in pages:
if sp in p:
page = p
break
if page:
stacks[thread.num] = page
continue
curr_thread.switch()
return stacks
def _fetch_via_exploration() -> dict[int, pwndbg.lib.memory.Page]:
"""
TODO/FIXME: This exploration is not great since it now hits on each stop
(based on how this function is used). Ideally, explored stacks should be
cached globally and cleared only with new debugged target.
This way, we should also explore the stack only for a maximum of few pages
so that we won't take too much time finding its bounds. Then, on each stop
we can explore one more (or a few more) pages for the given current stack
we are currently on, ideally not taking the precious time of our users.
An alternative to this is dumping this functionality completely and this
will be decided hopefully after a next release.
"""
stacks: dict[int, pwndbg.lib.memory.Page] = {}
curr_thread = gdb.selected_thread()
for thread in gdb.selected_inferior().threads():
thread.switch()
pwndbg.gdblib.regs.__getattr__.cache.clear()
sp = pwndbg.gdblib.regs.sp
# Skip if sp is None or 0
# (it might be 0 if we debug a qemu kernel)
if not sp:
continue
sp_low = sp & ~(0xFFF)
sp_low -= 0x1000
start = sp_low
stop = find_upper_stack_boundary(sp)
page = pwndbg.lib.memory.Page(
start, stop - start, 6 if not is_executable() else 7, 0, f"[stack:{thread.num}]"
)
stacks[thread.num] = page
continue
curr_thread.switch()
return stacks

@ -146,7 +146,7 @@ def get() -> tuple[pwndbg.lib.memory.Page, ...]:
return (pwndbg.lib.memory.Page(0, pwndbg.gdblib.arch.ptrmask, 7, 0, "[qemu]"),)
pages.extend(info_files())
pages.extend(pwndbg.gdblib.stack.stacks.values())
pages.extend(pwndbg.gdblib.stack.get().values())
inside_no_proc_maps_search = False
pages.extend(explored_pages)
@ -195,7 +195,7 @@ def explore(address_maybe: int) -> Any | None:
return None
flags |= 2 if pwndbg.gdblib.memory.poke(address_maybe) else 0
flags |= 1 if not pwndbg.gdblib.stack.nx else 0
flags |= 1 if not pwndbg.gdblib.stack.is_executable() else 0
page = find_boundaries(address_maybe)
page.objfile = "<explored>"

@ -12,7 +12,7 @@ def test_memory_read_write(start_binary):
Tests simple pwndbg's memory read/write operations with different argument types
"""
start_binary(REFERENCE_BINARY)
stack_addr = next(iter(pwndbg.gdblib.stack.stacks.values())).vaddr
stack_addr = next(iter(pwndbg.gdblib.stack.get().values())).vaddr
# Testing write(addr, str)
val = "X" * 50

Loading…
Cancel
Save