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 # cyclic
```text ```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] [count] [filename]
``` ```
@ -22,7 +23,9 @@ Cyclic pattern creator/finder.
|-h|--help|show this help message and exit| |-h|--help|show this help message and exit|
|-a|--alphabet|The alphabet to use in the cyclic pattern (default: abcdefghijklmnopqrstuvwxyz)| |-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)| |-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)| |-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. --> <!-- 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------------ --> <!-- ------------\>8---- ----\>8---- ----\>8------------ -->

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import signal
import string import string
from typing import Optional from typing import Optional
@ -8,10 +9,82 @@ from pwnlib.util.cyclic import cyclic
from pwnlib.util.cyclic import cyclic_find from pwnlib.util.cyclic import cyclic_find
import pwndbg.aglib.arch import pwndbg.aglib.arch
import pwndbg.aglib.memory
import pwndbg.aglib.proc
import pwndbg.commands import pwndbg.commands
import pwndbg.lib.regs
from pwndbg.color import message from pwndbg.color import message
from pwndbg.commands import CommandCategory 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 = argparse.ArgumentParser(description="Cyclic pattern creator/finder.")
parser.add_argument( 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)", 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 = parser.add_mutually_exclusive_group(required=False)
group.add_argument( 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)", 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( group.add_argument(
"count", "count",
type=int, type=int,
@ -61,10 +149,16 @@ parser.add_argument(
@pwndbg.commands.Command(parser, command_name="cyclic", category=CommandCategory.MISC) @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: if length is None:
length = pwndbg.aglib.arch.ptrsize length = pwndbg.aglib.arch.ptrsize
if detect:
detect_register_patterns(alphabet, length, timeout)
return
if lookup: if lookup:
lookup = pwndbg.commands.fix(lookup, sloppy=True) lookup = pwndbg.commands.fix(lookup, sloppy=True)

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