From 4fc51a71c5d577ea14ee11ddad713ca8a0ee64a4 Mon Sep 17 00:00:00 2001 From: patryk4815 Date: Sat, 13 Dec 2025 21:24:41 +0100 Subject: [PATCH] Add fedora tests & docker (#3476) * add fedora41 * fix * fix * fix * fix launched_sleep_binary * fix lint * dnf fix for py3.14 * rm env PIP_NO_CACHE_DIR * remove duplicate tests: test_context_commands.py * cleanup mallocng tests * lint * fix test * fix lint * enable test test_mallocng_find * fix rip * fix ptmalloc crash? * fix ptmalloc crash? --------- Co-authored-by: k4lizen <124312252+k4lizen@users.noreply.github.com> --- .github/workflows/docker.yml | 4 +- Dockerfile | 1 - Dockerfile.arch | 1 - Dockerfile.dnf | 55 ++ docker-compose.yml | 24 + pwndbg/aglib/heap/ptmalloc.py | 10 +- setup-dev.sh | 7 +- tests/library/dbg/tests/test_mallocng.py | 27 +- .../gdb/tests/test_context_commands.py | 585 ------------------ 9 files changed, 104 insertions(+), 610 deletions(-) create mode 100644 Dockerfile.dnf delete mode 100644 tests/library/gdb/tests/test_context_commands.py diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8ba5264f3..9b517a5fd 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - images: [ubuntu22.04, ubuntu24.04, debian12, archlinux] + images: [ubuntu22.04, ubuntu24.04, debian12, archlinux, fedora41, fedora42, fedora43] runs-on: ubuntu-latest timeout-minutes: 30 @@ -45,7 +45,7 @@ jobs: strategy: fail-fast: false matrix: - images: [ubuntu24.04] + images: [ubuntu24.04, fedora41, fedora42, fedora43] runs-on: ubuntu-24.04-arm timeout-minutes: 30 diff --git a/Dockerfile b/Dockerfile index 8623687b2..1f90e5894 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,6 @@ FROM $image AS base WORKDIR /pwndbg -ENV PIP_NO_CACHE_DIR=true ENV LANG=en_US.utf8 ENV TZ=America/New_York ENV PWNDBG_VENV_PATH=/venv diff --git a/Dockerfile.arch b/Dockerfile.arch index 3b83ff491..f8afcf1c9 100644 --- a/Dockerfile.arch +++ b/Dockerfile.arch @@ -13,7 +13,6 @@ FROM $image WORKDIR /pwndbg -ENV PIP_NO_CACHE_DIR=true ENV LANG=en_US.utf8 ENV TZ=America/New_York ENV PWNDBG_VENV_PATH=/venv diff --git a/Dockerfile.dnf b/Dockerfile.dnf new file mode 100644 index 000000000..68b572510 --- /dev/null +++ b/Dockerfile.dnf @@ -0,0 +1,55 @@ +# This dockerfile was created for development & testing purposes, for DNF-based distro. +# +# Build as: docker build -f Dockerfile.dnf -t pwndbg . +# +# For testing use: docker run --rm -it --cap-add=SYS_PTRACE --security-opt seccomp=unconfined pwndbg bash +# +# For development, mount the directory so the host changes are reflected into container: +# docker run -it --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -v `pwd`:/pwndbg pwndbg bash +# + +ARG image=fedora:41 +FROM $image + +WORKDIR /pwndbg + +ENV LANG=en_US.utf8 +ENV TZ=America/New_York +ENV PWNDBG_VENV_PATH=/venv +ENV UV_PROJECT_ENVIRONMENT=/venv + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \ + dnf -y install \ + glibc-langpack-en \ + glibc-locale-source \ + vim-minimal && \ + localedef -i en_US -f UTF-8 en_US.UTF-8 && \ + dnf clean all && \ + rm -rf /var/cache/dnf + +# setup.sh needs scripts/common.sh +COPY ./scripts/common.sh /pwndbg/scripts/ + +COPY ./setup.sh /pwndbg/ +COPY ./uv.lock /pwndbg/ +COPY ./pyproject.toml /pwndbg/ + +# pyproject.toml requires these files, pip install would fail +RUN touch README.md && mkdir pwndbg && touch pwndbg/empty.py + +# fix for py3.14 +# RUN dnf install -y uv git python3-devel patch ncurses-devel gcc +# RUN uv sync --all-groups --all-extras +RUN dnf install -y uv git python3-devel patch ncurses-devel gcc +RUN ./setup.sh + +# Comment these lines if you won't run the tests. +COPY ./setup-dev.sh /pwndbg/ +RUN ./setup-dev.sh + +# Cleanup dummy files +RUN rm README.md && rm -rf pwndbg + +COPY . /pwndbg/ + +ENV PATH="${PWNDBG_VENV_PATH}/bin:${PATH}" diff --git a/docker-compose.yml b/docker-compose.yml index dcb5dd60a..12ab7bf95 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,3 +52,27 @@ services: dockerfile: Dockerfile.arch args: image: archlinux:latest + + fedora41: + <<: *base-spec + build: + context: . + dockerfile: Dockerfile.dnf + args: + image: fedora:41 + + fedora42: + <<: *base-spec + build: + context: . + dockerfile: Dockerfile.dnf + args: + image: fedora:42 + + fedora43: + <<: *base-spec + build: + context: . + dockerfile: Dockerfile.dnf + args: + image: fedora:43 diff --git a/pwndbg/aglib/heap/ptmalloc.py b/pwndbg/aglib/heap/ptmalloc.py index 6cf3bba26..d9774bd11 100644 --- a/pwndbg/aglib/heap/ptmalloc.py +++ b/pwndbg/aglib/heap/ptmalloc.py @@ -513,10 +513,9 @@ class Heap: raise ValueError(f"Cannot build heap object on an unmapped address ({hex(addr)})") heap_info = allocator.get_heap(addr) - try: + ar_ptr = None + if heap_info is not None: ar_ptr = int(heap_info["ar_ptr"]) - except pwndbg.dbg_mod.Error: - ar_ptr = None if ar_ptr is not None and ar_ptr in (ar.address for ar in allocator.arenas): # Case 2; non-main arena. @@ -1713,7 +1712,10 @@ class DebugSymsHeap(GlibcMemoryAllocator[pwndbg.dbg_mod.Type, pwndbg.dbg_mod.Val """Find & read the heap_info struct belonging to the chunk at 'addr'.""" if self.heap_info is None: return None - return pwndbg.aglib.memory.get_typed_pointer_value(self.heap_info, heap_for_ptr(addr)) + haddr = heap_for_ptr(addr) + if pwndbg.aglib.memory.peek(haddr) is None: + return None + return pwndbg.aglib.memory.get_typed_pointer_value(self.heap_info, haddr) def get_tcache( self, tcache_addr: int | pwndbg.dbg_mod.Value | None = None diff --git a/setup-dev.sh b/setup-dev.sh index b3d058186..68afc8ab3 100755 --- a/setup-dev.sh +++ b/setup-dev.sh @@ -158,12 +158,15 @@ EOF install_dnf() { sudo dnf upgrade || true sudo dnf install -y \ + make \ nasm \ gcc \ curl \ wget \ - gdb \ + musl-gcc \ + g++ \ parallel \ + qemu-system-x86 \ qemu-system-arm \ qemu-user @@ -176,7 +179,7 @@ install_dnf() { command -v go &> /dev/null || sudo dnf install -y go if [[ "$1" != "" ]]; then - sudo dnf install shfmt + sudo dnf install -y shfmt fi } diff --git a/tests/library/dbg/tests/test_mallocng.py b/tests/library/dbg/tests/test_mallocng.py index e0360c15e..cecc2070d 100644 --- a/tests/library/dbg/tests/test_mallocng.py +++ b/tests/library/dbg/tests/test_mallocng.py @@ -38,18 +38,18 @@ async def test_mallocng_slot_user(ctrl: Controller, binary: str): expected_output = [ "slab", - f" group: {re_addr} ", - f" meta: {re_addr} ", + rf" group: {re_addr}\s+", + rf" meta: {re_addr}\s+", "general", - f" start: {re_addr} ", - f" user start: {re_addr} aka `p`", - rf" end: {re_addr} start \+ stride - 4", + rf" start: {re_addr}\s+", + rf" user start: {re_addr}\s+aka `p`", + rf" end: {re_addr}\s+start \+ stride - 4", " stride: 0x60 distance between adjacent slots", """ user size: 0x50 aka "nominal size", `n`""", r" slack: 0x0 \(0x0\) slot's unused memory \/ 0x10", " state: allocated ", "in-band", - r" offset: 0x[0-9] \(0x[0-9]{0,1}0\) distance to first slot start \/ 0x10", + r" offset: 0x[0-9]\s+\(0x[0-9]{0,1}0\)\s+distance to first slot start \/ 0x10", r" index: 0x0 index of slot in its group", " hdr reserved: 0x5 describes: end - p - n", " use ftr reserved", @@ -217,16 +217,16 @@ async def test_mallocng_group(ctrl: Controller, binary: str): expected_out = [ "group", f" @ {re_addr} - {re_addr}", - f" meta: {re_addr} ", + rf" meta: {re_addr}\s+", " active_idx: 0x4 ", - f" storage: {re_addr} start of slots", + rf" storage: {re_addr}\s+start of slots", "---", " group size: 0x1f0 ", "meta", f" @ {re_addr}", - f" prev: {re_addr} ", - f" next: {re_addr} ", - f" mem: {re_addr} the group", + rf" prev: {re_addr}\s+", + rf" next: {re_addr}\s+", + rf" mem: {re_addr}\s+the group", " avail_mask: 0x18 0b00000000000000000000000000011000", " freed_mask: 0x0 0b00000000000000000000000000000000", r" last_idx: 0x4 \(cnt: 0x5\) index of last slot", @@ -346,11 +346,8 @@ async def test_mallocng_find(ctrl: Controller, binary: str): await launch_to(ctrl, binary, "break_here") await ctrl.finish() - if pwndbg.aglib.arch.name != "x86-64": - pytest.skip("TODO multiarch") - # Check no slot found - find_out = color.strip(await ctrl.execute_and_capture("ng-find $rip")) + find_out = color.strip(await ctrl.execute_and_capture("ng-find $pc")) assert "No slot found containing that address.\n" == find_out buffer1_addr = int(pwndbg.dbg.selected_frame().evaluate_expression("buffer1")) diff --git a/tests/library/gdb/tests/test_context_commands.py b/tests/library/gdb/tests/test_context_commands.py deleted file mode 100644 index a404b0c60..000000000 --- a/tests/library/gdb/tests/test_context_commands.py +++ /dev/null @@ -1,585 +0,0 @@ -from __future__ import annotations - -import re - -import gdb -import pytest - -import pwndbg.aglib.memory -import pwndbg.aglib.regs -import pwndbg.commands -import pwndbg.commands.canary -import pwndbg.commands.context - -from . import get_binary - -REFERENCE_BINARY = get_binary("reference-binary.native.out") -USE_FDS_BINARY = get_binary("use-fds.native.out") -TABSTOP_BINARY = get_binary("tabstop.native.out") -SYSCALLS_BINARY = get_binary("syscalls.x86-64.out") -MANGLING_BINARY = get_binary("symbol_1600_and_752.native.out") - - -def test_context_disasm_show_fd_filepath(start_binary): - """ - Tests context disasm command and whether it shows properly opened fd filepath - """ - start_binary(USE_FDS_BINARY) - - # Run until main - gdb.execute("break main") - gdb.execute("continue") - - # Stop on read(0, ...) -> should show /dev/pts/X or pipe:X on CI - gdb.execute("nextcall") - - out = pwndbg.commands.context.context_disasm() - assert "[ DISASM / x86-64 / set emulate on ]" in out[0] # Sanity check - - call_read_line_idx = out.index(next(line for line in out if "" in line)) - lines_after_call_read = out[call_read_line_idx:] - - line_call_read, line_fd, line_buf, line_nbytes, *_rest = lines_after_call_read - - assert "call read@plt" in line_call_read - - # When running tests with GNU Parallel, sometimes the file name looks - # '/tmp/parZ4YC4.par', and occasionally '(deleted)' is present after the - # filename - line_fd = line_fd.strip() - assert re.match( - r"fd:\s+1 \((/dev/pts/\d+|/tmp/par.+\.par(?: \(deleted\))?|pipe:\[\d+\])\)", line_fd - ) - - line_buf = line_buf.strip() - assert re.match(r"buf:\s+0x[0-9a-f]+(?: \{buf\})? ◂— 0", line_buf) - - line_nbytes = line_nbytes.strip() - assert re.match(r"nbytes:\s+0", line_nbytes) - - # Stop on open(...) - gdb.execute("nextcall") - # Stop on read(...) -> should show use-fds.out - gdb.execute("nextcall") - - out = pwndbg.commands.context.context_disasm() - assert "[ DISASM / x86-64 / set emulate on ]" in out[0] # Sanity check - - call_read_line_idx = out.index(next(line for line in out if "" in line)) - lines_after_call_read = out[call_read_line_idx:] - - line_call_read, line_fd, line_buf, line_nbytes, *_rest = lines_after_call_read - - line_fd = line_fd.strip() - assert re.match(r"fd:\s+3 \(.*?/tests/binaries/host/use-fds.native.out\)", line_fd) - - line_buf = line_buf.strip() - assert re.match(r"buf:\s+0x[0-9a-f]+(?: \{buf\})? ◂— 0", line_buf) - - line_nbytes = line_nbytes.strip() - assert re.match(r"nbytes:\s+0x10", line_nbytes) - - -@pytest.mark.parametrize("sections", ("''", '""', "none", "-", "")) -def test_empty_context_sections(start_binary, sections): - start_binary(USE_FDS_BINARY) - - # Sanity check - default_ctx_sects = "regs disasm code ghidra stack backtrace expressions threads heap_tracker" - assert pwndbg.config.context_sections.value == default_ctx_sects - assert gdb.execute("context", to_string=True) != "" - - # Actual test check - gdb.execute(f"set context-sections {sections}", to_string=True) - assert pwndbg.config.context_sections.value == "" - assert gdb.execute("context", to_string=True) == "" - - # Bring back old values && sanity check - gdb.execute(f"set context-sections {default_ctx_sects}") - assert pwndbg.config.context_sections.value == default_ctx_sects - assert gdb.execute("context", to_string=True) != "" - - -def test_source_code_tabstop(start_binary): - start_binary(TABSTOP_BINARY) - - # Run until line 6 - gdb.execute("break tabstop.native.c:6") - gdb.execute("continue") - - # Default context-code-tabstop = 8 - src = gdb.execute("context code", to_string=True) - assert """ 1 #include \n""" in src - assert """ 2 \n""" in src - assert """ 3 int main() {\n""" in src - assert """ 4 // test mix indent\n""" in src - assert """ 5 do {\n""" in src - assert """ 6 puts("tab line");\n""" in src - assert """ 7 } while (0);\n""" in src - assert """ 8 return 0;\n""" in src - assert """ 9 }\n""" in src - assert """10 \n""" in src - - # Test context-code-tabstop = 2 - gdb.execute("set context-code-tabstop 2") - src = gdb.execute("context code", to_string=True) - assert """ 1 #include \n""" in src - assert """ 2 \n""" in src - assert """ 3 int main() {\n""" in src - assert """ 4 // test mix indent\n""" in src - assert """ 5 do {\n""" in src - assert """ 6 puts("tab line");\n""" in src - assert """ 7 } while (0);\n""" in src - assert """ 8 return 0;\n""" in src - assert """ 9 }\n""" in src - assert """10 \n""" in src - - # Disable context-code-tabstop - gdb.execute("set context-code-tabstop 0") - src = gdb.execute("context code", to_string=True) - assert """ 1 #include \n""" in src - assert """ 2 \n""" in src - assert """ 3 int main() {\n""" in src - assert """ 4 \t// test mix indent\n""" in src - assert """ 5 do {\n""" in src - assert """ 6 \t\tputs("tab line");\n""" in src - assert """ 7 } while (0);\n""" in src - assert """ 8 return 0;\n""" in src - assert """ 9 }\n""" in src - assert """10 \n""" in src - - -def test_context_disasm_syscalls_args_display(start_binary): - start_binary(SYSCALLS_BINARY) - gdb.execute("nextsyscall") - dis = gdb.execute("context disasm", to_string=True) - assert dis == ( - "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA\n" - "──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────\n" - " 0x400080 <_start> mov eax, 0 EAX => 0\n" - " 0x400085 <_start+5> mov edi, 0x1337 EDI => 0x1337\n" - " 0x40008a <_start+10> mov esi, 0xdeadbeef ESI => 0xdeadbeef\n" - " 0x40008f <_start+15> mov ecx, 0x10 ECX => 0x10\n" - " ► 0x400094 <_start+20> syscall \n" - " fd: 0x1337\n" - " buf: 0xdeadbeef\n" - " nbytes: 0\n" - " 0x400096 <_start+22> mov eax, 0xa EAX => 0xa\n" - " 0x40009b <_start+27> int 0x80 \n" - " 0x40009d add byte ptr [rax], al\n" - " 0x40009f add byte ptr [rax], al\n" - " 0x4000a1 add byte ptr [rax], al\n" - " 0x4000a3 add byte ptr [rax], al\n" - "────────────────────────────────────────────────────────────────────────────────\n" - ) - - gdb.execute("nextsyscall") - dis = gdb.execute("context disasm", to_string=True) - assert dis == ( - "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA\n" - "──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────\n" - " 0x400085 <_start+5> mov edi, 0x1337 EDI => 0x1337\n" - " 0x40008a <_start+10> mov esi, 0xdeadbeef ESI => 0xdeadbeef\n" - " 0x40008f <_start+15> mov ecx, 0x10 ECX => 0x10\n" - " 0x400094 <_start+20> syscall \n" - " 0x400096 <_start+22> mov eax, 0xa EAX => 0xa\n" - " ► 0x40009b <_start+27> int 0x80 \n" - " name: 0x1337\n" - " 0x40009d add byte ptr [rax], al\n" - " 0x40009f add byte ptr [rax], al\n" - " 0x4000a1 add byte ptr [rax], al\n" - " 0x4000a3 add byte ptr [rax], al\n" - " 0x4000a5 add byte ptr [rax], al\n" - "────────────────────────────────────────────────────────────────────────────────\n" - ) - - -def test_context_disasm_syscalls_args_display_no_emulate(start_binary): - gdb.execute("set emulate off") - - start_binary(SYSCALLS_BINARY) - gdb.execute("nextsyscall") - dis = gdb.execute("context disasm", to_string=True) - assert dis == ( - "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA\n" - "─────────────────────[ DISASM / x86-64 / set emulate off ]──────────────────────\n" - " 0x400080 <_start> mov eax, 0 EAX => 0\n" - " 0x400085 <_start+5> mov edi, 0x1337 EDI => 0x1337\n" - " 0x40008a <_start+10> mov esi, 0xdeadbeef ESI => 0xdeadbeef\n" - " 0x40008f <_start+15> mov ecx, 0x10 ECX => 0x10\n" - " ► 0x400094 <_start+20> syscall \n" - " fd: 0x1337\n" - " buf: 0xdeadbeef\n" - " nbytes: 0\n" - " 0x400096 <_start+22> mov eax, 0xa EAX => 0xa\n" - " 0x40009b <_start+27> int 0x80 \n" - " 0x40009d add byte ptr [rax], al\n" - " 0x40009f add byte ptr [rax], al\n" - " 0x4000a1 add byte ptr [rax], al\n" - " 0x4000a3 add byte ptr [rax], al\n" - "────────────────────────────────────────────────────────────────────────────────\n" - ) - - gdb.execute("nextsyscall") - dis = gdb.execute("context disasm", to_string=True) - assert dis == ( - "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA\n" - "─────────────────────[ DISASM / x86-64 / set emulate off ]──────────────────────\n" - " 0x400085 <_start+5> mov edi, 0x1337 EDI => 0x1337\n" - " 0x40008a <_start+10> mov esi, 0xdeadbeef ESI => 0xdeadbeef\n" - " 0x40008f <_start+15> mov ecx, 0x10 ECX => 0x10\n" - " 0x400094 <_start+20> syscall \n" - " 0x400096 <_start+22> mov eax, 0xa EAX => 0xa\n" - " ► 0x40009b <_start+27> int 0x80 \n" - " name: 0x1337\n" - " 0x40009d add byte ptr [rax], al\n" - " 0x40009f add byte ptr [rax], al\n" - " 0x4000a1 add byte ptr [rax], al\n" - " 0x4000a3 add byte ptr [rax], al\n" - " 0x4000a5 add byte ptr [rax], al\n" - "────────────────────────────────────────────────────────────────────────────────\n" - ) - - -def test_context_backtrace_show_proper_symbol_names(start_binary): - start_binary(MANGLING_BINARY) - gdb.execute("break A::foo") - gdb.execute("continue") - - backtrace = gdb.execute("context backtrace", to_string=True).split("\n") - - assert backtrace[0] == "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA" - assert ( - backtrace[1] - == "─────────────────────────────────[ BACKTRACE ]──────────────────────────────────" - ) - - assert re.match(r".*0 0x[0-9a-f]+ A::foo\(int, int\)", backtrace[2]) - - # Match A::call_foo()+38 or similar: the offset may change so we match \d+ at the end - assert re.match(r".*1 0x[0-9a-f]+ A::call_foo\(\)\+\d+", backtrace[3]) - - # Match main+87 or similar offset - assert re.match(r".*2 0x[0-9a-f]+ main\+\d+", backtrace[4]) - - # Match __libc_start_main+243 or similar offset - # Note: on Ubuntu 22.04 there will be __libc_start_call_main and then __libc_start_main - # but on older distros there will be only __libc_start_main - # Let's not bother too much about it and make it the last call assertion here - assert re.match( - r".*3 0x[0-9a-f]+ (__libc_start_main|__libc_start_call_main)\+\d+", backtrace[5] - ) - - assert ( - backtrace[-2] - == "────────────────────────────────────────────────────────────────────────────────" - ) - assert backtrace[-1] == "" - - -def test_context_disasm_works_properly_with_disasm_flavor_switch(start_binary): - start_binary(SYSCALLS_BINARY) - - def assert_intel(out): - assert "mov eax, 0" in out[2] - assert "mov edi, 0x1337" in out[3] - assert "mov esi, 0xdeadbeef" in out[4] - assert "mov ecx, 0x10" in out[5] - assert "syscall" in out[6] - - def assert_att(out): - assert "mov movl $0, %eax" not in out[2] - assert "mov movl $0x1337, %edi" not in out[3] - assert "mov movl $0xdeadbeef, %esi" not in out[4] - assert "mov movl $0x10, %ecx" not in out[5] - assert "syscall" in out[6] - - out = gdb.execute("context disasm", to_string=True).split("\n") - assert out[0] == "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA" - assert ( - out[1] == "──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────" - ) - assert_intel(out) - - gdb.execute("set disassembly-flavor att") - assert out[0] == "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA" - assert ( - out[1] == "──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────" - ) - assert_att(out) - - -@pytest.mark.parametrize("patch_or_api", (True, False)) -def test_context_disasm_proper_render_on_mem_change_issue_1818(start_binary, patch_or_api): - start_binary(SYSCALLS_BINARY) - - old = gdb.execute("context disasm", to_string=True).split("\n") - - # Just a sanity check - assert old[0] == "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA" - assert "mov eax, 0" in old[2] - assert "mov edi, 0x1337" in old[3] - assert "mov esi, 0xdeadbeef" in old[4] - assert "mov ecx, 0x10" in old[5] - assert "syscall" in old[6] - - # 5 bytes because 'mov eax, 0' is 5 bytes long - if patch_or_api: - gdb.execute("patch $rip nop;nop;nop;nop;nop", to_string=True) - else: - # Do the same, but through write API - pwndbg.aglib.memory.write(pwndbg.aglib.regs.pc, b"\x90" * 5) - - # Actual test: we expect the read memory to be different now ;) - # (and not e.g. returned incorrectly from a not cleared cache) - new = gdb.execute("context disasm", to_string=True).split("\n") - - assert new[0] == "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA" - assert "nop" in new[2] - assert "nop" in new[3] - assert "nop" in new[4] - assert "nop" in new[5] - assert "nop" in new[6] - assert "mov edi, 0x1337" in new[7] - assert "mov esi, 0xdeadbeef" in new[8] - assert "mov ecx, 0x10" in new[9] - assert "syscall" in new[10] - - -ONE_GADGET_BINARY = get_binary("onegadget.x86-64.out") - - -def test_context_disasm_fsbase_annotations(start_binary): - """ - This test checks that fsbase support in annotations is working properly. - - If this breaks, either our x86 memory operand parser is broken, we cannot fetch fsbase, or we are not passing FSBASE to Unicorn. - See: https://github.com/pwndbg/pwndbg/pull/2317 - - For this test, we use a binary we know has a stack canary. - Between compilations and between x86 vs x86_64, the exact instruction changes, but matches a regex pattern. - - """ - start_binary(ONE_GADGET_BINARY) - - gdb.execute("b break_here") - gdb.execute("c") - - # In view, there should now be the fs/gs memory reference - output = gdb.execute("context disasm", to_string=True).split("\n") - - pattern = re.compile(r"\b(mov|sub)\s+\w+,\s+(qword|dword)\s+ptr\s+(gs|fs):\[0x[0-9a-f]+\]") - found = False - for line in output: - if pattern.search(line): - found = True - break - - assert found - - -LONG_FUNCTION_X64_BINARY = get_binary("long_function.x86-64.out") - - -def test_context_disasm_call_instruction_split(start_binary): - """ - This checks for the following scenario: - We are on a `call` instruction, and `si` to enter the function. Then, we do `fin` to return to the caller. - There should be a split in the disassembly after the call instruction. - """ - - start_binary(LONG_FUNCTION_X64_BINARY) - - gdb.execute("start") - # Call ctx so instructions get disassembled and cached - gdb.execute("ctx") - - gdb.execute("si") - gdb.execute("fin") - - dis = gdb.execute("context disasm", to_string=True) - dis = pwndbg.color.strip(dis) - - expected = ( - "LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA\n" - "──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────\n" - " 0x400080 <_start> call function \n" - " \n" - " ► 0x400085 <_start+5> mov eax, 2 EAX => 2\n" - " 0x40008a <_start+10> mov ebx, 3 EBX => 3\n" - " 0x40008f <_start+15> add rax, rbx RAX => 5 (2 + 3)\n" - " 0x400092 <_start+18> xor rax, rbx RAX => 6 (5 ^ 3)\n" - " 0x400095 <_start+21> nop \n" - " 0x400096 <_start+22> jmp exit \n" - " ↓\n" - " 0x4000ab mov eax, 0x3c EAX => 0x3c\n" - " 0x4000b0 mov edi, 0 EDI => 0\n" - " 0x4000b5 syscall \n" - " 0x4000b7 add byte ptr [rax], al\n" - "────────────────────────────────────────────────────────────────────────────────\n" - ) - - assert dis == expected - - -def test_context_hide_sections(start_binary): - start_binary(SYSCALLS_BINARY) - - # Disable one section - out = gdb.execute("context", to_string=True) - assert "REGISTERS" in out - assert "STACK" in out - gdb.execute("context regs --off") - out = gdb.execute("context", to_string=True) - assert "REGISTERS" not in out - assert "STACK" in out - gdb.execute("context regs --on") - out = gdb.execute("context", to_string=True) - assert "REGISTERS" in out - assert "STACK" in out - - # Disable multiple sections - gdb.execute("context stack disasm --off") - out = gdb.execute("context", to_string=True) - assert "STACK" not in out - assert "DISASM" not in out - gdb.execute("context stack --on") - out = gdb.execute("context", to_string=True) - assert "STACK" in out - assert "DISASM" not in out - gdb.execute("context stack disasm --on") - out = gdb.execute("context", to_string=True) - assert "STACK" in out - assert "DISASM" in out - - # Disable all sections at once - gdb.execute("context --off") - out = gdb.execute("context", to_string=True) - assert len(out) == 0 - gdb.execute("context --on") - out = gdb.execute("context", to_string=True) - assert "REGISTERS" in out - assert "DISASM" in out - - -def test_context_history_prev_next(start_binary): - start_binary(LONG_FUNCTION_X64_BINARY) - - # Add two context outputs to the history - first_ctx = gdb.execute("ctx", to_string=True) - gdb.execute("si") - second_ctx = gdb.execute("ctx", to_string=True) - assert first_ctx != second_ctx - - # Go back to the first context - gdb.execute("contextprev") - history_ctx = gdb.execute("ctx", to_string=True) - assert first_ctx == history_ctx.replace(" (history 1/2)", "") - assert "(history 1/2)" in history_ctx - - # Go to the second context again - gdb.execute("contextnext") - history_ctx = gdb.execute("ctx", to_string=True) - assert second_ctx == history_ctx.replace(" (history 2/2)", "") - assert "(history 2/2)" in history_ctx - - # Make sure new events are displayed right away - # and disable the history scroll. - gdb.execute("si") - # Execute twice since the prompt hook isn't installed in tests - # which causes the legend to still have the (history 2/2) string at first. - gdb.execute("ctx", to_string=True) - third_ctx = gdb.execute("ctx", to_string=True) - assert history_ctx != third_ctx - assert "(history " not in third_ctx - - # Check if cwatch expressions are also stored in the history - gdb.execute("cwatch $rip") - gdb.execute("cwatch execute 'p/z $rsp'") - fourth_ctx = gdb.execute("ctx", to_string=True) - assert "1: $rip = " in fourth_ctx - assert "2: p/z $rsp\n$1 = 0x" in fourth_ctx - - # The next context shows a different output variable $2 - gdb.execute("si") - fifth_ctx = gdb.execute("ctx", to_string=True) - assert "1: $rip = " in fifth_ctx - assert "2: p/z $rsp\n$2 = 0x" in fifth_ctx - - # Check that the expression section shows the old gdb variable $1 again. - gdb.execute("contextprev") - history_ctx = gdb.execute("ctx", to_string=True) - assert "1: $rip = " in history_ctx - assert "2: p/z $rsp\n$1 = 0x" in history_ctx - - gdb.execute("cunwatch 2") - gdb.execute("cunwatch 1") - - -def test_context_history_search(start_binary): - start_binary(REFERENCE_BINARY) - - gdb.execute("break main") - gdb.execute("break break_here") - - gdb.execute("starti") - gdb.execute("context") - gdb.execute("continue") - gdb.execute("context") - gdb.execute("continue") - gdb.execute("context") - - for _ in range(5): - gdb.execute("ni") - gdb.execute("context") - - # Search for something in the past - search_result = gdb.execute("contextsearch puts@plt", to_string=True) - assert "Found 1 match. Selected entry 2 for match in section 'disasm'." in search_result - - # Search for something that happened later and have the search wrap around - search_result = gdb.execute("contextsearch 'Hello World'", to_string=True) - assert "No more matches before the current entry. Starting from the top." in search_result - assert "Found 7 matches. Selected entry 8 for match in section " in search_result - search_result = gdb.execute("contextsearch 'Hello World'", to_string=True) - assert "Found 7 matches. Selected entry 7 for match in section " in search_result - - # Select a section to search in - search_result = gdb.execute("contextsearch 'Hello World' disasm", to_string=True) - assert "Found 1 match. Selected entry 2 for match in section 'disasm'." in search_result - - # Search for something that doesn't exist - search_result = gdb.execute("contextsearch 'nonexistent'", to_string=True) - assert "String 'nonexistent' not found in context history." in search_result - - # Search in non-existing section - search_result = gdb.execute("ctxsearch 'Hello World' nonexistent", to_string=True) - assert "Section 'nonexistent' not found in context history." in search_result - - -def test_context_output_redirection(start_binary): - start_binary(REFERENCE_BINARY) - - # Test CallOutput redirection - def receive_output(output): - receive_output.context_output = output - - receive_output.context_output = "" - - pwndbg.commands.context.contextoutput( - "regs", - receive_output, - clearing=True, - banner="top", - width=80, - ) - - gdb.execute("start") - - out = gdb.execute("ctx", to_string=True) - assert "REGISTERS" not in out - assert "STACK" in out - assert "REGISTERS" in receive_output.context_output - assert "STACK" not in receive_output.context_output - - pwndbg.commands.context.resetcontextoutput("regs")