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 <dominik.b.czarnota@gmail.com>
pull/3184/head
Rachit Kumar Pandey 4 months ago committed by GitHub
parent 12237f4c0b
commit 2f19e96f49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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)|
<!-- END OF AUTOGENERATED PART. Do not modify this line or the line below, they mark the end of the auto-generated part of the file. If you want to extend the documentation in a way which cannot easily be done by adding to the command help description, write below the following line. -->
<!-- ------------\>8---- ----\>8---- ----\>8------------ -->

@ -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, "<Timeout>"))
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)

@ -82,3 +82,65 @@ def test_command_cyclic_wrong_length():
assert out == (
"Lookup pattern must be 4 bytes (use `-n <length>` 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}"

Loading…
Cancel
Save