From 2f19e96f4927d9d047915de6670e47616bb5218c Mon Sep 17 00:00:00 2001 From: Rachit Kumar Pandey <66690593+ArmoredVortex@users.noreply.github.com> Date: Mon, 28 Jul 2025 15:53:02 +0530 Subject: [PATCH] feat(cyclic): Add --detect flag in cyclic command (#3162) * feat(cyclic): Add --detect flag to find patterns in registers * regenerate docs * Update pwndbg/commands/cyclic.py * add tests for `cyclic --detect` * Add timeout argument for --detect * update docs --------- Co-authored-by: Disconnect3d --- docs/commands/misc/cyclic.md | 5 +- pwndbg/commands/cyclic.py | 96 ++++++++++++++++++- .../library/gdb/tests/test_command_cyclic.py | 62 ++++++++++++ 3 files changed, 161 insertions(+), 2 deletions(-) diff --git a/docs/commands/misc/cyclic.md b/docs/commands/misc/cyclic.md index e11e2d160..48cbe4c66 100644 --- a/docs/commands/misc/cyclic.md +++ b/docs/commands/misc/cyclic.md @@ -2,7 +2,8 @@ # cyclic ```text -usage: cyclic [-h] [-a charset] [-n length] [-l lookup_value] +usage: cyclic [-h] [-a charset] [-n length] [-t seconds] [-l lookup_value] + [-d] [count] [filename] ``` @@ -22,7 +23,9 @@ Cyclic pattern creator/finder. |-h|--help|show this help message and exit| |-a|--alphabet|The alphabet to use in the cyclic pattern (default: abcdefghijklmnopqrstuvwxyz)| |-n|--length|Size of the unique subsequences (defaults to the pointer size for the current arch)| +|-t|--timeout|Timeout in seconds for --detect (default: 2)| |-o|--lookup|Do a lookup instead of printing the sequence (accepts constant values as well as expressions)| +|-d|--detect|Detect cyclic patterns in registers (Immediate values and memory pointed to by registers)| diff --git a/pwndbg/commands/cyclic.py b/pwndbg/commands/cyclic.py index bd5ed69d0..6fe75729c 100644 --- a/pwndbg/commands/cyclic.py +++ b/pwndbg/commands/cyclic.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import signal import string from typing import Optional @@ -8,10 +9,82 @@ from pwnlib.util.cyclic import cyclic from pwnlib.util.cyclic import cyclic_find import pwndbg.aglib.arch +import pwndbg.aglib.memory +import pwndbg.aglib.proc import pwndbg.commands +import pwndbg.lib.regs from pwndbg.color import message from pwndbg.commands import CommandCategory + +class TimeoutException(Exception): + """Custom exception for signal-based timeouts.""" + + pass + + +def detect_register_patterns(alphabet, length, timeout) -> None: + if not pwndbg.aglib.proc.alive: + print(message.error("Error: Process is not running.")) + return + + ptr_size = pwndbg.aglib.arch.ptrsize + endian = pwndbg.aglib.arch.endian + found_patterns = [] + + def alarm_handler(signum, frame): + raise TimeoutException + + original_handler = signal.signal(signal.SIGALRM, alarm_handler) + + current_arch_name = pwndbg.aglib.arch.name + register_set = pwndbg.lib.regs.reg_sets[current_arch_name] + all_register_names = register_set.all + + for reg_name in all_register_names: + value = pwndbg.aglib.regs[reg_name] + if value is None: + continue + + try: + signal.alarm(timeout) + value_bytes = value.to_bytes(ptr_size, endian) + offset = cyclic_find(value_bytes, alphabet=alphabet, n=length) + if offset != -1: + found_patterns.append((reg_name, value, offset)) + except TimeoutException: + found_patterns.append((reg_name, value, "SKIPPED (Timeout)")) + finally: + signal.alarm(0) + + if pwndbg.aglib.memory.is_readable_address(value): + try: + signal.alarm(timeout) + mem_value = pwndbg.aglib.memory.read(value, length) + offset = cyclic_find(mem_value, alphabet=alphabet, n=length) + if offset != -1: + found_patterns.append((f"{reg_name}->", value, offset)) + except TimeoutException: + found_patterns.append((f"{reg_name}->", value, "")) + finally: + signal.alarm(0) + + # Restore the original signal handler + signal.signal(signal.SIGALRM, original_handler) + + if not found_patterns: + print(message.notice("No cyclic patterns found.")) + return + + max_reg_width = 2 + max(max(len(reg) for reg, _, _ in found_patterns), 10) + max_val_width = 2 + max(len(hex(val)) for _, val, _ in found_patterns) + + print(f"{'Register':<{max_reg_width}} {'Value':<{max_val_width}} {'Offset'}") + print(f"{'----------':<{max_reg_width}} {'------------------':<{max_val_width}} {'------'}") + for reg, val, off in found_patterns: + print(f"{reg:<{max_reg_width}} {val:<#{max_val_width}x} {off}") + + parser = argparse.ArgumentParser(description="Cyclic pattern creator/finder.") parser.add_argument( @@ -31,6 +104,14 @@ parser.add_argument( help="Size of the unique subsequences (defaults to the pointer size for the current arch)", ) +parser.add_argument( + "-t", + "--timeout", + metavar="seconds", + type=int, + default=2, + help="Timeout in seconds for --detect", +) group = parser.add_mutually_exclusive_group(required=False) group.add_argument( @@ -44,6 +125,13 @@ group.add_argument( help="Do a lookup instead of printing the sequence (accepts constant values as well as expressions)", ) +group.add_argument( + "-d", + "--detect", + action="store_true", + help="Detect cyclic patterns in registers (Immediate values and memory pointed to by registers)", +) + group.add_argument( "count", type=int, @@ -61,10 +149,16 @@ parser.add_argument( @pwndbg.commands.Command(parser, command_name="cyclic", category=CommandCategory.MISC) -def cyclic_cmd(alphabet, length: Optional[int], lookup, count=100, filename="") -> None: +def cyclic_cmd( + alphabet, length: Optional[int], lookup, detect, count=100, filename="", timeout=2 +) -> None: if length is None: length = pwndbg.aglib.arch.ptrsize + if detect: + detect_register_patterns(alphabet, length, timeout) + return + if lookup: lookup = pwndbg.commands.fix(lookup, sloppy=True) diff --git a/tests/library/gdb/tests/test_command_cyclic.py b/tests/library/gdb/tests/test_command_cyclic.py index e24f509d0..9923af0b6 100644 --- a/tests/library/gdb/tests/test_command_cyclic.py +++ b/tests/library/gdb/tests/test_command_cyclic.py @@ -82,3 +82,65 @@ def test_command_cyclic_wrong_length(): assert out == ( "Lookup pattern must be 4 bytes (use `-n ` to lookup pattern of different length)\n" ) + + +def test_command_cyclic_detect(start_binary): + """ + Tests the `cyclic --detect` command for: + 1. A direct value in a register. + 2. A pointer to a value on the stack. + 3. A value from a custom alphabet. + """ + start_binary(REFERENCE_BINARY) + + ptr_size = pwndbg.aglib.arch.ptrsize + endian = pwndbg.aglib.arch.endian + + offset_rax = 20 + pattern_default = cyclic(length=100, n=ptr_size) + value_rax = int.from_bytes(pattern_default[offset_rax : offset_rax + ptr_size], endian) + pwndbg.aglib.regs.rax = value_rax + + offset_rbx_ptr = 40 + stack_addr = pwndbg.aglib.regs.rsp + pwndbg.aglib.memory.write( + stack_addr, pattern_default[offset_rbx_ptr : offset_rbx_ptr + ptr_size] + ) + pwndbg.aglib.regs.rbx = stack_addr + + offset_rcx = 15 + alphabet_custom = b"0123456789ABCDEF" + pattern_custom = cyclic(length=100, n=ptr_size, alphabet=alphabet_custom) + value_rcx = int.from_bytes(pattern_custom[offset_rcx : offset_rcx + ptr_size], endian) + pwndbg.aglib.regs.rcx = value_rcx + + out_default = gdb.execute("cyclic --detect", to_string=True) + + out_custom = gdb.execute(f"cyclic --detect -a {alphabet_custom.decode()}", to_string=True) + + results_default = { + parts[0]: int(parts[-1]) + for line in out_default.strip().split("\n")[2:] # Skip header lines + if (parts := line.split()) + } + + results_custom = { + parts[0]: int(parts[-1]) + for line in out_custom.strip().split("\n")[2:] # Skip header lines + if (parts := line.split()) + } + + assert "rax" in results_default, "Pattern in RAX not detected" + assert ( + results_default["rax"] == offset_rax + ), f"Incorrect offset for RAX: Got {results_default['rax']}, expected {offset_rax}" + + assert "rbx->" in results_default, "Pattern pointed to by RBX not detected" + assert ( + results_default["rbx->"] == offset_rbx_ptr + ), f"Incorrect offset for RBX->: Got {results_default['rbx->']}, expected {offset_rbx_ptr}" + + assert "rcx" in results_custom, "Pattern in RCX with custom alphabet not detected" + assert ( + results_custom["rcx"] == offset_rcx + ), f"Incorrect offset for RCX: Got {results_custom['rcx']}, expected {offset_rcx}"