diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 42488816..fd7add53 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -19,7 +19,7 @@ jobs:
timeout-minutes: 5
strategy:
matrix:
- python-version: ["3.12"]
+ python-version: ["3.13"]
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 65b66948..c812e01f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -19,15 +19,17 @@ jobs:
timeout-minutes: 15
strategy:
matrix:
- python-version: ["3.10", "3.12"]
+ python-version: ["3.10", "3.12", "3.13"]
+ arch: ["i386", "amd64"]
steps:
- uses: actions/checkout@v4
- name: Install native dependencies
run: |
+ sudo dpkg --add-architecture i386
sudo apt-get update
- sudo apt-get install -y --no-install-recommends libdwarf-dev libelf-dev libiberty-dev linux-headers-generic libc6-dbg
+ sudo apt-get install -y --no-install-recommends libdwarf-dev libelf-dev libiberty-dev linux-headers-generic libc6-dbg libc6-dbg:i386
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
@@ -47,4 +49,4 @@ jobs:
- name: Test with pytest
run: |
- cd test/amd64 && pytest --ignore=other_tests
+ cd test && PLATFORM=${{ matrix.arch }} python -m pytest --ignore=other_tests
diff --git a/.gitignore b/.gitignore
index 1b07472a..6bed1273 100644
--- a/.gitignore
+++ b/.gitignore
@@ -123,6 +123,7 @@ venv.bak/
# mkdocs documentation
/site
+docs/from_pydoc/
# mypy
.mypy_cache/
diff --git a/README.md b/README.md
index 66f29519..426dad10 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ With libdebug you have full control of the flow of your debugged executable. Wit
- Catch and hijack signals
- Debug multithreaded applications with ease
- Seamlessly switch to GDB for interactive analysis
-- Soon to be multiarch (currently only supports Linux AMD64)
+- Multiarch: currently supports Linux AMD64 and AArch64 and i386 (both native and in 32-bit compatibility mode)
When running the same executable multiple times, choosing efficient implementations can make the difference. For this reason, libdebug prioritizes performance.
@@ -62,8 +62,11 @@ d.cont()
# Print RAX
print(f"RAX is {hex(d.regs.rax)}")
-# Kill the process
-d.kill()
+# Write to memory
+d.memory[0x10ad, 8, "binary"] = b"Hello!\x00\x00"
+
+# Continue the execution
+d.cont()
```
The above script will run the binary `test` in the working directory and stop at the function corresponding to the symbol "function". It will then print the value of the RAX register and kill the process.
@@ -75,7 +78,9 @@ There is so much more that can be done with libdebug. Please read the [documenta
libdebug offers many advanced features. Take a look at this script doing magic with signals:
```python
-from libdebug import debugger
+from libdebug import debugger, libcontext
+
+libcontext.terminal = ['tmux', 'splitw', '-h']
# Define signal catchers
def catcher_SIGUSR1(t: ThreadContext, catcher: SignalCatcher) -> None:
@@ -88,6 +93,9 @@ def catcher_SIGINT(t: ThreadContext, catcher: SignalCatcher) -> None:
def catcher_SIGPIPE(t: ThreadContext, catcher: SignalCatcher) -> None:
print(f"SIGPIPE: Signal number {catcher}")
+def handler_geteuid(t: ThreadContext, handler: SyscallHandler) -> None:
+ t.regs.rax = 0x0
+
# Initialize the debugger
d = debugger('/path/to/executable', continue_to_binary_entrypoint=False, aslr=False)
@@ -103,6 +111,8 @@ d.hijack_signal("SIGINT", "SIGPIPE", recursive=True)
# Define which signals to block
d.signals_to_block = ["SIGPOLL", "SIGIO", "SIGALRM"]
+d.handle_syscall("geteuid", on_exit=handler_geteuid)
+
# Continue execution
d.cont()
@@ -114,6 +124,46 @@ catcher3.disable()
bp = d.breakpoint(0xdeadc0de, hardware=True)
d.cont()
+d.wait()
+
+d.gdb()
+```
+
+## Auto Interrupt on Command
+libdebug also allows you to make all commands execute as soon as possible, without having to wait for a stopping event. To enable this mode, you can use the `auto_interrupt_on_command=True`
+
+```python
+from libdebug import debugger
+
+d = debugger("/path/to/executable", auto_interrupt_on_command=True)
+
+pipes = d.run()
+
+bp = d.breakpoint("function")
+
+d.cont()
+
+# Read shortly after the cont is issued
+# The process is forcibly stopped to read the register
+value = d.regs.rax
+print(f"RAX is {hex(value)}")
+
+system_offset = d.symbols.filter("system")[0].start
+libc_base = d.maps.filter("libc")[0].base
+
+system_address = libc_base + system_offset
+
+d.memory[0x12ebe, 8, "libc"] = int.to_bytes(system_address, 8, "little")
+
+d.cont()
+d.wait()
+
+# Here we should be at the breakpoint
+
+# This value is read while the process is stopped at the breakpoint
+ip_value = d.regs.rip
+
+print(f"RIP is {hex(ip_value)}")
d.kill()
```
diff --git a/docs/Makefile b/docs/Makefile
deleted file mode 100644
index d0c3cbf1..00000000
--- a/docs/Makefile
+++ /dev/null
@@ -1,20 +0,0 @@
-# Minimal makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line, and also
-# from the environment for the first two.
-SPHINXOPTS ?=
-SPHINXBUILD ?= sphinx-build
-SOURCEDIR = source
-BUILDDIR = build
-
-# Put it first so that "make" without argument is like "make help".
-help:
- @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-
-.PHONY: help Makefile
-
-# Catch-all target: route all unknown targets to Sphinx using the new
-# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
-%: Makefile
- @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/assets/backtrace_plot.png b/docs/assets/backtrace_plot.png
new file mode 100644
index 00000000..f2087fa8
Binary files /dev/null and b/docs/assets/backtrace_plot.png differ
diff --git a/docs/assets/backtrace_plot_dark.png b/docs/assets/backtrace_plot_dark.png
new file mode 100644
index 00000000..edb6811d
Binary files /dev/null and b/docs/assets/backtrace_plot_dark.png differ
diff --git a/docs/source/_static/favicon.ico b/docs/assets/favicon.ico
similarity index 100%
rename from docs/source/_static/favicon.ico
rename to docs/assets/favicon.ico
diff --git a/docs/assets/hijack-dark.webp b/docs/assets/hijack-dark.webp
new file mode 100644
index 00000000..69758f5a
Binary files /dev/null and b/docs/assets/hijack-dark.webp differ
diff --git a/docs/assets/hijack.webp b/docs/assets/hijack.webp
new file mode 100644
index 00000000..6b73f579
Binary files /dev/null and b/docs/assets/hijack.webp differ
diff --git a/docs/assets/libdebug_header.webp b/docs/assets/libdebug_header.webp
new file mode 100644
index 00000000..a96d3125
Binary files /dev/null and b/docs/assets/libdebug_header.webp differ
diff --git a/docs/assets/libdebug_logo.webp b/docs/assets/libdebug_logo.webp
new file mode 100644
index 00000000..93d95396
Binary files /dev/null and b/docs/assets/libdebug_logo.webp differ
diff --git a/docs/assets/libdebug_logo_horiz_dark.webp b/docs/assets/libdebug_logo_horiz_dark.webp
new file mode 100644
index 00000000..9b25af9c
Binary files /dev/null and b/docs/assets/libdebug_logo_horiz_dark.webp differ
diff --git a/docs/assets/libdebug_logo_horiz_light.webp b/docs/assets/libdebug_logo_horiz_light.webp
new file mode 100644
index 00000000..987ca03b
Binary files /dev/null and b/docs/assets/libdebug_logo_horiz_light.webp differ
diff --git a/docs/assets/pipe_logging.jpeg b/docs/assets/pipe_logging.jpeg
new file mode 100644
index 00000000..0f6643dd
Binary files /dev/null and b/docs/assets/pipe_logging.jpeg differ
diff --git a/docs/assets/pprint_backtrace.jpeg b/docs/assets/pprint_backtrace.jpeg
new file mode 100644
index 00000000..d4ce0b82
Binary files /dev/null and b/docs/assets/pprint_backtrace.jpeg differ
diff --git a/docs/assets/pprint_maps.jpeg b/docs/assets/pprint_maps.jpeg
new file mode 100644
index 00000000..afdac2ba
Binary files /dev/null and b/docs/assets/pprint_maps.jpeg differ
diff --git a/docs/assets/pprint_regs.jpeg b/docs/assets/pprint_regs.jpeg
new file mode 100644
index 00000000..dc83064a
Binary files /dev/null and b/docs/assets/pprint_regs.jpeg differ
diff --git a/docs/assets/pprint_syscalls.jpeg b/docs/assets/pprint_syscalls.jpeg
new file mode 100644
index 00000000..ae8fe760
Binary files /dev/null and b/docs/assets/pprint_syscalls.jpeg differ
diff --git a/docs/assets/sync_async.webp b/docs/assets/sync_async.webp
new file mode 100644
index 00000000..454c6123
Binary files /dev/null and b/docs/assets/sync_async.webp differ
diff --git a/docs/assets/sync_async_dark.webp b/docs/assets/sync_async_dark.webp
new file mode 100644
index 00000000..5976e79e
Binary files /dev/null and b/docs/assets/sync_async_dark.webp differ
diff --git a/docs/basics/command_queue.md b/docs/basics/command_queue.md
new file mode 100644
index 00000000..52f19022
--- /dev/null
+++ b/docs/basics/command_queue.md
@@ -0,0 +1,84 @@
+---
+icon: material/queue-first-in-last-out
+search:
+ boost: 4
+---
+# :material-queue-first-in-last-out: Default VS ASAP Mode
+For most commands that can be issued in **libdebug**, it is necessary that the traced process stops running. When the traced process stops running as a result of a [stopping event](../../stopping_events/stopping_events), **libdebug** can inspect the state and intervene in its control flow. When one of these commands is used in the script as the process is still running, **libdebug** will wait for the process to stop before executing the command.
+
+In the following example, the content of the `RAX` register is printed after the program hits the breakpoint or stops for any other reason:
+
+```python
+from libdebug import debugger
+
+d = debugger("program")
+d.run()
+
+d.breakpoint("func")
+
+d.cont()
+
+print(f"RAX: {hex(d.regs.rax)}")
+```
+
+!!! INFO "Script execution"
+ Please note that, after resuming execution of the tracee process, the script will continue to run. This means that the script will not wait for the process to stop before continuing with the rest of the script. If the next command is a **libdebug** command that requires the process to be stopped, the script will then wait for a [stopping event](../../stopping_events/stopping_events) before executing that command.
+
+In the following example, we make a similar scenario, but show how you can inspect the state of the process by arbitrarily stopping it in the default mode.
+
+```python
+d = debugger("program")
+
+d.run()
+
+d.breakpoint("func")
+
+d.cont()
+
+print(f"RAX: {hex(d.regs.rax)}") # (1)
+
+d.cont()
+d.interrupt() # (2)
+
+print(f"RAX: {hex(d.regs.rax)}") # (3)
+
+d.cont()
+
+[...]
+```
+
+1. This is the value of RAX at the breakpoint.
+2. Stop the process shortly after the process resumes.
+3. This is the value of RAX at the arbitrary stop (shortly after the breakpoint).
+
+## :material-run-fast: ASAP Mode
+If you want the command to be executed As Soon As Possible (ASAP) instead of waiting for a [stopping event](../../stopping_events/stopping_events), you can specify it when creating the [Debugger](../../from_pydoc/generated/debugger/debugger/) object. In this mode, the debugger will stop the process and issue the command as it runs your script without waiting. The following script has the same behavior as the previous one, using the corresponding option:
+
+```python
+d = debugger("program", auto_interrupt_on_command=True)
+
+d.run()
+
+d.breakpoint("func")
+
+d.cont()
+d.wait()
+
+print(f"RAX: {hex(d.regs.rax)}") # (1)
+
+d.cont()
+
+print(f"RAX: {hex(d.regs.rax)}") # (2)
+
+d.cont()
+
+[...]
+```
+
+1. This is the value of RAX at the breakpoint.
+2. This is the value of RAX shortly after the breakpoint. The process is forcibly stopped to read the register.
+
+For the sake of this example the `wait()` method is used to wait for the [stopping event](../../stopping_events/stopping_events) (in this case, a breakpoint). This enforces the syncronization of the execution to the stopping point that we want to reach. Read more about the `wait()` method in the section dedicated to [control flow](../control_flow) commands.
+
+!!! TIP "Pwning with **libdebug**"
+ Respectable pwners in the field find that the ASAP polling mode is particularly useful when writing exploits.
diff --git a/docs/basics/control_flow_commands.md b/docs/basics/control_flow_commands.md
new file mode 100644
index 00000000..5822d456
--- /dev/null
+++ b/docs/basics/control_flow_commands.md
@@ -0,0 +1,114 @@
+---
+icon: material/arrow-down-right
+search:
+ boost: 4
+---
+# :material-arrow-down-right: Control Flow Commands
+
+Control flow commands allow you to set step through the code, stop execution and resume it at your pleasure.
+
+## :material-ray-end-arrow: Stepping
+A basic feature of any debugger is the ability to step through the code. **libdebug** provides several methods to step, some of which will be familiar to users of other debuggers.
+
+### :material-debug-step-into: Single Step
+The `step()` command executes the instruction at the instruction pointer and stops the process. When possible, it uses the hardware single-step feature of the CPU for better performance.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.step()
+ ```
+
+### :material-debug-step-over: Next
+The `next()` command executes the current instruction at the instruction pointer and stops the process. If the instruction is a function call, it will execute the whole function and stop at the instruction following the call. In other debuggers, this command is known as "step over".
+
+Please note that the `next()` command resumes the execution of the program if the instruction is a function call. This means that the debugger can encounter [stopping events](../../stopping_events/stopping_events) in the middle of the function, causing the command to return before the function finishes.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.next()
+ ```
+
+!!! WARNING "Damn heuristics!"
+ The `next()` command uses heuristics to determine if the instruction is a function call and to find the stopping point. This means that the command may not work as expected in some cases (e.g. functions called with a jump, non-returning calls).
+
+### :material-debug-step-over::material-debug-step-over: Step Until
+
+The `step_until()` command executes single steps until a specific address is reached. Optionally, you can also limit steps to a maximum count (default value is -1, meaning no limit).
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.step_until(position, max_steps=-1, file='hybrid')
+ ```
+
+The file parameter can be used to specify the choice on relative addressing. Refer to the [memory access](../memory_access/#absolute-and-relative-addressing) section for more information on addressing modes.
+
+## :material-step-forward: Continuing
+
+The `cont()` command continues the execution.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.cont()
+ ```
+
+ For example, in the following script, **libdebug** will not wait for the process to stop before checking d.dead. To change this behavior, you can use the `wait()` command right after the `cont()`.
+ ```python
+ from libdebug import debugger
+
+ d = debugger("program_that_dies_tragically")
+
+ d.run()
+
+ d.cont()
+
+ if d.dead:
+ print("The program is dead!")
+
+ ```
+
+### :material-clock-alert-outline: The `wait()` Method
+
+The `wait()` command is likely the most important in **libdebug**. Loved by most and hated by many, it instructs the debugger to wait for a [stopping event](../../stopping_events/stopping_events) before continuing with the execution of the script.
+
+!!! ABSTRACT "Example"
+ In the following script, **libdebug** will wait for the process to stop before printing "provola".
+ ```python
+ from libdebug import debugger
+
+ d = debugger("program_that_dies_tragically")
+
+ d.run()
+
+ d.cont()
+ d.wait()
+
+ print("provola")
+ ```
+
+### :material-stop: Interrupt
+You can manually issue a stopping signal to the program using the `interrupt()` command. Clearly, this command is issued as soon as it is executed within the script.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.interrupt()
+ ```
+
+## :material-debug-step-out: Finish
+
+The `finish()` command continues execution until the current function returns or a breakpoint is hit. In other debuggers, this command is known as "step out".
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.finish(heuristic='backtrace')
+ ```
+
+!!! WARNING "Damn heuristics!"
+ The `finish()` command uses heuristics to determine the end of a function. While **libdebug** allows to choose the heuristic, it is possible that none of the available options work in some specific cases. (e.g. tail-calls, non-returning calls).
+
+### Available Heuristics
+The `finish()` command allows you to choose the heuristic to use. If you don't specify any, the `"backtrace"` heuristic will be used. The following heuristics are available:
+
+| Heuristic | Description |
+|-----------|-------------|
+| `backtrace` | The `backtrace` heuristic uses the return address on the function stack frame to determine the end of the function. This is the default heuristic but may fail in case of broken stack, rare execution flows, and obscure compiler optimizations. |
+| `step-mode` | The `step-mode` heuristic uses repeated single steps to execute instructions until a `ret` instruction is reached. Nested calls are handled, when the calling convention is respected. This heuristic is slower and may fail in case of rare execution flows and obscure compiler optimizations. |
diff --git a/docs/basics/detach_and_gdb.md b/docs/basics/detach_and_gdb.md
new file mode 100644
index 00000000..b97d4ab7
--- /dev/null
+++ b/docs/basics/detach_and_gdb.md
@@ -0,0 +1,83 @@
+---
+icon: material/location-exit
+search:
+ boost: 4
+---
+# :material-location-exit: Detach and GDB Migration
+
+In **libdebug**, you can detach from the debugged process and continue execution with the `detach()` method.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.detach()
+ ```
+
+!!! WARNING "Detaching from a running process"
+ Remember that detaching from a process is meant to be used when the process is stopped. If the process is running, the command will wait for a [stopping event](../../stopping_events/stopping_events). To forcibly stop the process, you can use the `interrupt()` method before migrating.
+
+## :simple-gnu: GDB Migration
+If at any time during your script you want to take a more traditional approach to debugging, you can seamlessly switch to [GDB](https://www.sourceware.org/gdb/). This will temporarily detach **libdebug** from the program and give you control over the program using GDB. Quitting GDB or using the `goback` command will return control to **libdebug**.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.gdb(
+ migrate_breakpoints: bool = True,
+ open_in_new_process: bool = True,
+ blocking: bool = True,
+ ) -> GdbResumeEvent:
+ ```
+
+| Parameter | Description |
+| --- | --- |
+| `migrate_breakpoints` | If set to `True`, **libdebug** will migrate the breakpoints to GDB. |
+| `open_in_new_process` | If set to `True`, **libdebug** will open GDB in a new process. |
+| `blocking` | If set to `True`, **libdebug** will wait for the user to terminate the GDB session to continue the script. |
+
+Setting the `blocking` to `False` is useful when you want to continue using the pipe interaction and other parts of your script as you take control of the debugging process.
+
+When `blocking` is set to `False`, the `gdb()` method will return a [GdbResumeEvent](../../from_pydoc/generated/data/gdb_resume_event/) object. This object can be used to wait for the GDB session to finish before continuing the script.
+
+!!! ABSTRACT "Example of using non-blocking GDB migration"
+ ```python
+ from libdebug import debugger
+ d = debugger("program")
+ pipe = d.run()
+
+ # Reach interesting point in the program
+ [...]
+
+ gdb_event = d.gdb(blocking = False)
+
+ pipe.sendline(b"dump interpret")
+
+ with open("dump.bin", "r") as f:
+ pipe.send(f.read())
+
+ gdb_event.join() # (1)
+
+ ```
+
+ 1. This will wait for the GDB session to finish before continuing the script.
+
+Please consider a few requirements when opening GDB in a new process. For this mode to work, **libdebug** needs to know which terminal emulator you are using. If not set, **libdebug** will try to detect this automatically. In some cases, detection may fail. You can manually set the terminal command in [libcontext](../../from_pydoc/generated/utils/libcontext). If instead of opening GDB in a new terminal window you want to use the current terminal, you can simply set the `open_in_new_process` parameter to `False`.
+
+!!! ABSTRACT "Example of setting the terminal with tmux"
+ ```python
+ from libdebug import libcontext
+
+ libcontext.terminal = ['tmux', 'splitw', '-h']
+ ```
+
+!!! WARNING "Migrating from a running process"
+ Remember that GDB Migration is meant to be used when the process is stopped. If the process is running, the command will wait for a [stopping event](../../stopping_events/stopping_events). To forcibly stop the process, you can use the `interrupt()` method before migrating.
+
+## :material-power: Graceful Termination
+If you are finished working with a [Debugger](../../from_pydoc/generated/debugger/debugger/) object and wish to deallocate it, you can terminate it using the `terminate()` command.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.terminate()
+ ```
+
+!!! WARNING "What happens to the running process?"
+ When you terminate a [Debugger](../../from_pydoc/generated/debugger/debugger/) object, the process is forcibly killed. If you wish to detach from the process and continue the execution before terminating the debugger, you should use the `detach()` command before.
\ No newline at end of file
diff --git a/docs/basics/kill_and_post_mortem.md b/docs/basics/kill_and_post_mortem.md
new file mode 100644
index 00000000..4f3fe068
--- /dev/null
+++ b/docs/basics/kill_and_post_mortem.md
@@ -0,0 +1,60 @@
+---
+icon: material/emoticon-dead-outline
+search:
+ boost: 4
+---
+
+# :material-emoticon-dead-outline: Process Death (and afterlife)
+The default behavior in **libdebug** is to kill the debugged process when the script exits. This is done to prevent the process from running indefinitely if the debugging script terminates or you forget to kill it manually. When creating a [Debugger](../../from_pydoc/generated/debugger/debugger/) object, you can set the `kill_on_exit` attribute to `False` to prevent this behavior:
+
+```python
+from libdebug import Debugger
+
+d = debugger("test", kill_on_exit=False)
+```
+
+You can also change this attribute in an existing [Debugger](../../from_pydoc/generated/debugger/debugger/) object at runtime:
+
+```python
+d.kill_on_exit = False
+```
+
+!!! INFO "Behavior when attaching to a process"
+ When debugging is initiated by attaching to an existing process, the `kill_on_exit` policy is enforced in the same way as when starting a new process.
+
+## :material-knife: Killing the Process
+
+You can kill the process any time the process is stopped using the `kill()` method:
+
+```python
+d.kill()
+```
+
+The method sends a `SIGKILL` signal to the process, which terminates it immediately. If the process is already dead, **libdebug** will throw an exception. When multiple threads are running, the `kill()` method will kill all threads under the parent process.
+
+!!! INFO "Process Stop"
+ The `kill()` method will not stop a running process, unless **libdebug** is operating in [ASAP Mode](../command_queue). Just like other commands, in the default mode, the `kill()` method will wait for the process to stop before executing.
+
+# Post Mortem Analysis
+You can check if the process is dead using the `dead` property:
+
+```python
+if not d.dead:
+ print("The process is not dead")
+else:
+ print("The process is dead")
+```
+
+!!! WARNING "The `running` property"
+ The [Debugger](../../from_pydoc/generated/debugger/debugger/) object also exposes the `running` property. This is not the opposite of `dead`. The `running` property is `True` when the process is not stopped and `False` otherwise. If execution was stopped by a [stopping event](../../stopping_events/stopping_events), the `running` property will be equal to `False`. However, in this case the process can still be alive.
+
+### Cause of Death
+Has your process passed away unexpectedly? We are sorry to hear that. If your process is indeed defunct, you can access the exit code and signal using `exit_code` and `exit_signal`. When there is no valid exit code or signal, these properties will return `None`.
+
+```python
+if d.dead:
+ print(f"The process exited with code {d.exit_code}")
+
+if d.dead:
+ print(f"The process exited with signal {d.exit_signal}")
+```
\ No newline at end of file
diff --git a/docs/basics/libdebug101.md b/docs/basics/libdebug101.md
new file mode 100644
index 00000000..38ae525a
--- /dev/null
+++ b/docs/basics/libdebug101.md
@@ -0,0 +1,88 @@
+---
+icon: material/school-outline
+search:
+ boost: 4
+---
+# :material-school-outline: **libdebug** 101
+Welcome to **libdebug**! When writing a script to debug a program, the first step is to create a [Debugger](../../from_pydoc/generated/debugger/debugger/) object. This object will be your main interface for debugging commands.
+
+```python
+from libdebug import debugger
+
+debugger = debugger(argv=["./program", "arg1", "arg2"]) # (1)
+```
+
+1. `argv` can either be a string (the name/path of the executable) or an array corresponding to the argument vector of the execution.
+
+!!! INFO "Am I already debugging?"
+ Creating a [Debugger](../../from_pydoc/generated/debugger/debugger/) object will not start the execution automatically. You can reuse the same debugger to iteratively run multiple instances of the program. This is particularly useful for smart bruteforcing or fuzzing scripts.
+
+ Performing debugger initialization each time is not required and can be expensive.
+
+ To run the executable, refer to [Running an Executable](../running_an_executable)
+
+### Environment
+Just as you would expect, you can also pass environment variables to the program using the `env` parameter. Here, the variables are passed as a string-string dictionary.
+
+```python
+from libdebug import debugger
+
+debugger = debugger("test", env = {"LD_PRELOAD": "musl_libc.so"})
+```
+
+### Address Space Layout Randomization (ASLR)
+Modern operating system kernels implement mitigations against predictable addresses in binary exploitation scenarios. One such feature is [ASLR](https://en.wikipedia.org/wiki/Address_space_layout_randomization), which randomizes the base address of mapped virtual memory pages (e.g., binary, libraries, stack). When debugging, this feature can become a nuisance for the user.
+
+By default, **libdebug** keeps ASLR enabled. The debugger `aslr` parameter can be used to change this behavior.
+
+```python
+from libdebug import debugger
+
+debugger = debugger("test", aslr=False)
+```
+
+### Binary Entry Point
+When a child process is spawned on the Linux kernel through the [`ptrace`](https://man7.org/linux/man-pages/man2/ptrace.2.html) system call, it is possible to trace it as soon as the loader has set up your executable. Debugging these first instructions inside the loader library is generally uninteresting.
+
+For this reason, the default behavior for **libdebug** is to continue until the binary entry point (1) is reached. When you need to start debugging from the very beginning, you can simply disable this behavior in the following way:
+{ .annotate }
+
+1. In Linux, the binary entry point corresponds to the `_start` / `__rt_entry` symbol in your binary executable. This function is the initial stub that calls the `main()` function in your executable, through a call to the standard library of your system (e.g., [`__libc_start_main`](https://refspecs.linuxbase.org/LSB_3.0.0/LSB-PDA/LSB-PDA/baselib---libc-start-main-.html), [`__rt_lib_init`](https://developer.arm.com/documentation/dui0475/m/the-c-and-c---library-functions-reference/--rt-entry))
+
+```python
+from libdebug import debugger
+
+debugger = debugger("test", continue_to_binary_entrypoint=False)
+```
+
+!!! WARNING "What the hell are you debugging?"
+ Please note that this feature assumes the binary is well-formed. If the ELF header is corrupt, the binary entrypoint will not be resolved correctly. As such, setting this parameter to `False` is a good practice when you don't want **libdebug** to rely on this information.
+
+### What else can I do?
+The [Debugger](../../from_pydoc/generated/debugger/debugger/) object has many more parameters it can take.
+!!! ABSTRACT "Function Signature"
+ ```python
+ debugger(
+ argv=[],
+ aslr=True,
+ env=None,
+ escape_antidebug=False,
+ continue_to_binary_entrypoint=True,
+ auto_interrupt_on_command=False,
+ fast_memory=False,
+ kill_on_exit=True
+ ) -> Debugger
+ ```
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| `argv` | `str` \| `list[str]` | Path to the binary or argv list |
+| `aslr` | `bool` | Whether to enable ASLR. Defaults to True. |
+| `env` | `dict[str, str]` | The environment variables to use. Defaults to the same environment of the parent process. |
+| `escape_antidebug` | `bool` | Whether to [automatically attempt to patch antidebugger detectors](../../quality_of_life/anti_debugging) based on `ptrace`. |
+| `continue_to_binary_entrypoint` | `bool` | Whether to automatically continue to the binary entrypoint. |
+| `auto_interrupt_on_command` | `bool` | Whether to run **libdebug** in [ASAP Mode](../command_queue). |
+| `fast_memory` | `bool` | Whether to use a [faster memory reading method](../memory_access#faster-memory-access). Defaults to False. |
+| `kill_on_exit` | `bool` | Whether to [kill the debugged process when the debugger exits](../kill_and_post_mortem). Defaults to True. |
+| **Return Value** |
+|[Debugger](../../from_pydoc/generated/debugger/debugger/) | `Debugger` | The debugger object |
\ No newline at end of file
diff --git a/docs/basics/memory_access.md b/docs/basics/memory_access.md
new file mode 100644
index 00000000..f20b3361
--- /dev/null
+++ b/docs/basics/memory_access.md
@@ -0,0 +1,139 @@
+---
+icon: fontawesome/solid/memory
+search:
+ boost: 4
+---
+# :fontawesome-solid-memory: Memory Access
+In **libdebug**, memory access is performed via the `memory` attribute of the [Debugger](../../from_pydoc/generated/debugger/debugger/) object or the [Thread Context](../../from_pydoc/generated/state/thread_context). When reading from memory, a [*bytes-like*](https://docs.python.org/3/glossary.html#term-bytes-like-object) object is returned. The following methods are available:
+
+=== "Single byte access"
+ Access a single byte of memory by providing the address as an integer.
+ ```python
+ d.memory[0x1000]
+ ```
+
+=== "Slice access"
+ Access a range of bytes by providing the start and end addresses as integers.
+ ```python
+ d.memory[0x1000:0x1010]
+ ```
+
+=== "Base and length"
+ Access a range of bytes by providing the base address and length as integers.
+ ```python
+ d.memory[0x1000, 0x10]
+ ```
+
+=== "Symbol access"
+ Access memory using a symbol name.
+ ```python
+ d.memory["function", 0x8]
+ ```
+
+ When specifying a symbol, you can also provide an offset. Contrary to what happens in GDB, the offset is always interpreted as hexadecimal.
+ ```python
+ d.memory["function+a8"]
+ ```
+
+=== "Symbol Range"
+ Access a range of bytes using a symbol name.
+ ```python
+ d.memory["function":"function+0f"]
+ ```
+ Please note that contrary to what happens in GDB, the offset is always interpreted as hexadecimal.
+
+---
+
+!!! INFO "Accessing memory with symbols"
+ Please note that, unless otherwise specified, symbols are resolved in the debugged binary only. To resolve symbols in shared libraries, you need to indicate it in the third parameter of the function.
+
+ ```python
+ d.memory["__libc_start_main", 0x8, "libc"]
+ ```
+
+Writing to memory works similarly. You can write a [*bytes-like*](https://docs.python.org/3/glossary.html#term-bytes-like-object) object to memory using the same addressing methods:
+
+```python
+d.memory[d.rsp, 0x10] = b"AAAAAAABC"
+d.memory["main_arena", 16, "libc"] = b"12345678"
+```
+
+!!! WARNING "Length/Slice when writing"
+ When writing to memory, slices and length are ignored in favor of the length of the specified [*bytes-like*](https://docs.python.org/3/glossary.html#term-bytes-like-object) object.
+
+ In the following example, only 4 bytes are written:
+
+ ```python
+ d.memory["main_arena", 50] = b"\x0a\xeb\x12\xfc"
+ ```
+
+## :material-relative-scale: Absolute and Relative Addressing
+
+Just like with symbols, memory addresses can also be accessed relative to a certain file base. **libdebug** uses `"hybrid"` addressing by default. This means it first attempts to resolve addresses as absolute. If the address does not correspond to an absolute one, it considers it relative to the base of the binary.
+
+You can use the third parameter of the memory access method to select the file you want to use as base (e.g., libc, ld, binary). If you want to force **libdebug** to use absolute addressing, you can specify `"absolute"` instead.
+
+!!! ABSTRACT "Examples of relative and absolute addressing"
+ ```python
+ # Absolute addressing
+ d.memory[0x7ffff7fcb200, 0x10, "absolute"]
+
+ # Hybrid addressing
+ d.memory[0x1000, 0x10, "hybrid"]
+
+ # Relative addressing
+ d.memory[0x1000, 0x10, "binary"]
+ d.memory[0x1000, 0x10, "libc"]
+ ```
+
+## :octicons-search-24: Searching inside Memory
+The `memory` attribute of the [Debugger](../../from_pydoc/generated/debugger/debugger/) object also allows you to search for specific values in the memory of the process. You can search for integers, strings, or [bytes-like](https://docs.python.org/3/glossary.html#term-bytes-like-object) objects.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.memory.find(
+ value: int | bytes | str,
+ file: str = "all",
+ start: int | None = None,
+ end: int | None = None,
+ ) -> list[int]:
+ ```
+
+**Parameters**:
+
+| Argument | Type | Description |
+| --- | --- | --- |
+| `value` | `int` \| `bytes` \| `str` | The value to search for. |
+| `file` | `str` | The backing file to search in (e.g, binary, libc, stack). |
+| `start` | `int` (optional) | The start address of the search (works with both relative and absolute). |
+| `end` | `int` (optional) | The end address of the search (works with both relative and absolute). |
+
+**Returns**:
+
+| Return | Type | Description |
+| --- | --- | --- |
+| `Addresses` | `list[int]` | List of memory addresses where the value was found. |
+
+!!! ABSTRACT "Usage Example"
+ ```python
+ bish_string_addr = d.memory.find("/bin/sh", file="libc")
+
+ value_address = d.memory.find(0x1234, file="stack", start=d.regs.rsp)
+ ```
+
+## :material-clock-fast: Faster Memory Access
+!!! EXAMPLE "Warning: This feature is Experimental!"
+ This feature is experimental and may not work as expected. Please report any issues you encounter [:octicons-issue-opened-24: here](https://github.com/libdebug/libdebug/issues).
+
+By default, **libdebug** reads and writes memory using the [`ptrace`](https://man7.org/linux/man-pages/man2/ptrace.2.html) system call interface. However, this is not the most efficient way to access memory and will likely be changed in future versions.
+
+To speed up memory access, you can already enable a faster system that relies on Linux's [procfs](https://docs.kernel.org/filesystems/proc.html). To use it, simply set the `fast_memory` parameter to `True` when creating the [Debugger](../../from_pydoc/generated/debugger/debugger/) object. You can also enable and disable this feature at runtime by accessing the debugger's attribute.
+
+=== "When creating the Debugger object"
+ ```python
+ d = debugger("test", fast_memory=True)
+ ```
+=== "At runtime"
+ ```python
+ d.fast_memory = True
+ ```
\ No newline at end of file
diff --git a/docs/basics/register_access.md b/docs/basics/register_access.md
new file mode 100644
index 00000000..f63f98be
--- /dev/null
+++ b/docs/basics/register_access.md
@@ -0,0 +1,402 @@
+---
+icon: material/hexadecimal
+search:
+ boost: 4
+---
+# :material-hexadecimal: Register Access
+**libdebug** offers a simple register access interface for supported architectures. Registers are accessible through the `regs` attribute of the [Debugger](../../from_pydoc/generated/debugger/debugger/) object or the [Thread Context](../../from_pydoc/generated/state/thread_context).
+
+!!! INFO "Multithreading"
+ In multi-threaded debugging, the `regs` attribute of the [Debugger](../../from_pydoc/generated/debugger/debugger/) object will return the registers of the main thread.
+
+The following is an example of how to interact with the `RAX` register in a debugger object on AMD64:
+
+| Operation | Code Snippet |
+| --------- | ----------------------------- |
+| Reading | `read_value = d.regs.rax` |
+| Writing | `d.regs.rax = read_value + 1` |
+
+Note that the register values are read and written as Python integers. This is true for all registers except for floating point ones, which are coherent with their type. To avoid confusion, we list available registers and their types below. Related registers are available to access as well.
+
+=== "AMD64"
+ | Register | Type | Related | Description |
+ |-----------|---------------|-----------------|---------------------------------------------------|
+ | **General Purpose** |
+ | RAX | Integer | EAX, AX, AH, AL | Accumulator register |
+ | RBX | Integer | EBX, BX, BH, BL | Base register |
+ | RCX | Integer | ECX, CX, CH, CL | Counter register |
+ | RDX | Integer | EDX, DX, DH, DL | Data register |
+ | RSI | Integer | ESI, SI | Source index for string operations |
+ | RDI | Integer | EDI, DI | Destination index for string operations |
+ | RBP | Integer | EBP, BP | Base pointer (frame pointer) |
+ | RSP | Integer | ESP, SP | Stack pointer |
+ | R8 | Integer | R8D, R8W, R8B | General-purpose register |
+ | R9 | Integer | R9D, R9W, R9B | General-purpose register |
+ | R10 | Integer | R10D, R10W, R10B| General-purpose register |
+ | R11 | Integer | R11D, R11W, R11B| General-purpose register |
+ | R12 | Integer | R12D, R12W, R12B| General-purpose register |
+ | R13 | Integer | R13D, R13W, R13B| General-purpose register |
+ | R14 | Integer | R14D, R14W, R14B| General-purpose register |
+ | R15 | Integer | R15D, R15W, R15B| General-purpose register |
+ | RIP | Integer | EIP | Instruction pointer |
+ | **Flags** |
+ | EFLAGS | Integer | | Flags register |
+ | **Segment Registers** |
+ | CS | Integer | | Code segment |
+ | DS | Integer | | Data segment |
+ | ES | Integer | | Extra segment |
+ | FS | Integer | | Additional segment |
+ | GS | Integer | | Additional segment |
+ | SS | Integer | | Stack segment |
+ | FS_BASE | Integer | | FS segment base address |
+ | GS_BASE | Integer | | GS segment base address |
+ | **Vector Registers** |
+ | XMM0 | Integer | | Lower 128 bits of YMM0/ZMM0 |
+ | XMM1 | Integer | | Lower 128 bits of YMM1/ZMM1 |
+ | XMM2 | Integer | | Lower 128 bits of YMM2/ZMM2 |
+ | XMM3 | Integer | | Lower 128 bits of YMM3/ZMM3 |
+ | XMM4 | Integer | | Lower 128 bits of YMM4/ZMM4 |
+ | XMM5 | Integer | | Lower 128 bits of YMM5/ZMM5 |
+ | XMM6 | Integer | | Lower 128 bits of YMM6/ZMM6 |
+ | XMM7 | Integer | | Lower 128 bits of YMM7/ZMM7 |
+ | XMM8 | Integer | | Lower 128 bits of YMM8/ZMM8 |
+ | XMM9 | Integer | | Lower 128 bits of YMM9/ZMM9 |
+ | XMM10 | Integer | | Lower 128 bits of YMM10/ZMM10 |
+ | XMM11 | Integer | | Lower 128 bits of YMM11/ZMM11 |
+ | XMM12 | Integer | | Lower 128 bits of YMM12/ZMM12 |
+ | XMM13 | Integer | | Lower 128 bits of YMM13/ZMM13 |
+ | XMM14 | Integer | | Lower 128 bits of YMM14/ZMM14 |
+ | XMM15 | Integer | | Lower 128 bits of YMM15/ZMM15 |
+ | YMM0 | Integer | | 256-bit AVX extension of XMM0 |
+ | YMM1 | Integer | | 256-bit AVX extension of XMM1 |
+ | YMM2 | Integer | | 256-bit AVX extension of XMM2 |
+ | YMM3 | Integer | | 256-bit AVX extension of XMM3 |
+ | YMM4 | Integer | | 256-bit AVX extension of XMM4 |
+ | YMM5 | Integer | | 256-bit AVX extension of XMM5 |
+ | YMM6 | Integer | | 256-bit AVX extension of XMM6 |
+ | YMM7 | Integer | | 256-bit AVX extension of XMM7 |
+ | YMM8 | Integer | | 256-bit AVX extension of XMM8 |
+ | YMM9 | Integer | | 256-bit AVX extension of XMM9 |
+ | YMM10 | Integer | | 256-bit AVX extension of XMM10 |
+ | YMM11 | Integer | | 256-bit AVX extension of XMM11 |
+ | YMM12 | Integer | | 256-bit AVX extension of XMM12 |
+ | YMM13 | Integer | | 256-bit AVX extension of XMM13 |
+ | YMM14 | Integer | | 256-bit AVX extension of XMM14 |
+ | YMM15 | Integer | | 256-bit AVX extension of XMM15 |
+ | ZMM0 | Integer | | 512-bit AVX-512 extension of XMM0 |
+ | ZMM1 | Integer | | 512-bit AVX-512 extension of XMM1 |
+ | ZMM2 | Integer | | 512-bit AVX-512 extension of XMM2 |
+ | ZMM3 | Integer | | 512-bit AVX-512 extension of XMM3 |
+ | ZMM4 | Integer | | 512-bit AVX-512 extension of XMM4 |
+ | ZMM5 | Integer | | 512-bit AVX-512 extension of XMM5 |
+ | ZMM6 | Integer | | 512-bit AVX-512 extension of XMM6 |
+ | ZMM7 | Integer | | 512-bit AVX-512 extension of XMM7 |
+ | ZMM8 | Integer | | 512-bit AVX-512 extension of XMM8 |
+ | ZMM9 | Integer | | 512-bit AVX-512 extension of XMM9 |
+ | ZMM10 | Integer | | 512-bit AVX-512 extension of XMM10 |
+ | ZMM11 | Integer | | 512-bit AVX-512 extension of XMM11 |
+ | ZMM12 | Integer | | 512-bit AVX-512 extension of XMM12 |
+ | ZMM13 | Integer | | 512-bit AVX-512 extension of XMM13 |
+ | ZMM14 | Integer | | 512-bit AVX-512 extension of XMM14 |
+ | ZMM15 | Integer | | 512-bit AVX-512 extension of XMM15 |
+ | **Floating Point (Legacy x87)** |
+ | ST(0)-ST(7)| Floating Point | | x87 FPU data registers |
+ | MM0-MM7 | Integer | | MMX registers |
+=== "i386"
+ | Register | Type | Related | Description |
+ |-----------|---------------|-----------------|---------------------------------------------------|
+ | **General Purpose** |
+ | EAX | Integer | AX, AH, AL | Accumulator register |
+ | EBX | Integer | BX, BH, BL | Base register |
+ | ECX | Integer | CX, CH, CL | Counter register |
+ | EDX | Integer | DX, DH, DL | Data register |
+ | ESI | Integer | SI | Source index for string operations |
+ | EDI | Integer | DI | Destination index for string operations |
+ | EBP | Integer | BP | Base pointer (frame pointer) |
+ | ESP | Integer | SP | Stack pointer |
+ | EIP | Integer | IP | Instruction pointer |
+ | **Flags** |
+ | EFLAGS | Integer | | Flags register |
+ | **Segment Registers** |
+ | CS | Integer | | Code segment |
+ | DS | Integer | | Data segment |
+ | ES | Integer | | Extra segment |
+ | FS | Integer | | Additional segment |
+ | GS | Integer | | Additional segment |
+ | SS | Integer | | Stack segment |
+ | **Floating Point Registers** |
+ | ST(0)-ST(7)| Floating Point | | x87 FPU data registers |
+ | **Vector Registers** |
+ | XMM0 | Integer | | Lower 128 bits of YMM0/ZMM0 |
+ | XMM1 | Integer | | Lower 128 bits of YMM1/ZMM1 |
+ | XMM2 | Integer | | Lower 128 bits of YMM2/ZMM2 |
+ | XMM3 | Integer | | Lower 128 bits of YMM3/ZMM3 |
+ | XMM4 | Integer | | Lower 128 bits of YMM4/ZMM4 |
+ | XMM5 | Integer | | Lower 128 bits of YMM5/ZMM5 |
+ | XMM6 | Integer | | Lower 128 bits of YMM6/ZMM6 |
+ | XMM7 | Integer | | Lower 128 bits of YMM7/ZMM7 |
+ | YMM0 | Integer | | 256-bit AVX extension of XMM0 |
+ | YMM1 | Integer | | 256-bit AVX extension of XMM1 |
+ | YMM2 | Integer | | 256-bit AVX extension of XMM2 |
+ | YMM3 | Integer | | 256-bit AVX extension of XMM3 |
+ | YMM4 | Integer | | 256-bit AVX extension of XMM4 |
+ | YMM5 | Integer | | 256-bit AVX extension of XMM5 |
+ | YMM6 | Integer | | 256-bit AVX extension of XMM6 |
+ | YMM7 | Integer | | 256-bit AVX extension of XMM7 |
+=== "AArch64"
+ | Register | Type | Alias(es) | Description |
+ |-----------|-----------------|------------------|--------------------------------------------------|
+ | **General Purpose** |
+ | X0 | Integer | W0 | Function result or argument |
+ | X1 | Integer | W1 | Function result or argument |
+ | X2 | Integer | W2 | Function result or argument |
+ | X3 | Integer | W3 | Function result or argument |
+ | X4 | Integer | W4 | Function result or argument |
+ | X5 | Integer | W5 | Function result or argument |
+ | X6 | Integer | W6 | Function result or argument |
+ | X7 | Integer | W7 | Function result or argument |
+ | X8 | Integer | W8 | Indirect result location (also called "IP0") |
+ | X9 | Integer | W9 | Temporary register |
+ | X10 | Integer | W10 | Temporary register |
+ | X11 | Integer | W11 | Temporary register |
+ | X12 | Integer | W12 | Temporary register |
+ | X13 | Integer | W13 | Temporary register |
+ | X14 | Integer | W14 | Temporary register |
+ | X15 | Integer | W15 | Temporary register (also called "IP1") |
+ | X16 | Integer | W16 | Platform Register (often used as scratch) |
+ | X17 | Integer | W17 | Platform Register (often used as scratch) |
+ | X18 | Integer | W18 | Platform Register |
+ | X19 | Integer | W19 | Callee-saved register |
+ | X20 | Integer | W20 | Callee-saved register |
+ | X21 | Integer | W21 | Callee-saved register |
+ | X22 | Integer | W22 | Callee-saved register |
+ | X23 | Integer | W23 | Callee-saved register |
+ | X24 | Integer | W24 | Callee-saved register |
+ | X25 | Integer | W25 | Callee-saved register |
+ | X26 | Integer | W26 | Callee-saved register |
+ | X27 | Integer | W27 | Callee-saved register |
+ | X28 | Integer | W28 | Callee-saved register |
+ | X29 | Integer | W29, FP | Frame pointer |
+ | X30 | Integer | W30, LR | Link register (return address) |
+ | XZR | Integer | WZR, ZR | Zero register (always reads as zero) |
+ | SP | Integer | | Stack pointer |
+ | PC | Integer | | Program counter |
+ | **Flags** |
+ | PSTATE | Integer | | [Processor state in exception handling](https://developer.arm.com/documentation/100933/0100/Processor-state-in-exception-handling) |
+ | **Vector Registers (SIMD/FP)** |
+ | V0 | Integer | | Vector or scalar register |
+ | V1 | Integer | | Vector or scalar register |
+ | V2 | Integer | | Vector or scalar register |
+ | V3 | Integer | | Vector or scalar register |
+ | V4 | Integer | | Vector or scalar register |
+ | V5 | Integer | | Vector or scalar register |
+ | V6 | Integer | | Vector or scalar register |
+ | V7 | Integer | | Vector or scalar register |
+ | V8 | Integer | | Vector or scalar register |
+ | V9 | Integer | | Vector or scalar register |
+ | V10 | Integer | | Vector or scalar register |
+ | V11 | Integer | | Vector or scalar register |
+ | V12 | Integer | | Vector or scalar register |
+ | V13 | Integer | | Vector or scalar register |
+ | V14 | Integer | | Vector or scalar register |
+ | V15 | Integer | | Vector or scalar register |
+ | V16 | Integer | | Vector or scalar register |
+ | V17 | Integer | | Vector or scalar register |
+ | V18 | Integer | | Vector or scalar register |
+ | V19 | Integer | | Vector or scalar register |
+ | V20 | Integer | | Vector or scalar register |
+ | V21 | Integer | | Vector or scalar register |
+ | V22 | Integer | | Vector or scalar register |
+ | V23 | Integer | | Vector or scalar register |
+ | V24 | Integer | | Vector or scalar register |
+ | V25 | Integer | | Vector or scalar register |
+ | V26 | Integer | | Vector or scalar register |
+ | V27 | Integer | | Vector or scalar register |
+ | V28 | Integer | | Vector or scalar register |
+ | V29 | Integer | | Vector or scalar register |
+ | V30 | Integer | | Vector or scalar register |
+ | V31 | Integer | | Vector or scalar register |
+ | Q0 | Integer | | Vector or scalar register |
+ | Q1 | Integer | | Vector or scalar register |
+ | Q2 | Integer | | Vector or scalar register |
+ | Q3 | Integer | | Vector or scalar register |
+ | Q4 | Integer | | Vector or scalar register |
+ | Q5 | Integer | | Vector or scalar register |
+ | Q6 | Integer | | Vector or scalar register |
+ | Q7 | Integer | | Vector or scalar register |
+ | Q8 | Integer | | Vector or scalar register |
+ | Q9 | Integer | | Vector or scalar register |
+ | Q10 | Integer | | Vector or scalar register |
+ | Q11 | Integer | | Vector or scalar register |
+ | Q12 | Integer | | Vector or scalar register |
+ | Q13 | Integer | | Vector or scalar register |
+ | Q14 | Integer | | Vector or scalar register |
+ | Q15 | Integer | | Vector or scalar register |
+ | Q16 | Integer | | Vector or scalar register |
+ | Q17 | Integer | | Vector or scalar register |
+ | Q18 | Integer | | Vector or scalar register |
+ | Q19 | Integer | | Vector or scalar register |
+ | Q20 | Integer | | Vector or scalar register |
+ | Q21 | Integer | | Vector or scalar register |
+ | Q22 | Integer | | Vector or scalar register |
+ | Q23 | Integer | | Vector or scalar register |
+ | Q24 | Integer | | Vector or scalar register |
+ | Q25 | Integer | | Vector or scalar register |
+ | Q26 | Integer | | Vector or scalar register |
+ | Q27 | Integer | | Vector or scalar register |
+ | Q28 | Integer | | Vector or scalar register |
+ | Q29 | Integer | | Vector or scalar register |
+ | Q30 | Integer | | Vector or scalar register |
+ | Q31 | Integer | | Vector or scalar register |
+ | D0 | Integer | | Vector or scalar register |
+ | D1 | Integer | | Vector or scalar register |
+ | D2 | Integer | | Vector or scalar register |
+ | D3 | Integer | | Vector or scalar register |
+ | D4 | Integer | | Vector or scalar register |
+ | D5 | Integer | | Vector or scalar register |
+ | D6 | Integer | | Vector or scalar register |
+ | D7 | Integer | | Vector or scalar register |
+ | D8 | Integer | | Vector or scalar register |
+ | D9 | Integer | | Vector or scalar register |
+ | D10 | Integer | | Vector or scalar register |
+ | D11 | Integer | | Vector or scalar register |
+ | D12 | Integer | | Vector or scalar register |
+ | D13 | Integer | | Vector or scalar register |
+ | D14 | Integer | | Vector or scalar register |
+ | D15 | Integer | | Vector or scalar register |
+ | D16 | Integer | | Vector or scalar register |
+ | D17 | Integer | | Vector or scalar register |
+ | D18 | Integer | | Vector or scalar register |
+ | D19 | Integer | | Vector or scalar register |
+ | D20 | Integer | | Vector or scalar register |
+ | D21 | Integer | | Vector or scalar register |
+ | D22 | Integer | | Vector or scalar register |
+ | D23 | Integer | | Vector or scalar register |
+ | D24 | Integer | | Vector or scalar register |
+ | D25 | Integer | | Vector or scalar register |
+ | D26 | Integer | | Vector or scalar register |
+ | D27 | Integer | | Vector or scalar register |
+ | D28 | Integer | | Vector or scalar register |
+ | D29 | Integer | | Vector or scalar register |
+ | D30 | Integer | | Vector or scalar register |
+ | D31 | Integer | | Vector or scalar register |
+ | S0 | Integer | | Vector or scalar register |
+ | S1 | Integer | | Vector or scalar register |
+ | S2 | Integer | | Vector or scalar register |
+ | S3 | Integer | | Vector or scalar register |
+ | S4 | Integer | | Vector or scalar register |
+ | S5 | Integer | | Vector or scalar register |
+ | S6 | Integer | | Vector or scalar register |
+ | S7 | Integer | | Vector or scalar register |
+ | S8 | Integer | | Vector or scalar register |
+ | S9 | Integer | | Vector or scalar register |
+ | S10 | Integer | | Vector or scalar register |
+ | S11 | Integer | | Vector or scalar register |
+ | S12 | Integer | | Vector or scalar register |
+ | S13 | Integer | | Vector or scalar register |
+ | S14 | Integer | | Vector or scalar register |
+ | S15 | Integer | | Vector or scalar register |
+ | S16 | Integer | | Vector or scalar register |
+ | S17 | Integer | | Vector or scalar register |
+ | S18 | Integer | | Vector or scalar register |
+ | S19 | Integer | | Vector or scalar register |
+ | S20 | Integer | | Vector or scalar register |
+ | S21 | Integer | | Vector or scalar register |
+ | S22 | Integer | | Vector or scalar register |
+ | S23 | Integer | | Vector or scalar register |
+ | S24 | Integer | | Vector or scalar register |
+ | S25 | Integer | | Vector or scalar register |
+ | S26 | Integer | | Vector or scalar register |
+ | S27 | Integer | | Vector or scalar register |
+ | S28 | Integer | | Vector or scalar register |
+ | S29 | Integer | | Vector or scalar register |
+ | S30 | Integer | | Vector or scalar register |
+ | S31 | Integer | | Vector or scalar register |
+ | H0 | Integer | | Vector or scalar register |
+ | H1 | Integer | | Vector or scalar register |
+ | H2 | Integer | | Vector or scalar register |
+ | H3 | Integer | | Vector or scalar register |
+ | H4 | Integer | | Vector or scalar register |
+ | H5 | Integer | | Vector or scalar register |
+ | H6 | Integer | | Vector or scalar register |
+ | H7 | Integer | | Vector or scalar register |
+ | H8 | Integer | | Vector or scalar register |
+ | H9 | Integer | | Vector or scalar register |
+ | H10 | Integer | | Vector or scalar register |
+ | H11 | Integer | | Vector or scalar register |
+ | H12 | Integer | | Vector or scalar register |
+ | H13 | Integer | | Vector or scalar register |
+ | H14 | Integer | | Vector or scalar register |
+ | H15 | Integer | | Vector or scalar register |
+ | H16 | Integer | | Vector or scalar register |
+ | H17 | Integer | | Vector or scalar register |
+ | H18 | Integer | | Vector or scalar register |
+ | H19 | Integer | | Vector or scalar register |
+ | H20 | Integer | | Vector or scalar register |
+ | H21 | Integer | | Vector or scalar register |
+ | H22 | Integer | | Vector or scalar register |
+ | H23 | Integer | | Vector or scalar register |
+ | H24 | Integer | | Vector or scalar register |
+ | H25 | Integer | | Vector or scalar register |
+ | H26 | Integer | | Vector or scalar register |
+ | H27 | Integer | | Vector or scalar register |
+ | H28 | Integer | | Vector or scalar register |
+ | H29 | Integer | | Vector or scalar register |
+ | H30 | Integer | | Vector or scalar register |
+ | H31 | Integer | | Vector or scalar register |
+ | B0 | Integer | | Vector or scalar register |
+ | B1 | Integer | | Vector or scalar register |
+ | B2 | Integer | | Vector or scalar register |
+ | B3 | Integer | | Vector or scalar register |
+ | B4 | Integer | | Vector or scalar register |
+ | B5 | Integer | | Vector or scalar register |
+ | B6 | Integer | | Vector or scalar register |
+ | B7 | Integer | | Vector or scalar register |
+ | B8 | Integer | | Vector or scalar register |
+ | B9 | Integer | | Vector or scalar register |
+ | B10 | Integer | | Vector or scalar register |
+ | B11 | Integer | | Vector or scalar register |
+ | B12 | Integer | | Vector or scalar register |
+ | B13 | Integer | | Vector or scalar register |
+ | B14 | Integer | | Vector or scalar register |
+ | B15 | Integer | | Vector or scalar register |
+ | B16 | Integer | | Vector or scalar register |
+ | B17 | Integer | | Vector or scalar register |
+ | B18 | Integer | | Vector or scalar register |
+ | B19 | Integer | | Vector or scalar register |
+ | B20 | Integer | | Vector or scalar register |
+ | B21 | Integer | | Vector or scalar register |
+ | B22 | Integer | | Vector or scalar register |
+ | B23 | Integer | | Vector or scalar register |
+ | B24 | Integer | | Vector or scalar register |
+ | B25 | Integer | | Vector or scalar register |
+ | B26 | Integer | | Vector or scalar register |
+ | B27 | Integer | | Vector or scalar register |
+ | B28 | Integer | | Vector or scalar register |
+ | B29 | Integer | | Vector or scalar register |
+ | B30 | Integer | | Vector or scalar register |
+ | B31 | Integer | | Vector or scalar register |
+
+
+!!! INFO "Hardware Support"
+ **libdebug** only exposes registers which are available on your CPU model. For AMD64, the list of available AVX registers is determined by checking the CPU capabilities. If you believe your CPU supports AVX registers but they are not available, we encourage your to open an [:octicons-issue-opened-24: Issue](https://github.com/libdebug/libdebug/issues) with your hardware details.
+
+## :material-filter: Filtering Registers
+The `regs` field of the [Debugger](../../from_pydoc/generated/debugger/debugger/) object or the [Thread Context](../../from_pydoc/generated/state/thread_context) can also be used to filter registers with specific values.
+
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.regs.filter(value: float) -> list[str]:
+ ```
+
+The filtering routine will look for the given value in both integer and floating point registers.
+
+!!! ABSTRACT "Example of Filtering Registers"
+ ```python
+ d.regs.rax = 0x1337
+
+ # Filter the value 0x1337 in the registers
+ results = d.regs.filter(0x1337)
+ print(f"Found in: {results}")
+ ```
\ No newline at end of file
diff --git a/docs/basics/running_an_executable.md b/docs/basics/running_an_executable.md
new file mode 100644
index 00000000..ede2aa33
--- /dev/null
+++ b/docs/basics/running_an_executable.md
@@ -0,0 +1,99 @@
+---
+icon: octicons/file-binary-24
+search:
+ boost: 4
+---
+# :octicons-file-binary-24: Running an Executable
+You have created your first debugger object, and now you want to run the executable. Calling the `run()` method will spawn a new child process and prepare it for the execution of your binary.
+
+```python
+from libdebug import debugger
+
+d = debugger("program")
+d.run()
+```
+At this point, the process execution is stopped, waiting for your commands.
+
+!!! INFO "A few things to keep in mind"
+ - Please remember that the process you are debugging (the tracee) and the debugger itself are running in different threads.
+ - Also note that breakpoints and other [stopping events](../../stopping_events/stopping_events) set by the user are not kept between different runs of the program. If you want to place them again, you should redo so after each call to `d.run()`. You cannot set breakpoints before calling `d.run()`.
+
+## :material-harddisk: Process I/O
+
+When execution is resumed, chances are that your process will need to take input and produce output. To interact with the standard input and output of the process, you can use the [PipeManager](../../from_pydoc/generated/commlink/pipe_manager) returned by the `run()` function.
+
+```python
+from libdebug import debugger
+
+d = debugger("program")
+pipe = d.run()
+
+d.cont()
+print(pipe.recvline().decode())
+d.wait()
+```
+
+All pipe receive-like methods have a timeout parameter that you can set. The default value, `timeout_default`, can be set globally as a parameter of the [PipeManager](../../from_pydoc/generated/commlink/pipe_manager) object. By default, this value is set to 2 seconds.
+
+!!! TIP "Changing the global timeout"
+ ```python
+ pipe = d.run()
+
+ pipe.timeout_default = 10 # (1)
+ ```
+
+ 1. This sets the default timeout for all pipe receive-like methods to 10 seconds.
+
+You can interact with the process's pipe manager using the following methods:
+
+| Method | Description |
+| -------------- | ----------- |
+| `recv` | Receives at most `numb` bytes from the target's stdout. **Parameters**: - `numb` (int) \[default = 4096\] - `timeout` (int) \[default = timeout_default\] |
+| `recverr` | Receives at most `numb` bytes from the target's stderr. **Parameters**: - `numb` (int) \[default = 4096\] - `timeout` (int) \[default = timeout_default\] |
+| `recvuntil` | Receives data from stdout until a specified delimiter is encountered for a certain number of occurrences. **Parameters**: - `delims` (bytes) - `occurrences` (int) \[default = 1\] - `drop` (bool) \[default = False\] - `timeout` (int) \[default = timeout_default\] - `optional` (bool) \[default = False\] |
+| `recverruntil` | Receives data from stderr until a specified delimiter is encountered for a certain number of occurrences. **Parameters**: - `delims` (bytes) - `occurrences` (int) \[default = 1\] - `drop` (bool) \[default = False\] - `timeout` (int) \[default = timeout_default\] - `optional` (bool) \[default = False\] |
+| `recvline` | Receives `numlines` lines from the target's stdout. **Parameters**: - `numlines` (int) \[default = 1\] - `drop` (bool) \[default = True\] - `timeout` (int) \[default = timeout_default\] - `optional` (bool) \[default = False\] |
+| `recverrline` | Receives `numlines` lines from the target's stderr. **Parameters**: - `numlines` (int) \[default = 1\] - `drop` (bool) \[default = True\] - `timeout` (int) \[default = timeout_default\] - `optional` (bool) \[default = False\] |
+| `send` | Sends `data` to the target's stdin. **Parameters**: - `data` (bytes) |
+| `sendafter` | Sends `data` after receiving a specified number of occurrences of a delimiter from stdout. **Parameters**: - `delims` (bytes) - `data` (bytes) - `occurrences` (int) \[default = 1\] - `drop` (bool) \[default = False\] - `timeout` (int) \[default = timeout_default\] - `optional` (bool) \[default = False\] |
+| `sendline` | Sends `data` followed by a newline to the target's stdin. **Parameters**: - `data` (bytes) |
+| `sendlineafter`| Sends a line of `data` after receiving a specified number of occurrences of a delimiter from stdout. **Parameters**: - `delims` (bytes) - `data` (bytes) - `occurrences` (int) \[default = 1\] - `drop` (bool) \[default = False\] - `timeout` (int) \[default = timeout_default\] - `optional` (bool) \[default = False\] |
+| `close` | Closes the connection to the target. |
+| `interactive` | Enters interactive mode, allowing manual send/receive operations with the target. Read more in the [dedicated section](#interactive-io). **Parameters**: - `prompt` (str) \[default = "$ "\] - `auto_quit` (bool) \[default = False\] |
+
+!!! INFO "When process is stopped"
+ When the process is stopped, the [PipeManager](../../from_pydoc/generated/commlink/pipe_manager) will not be able to receive new (unbuffered) data from the target. For this reason, the API includes a parameter called `optional`.
+
+ When set to `True`, **libdebug** will not necessarily expect to receive data from the process when it is stopped. When set to `False`, any recv-like instruction (including `sendafter` and `sendlineafter`) will fail with an exception when the process is not running.
+
+ Operations on stdin like `send` and `sendline` are not affected by this limitation, since the kernel will buffer the data until the process is resumed.
+
+### :material-keyboard: Interactive I/O
+The [PipeManager](../../from_pydoc/generated/commlink/pipe_manager) contains a method called `interactive()` that allows you to directly interact with the process's standard I/O. This method will print characters from standard output and error and read your inputs, letting you interact naturally with the process. The `interactive()` method is blocking, so the execution of the script will wait for the user to terminate the interactive session. To quit an interactive session, you can press `Ctrl+C` or `Ctrl+D`.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ pipe.interactive(prompt: str = prompt_default, auto_quit: bool = False):
+ ```
+
+The `prompt` parameter sets the line prefix in the terminal (e.g. `"$ "` and `"> "` will produce `$ cat flag` and `> cat flag` respectively). By default, it is set to `"$ "`. The `auto_quit` parameter, when set to `True`, will automatically quit the interactive session when the process is stopped.
+
+If any of the file descriptors of standard input, output, or error are closed, a warning will be printed.
+
+## :fontawesome-solid-syringe: Attaching to a Running Process
+If you want to attach to a running process instead of spawning a child, you can use the `attach()` method in the [Debugger](../../from_pydoc/generated/debugger/debugger/) object. This method will attach to the process with the specified PID.
+
+```python
+from libdebug import debugger
+
+d = debugger("test")
+
+pid = 1234
+
+d.attach(pid)
+```
+
+The process will stop upon attachment, waiting for your commands.
+
+!!! WARNING "Ptrace Scope"
+ **libdebug** uses the `ptrace` system call to interact with the process. For security reasons, this system call is limited by the kernel according to a [`ptrace_scope`](https://www.kernel.org/doc/Documentation/security/Yama.txt) parameter. Different systems have different default values for this parameter. If the `ptrace` system call is not allowed, the `attach()` method will raise an exception notifying you of this issue.
\ No newline at end of file
diff --git a/docs/basics/supported_systems.md b/docs/basics/supported_systems.md
new file mode 100644
index 00000000..fe5d9b5a
--- /dev/null
+++ b/docs/basics/supported_systems.md
@@ -0,0 +1,31 @@
+---
+icon: octicons/cpu-24
+search:
+ boost: 4
+---
+# :octicons-cpu-24: Supported Systems
+
+## :fontawesome-solid-computer: Operating Systems
+Currently, **libdebug** only supports the :simple-linux: GNU/Linux Operating System.
+
+## :fontawesome-solid-microchip: Architectures
+
+
+| Architecture | Alias | Support |
+| :--------------------------------------------------------------------------- | :-----------------------: | :------------------------------: |
+| :simple-intel: x86_64 | AMD64 | :material-check-all: Stable |
+| :simple-intel: i386 over AMD64| 32-bit compatibility mode | :material-check: Alpha |
+| :simple-intel: i386 | IA-32 | :material-check: Alpha |
+| :simple-arm: ARM 64-bit | AArch64 | :material-check-all: Beta |
+| :simple-arm: ARM 32-bit | ARM32 | :material-close: Not Supported | |
+
+
+!!! TIP "Forcing a specific architecture"
+ If for any reason you need to force **libdebug** to use a specific architecture (e.g., corrupted ELF), you can do so by setting the `arch` parameter in the [Debugger](../../from_pydoc/generated/debugger/debugger/) object. For example, to force the debugger to use the x86_64 architecture, you can use the following code:
+ ```python
+ from libdebug import debugger
+
+ d = debugger("program", ...)
+
+ d.arch = "amd64"
+ ```
\ No newline at end of file
diff --git a/docs/blog/.authors.yml b/docs/blog/.authors.yml
new file mode 100644
index 00000000..6df7b08f
--- /dev/null
+++ b/docs/blog/.authors.yml
@@ -0,0 +1,21 @@
+authors:
+ team:
+ name: The libdebug Team
+ description: Maintainers
+ avatar: https://libdebug.org/images/libdebug_logo.webp
+ io_no:
+ name: Io_no
+ description: Maintainer
+ avatar: https://avatars.githubusercontent.com/u/58036457?v=4
+ mrindeciso:
+ name: MrIndeciso
+ description: Maintainer
+ avatar: https://avatars.githubusercontent.com/u/6383492?v=4
+ frank01001:
+ name: Frank01001
+ description: Maintainer and Documentation Editor
+ avatar: https://avatars.githubusercontent.com/u/3826161?v=4
+ jinblack:
+ name: Jinblack
+ description: Founder
+ avatar: https://avatars.githubusercontent.com/u/58036457?v=4
diff --git a/docs/blog/index.md b/docs/blog/index.md
new file mode 100644
index 00000000..f25d0368
--- /dev/null
+++ b/docs/blog/index.md
@@ -0,0 +1,2 @@
+# :material-email-newsletter: Blogposts
+
diff --git a/docs/blog/posts/13-10-24-15-00.md b/docs/blog/posts/13-10-24-15-00.md
new file mode 100644
index 00000000..63077a1f
--- /dev/null
+++ b/docs/blog/posts/13-10-24-15-00.md
@@ -0,0 +1,15 @@
+---
+authors:
+ - frank01001
+date:
+ created: 2024-10-13
+draft: false
+---
+
+# A New Documentation
+Hello, World! Thank for using **libdebug**. We are proud to roll out our new documentation along with version 0.7.0. This new documentation is powered by [MkDocs](https://www.mkdocs.org/) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/). We hope you find it more intuitive and easier to navigate.
+
+We have expanded the documentation to cover more topics and provide more examples. We also tried to highlight some common difficulties that have been reported. Also, thanks to the mkdocs search plugin, you can more easily find what you are looking for, both in the documentation and pages generated from Pydoc.
+
+We hope you enjoy the new documentation. If you find any mistakes or would like to suggest improvements, please let us know by opening an :octicons-issue-opened-24: issue on our [GitHub repository](https://github.com/libdebug/libdebug/issues).
+
diff --git a/docs/blog/posts/14-10-24-08-00.md b/docs/blog/posts/14-10-24-08-00.md
new file mode 100644
index 00000000..7fa30748
--- /dev/null
+++ b/docs/blog/posts/14-10-24-08-00.md
@@ -0,0 +1,16 @@
+---
+authors:
+ - team
+date:
+ created: 2024-10-14
+draft: false
+---
+
+# See you at ACM CCS 2024!
+We are excited to announce that we will be presenting a poster on **libdebug** at the 2024 ACM Conference on Computer and Communications Security (ACM CCS 2024). The conference will be held in Salt Lake City, Utah. The poster session is October 16th at 16:30. We will be presenting the rationale behind **libdebug** and demonstrating how it can be used in some cool use cases.
+
+If you are attending the conference, please stop by our poster and say hello. We would love to meet you and hear about your ideas. We are also looking forward to hearing about your research and how **libdebug** can help you in your work. Come by and grab some swag!
+
+---
+Link to the conference: [ACM CCS 2024](https://www.sigsac.org/ccs/CCS2024/)
+Link to the poster information: [**libdebug** Poster](https://libdebug.org/other/ccs24.html)
\ No newline at end of file
diff --git a/docs/code_examples/example_cc24.md b/docs/code_examples/example_cc24.md
new file mode 100644
index 00000000..8f8b784f
--- /dev/null
+++ b/docs/code_examples/example_cc24.md
@@ -0,0 +1,69 @@
+---
+icon: material/shield
+search:
+ boost: 0.8
+---
+# :material-shield: CyberChallenge 2024 - Workshop
+This script was used to showcase the power of **libdebug** during the Workshop at the [CyberChallenge.IT](https://cyberchallenge.it/) 2024 Finals. An explanation of the script, along with a brief introduction to **libdebug**, is available in the [official stream of the event](https://www.youtube.com/live/Ten8S50Fy7s?si=w8usHtnN5v6FWipQ&t=8253), starting from timestamp 2:17:00.
+
+```python
+from libdebug import debugger
+from string import ascii_letters, digits
+
+# Enable the escape_antidebug option to bypass the ptrace call
+d = debugger("main", escape_antidebug=True)
+
+def callback(_, __):
+ # This will automatically issue a continue when the breakpoint is hit
+ pass
+
+def on_enter_nanosleep(t, _):
+ # This sets every argument to NULL to make the syscall fail
+ t.syscall_arg0 = 0
+ t.syscall_arg1 = 0
+ t.syscall_arg2 = 0
+ t.syscall_arg3 = 0
+
+alphabet = ascii_letters + digits + "_{}"
+
+flag = b""
+best_hit_count = 0
+
+while True:
+ for c in alphabet:
+ r = d.run()
+
+ # Any time we call run() we have to reset the breakpoint and syscall handler
+ bp = d.breakpoint(0x13e1, hardware=True, callback=callback, file="binary")
+ d.handle_syscall("clock_nanosleep", on_enter=on_enter_nanosleep)
+
+ d.cont()
+
+ r.sendline(flag + c.encode())
+
+ # This makes the debugger wait for the process to terminate
+ d.wait()
+
+ response = r.recvline()
+
+ # `run()` will automatically kill any still-running process, but it's good practice to do it manually
+ d.kill()
+
+ if b"Yeah" in response:
+ # The flag is correct
+ flag += c.encode()
+ print(flag)
+ break
+
+ if bp.hit_count > best_hit_count:
+ # We have found a new flag character
+ best_hit_count = bp.hit_count
+ flag += c.encode()
+ print(flag)
+ break
+
+ if c == "}":
+ break
+
+print(flag)
+```
\ No newline at end of file
diff --git a/docs/code_examples/example_nlinks.md b/docs/code_examples/example_nlinks.md
new file mode 100644
index 00000000..72a86e96
--- /dev/null
+++ b/docs/code_examples/example_nlinks.md
@@ -0,0 +1,131 @@
+---
+icon: material/skull-crossbones-outline
+search:
+ boost: 0.8
+---
+# :material-skull-crossbones-outline: DEF CON Quals 2023 - nlinks
+This is a script that solves the challenge [nlinks](https://github.com/Nautilus-Institute/quals-2023/tree/main/nlinks) from DEF CON Quals 2023. Please find the binary executables [here](https://github.com/libdebug/libdebug/tree/main/test/amd64).
+```python
+def get_passsphrase_from_class_1_binaries(previous_flag):
+ flag = b""
+
+ d = debugger("CTF/1")
+ r = d.run()
+
+ bp = d.breakpoint(0x7EF1, hardware=True, file="binary")
+
+ d.cont()
+
+ r.recvuntil(b"Passphrase:\n")
+
+ # We send a fake flag after the valid password
+ r.send(previous_flag + b"a" * 8)
+
+ for _ in range(8):
+ # Here we reached the breakpoint
+ if not bp.hit_on(d):
+ print("Here we should have hit the breakpoint")
+
+ offset = ord("a") ^ d.regs.rbp
+ d.regs.rbp = d.regs.r13
+
+ # We calculate the correct character value and append it to the flag
+ flag += (offset ^ d.regs.r13).to_bytes(1, "little")
+
+ d.cont()
+
+ r.recvline()
+
+ d.kill()
+
+ # Here the value of flag is b"\x00\x006\x00\x00\x00(\x00"
+ return flag
+
+def get_passsphrase_from_class_2_binaries(previous_flag):
+ bitmap = {}
+ lastpos = 0
+ flag = b""
+
+ d = debugger("CTF/2")
+ r = d.run()
+
+ bp1 = d.breakpoint(0xD8C1, hardware=True, file="binary")
+ bp2 = d.breakpoint(0x1858, hardware=True, file="binary")
+ bp3 = d.breakpoint(0xDBA1, hardware=True, file="binary")
+
+ d.cont()
+
+ r.recvuntil(b"Passphrase:\n")
+ r.send(previous_flag + b"a" * 8)
+
+ while True:
+ if d.regs.rip == bp1.address:
+ # Prepare for the next element in the bitmap
+ lastpos = d.regs.rbp
+ d.regs.rbp = d.regs.r13 + 1
+ elif d.regs.rip == bp2.address:
+ # Update the bitmap
+ bitmap[d.regs.r12 & 0xFF] = lastpos & 0xFF
+ elif d.regs.rip == bp3.address:
+ # Use the bitmap to calculate the expected character
+ d.regs.rbp = d.regs.r13
+ wanted = d.regs.rbp
+ needed = 0
+ for i in range(8):
+ if wanted & (2**i):
+ needed |= bitmap[2**i]
+ flag += chr(needed).encode()
+
+ if bp3.hit_count == 8:
+ # We have found all the characters
+ d.cont()
+ break
+
+ d.cont()
+
+ d.kill()
+
+ # Here the value of flag is b"\x00\x00\x00\x01\x00\x00a\x00"
+ return flag
+
+def get_passsphrase_from_class_3_binaries():
+ flag = b""
+
+ d = debugger("CTF/0")
+ r = d.run()
+
+ bp = d.breakpoint(0x91A1, hardware=True, file="binary")
+
+ d.cont()
+
+ r.send(b"a" * 8)
+
+ for _ in range(8):
+
+ # Here we reached the breakpoint
+ if not bp.hit_on(d):
+ print("Here we should have hit the breakpoint")
+
+ offset = ord("a") - d.regs.rbp
+ d.regs.rbp = d.regs.r13
+
+ # We calculate the correct character value and append it to the flag
+ flag += chr((d.regs.r13 + offset) % 256).encode("latin-1")
+
+ d.cont()
+
+ r.recvline()
+
+ d.kill()
+
+ # Here the value of flag is b"BM8\xd3\x02\x00\x00\x00"
+ return flag
+
+def run_nlinks():
+ flag0 = get_passsphrase_from_class_3_binaries()
+ flag1 = get_passsphrase_from_class_1_binaries(flag0)
+ flag2 = get_passsphrase_from_class_2_binaries(flag1)
+
+ print(flag0, flag1, flag2)
+
+```
\ No newline at end of file
diff --git a/docs/code_examples/examples_index.md b/docs/code_examples/examples_index.md
new file mode 100644
index 00000000..c2898841
--- /dev/null
+++ b/docs/code_examples/examples_index.md
@@ -0,0 +1,7 @@
+---
+icon: material/flag-variant
+search:
+ boost: 1
+---
+# :material-flag-variant: Examples Index
+This chapter contains a collection of examples showcasing the power of **libdebug** in various scenarios. Each example is a script that uses the library to solve a specific challenge or demonstrate a particular feature.
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..1774f64d
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,122 @@
+---
+search:
+ boost: 2
+---
+# Home
+
+
+
+
+
+---
+
+[](https://libdebug.org)
+[](https://doi.org/10.5281/zenodo.13151549)
+
+
+## :material-lightning-bolt: Quick Start
+Welcome to **libdebug**! This powerful Python library can be used to debug your binary executables programmatically, providing a robust, user-friendly interface. Debugging multithreaded applications can be a nightmare, but **libdebug** has you covered. Hijack and manage signals and syscalls with a simple API.
+
+!!! INFO "Supported Systems"
+ **libdebug** currently supports Linux under the x86_64, x86 and ARM64 architectures. Other operating systems and architectures are not supported at this time.
+
+## Dependencies
+To install **libdebug**, you first need to have some dependencies that will not be automatically resolved. These dependencies are libraries, utilities and development headers which are required by **libdebug** to compile its internals during installation.
+
+=== ":material-ubuntu: Ubuntu"
+ ```bash
+ sudo apt install -y python3 python3-dev libdwarf-dev libelf-dev libiberty-dev linux-headers-generic libc6-dbg
+ ```
+
+=== ":material-arch: Arch Linux"
+ ```bash
+ sudo pacman -S python libelf libdwarf gcc make debuginfod
+ ```
+
+=== ":material-fedora: Fedora"
+ ```bash
+ sudo dnf install -y python3 python3-devel kernel-devel binutils-devel libdwarf-devel
+ ```
+
+=== ":material-debian: Debian"
+ ```bash
+ sudo apt install -y python3 python3-dev libdwarf-dev libelf-dev libiberty-dev linux-headers-generic libc6-dbg
+ ```
+
+!!! QUESTION "Is your distro missing?"
+ If you are using a Linux distribution that is not included in this section, you can search for equivalent packages for your distro. Chances are the naming convention of your system's repository will only change a prefix or suffix.
+
+
+## Installation
+Installing **libdebug** once you have dependencies is as simple as running the following command:
+
+=== "stable"
+ ```bash
+ python3 -m pip install libdebug
+ ```
+=== "development"
+ ```bash
+ python3 -m pip install git+https://github.com/libdebug/libdebug.git@dev
+ ```
+
+If you want to test your installation when installing from source, we provide a suite of tests that you can run:
+
+```bash title="Testing your installation"
+cd test
+python run_suite.py
+```
+
+## Your First Script
+Now that you have **libdebug** installed, you can start using it in your scripts. Here is a simple example of how to use **libdebug** to debug an executable:
+
+```python title="libdebug's Hello World!"
+from libdebug import debugger
+
+d = debugger("./test") # (1)
+
+# Start debugging from the entry point
+d.run() # (2)
+
+my_breakpoint = d.breakpoint("function") # (3)
+
+# Continue the execution until the breakpoint is hit
+d.cont() # (4)
+
+# Print RAX
+print(f"RAX is {hex(d.regs.rax)}") # (5)
+```
+
+1. A debugger is created for the `test` executable
+2. The process is spawned and the entry point is reached
+3. A breakpoint is placed at the symbol `` in the binary
+4. A continuation command is issued, execution resumes
+5. The value of the RAX register is read and printed
+
+## Conflicts with other Python packages
+!!! BUG "Using pwntools alongside **libdebug**"
+ The current version of **libdebug** is incompatible with [pwntools](https://github.com/Gallopsled/pwntools).
+
+ While having both installed in your Python environment is not a problem, starting a process with pwntools in a **libdebug** script will cause unexpected behaviors as a result of some race conditions.
+
+Examples of some known issues include:
+
+- `ptrace` not intercepting SIGTRAP signals when the process is run with pwntools. This behavior is described in [:octicons-issue-opened-24: Issue #48](https://github.com/libdebug/libdebug/issues/48).
+- Attaching **libdebug** to a process that was started with pwntools with `shell=True` will cause the process to attach to the shell process instead. This behavior is described in [:octicons-issue-opened-24: Issue #57](https://github.com/libdebug/libdebug/issues/57).
+
+## :fontawesome-solid-clock-rotate-left: Older versions of the documentation
+The documentation for versions of **libdebug** older that 0.7.0 has to be accessed manually at [http://docs.libdebug.org/archive/VERSION](http://docs.libdebug.org/archive/VERSION), where `VERSION` is the version number you are looking for.
+
+## :material-format-quote-open: Cite Us
+Need to cite **libdebug** in your research? Use the following BibTeX entry:
+
+```bibtex
+@software{libdebug_2024,
+ title = {libdebug: {Build} {Your} {Own} {Debugger}},
+ copyright = {MIT Licence},
+ url = {https://libdebug.org},
+ publisher = {libdebug.org},
+ author = {Digregorio, Gabriele and Bertolini, Roberto Alessandro and Panebianco, Francesco and Polino, Mario},
+ year = {2024},
+ doi = {10.5281/zenodo.13151549},
+}
+```
\ No newline at end of file
diff --git a/docs/logging/liblog.md b/docs/logging/liblog.md
new file mode 100644
index 00000000..6c2c1d4a
--- /dev/null
+++ b/docs/logging/liblog.md
@@ -0,0 +1,72 @@
+---
+icon: material/note-text
+search:
+ boost: 4
+---
+# :material-note-text: Logging
+Debugging an application with the freedom of a rich API can lead to flows which are hard to unravel. To aid the user in the debugging process, **libdebug** provides logging. The logging system is implemented in the submodule [`liblog`](../../from_pydoc/generated/liblog) and adheres to the [Python logging system](https://docs.python.org/3/library/logging.html).
+
+## Event Logging
+By default, **libdebug** only prints critical logs such as warnings and errors. However, the user can enable more verbose logging by setting the `argv` parameter of the script.
+
+The available logging modes for events are:
+
+| Mode | Description |
+| --- | --- |
+| `debugger` | Logs related to the debugging operations performed on the process by **libdebug**. |
+| `pipe` | Logs related to interactions with the process pipe: bytes received and bytes sent. |
+| `dbg` | Combination of the `pipe` and `debugger` options. |
+
+!!! WARNING "pwntools compatibility"
+ As reported in this documentation, the `argv` parameters passed to **libdebug** are *lowercase*. This choice is made to avoid conflicts with [pwntools](https://github.com/Gallopsled/pwntools), which intercepts all uppercase arguments.
+
+### :octicons-bug-24: Debugger Logging
+The `debugger` option displays all logs related to the debugging operations performed on the process by libdebug.
+
+
+
+### :material-pipe: Pipe Logging
+The `pipe` option, on the other hand, displays all logs related to interactions with the process pipe: bytes received and bytes sent.
+
+
+
+### :material-vector-union: The best of both worlds
+The `dbg` option is the combination of the `pipe` and `debugger` options. It displays all logs related to the debugging operations performed on the process by libdebug, as well as interactions with the process pipe: bytes received and bytes sent.
+
+## :fontawesome-solid-person-military-pointing: Changing logging levels at runtime
+**libdebug** defines logging levels and information types to allow the user to filter the granularity of the the information they want to see. Logger levels for each event type can be changed at runtime using the [`libcontext`](../../from_pydoc/generated/utils/libcontext) module.
+
+!!! ABSTRACT "Example of setting logging levels"
+ ```python
+ from libdebug import libcontext
+
+ libcontext.general_logger = 'DEBUG'
+ libcontext.pipe_logger = 'DEBUG'
+ libcontext.debugger_logger = 'DEBUG'
+ ```
+
+| Logger | Description | Supported Levels | Default Level |
+| --- | --- | --- | --- |
+| `general_logger` | Logger used for general **libdebug** logs, different from the `pipe` and `debugger` logs. | `DEBUG`, `INFO`, `WARNING`, `SILENT` | `INFO` |
+| `pipe_logger` | Logger used for pipe logs. | `DEBUG`, `SILENT` | `SILENT` |
+| `debugger_logger` | Logger used for debugger logs. | `DEBUG`, `SILENT` | `SILENT` |
+
+Let's see what each logging level actually logs:
+
+| Log Level | Debug Logs | Information Logs | Warnings |
+|-----------|------------|------------------|----------|
+| DEBUG | :material-check: | :material-check: | :material-check: |
+| INFO | | :material-check: | :material-check: |
+| WARNING | | | :material-check: |
+| SILENT | | | |
+
+
+### :material-script-text-play-outline: Temporary logging level changes
+Logger levels can be temporarily changed at runtime using a `with` statement, as shown in the following example.
+
+```python
+from libdebug import libcontext
+
+with libcontext.tmp(pipe_logger='SILENT', debugger_logger='DEBUG'):
+ r.sendline(b'gimme the flag')
+```
\ No newline at end of file
diff --git a/docs/make.bat b/docs/make.bat
deleted file mode 100644
index 747ffb7b..00000000
--- a/docs/make.bat
+++ /dev/null
@@ -1,35 +0,0 @@
-@ECHO OFF
-
-pushd %~dp0
-
-REM Command file for Sphinx documentation
-
-if "%SPHINXBUILD%" == "" (
- set SPHINXBUILD=sphinx-build
-)
-set SOURCEDIR=source
-set BUILDDIR=build
-
-%SPHINXBUILD% >NUL 2>NUL
-if errorlevel 9009 (
- echo.
- echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
- echo.installed, then set the SPHINXBUILD environment variable to point
- echo.to the full path of the 'sphinx-build' executable. Alternatively you
- echo.may add the Sphinx directory to PATH.
- echo.
- echo.If you don't have Sphinx installed, grab it from
- echo.https://www.sphinx-doc.org/
- exit /b 1
-)
-
-if "%1" == "" goto help
-
-%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
-goto end
-
-:help
-%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
-
-:end
-popd
diff --git a/docs/modules.rst b/docs/modules.rst
deleted file mode 100644
index 306266f1..00000000
--- a/docs/modules.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-libdebug
-========
-
-.. toctree::
- :maxdepth: 4
-
- libdebug
diff --git a/docs/multithreading/multithreading.md b/docs/multithreading/multithreading.md
new file mode 100644
index 00000000..134ad333
--- /dev/null
+++ b/docs/multithreading/multithreading.md
@@ -0,0 +1,44 @@
+---
+icon: material/table-column-plus-after
+search:
+ boost: 4
+---
+# :material-table-column-plus-after: Debugging Multithreaded Applications
+Debugging multi-threaded applications can be a daunting task, particularly in an interactive debugger that is designed to operate on one thread at a time. **libdebug** offers a few features that will help you debug multi-threaded applications more intuitively and efficiently.
+
+## :material-human-male-child: Child Threads
+Threads of a running process in the [POSIX](https://en.wikipedia.org/wiki/Pthreads) standard are children of the main process. They are created by system calls such as [fork](https://man7.org/linux/man-pages/man2/fork.2.html), [clone](https://man7.org/linux/man-pages/man2/clone.2.html) and [clone3](https://elixir.bootlin.com/linux/v6.10/source/kernel/fork.c#L3082). In the Linux kernel, the [ptrace](https://man7.org/linux/man-pages/man2/ptrace.2.html) system call allows a tracer to identify new threads of the debugged process and retrieve their thread id (tid).
+
+**libdebug** automatically registers new threads and exposes their state with the same API as the main [Debugger](../../from_pydoc/generated/debugger/debugger/) object. While technically threads can be running or stopped independently, **libdebug** will enforce a coherent state. This means that if a thread is stopped, all other threads will be stopped as well and if a continuation command is issued, all threads will be resumed.
+
+To access the threads of a process, you can use the `threads` attribute of the [Debugger](../../from_pydoc/generated/debugger/debugger/) object. This attribute will return a list of [ThreadContext](../../from_pydoc/generated/state/thread_context/) objects, each representing a thread of the process. Similarly, you can access the [Debugger](../../from_pydoc/generated/debugger/debugger/) object from any [ThreadContext](../../from_pydoc/generated/state/thread_context/) through the `debugger` attribute.
+
+!!! WARNING "Meaning of the debugger object"
+ When accessing state fields of the [Debugger](../../from_pydoc/generated/debugger/debugger/) object (e.g. registers, memory), the debugger will act as an alias for the main thread. For example, doing d.regs.rax will be equivalent to doing d.threads[0].regs.rax.
+
+!!! INFO "Child Processes"
+ **libdebug** does not support debugging child processes (only threads). If a child process is created by the main process, a warning will be printed, prompting the user to attach to the child process manually.
+
+## :material-share: Shared and Unshared State
+Each thread has its own register set, stack, and instruction pointer. However, there are shared resources between threads that you should be aware of:
+
+- :fontawesome-solid-memory: The virtual address space is mostly shared between threads. Currently, **libdebug** does not handle the multiprocessing.
+
+- :material-sign-caution: Software breakpoints are implemented through code patching in the process memory. This means that a breakpoint set in one thread will be replicated across all threads.
+ - When using [synchronous](../../stopping_events/debugging_flow) breakpoints, you will need to "diagnose" the stopping event to determine which thread triggered the breakpoint. You can do this by checking the return value of the [`hit_on()`](../../stopping_events/debugging_flow/#hit-records) method of the [Breakpoint](../../from_pydoc/generated/data/breakpoint/) object. Passing the [ThreadContext](../../from_pydoc/generated/state/thread_context/) as an argument will return `True` if the breakpoint was hit by that thread.
+
+ - When using [asynchronous](../../stopping_events/debugging_flow) breakpoints, the breakpoint will be more intuitive to handle, as the signature of the [callback function](../../stopping_events/breakpoints#callback-signature) includes the [ThreadContext](../../from_pydoc/generated/state/thread_context/) object that triggered the breakpoint.
+
+- :octicons-cpu-24: While hardware breakpoints are thread-specific, **libdebug** mirrors them across all threads. This is done to avoid asymmetries with software breakpoints. Watchpoints are hardware breakpoints, so this applies to them as well.
+
+- :fontawesome-solid-terminal: For consistency, [syscall handlers](../../stopping_events/syscalls) are also enabled across all threads. The same considerations for synchronous and asynchronous breakpoints apply here as well.
+
+!!! WARNING "Concurrency in Syscall Handling"
+ When debugging entering and exiting events in syscalls, be mindful of the scheduling. The kernel may schedule a different thread to handle the syscall exit event right after the enter event of another thread.
+
+- :material-traffic-light-outline: [Signal Catching](../../stopping_events/signals) is also shared among threads. Apart from consistency, this is a necessity. In fact, the kernel does not guarantee that a signal sent to a process will be dispatched to a specific thread.
+ - By contrast, when sending arbitrary signals through the [ThreadContext](../../from_pydoc/generated/state/thread_context/) object, the signal will be sent to the requested thread.
+
+
+!!! EXAMPLE "How to access TLS?"
+ While the virtual address space is shared between threads, each thread has its own [Thread Local Storage (TLS)](https://en.wikipedia.org/wiki/Thread-local_storage) area. As it stands, **libdebug** does not provide a direct interface to the TLS area.
\ No newline at end of file
diff --git a/docs/overrides/main.html b/docs/overrides/main.html
new file mode 100644
index 00000000..3cf182cf
--- /dev/null
+++ b/docs/overrides/main.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+
+{% block outdated %}
+ You're not viewing the latest version.
+
+ Click here to go to latest.
+
+{% endblock %}
\ No newline at end of file
diff --git a/docs/quality_of_life/anti_debugging.md b/docs/quality_of_life/anti_debugging.md
new file mode 100644
index 00000000..34994f55
--- /dev/null
+++ b/docs/quality_of_life/anti_debugging.md
@@ -0,0 +1,56 @@
+---
+icon: material/run-fast
+search:
+ boost: 4
+---
+## :material-run-fast: Automatic Evasion of Anti-Debugging Techniques
+A common anti-debugging technique for Linux ELF binaries is to invoke the `ptrace` syscall with the `PTRACE_TRACEME` argument. The syscall will fail if the binary is currently being traced by a debugger, as the kernel forbids a process from being traced by multiple debuggers.
+
+Bypassing this technique involves intercepting such syscalls and altering the return value to make the binary believe that it is not being traced. While this can absolutely be performed manually, **libdebug** comes with a pre-made implementation that can save you precious time.
+
+To enable this feature, set the `escape_antidebug` property to `True` when creating the debugger object. The debugger will take care of the rest.
+
+!!! ABSTRACT "Example"
+ \> C source code
+ ```C
+ #include
+ #include
+ #include
+
+ int main()
+ {
+
+ if (ptrace(PTRACE_TRACEME, 0, NULL, 0) == -1) // (1)
+ {
+ puts("No cheating! Debugger detected.\n"); // (2)
+ exit(1);
+ }
+
+ puts("Congrats! Here's your flag:\n"); // (3)
+ puts("flag{y0u_sn3aky_guy_y0u_tr1ck3d_m3}\n");
+
+ return 0;
+ }
+ ```
+
+ 1. Call ptrace with `PTRACE_TRACEME` to detect if we are being debugged
+ 2. If the call fails, it means the program is being debugged
+ 3. If the program is not being debugged, print the flag
+
+ \> **libdebug** script
+ ```python
+ from libdebug import debugger
+
+ d = debugger("evasive_binary",
+ escape_antidebug=True)
+
+ pipe = d.run()
+
+ d.cont()
+ out = pipe.recvline(numlines=2)
+ d.wait()
+
+ print(out.decode())
+ ```
+
+ Execution of the script will print the flag, even if the binary is being debugged.
\ No newline at end of file
diff --git a/docs/quality_of_life/memory_maps.md b/docs/quality_of_life/memory_maps.md
new file mode 100644
index 00000000..e4d2c81f
--- /dev/null
+++ b/docs/quality_of_life/memory_maps.md
@@ -0,0 +1,35 @@
+---
+icon: material/map-plus
+search:
+ boost: 4
+---
+# :material-map-plus: Memory Maps
+Virtual memory is a fundamental concept in operating systems. It allows the operating system to provide each process with its own address space, which is isolated from other processes. This isolation is crucial for security and stability reasons. The memory of a process is divided into regions called *memory maps*. Each memory map has a starting address, an ending address, and a set of permissions (read, write, execute).
+
+In **libdebug**, you can access the memory maps of a process using the `maps` attribute of the [Debugger](../../from_pydoc/generated/debugger/debugger/) object.
+
+The `maps` attribute returns a list of [MemoryMap](../../from_pydoc/generated/data/memory_map/) objects, which contain the following attributes:
+
+| Attribute | Type | Description |
+|-----------|------|-------------|
+| `start` | `int` | The start address of the memory map. There is also an equivalent alias called `base`. |
+| `end` | `int` | The end address of the memory map. |
+| `permissions` | `str` | The permissions of the memory map. |
+| `size` | `int` | The size of the memory map. |
+| `offset` | `int` | The offset of the memory map relative to the backing file. |
+| `backing_file` | `str` | The backing file of the memory map, or the symbolic name of the memory map. |
+
+## :material-filter: Filtering Memory Maps
+You can filter memory maps based on their attributes using the `filter()` method of the `maps` attribute. The `filter()` method accepts a value that can be either a memory address (`int`) or a symbolic name (`str`) and returns a list of [MemoryMap](../../from_pydoc/generated/data/memory_map/) objects that match the criteria.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.maps.filter(value: int | str) -> MemoryMapList[MemoryMap]:
+ ```
+
+The behavior of the memory map filtering depends on the type of the `value` parameter:
+
+| Queried Value | Return Value |
+|-------------| ------------|
+| Integer (memory address) | Map that contains the address |
+| String (symbolic map name) | List of maps with backing file matching the symbolic name |
diff --git a/docs/quality_of_life/pretty_printing.md b/docs/quality_of_life/pretty_printing.md
new file mode 100644
index 00000000..a4d1bd70
--- /dev/null
+++ b/docs/quality_of_life/pretty_printing.md
@@ -0,0 +1,57 @@
+---
+icon: material/flower-tulip-outline
+search:
+ boost: 4
+---
+## :material-flower-tulip-outline: Pretty Printing
+**libdebug** offers utilities to visualize the process's state in a human-readable format and with color highlighting. This can be especially useful when debugging complex binaries or when you need to quickly understand the behavior of a program.
+
+### :material-hexadecimal: Registers Pretty Printing
+There are two functions available to print the registers of a thread: `pprint_registers()` and `print_registers_all()`. The former will print the current values of the most commonly-interesting registers, while the latter will print all available registers.
+
+
+
+!!! TIP "Aliases"
+ If you don't like long function names, you can use aliases for the two register pretty print functions. The shorter aliases are `pprint_regs()` and `print_regs_all()`.
+
+
+### :fontawesome-solid-terminal: Syscall Trace Pretty Printing
+When debugging a binary, it is often much faster to guess what the intended functionality is by looking at the syscalls that are being invoked. **libdebug** offers a function that will intercept any syscall and print its arguments and return value. This can be done by setting the property `pprint_syscalls = True` in the [Debugger](../../from_pydoc/generated/debugger/debugger) object or [ThreadContext](../../from_pydoc/generated/state/thread_context) object and resuming the process.
+
+!!! ABSTRACT "Syscall Trace PPrint Syntax"
+ ```python
+ d.pprint_syscalls = True
+ d.cont()
+ ```
+
+The output will be printed to the console in color according to the following coding:
+
+| Format | Description |
+| --- | --- |
+| blue | Syscall name |
+| red | Syscall was intercepted and handled by a callback (either a basic handler or a hijack) |
+| yellow | Value given to a syscall argument in hexadecimal |
+| strikethrough | Syscall was hijacked or a value was changed, the new syscall or value follows the striken text |
+
+[Handled syscalls](../../stopping_events/syscalls) with a callback associated with them will be listed as such. Additionally, syscalls [hijacked](../../stopping_events/debugging_flow#hijacking) through the **libdebug** API will be highlighted as striken through, allowing you to monitor both the original behavior and your own changes to the flow. The id of the thread that made the syscall will be printed in the beginning of the line in white bold.
+
+
+
+### :material-map-search: Memory Maps Pretty Printing
+To pretty print the memory maps of a process, you can simply use the `pprint_maps()` function. This will print the memory maps of the process in a human-readable format, with color highlighting to distinguish between different memory regions.
+
+| Format | Description |
+| --- | --- |
+| underlined | Memory map with read, write, and execute permissions |
+| red | Memory map with execute permissions |
+| yellow | Memory map with write permissions |
+| green | Memory map with read permission only |
+| white | Memory map with no permissions |
+
+
+
+
+### :octicons-stack-24: Stack Trace Pretty Printing
+To pretty print the stack trace ([backtrace](../stack_frame_utils)) of a process, you can use the `pprint_backtrace()` function. This will print the stack trace of the process in a human-readable format.
+
+
\ No newline at end of file
diff --git a/docs/quality_of_life/quality_of_life.md b/docs/quality_of_life/quality_of_life.md
new file mode 100644
index 00000000..9f5aa0d8
--- /dev/null
+++ b/docs/quality_of_life/quality_of_life.md
@@ -0,0 +1,22 @@
+---
+icon: material/robot-happy
+search:
+ boost: 4
+---
+# :material-robot-happy: Quality of Life Features
+For your convenience, **libdebug** offers a few functions that will speed up your debugging process.
+
+### [:material-flower-tulip-outline: Pretty Printing](../pretty_printing/)
+Visualizing the state of the process you are debugging can be a daunting task. **libdebug** offers utilities to print registers, memory maps, syscalls, and more in a human-readable format and with color highlighting.
+
+### [:material-alphabetical: Symbol Resolution](../symbols/)
+**libdebug** can resolve symbols in the binary and shared libraries. With big binaries, this can be a computationally intensive, especially if your script needs to be run multiple types. You can set symbol resolution levels and specify where to look for symbols according to your needs.
+
+### [:material-map-plus: Memory Maps](../memory_maps/)
+**libdebug** offers utilities to retrieve the memory maps of a process. This can be useful to understand the memory layout of the process you are debugging.
+
+### [:octicons-stack-24: Stack Frame Utils](../stack_frame_utils/)
+**libdebug** offers utilities to resolve the return addresses of a process.
+
+### [:material-run-fast: Evasion of Anti-Debugging](../anti_debugging/)
+**libdebug** offers a few functions that will help you evade simple anti-debugging techniques. These functions can be used to bypass checks for the presence of a debugger.
\ No newline at end of file
diff --git a/docs/quality_of_life/stack_frame_utils.md b/docs/quality_of_life/stack_frame_utils.md
new file mode 100644
index 00000000..9b6ab65b
--- /dev/null
+++ b/docs/quality_of_life/stack_frame_utils.md
@@ -0,0 +1,33 @@
+---
+icon: octicons/stack-24
+search:
+ boost: 4
+---
+# :octicons-stack-24: Stack Frame Utils
+Function calls in a binary executable are made according to a system calling convention. One constant in these conventions is the use of a stack frame to store the return addresses to resume at the end of the function.
+
+Different architectures have slightly different ways to retrieve the return address (for example, in AArch64, the latest return address is stored in `x30`, the Link Register). To abstract these differences, **libdebug** provides common utilities to resolve the stack trace (*backtrace*) of the running process (or thread).
+
+
+
+
+
+
+**libdebug**'s *backtrace* is structured like a LIFO stack, with the top-most value being the current instruction pointer. Subsequent values are the return addresses of the functions that were called to reach the current instruction pointer.
+
+!!! ABSTRACT "Backtrace usage example"
+ ```python
+ from libdebug import debugger
+
+ d = debugger("test_backtrace")
+ d.run()
+
+ # A few calls later...
+ [...]
+
+ current_ip = d.backtrace()[0]
+ return_address = d.backtrace()[1]
+ other_return_addresses = d.backtrace()[2:]
+ ```
+
+Additionally, the field `saved_ip` of the [Debugger](../../from_pydoc/generated/debugger/debugger/) or [ThreadContext](../../from_pydoc/generated/state/thread_context/) objects will contain the return address of the current function.
\ No newline at end of file
diff --git a/docs/quality_of_life/symbols.md b/docs/quality_of_life/symbols.md
new file mode 100644
index 00000000..6eb74dc4
--- /dev/null
+++ b/docs/quality_of_life/symbols.md
@@ -0,0 +1,80 @@
+---
+icon: material/alphabetical
+search:
+ boost: 4
+---
+## :material-alphabetical: Symbol Resolution
+As described in the [memory access](../../basics/memory_access/#absolute-and-relative-addressing) section, many functions in **libdebug** accept symbols as an alternative to actual addresses.
+
+You can list all resolved symbols in the binary and shared libraries using the `symbols` attribute of the [Debugger](../../from_pydoc/generated/debugger/debugger/) object. This attribute returns a [SymbolList](../../from_pydoc/generated/data/symbol_list/) object.
+
+This object grants the user hybrid access to the symbols: as a dict or as a list. Tor example, the following lines of code all have a valid syntax:
+
+```python
+d.symbols['printf'] #(1)
+d.symbols[0] #(2)
+d.symbols['printf'][0] #(3)
+```
+
+1. Returns a list of symbols that match the string `printf` exactly.
+2. Returns the first symbol in the list.
+3. Returns the first symbol that matches the string `printf` exactly.
+
+Please note that the dict-like access returns exact matches with the symbol name. If you want to filter for symbols that contain a specific string, read [the dedicated section](#symbol-filtering).
+
+!!! INFO "C++ Demangling"
+ Reverse-engineering of C++ binaries can be a struggle. To help out, **libdebug** automatically demangles C++ symbols.
+
+### :material-pyramid: Symbol Resolution Levels
+With large binaries and libraries, parsing symbols can become an expensive operation. Because of this, **libdebug** offers the possibility of choosing among 5 levels of symbol resolution. To set the symbol resolution level, you can use the `sym_lvl` property of the [`libcontext`](../../from_pydoc/generated/utils/libcontext) module. The default value is level 5.
+
+| Level | Description |
+|-------|-------------|
+| 0 | Symbol resolution is disabled. |
+| 1 | Parse the ELF symbol table (.symtab) and dynamic symbol table (.dynsym). |
+| 2 | Parse the ELF DWARF. |
+| 3 | Follow the external debug file link in the .gnu_debuglink and/or .gnu_debugaltlink sections. If the file is present in the system, read its .symtab and .dynsym. |
+| 4 | Parse the external debug file DWARF, if the file exists in the system. |
+| 5 | Download the external debug file using `debuginfod`. The file is cached in the default folder for `debuginfod`. |
+
+Upon searching for symbols, **libdebug** will proceed from the lowest level to the set maximum.
+
+!!! ABSTRACT "Example of setting the symbol resolution level"
+ ```python
+ from libdebug import libcontext
+
+ libcontext.sym_lvl = 3
+ d.breakpoint('main')
+ ```
+
+If you want to change the symbol resolution level temporarily, you can use a `with` statement along with the `tmp` method of the `libcontext` module.
+
+!!! ABSTRACT "Example of temporary resolution level change"
+ ```python
+ from libdebug import libcontext
+
+ with libcontext.tmp(sym_lvl = 5):
+ d.breakpoint('main')
+ ```
+
+## :material-filter: Symbol Filtering
+The `symbols` attribute of the [Debugger](../../from_pydoc/generated/debugger/debugger/) object allows you to filter symbols in the binary and shared libraries.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.symbols.filter(value: int | str) -> SymbolList[Symbol]
+ ```
+
+Given a symbol name or address, this function returns a [SymbolList](../../from_pydoc/generated/data/symbol_list/). The list will conta
+
+[Symbol](../../from_pydoc/generated/data/symbol/) objects contain the following attributes:
+
+| Attribute | Type | Description |
+|-----------|------|-------------|
+| `start` | `int` | The start address of the symbol. |
+| `end` | `int` | The end address of the symbol. |
+| `name` | `str` | The name of the symbol. |
+| `backing_file` | `str` | The file where the symbol is defined (e.g., binary, libc, ld). |
+
+!!! INFO "Slow Symbol Resolution"
+ Please keep in mind that symbol resolution can be an expensive operation on large binaries and shared libraries. If you are experiencing performance issues, you can set the [symbol resolution level](#symbol-resolution-levels) to a lower value.
\ No newline at end of file
diff --git a/docs/regenerate_docs.sh b/docs/regenerate_docs.sh
deleted file mode 100755
index c44e3817..00000000
--- a/docs/regenerate_docs.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-make clean
-rm source/libdebug.*
-sphinx-autogen -o source/_autosummary source/index.rst
-sphinx-apidoc -o source/ ../
-make html
-cp build/html/_static/favicon.ico build/html/favicon.ico
\ No newline at end of file
diff --git a/docs/scrape_code.py b/docs/scrape_code.py
new file mode 100644
index 00000000..7998b2a9
--- /dev/null
+++ b/docs/scrape_code.py
@@ -0,0 +1,49 @@
+import os
+
+folder_to_scrape = "../libdebug"
+
+out_folder = "./from_pydoc/generated"
+
+if os.path.exists(out_folder):
+ os.system(f"rm -rf {out_folder}")
+
+def create_if_not_exists(folder):
+ if not os.path.exists(folder):
+ os.makedirs(folder)
+
+def md_rename(file):
+ return file.replace(".py", ".md")
+
+for root, dirs, files in os.walk(folder_to_scrape):
+ for file in files:
+ # Skip init files and such
+ if file.startswith("_"):
+ continue
+
+ # Only scrape .py, .c, and .h files
+ if file.endswith(".py"):
+ file_path = os.path.join(root, file)
+ print(f"File path: {file_path}")
+
+ # File path relative to libdebug/libdebug
+ file_path = file_path.replace(folder_to_scrape + "/", "")
+ print(f"Scraping {file_path}")
+
+ if "/" in file_path:
+ folder_to_create = os.path.join(out_folder, file_path[:file_path.rfind("/")])
+ else:
+ folder_to_create = out_folder
+
+ create_if_not_exists(folder_to_create)
+ print(f"Creating folder {folder_to_create}")
+
+ new_file_path = os.path.join(folder_to_create, md_rename(file))
+ print(f"Creating {new_file_path}")
+
+ # Write the file, including a yml header to avoid search priority problems
+ with open(new_file_path, "w") as f:
+ module_name = "libdebug." + file_path.split(".")[0].replace("/", ".")
+
+ what_to_write = f"---\ntitle: {module_name}\nboost: 0.5\n---\n# {module_name}\n::: {module_name}\n"
+
+ f.write(what_to_write)
\ No newline at end of file
diff --git a/docs/source/_static/libdebug.png b/docs/source/_static/libdebug.png
deleted file mode 100644
index 4fbbbeaf..00000000
Binary files a/docs/source/_static/libdebug.png and /dev/null differ
diff --git a/docs/source/_static/libdebug_icon.png b/docs/source/_static/libdebug_icon.png
deleted file mode 100644
index 30870766..00000000
Binary files a/docs/source/_static/libdebug_icon.png and /dev/null differ
diff --git a/docs/source/basic_features.rst b/docs/source/basic_features.rst
deleted file mode 100644
index 7a9ab062..00000000
--- a/docs/source/basic_features.rst
+++ /dev/null
@@ -1,328 +0,0 @@
-
-Basic Features
-==============
-When writing a script to debug a program, the first step is to create a Debugger object :class:`libdebug.debugger.debugger.Debugger`:
-
-.. code-block:: python
-
- from libdebug import debugger
- debugger = debugger(argv=["./program", "arg1", "arg2"])
-
-
-This will be your main interface to the debugger. You can either pass the name of the executable as a string, or a list of argv parameters for the execution.
-
-Just as you would expect, you can also pass environment variables to the program using the env parameter. Here, the variables are passed as a string-string dictionary.
-
-By default, debugged programs are run with ASLR disabled. If you want to enable it, you can set the `aslr` parameter to True.
-
-You can also choose to debug the program starting from the just after the *execve* call, following the flow of the loader. By default, the debugger will continue to the entry point of the binary before giving you control. You can change this behavior by setting the `continue_to_binary_entrypoint` parameter to False.
-
-Please note that this feature assumes the binary is well-formed. If the ELF header is corrupt, the binary entrypoint will not be resolved correctly. As such, setting this parameter to False is a good practice when you don't want libdebug to rely on that information.
-
-Creating a debugger object will not start the execution automatically. In fact, you can reuse the same debugger to iteratively run multiple instances of the program. This is particularly useful for smart bruteforcing or fuzzing scripts.
-
-As for the other parameters of the debugger, we will mention them later in the documentation.
-
-Running the program
--------------------
-
-After creating the debugger object, you can start the execution of the program using the `run()` method. This method will start execution on a child process and, unless otherwise specified, continue to the entry point.
-
-.. code-block:: python
-
- d = debugger("program")
- pipes = d.run()
-
-
-The `run()` command returns a `PipeManager` object, which you can use to interact with the program's standard input, output, and error. To read more about the PipeManager interface, please refer to the PipeManager documentation :class:`libdebug.utils.pipe_manager.PipeManager`. Please note that breakpoints are not kept between different runs of the program. If you want to set a breakpoint again, you should do so after the program has restarted.
-
-Any process will be automatically killed when the debugging script exits. If you want to prevent this behavior, you can set the `kill_on_exit` parameter to False when creating the debugger object, or set the companion attribute `kill_on_exit` to False at runtime.
-
-The command queue
------------------
-Control flow commands, register access and memory access are all done through the command queue. This is a FIFO queue of commands that are executed in order.
-
-While the inner workings of the command queue are transparent to the user, it is important to understand how its handling impacts the flow of the debugging script:
-
-By default, a command is polled from the queue when the execution stops, either as a result of handling a breakpoint, a signal, or other similar events.
-In the following example, the content of the RAX register is printed after the program hits the breakpoint or stops for any other reason:
-
-.. code-block:: python
-
- d = debugger("program")
-
- d.breakpoint("func")
-
- d.cont()
-
- print(f"RAX: {hex(d.regs.rax)}")
-
-This flow is similar to how a GDB script would work, allowing for a more intuitive starting point. If you would like to have more control, however, you can disable this behavior to make sure the command queue is polled as soon as possible.
-
-This can be done by setting the `auto_interrupt_on_command` parameter to True when creating the debugger object. In this new scenario, we would have to modify the script to recreate the previous flow.
-
-.. code-block:: python
-
- d = debugger("program", auto_interrupt_on_command=True)
-
- d.breakpoint("func")
-
- d.cont()
- d.wait()
-
- print(f"RAX: {hex(d.regs.rax)}")
-
-The `wait()` method waits for the running process to stop before going forward with the script. Adding the `d.wait()` command will make sure the register access doesn't happen before hitting the breakpoint or any other stopping event. If the `wait()` method is omitted, the register access will happen as soon as possible after the continue command is issued. Please remember that accessing a property like registers will stop the process. Sending a continue command afterwards will make the process run again.
-
-
-You can manually send a stopping signal to the program using the `interrupt()` method. This will stop the execution of the program and allow you to access the registers and memory. The syntax is as follows:
-
-.. code-block:: python
-
- d.interrupt()
-
-Register Access
-===============
-.. _register-access-paragraph:
-
-libdebug offers a simple register access interface for supported architectures. The registers are accessed through the `regs`` attribute of the debugger object. The field includes both general purpose and special registers, as well as the flags register. Effectively, any register that can be accessed by an assembly instruction, can also be accessed through the regs attribute. The debugger specifically exposes properties of the main thread, including the registers. See :doc:`multithreading` to learn how to access registers and other properties from different threads.
-
-Floating point and vector registers are available as well. The syntax is identical to the one used for integer registers.
-For amd64, the list of available AVX registers is determined during installation by checking the CPU capabilities, thus special registers, such as `zmm0` to `zmm31`, are available only on CPUs that support the specific ISA extension.
-If you believe that your target CPU supports AVX registers, but they are not available during debugging, please file an issue on the GitHub repository and include your precise hardware details, so that we can investigate and resolve the issue.
-
-Memory Access
-====================================
-
-Memory access is done through the memory attribute of the debugger object or the ThreadContext. Since virtual memory is shared between threads, accessing one or the other makes no difference.
-When reading from memory, a *bytes-like* object is returned. The memory API is flexible, allowing you to access memory in different ways. The following methods are available:
-
-- **Single byte access**
-You can access a single byte of memory by providing the address as an integer. For example, to access the byte at address 0x1000, you would use the following code:
-
-.. code-block:: python
-
- d.memory[0x1000]
-
-- **Slice access**
-You can access a range of bytes by providing the start and end addresses as integers. For example, to access the bytes from 0x1000 to 0x1010, you would use the following code:
-
-.. code-block:: python
-
- d.memory[0x1000:0x1010]
-
-- **Base and length**
-You can access a range of bytes by providing the base address and the length as integers. For example, to access the bytes from 0x1000 to 0x1010, you would use the following code:
-
-.. code-block:: python
-
- d.memory[0x1000, 0x10]
-
-- **Symbol access**
-You can access memory by providing a symbol name. For example, to access the bytes from the address of the symbol `main_arena` to the address of the symbol `main_arena+8`, you would use the following code:
-
-.. code-block:: python
-
- d.memory["main_arena", 0x8]
-
-or
-
-.. code-block:: python
-
- d.memory["main_arena":"main_arena+8"]
-
-
-Writing to memory works in a similar way. You can write a *bytes-like* object to memory using the addressing methods you already know:
-
-.. code-block:: python
-
- d.memory[d.rsp, 0x10] = b"AAAAAAABC"
- d.memory["main_arena"] = b"12345678"
-
-Please note that proving a shorter byte-like object than the length you are trying to write will result in zero padding.
-If the byte-like object is longer than the length you are trying to write, the FULL object will be written to memory ignoring the range you provided. A warning is printed in this case.
-
-Absolute and Relative Addressing
--------------------
-
-When accessing memory, you can use both absolute and relative addressing. Absolute addressing is the most common way to access memory, where you provide the exact address you want to access. Relative addressing is a more advanced way to access memory, where you provide an address relative to a base address.
-By default, the memory access in libdebug is done using an hybrid addressing mode. This means that libdebug will try to resolve the address as an absolute address first. If the address is not found, libdebug will try to resolve the address as a relative address, using as base the one of the binary. In this case, a warning will be printed.
-You can force the addressing mode by using the following syntax:
-
-.. code-block:: python
-
- d.memory[0x1000, 0x10, "absolute"]
- d.memory[0x1000, 0x10, "hybrid"]
-
-If you specify a full or a substring of a file name, libdebug will search for the memory map of the file and use the base address of the file as the base address for the relative addressing. If the file is not found or multiple matches are found, an exception is raised.
-
-.. code-block:: python
-
- d.memory[0x1000, 0x10, "file_name"]
- d.memory[0x1000, 0x10, "other_file_name"]
-
-You can also use the wildcard string "binary" to use the base address of the binary as the base address for the relative addressing. The same behavior is applied if you pass a string corresponding to the binary name.
-
-Faster Memory Access
--------------------
-
-By default, libdebug uses the kernel's ptrace interface to access memory. This is guaranteed to work, but it might be slow during large memory transfers.
-To speed up memory access, we provide a secondary system that relies on /proc/$pid/mem for read and write operations. You can enable this feature by setting `fast_memory` to True when instancing the debugger.
-The final behavior is identical, but the speed is significantly improved.
-
-Additionally, you can mix the two memory access methods by changing the `fast_memory` attribute of the debugger at runtime:
-
-.. code-block:: python
-
- d.fast_memory = True
-
- # ...
-
- d.fast_memory = False
-
-Control Flow Commands
-====================================
-
-The control flow commands are the main way to interact with the debugger. They allow you to set breakpoints, step through the program, and control the execution flow. The following commands are available:
-
-Stepping
---------
-
-When debuggin an executable, it is sometimes useful to step through the program one assembly instruction at a time. Just like in other debuggers, libdebug offers the step commands to help you with this task.
-
-Single Step
-^^^^^^^^^^^
-
-The `step` command will execute the next instruction and stop the execution. The syntax is as follows:
-
-.. code-block:: python
-
- d.step()
-
-Step Until
-^^^^^^^^^^
-
-Sometimes, you may want to step through the program until a specific address is reached. The `step_until` command will execute steps (hardware step if available) until the program counter reaches the specified address.
-
-Optionally, you can specify a maximum number of steps that are performed before returning. The syntax is as follows:
-
-.. code-block:: python
-
- d.step_until(position=0x40003b, max_steps=1000)
-
-Continuing
-----------
-
-Exactly as you would expect, the `cont()` command will continue the execution of the program until a breakpoint is hit or the program stops for any other reason. The syntax is as follows:
-
-.. code-block:: python
-
- d.cont()
-
-Finish
-^^^^^^
-
-The `finish` command is a more advanced version of the continue command. It will continue the execution of the program until the current function returns, a breakpoint is hit or the program stop for any other reason.
-
-Please note that the concept of "current function" is not as simple as it may seem. Boundaries between functions can become nuanced as a result of compiler optimizations, packing and inlining.
-
-Because of this, the finish command needs to use one of the available heuristics to resolve the end of the function.
-
-Remember that some cases may not be handled correctly by any of the heuristics, causing unexpected behavior. The syntax is as follows:
-
-.. code-block:: python
-
- d.finish(heuristic="backtrace")
-
-The available heuristics are:
-
-- **backtrace**: This heuristic uses the saved return address found on the stack or on a dedicated register to find the return address of the current function. A breakpoint is applied to the resolved address and execution is continued. This is the fastest heuristic and is fairly reliable, but it may not work in the presence of self-modifying code.
-- **step-mode**: This heuristic steps one instruction at a time until the ret instruction is executed in the current frame (nested calls are handled). This is a reliable heuristic, but is slow and fails in the case of internal tailcalls or similar optimizations.
-
-The default heuristic when none is specified is "backtrace".
-
-Next
-^^^^
-
-The `next` command is similar to the `step` command, but when a ``call`` instruction is found, it will continue until the end of the function being called or until the process stops for other reasons. The syntax is as follows:
-
-.. code-block:: python
-
- d.next()
-
-Detach and GDB Migration
-====================================
-
-If at any time during your script you want to take a more interactive approach to debugging, you can use the `gdb()` method. This will temporarily detach libdebug from the program and give you control over the program using GDB. Quitting GDB will return control to libdebug. The syntax is as follows:
-
-.. code-block:: python
-
- d.gdb()
-
-Optionally, you can specify `open_in_new_process=False` to execute GDB on the same process as the script. This way you can have gdb inlined in the same terminal session. You will be able to return to your script by using the command `goback`. The syntax is as follows:
-
-.. code-block:: python
-
- d.gdb(open_in_new_process=False)
-
-Depending on your use case, you may want to detach from the program and continue execution without either libdebug or GDB. The `detach()` method will detach libdebug from the program and continue execution. The syntax is as follows:
-
-.. code-block:: python
-
- d.detach()
-
-An alternative to running the program from the beginning and to resume libdebug control after detaching is to use the `attach()` method. The syntax is as follows:
-
-.. code-block:: python
-
- d.attach(pid)
-
-Do note that libdebug automatically kills any running process when the debugging script exits, even if the debugger has detached from it.
-If you want to prevent this behavior, you can set the `kill_on_exit` parameter to False when creating the debugger object, or set the companion attribute `kill_on_exit` to False at runtime.
-
-Graceful Termination
-====================
-
-If you want to kill the process being debugged, you can use the `kill()` method. When repeatedly running new instances of debugged program, remember to call the `kill()` command on old instances to avoid large memory usage. The syntax is as follows:
-
-.. code-block:: python
-
- d.kill()
-
-When you are done with the debugger object, you can terminate the background thread using the `terminate()` method. This will free up resources and should be used only when the debugger object is no longer needed. The syntax is as follows:
-
-.. code-block:: python
-
- d.terminate()
-
-
-Post Mortem Analysis
-====================
-You can check at every moment if the whole process (or a specific thread) is dead by using the `dead` property. The syntax is as follows:
-
-.. code-block:: python
-
- if not d.dead:
- print("The process is not dead")
- else:
- print("The process is dead")
-
-Moreover, after the process has died, you can check the exit code and the exit signal by using the `exit_code` and `exit_signal` properties, respectively. The syntax is as follows:
-
-.. code-block:: python
-
- if d.dead:
- print(f"The process exited with code {d.exit_code}")
-
-.. code-block:: python
-
- if d.dead:
- print(f"The process exited with signal {d.exit_signal}")
-
-You can also access registers after the process has died. This is useful for *post-mortem* analysis.
-
-
-Supported Architectures
-=======================
-
-libdebug currently only supports Linux under the x86_64 (AMD64) and AArch64 (ARM64) architectures. Support for other architectures is planned for future releases. Stay tuned.
diff --git a/docs/source/breakpoints.rst b/docs/source/breakpoints.rst
deleted file mode 100644
index 88833437..00000000
--- a/docs/source/breakpoints.rst
+++ /dev/null
@@ -1,128 +0,0 @@
-Breakpoints
-===========
-
-Breakpoints and watchpoints are a powerful way to debug your code. They allow you to pause the execution of your code at a specific point and inspect the state of your program.
-
-Software breakpoints in the Linux kernel are implemented by patching the running code with an interrupt instruction that is conventionally used for debugging. For example, in the i386 and AMD64 architectures, `int3` is used. When the `int3` instruction is executed, the CPU raises a `SIGTRAP` signal, which is caught by the debugger. The debugger then restores the original instruction and resumes the execution of the program. Software breakpoints are unlimited, but they can break when the program uses self-modifying code.
-
-Hardware breakpoints are a more reliable way to set breakpoints than software breakpoints. They are also faster and more flexible. However, hardware breakpoints are limited in number and are hardware-dependent.
-
-Breakpoints
------------
-
-libdebug provides a simple API to set breakpoints in your debugged program. The `breakpoint()` function sets a breakpoint at a specific address.
-
-.. code-block:: python
-
- from libdebug import debugger
-
- d = debugger("./test_program")
-
- d.run()
-
- bp = d.breakpoint(0x10ab)
-
- d.cont()
-
-In the provided example, the debugger will set a breakpoint at the address `0x10ab` (relative to the program's base address) and continue the execution of the program.
-
-When the program reaches the breakpoint, it will pause. You can enable and disable breakpoint `bp` with the `bp.enable()` and `bp.disable()` functions, respectively.
-
-Breakpoint hits
-^^^^^^^^^^^^^^^
-
-Let's now assume to have a program that executes a specific instruction multiple times. Depending on your use-case, you could be interested in the number of times a certain breakpoint has been hit. You could also want to perform a certain set of actions in your script as a result of the hit.
-
-The callback and hit_count properties of a Breakpoint object are useful for exactly this purpose. The following syntax is used to set a callback function for your breakpoint:
-
-.. code-block:: python
-
- def on_breakpoint_hit(t, bp):
- print(f"RAX: {t.regs.rax}")
-
- d.breakpoint(0x11f0, callback=on_breakpoint_hit)
-
-The signature of a callback function is as follows:
-
-.. code-block:: python
-
- def callback(t: ThreadContext, bp: Breakpoint) -> None:
-
-The first parameter is a thread context object. This kind of object is described in :doc:`multithreading`.
-The second parameter is the breakpoint object that triggered the callback.
-
-As for the hit_count property, the following is an example of how to it:
-
-.. code-block:: python
-
- while bp.hit_count < 100:
- d.cont()
- print(f"Hit count: {bp.hit_count}")
-
-
-Symbolic addressing
-^^^^^^^^^^^^^^^^^^^
-
-Just like with memory access, you can use symbolic addressing to set breakpoints. The following syntax is used to set a breakpoint at a specific function:
-
-.. code-block:: python
-
- d.breakpoint("vuln")
-
-Relative addressing with respect to a symbol is also supported. The offset is specified as an hexadecimal number following the symbol name:
-
-.. code-block:: python
-
- d.breakpoint("vuln+1f")
-
-Hardware breakpoints
-^^^^^^^^^^^^^^^^^^^^
-
-You can easily set a hardware breakpoint with the same api as a software breakpoint. Just set the hardware parameter to True:
-
-.. code-block:: python
-
- d.breakpoint(0x10ab, hardware=True)
-
-As previously mentioned, hardware breakpoints are limited in number. For example, in the x86 architecture, there are only 4 hardware breakpoints available. If you exceed that number, a `RuntimeError` will be raised.
-
-Watchpoints
------------
-
-Watchpoints are a special type of hardware breakpoint that triggers when a specific memory location is accessed. You can set a watchpoint to trigger on read, write, read/write, or execute access.
-
-Features of watchpoints are shared with breakpoints, so you can set callbacks, check the `hit_count` and activate / deactivate the watchpoint in the same way. While you can use the breakpoint API to set up a breakpoint, a specific API is provided on watchpoints for your convenience:
-
-.. code-block:: python
-
- def watchpoint(
- position=...,
- condition=...,
- length=...,
- callback=...) -> Breakpoint:
-
-Again, the position can be specified both as a relative address or as a symbol.
-The condition parameter specifies the type of access that triggers the watchpoint. The following values are supported in all architectures:
-
-- ``"w"``: write access
-- ``"rw"``: read/write access
-- ``"x"``: execute access
-
-AArch64 additionally supports:
-
-- ``"r"``: read access
-
-By default, the watchpoint is triggered only on write access.
-
-The length parameter specifies the size of the word being watched.
-In x86_64 (amd64) the following values are supported:
-
-- ``1``: byte
-- ``2``: word
-- ``4``: dword
-- ``8``: qword
-
-AArch64 supports any length from 1 to 8 bytes.
-
-By default, the watchpoint is set to watch a byte.
-
diff --git a/docs/source/catch_signals.rst b/docs/source/catch_signals.rst
deleted file mode 100644
index de20b42e..00000000
--- a/docs/source/catch_signals.rst
+++ /dev/null
@@ -1,128 +0,0 @@
-Signals
-=======
-
-libdebug supports catching of signals. You can, in fact, execute a callback or pause the script when a specific signal directed at the debugged process is intercepted by the tracer.
-
-The following is the signature of the callback function:
-
-.. code-block:: python
-
- def callback(d: ThreadContext, catcher: SignalCatcher) -> None:
-
-along with the thread where the signal was intercepted from, the callback is also passed the `SignalCatcher` object.
-
-When registering a signal catcher, you can either specify the signal number or the conventional signal name (e.g. 'SIGINT').
-
-.. code-block:: python
-
- # Define the callback function
- def catcher_SIGUSR1(t, catcher):
- t.signal = 0x0
- print("Look mum, I'm catching a signal")
-
- def catcher_SIGINT(t, catcher):
- print("Look mum, I'm catching another signal")
-
- # Register the signal catchers
- catcher1 = d.catch_signal(10, callback=catcher_SIGUSR1)
- catcher2 = d.catch_signal('SIGINT', callback=catcher_SIGINT)
-
- d.cont()
-
-
-You can also decide to pause the script when a signal is caught by not specifying a callback function.
-
-.. code-block:: python
-
- catcher = d.catch_signal(10)
- d.cont()
-
- d.wait()
- if catcher.hit_on(d):
- print("Signal 10 was caught")
-
-You can enable and disable a signal catcher `catcher` with the `catcher.enable()` and `catcher.disable()` functions, respectively.
-
-As with breakpoints and syscall handlers, you can access the `hit_count` property to get the number of times the signal was caught:
-
-.. code-block:: python
-
- while catcher.hit_count < 100:
- d.cont()
- print(f"Hit count: {catcher.hit_count}")
-
-Note: You cannot catch **SIGSTOP**, **SIGTRAP**, and **SIGKILL**.
-
-These signals are internally used by ptrace and the debugger, or are enforced by the kernel to be passed directly to the child process without the possibility of being caught.
-
-Just like with syscalls, there can be at most one user-defined catcher for each signal.
-
-If a new catcher is defined for a signal that is already catched or hijacked, the new catcher will replace the old one, and a warning is printed.
-
-Signal Filtering
-----------------
-Instead of setting a catcher on signals, you might want to filter which signals are not to be forwarded to the debugged process during execution.
-
-By default, all signals not related to libdebug internals are forwarded. For example, SIGSTOP is never passed to the process.
-
-.. code-block:: python
-
- d.signals_to_block = [10, 15, 'SIGINT', 3, 13]
-
-
-
-Arbitrary Signals
------------------
-You can also send an arbitrary signal to the process. The signal will be forwarded upon calling `d.cont()`, just before continuing the execution.
-
-.. code-block:: python
-
- d.signal = 10
- d.cont()
-
-The same syntax will work in multithreaded applications, by setting the signal on the desired thread context object. See :doc:`multithreading` for more information.
-
-Signal Hijacking
-----------------
-libdebug also provides a direct way to intercept a signal and modify it before sending it to the child process. In other words, it allows you to hijack an incoming signal and change it to a different signal. This works in a similar way to syscall hijacking.
-
-When registering a signal hijack, you can either specify the signal number or the conventional signal name (e.g. 'SIGINT').
-
-.. code-block:: python
-
- catcher1 = d.hijack_signal("SIGQUIT", "SIGTERM")
- catcher2 = d.hijack_signal("SIGINT", 10)
-
-Note: Just like with catchers, you cannot hijack **SIGSTOP**, **SIGTRAP**, and **SIGKILL**.
-
-These signals are internally used by ptrace and the debugger, or are enforced by the kernel to be passed directly to the child process without the possibility of being caught.
-
-Hijacking Loop Detection
-^^^^^^^^^^^^^^^^^^^^^^^^
-When carelessly hijacking syscalls, it could happen that loops are created. libdebug automatically performs checks to avoid these situations with signal hijacking and raises an exception if an infinite loop is detected.
-
-For example, the following code raises a `RuntimeError`:
-
-.. code-block:: python
-
- catcher1 = d.hijack_signal("SIGPIPE", "SIGINT")
- catcher2 = d.hijack_signal("SIGINT", "SIGPIPE")
-
-Recursion
-^^^^^^^^^
-Mixing signal catching and hijacking can become messy. Because of this, libdebug provides users with the choice of whether to execute the catcher for a signal that was triggered *by* a hijack.
-
-This behavior is enabled by the parameter `recursive`, available when instantiating a hijack or a catcher. By default, the parameter is set to False.
-
-In the following example, we replace the SIGINT signal with a SIGPIPE, but we do not want to execute the callback function for the SIGPIPE signal.
-
-For this reason, we set `recursive` to False upon registering the hijack.
-
-.. code-block:: python
-
- def catcher_SIGPIPE(d: ThreadContext, catcher: SignalCatcher):
- print("entering write")
-
- d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
- d.hijack_signal("SIGINT", "SIGPIPE", recursive=False)
-
diff --git a/docs/source/conf.py b/docs/source/conf.py
deleted file mode 100644
index 85d0b263..00000000
--- a/docs/source/conf.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import os
-import sys
-import sphinx
-
-
-# Configuration file for the Sphinx documentation builder.
-#
-# For the full list of built-in configuration values, see the documentation:
-# https://www.sphinx-doc.org/en/master/usage/configuration.html
-
-# -- Project information -----------------------------------------------------
-# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
-
-project = 'libdebug'
-copyright = '2024, Gabriele Digregorio, Roberto Alessandro Bertolini, Francesco Panebianco'
-author = 'JinBlack, Io_no, MrIndeciso, Frank01001'
-release = '0.6.0'
-
-# -- General configuration ---------------------------------------------------
-# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
-
-extensions = [
- 'sphinx.ext.napoleon',
- 'sphinx.ext.autodoc',
- 'sphinx.ext.autosummary',
- 'sphinx.ext.viewcode',
- 'sphinx_code_tabs'
-]
-
-templates_path = ['_templates']
-exclude_patterns = []
-
-autodoc_default_options = {
- 'undoc-members': True,
- 'private-members': False,
- 'member-order': 'bysource'
-}
-
-# -- Options for HTML output -------------------------------------------------
-# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
-
-html_theme = 'pydata_sphinx_theme'
-html_static_path = ['_static']
-
-# Logging setup
-logger = sphinx.util.logging.getLogger(__name__)
\ No newline at end of file
diff --git a/docs/source/examples.rst b/docs/source/examples.rst
deleted file mode 100644
index 22c8be06..00000000
--- a/docs/source/examples.rst
+++ /dev/null
@@ -1,204 +0,0 @@
-Code Examples
-=============
-
-This section contains some examples of how to use libdebug. They are either snippets taken from our write-ups of CTF challenges, or ad-hoc examples. Please find the binary executables in the test/binaries folder.
-
-CyberChallenge.IT 2024 - Workshop
--------------------------------------------------------------------
-
-.. This is a script that was used during the workshop presentation at the CyberChallenge.it Finals 2024.
-This script was used to show the various features of libdebug during the Workshop at the CyberChallenge.IT 2024 Finals.
-An explanation of the script, along with a brief introduction to libdebug, is available in the `official stream of the event `_, starting from 2:17:00.
-
-
-.. code-block:: python
-
- from libdebug import debugger
- from string import ascii_letters, digits
-
-
- # Enable the escape_antidebug option to bypass the ptrace call
- d = debugger("main", escape_antidebug=True)
-
- def callback(_, __):
- # This will automatically issue a continue when the breakpoint is hit
- pass
-
- def on_enter_nanosleep(t, _):
- # This sets every argument to NULL to make the syscall fail
- t.syscall_arg0 = 0
- t.syscall_arg1 = 0
- t.syscall_arg2 = 0
- t.syscall_arg3 = 0
-
- alphabet = ascii_letters + digits + "_{}"
-
- flag = b""
- best_hit_count = 0
-
- while True:
- for c in alphabet:
- r = d.run()
-
- # Any time we call run() we have to reset the breakpoint and syscall handler
- bp = d.breakpoint(0x13e1, hardware=True, callback=callback, file="binary")
- d.handle_syscall("clock_nanosleep", on_enter=on_enter_nanosleep)
-
- d.cont()
-
- r.sendline(flag + c.encode())
-
- # This makes the debugger wait for the process to terminate
- d.wait()
-
- response = r.recvline()
-
- # `run()` will automatically kill any still-running process, but it's good practice to do it manually
- d.kill()
-
- if b"Yeah" in response:
- # The flag is correct
- flag += c.encode()
- print(flag)
- break
-
- if bp.hit_count > best_hit_count:
- # We have found a new flag character
- best_hit_count = bp.hit_count
- flag += c.encode()
- print(flag)
- break
-
- if c == "}":
- break
-
- print(flag)
-
-DEFCON Quals 2023 - nlinks
---------------------------
-
-This is a script that solves the challenge `nlinks `_ from DEFCON Quals 2023. Please find the binary executables in the test/CTF folder.
-
-.. code-block:: python
-
- def get_passsphrase_from_class_1_binaries(previous_flag):
- flag = b""
-
- d = debugger("CTF/1")
- r = d.run()
-
- bp = d.breakpoint(0x7EF1, hardware=True, file="binary")
-
- d.cont()
-
- r.recvuntil(b"Passphrase:\n")
-
- # We send a fake flag after the valid password
- r.send(previous_flag + b"a" * 8)
-
- for _ in range(8):
- # Here we reached the breakpoint
- if not bp.hit_on(d):
- print("Here we should have hit the breakpoint")
-
- offset = ord("a") ^ d.regs.rbp
- d.regs.rbp = d.regs.r13
-
- # We calculate the correct character value and append it to the flag
- flag += (offset ^ d.regs.r13).to_bytes(1, "little")
-
- d.cont()
-
- r.recvline()
-
- d.kill()
-
- # Here the value of flag is b"\x00\x006\x00\x00\x00(\x00"
- return flag
-
- def get_passsphrase_from_class_2_binaries(previous_flag):
- bitmap = {}
- lastpos = 0
- flag = b""
-
- d = debugger("CTF/2")
- r = d.run()
-
- bp1 = d.breakpoint(0xD8C1, hardware=True, file="binary")
- bp2 = d.breakpoint(0x1858, hardware=True, file="binary")
- bp3 = d.breakpoint(0xDBA1, hardware=True, file="binary")
-
- d.cont()
-
- r.recvuntil(b"Passphrase:\n")
- r.send(previous_flag + b"a" * 8)
-
- while True:
- if d.regs.rip == bp1.address:
- # Prepare for the next element in the bitmap
- lastpos = d.regs.rbp
- d.regs.rbp = d.regs.r13 + 1
- elif d.regs.rip == bp2.address:
- # Update the bitmap
- bitmap[d.regs.r12 & 0xFF] = lastpos & 0xFF
- elif d.regs.rip == bp3.address:
- # Use the bitmap to calculate the expected character
- d.regs.rbp = d.regs.r13
- wanted = d.regs.rbp
- needed = 0
- for i in range(8):
- if wanted & (2**i):
- needed |= bitmap[2**i]
- flag += chr(needed).encode()
-
- if bp3.hit_count == 8:
- # We have found all the characters
- d.cont()
- break
-
- d.cont()
-
- d.kill()
-
- # Here the value of flag is b"\x00\x00\x00\x01\x00\x00a\x00"
- return flag
-
- def get_passsphrase_from_class_3_binaries():
- flag = b""
-
- d = debugger("CTF/0")
- r = d.run()
-
- bp = d.breakpoint(0x91A1, hardware=True, file="binary")
-
- d.cont()
-
- r.send(b"a" * 8)
-
- for _ in range(8):
-
- # Here we reached the breakpoint
- if not bp.hit_on(d):
- print("Here we should have hit the breakpoint")
-
- offset = ord("a") - d.regs.rbp
- d.regs.rbp = d.regs.r13
-
- # We calculate the correct character value and append it to the flag
- flag += chr((d.regs.r13 + offset) % 256).encode("latin-1")
-
- d.cont()
-
- r.recvline()
-
- d.kill()
-
- # Here the value of flag is b"BM8\xd3\x02\x00\x00\x00"
- return flag
-
- def run_nlinks():
- flag0 = get_passsphrase_from_class_3_binaries()
- flag1 = get_passsphrase_from_class_1_binaries(flag0)
- flag2 = get_passsphrase_from_class_2_binaries(flag1)
-
- print(flag0, flag1, flag2)
diff --git a/docs/source/handle_syscalls.rst b/docs/source/handle_syscalls.rst
deleted file mode 100644
index 720ec308..00000000
--- a/docs/source/handle_syscalls.rst
+++ /dev/null
@@ -1,112 +0,0 @@
-Syscalls
-========
-
-libdebug allows the user to manage syscalls of the debugged program. Specifically, you can choose to **handle** or **hijack** a specific syscall.
-
-In the case of *handling*, the user can provide a callback function that will be called whenever the handled syscall is executed or decide to pause the script when the syscall is executed.
-
-In the case of *hijacking*, the user can modify the syscall that was supposed to be executed, either by changing its parameters or replacing it with another syscall.
-
-Handlers
---------
-When handling a syscall, the user can provide up to two callback functions that will be called whenever the handled syscall is executed. One that is called before executing the syscall (`on_enter`), the other is called after executing the syscall (`on_exit`).
-
-Please note that it is not necessary to specify both `on_enter` and `on_exit` callbacks. It is sufficient to specify only one of them. The callback function must have the following signature:
-
-.. code-block:: python
-
- def callback(t: ThreadContext, handler: SyscallHandler) -> None:
-
-The first parameter is a thread context object. This kind of object is described in :doc:`multithreading`.
-The second parameter is the handler object that triggered the callback.
-
-When choosing which syscall to handle, you can either specify its number or its name. The following example shows how to handle the `open` syscall:
-
-.. code-block:: python
-
- def on_enter_open(t: ThreadContext, handler: SyscallHandler):
- print("entering open")
- t.syscall_arg0 = 0x1
-
- def on_exit_open(t: ThreadContext, handler: SyscallHandler):
- print("exiting open")
- t.syscall_return = 0x0
-
- handler = d.handle_syscall(syscall="open", on_enter=on_enter_open, on_exit=on_exit_open)
-
-You can also decide to pause the script when a syscall is executed by not specifying a callback function.
-
-.. code-block:: python
-
- handler = d.handle_syscall(syscall="open")
- d.cont()
-
- d.wait()
- if handler.hit_on_enter(d):
- print("open syscall was entered")
- elif handler.hit_on_exit(d):
- print("open syscall was exited")
-
-If the user chooses to pass the common name of the syscall, a definition list for Linux syscalls will be fetched from `mebeim's syscall list `__. The list is then cached internally.
-
-You can enable and disable a syscall handle `handler` with the `handler.enable()` and `handler.disable()` functions, respectively.
-
-Exactly as with breakpoints, you can access the `hit_count` property to get the number of times the syscall was executed:
-
-.. code-block:: python
-
- while handler.hit_count < 100:
- d.cont()
- print(f"Hit count: {handler.hit_count}")
-
-Please note that there can be at most **one** user-defined handler for each syscall.
-
-Builtin handlers of syscalls within libdebug does not cound toward that limit. For example, the pretty print function (described in :doc:`multithreading`) will not count as a user-defined handler.
-
-If a new handler is defined for a syscall that is already handled or hijacked, the new handler will replace the old one, and a warning will be printed.
-
-For example, in the following code, `handler_2` will override `handler_1`, showing a warning:
-
-.. code-block:: python
-
- handler_1 = d.handle_syscall(syscall="open", on_enter=on_enter_open_1, on_exit=on_exit_open_1)
- handler_2 = d.handle_syscall(syscall="open", on_enter=on_enter_open_2, on_exit=on_exit_open_2)
-
-Hijacking
----------
-
-While handling a syscall allows the user to monitor the syscall execution, hijacking a syscall allows the user to *alter* the syscall execution.
-
-When hijacking a syscall, the user can provide an alternative syscall to be executed in place of the original one:
-
-.. code-block:: python
-
- handler = d.hijack_syscall("read", "write")
-
-In this example, the `read` syscall will be replaced by the `write` syscall. The parameters of the `read` syscall will be passed to the `write` syscall.
-Again, it is possible to specify a syscall by its number in the syscall table or by its common name.
-
-For your comodity, you can also easily provide the syscall parameters to be used when the hijacked syscall is executed:
-
-.. code-block:: python
-
- handler = d.hijack_syscall("read", "write", syscall_arg0=0x1, syscall_arg1=write_buffer, syscall_arg2=0x100)
-
-Hijacking Loop Detection
-^^^^^^^^^^^^^^^^^^^^^^^^
-
-When carelessly hijacking syscalls, it could happen that loops are created. libdebug automatically performs checks to avoid these situations with syscall hijacking and raises an exception if an infinite loop is detected.
-
-For example, the following code raises a `RuntimeError`:
-
-.. code-block:: python
-
- handler = d.hijack_syscall("read", "write")
- handler = d.hijack_syscall("write", "read")
-
-
-Recursion
-^^^^^^^^^
-Mixing syscall handling and hijacking can become messy. Because of this, libdebug provides users with the choice of whether to execute the handler for a syscall that was triggered *by* a hijack.
-
-This behavior is enabled by the parameter `recursive`, available when instantiating a hijack or a handler. By default, the parameter is set to False.
\ No newline at end of file
diff --git a/docs/source/index.rst b/docs/source/index.rst
deleted file mode 100644
index 60aca123..00000000
--- a/docs/source/index.rst
+++ /dev/null
@@ -1,169 +0,0 @@
-.. libdebug documentation master file, created by
- sphinx-quickstart on Sun Jun 2 14:40:43 2024.
- You can adapt this file completely to your liking, but it should at least
- contain the root `toctree` directive.
-
-.. image:: _static/libdebug.png
- :alt: libdebug logo
- :align: center
-
-----
-
-.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.13151549.svg
- :target: https://doi.org/10.5281/zenodo.13151549
-
-Quick Start
-====================================
-Welcome to libdebug! This powerful Python library can be used to debug your binary executables programmatically, providing a robust, user-friendly interface.
-
-Debugging multithreaded applications can be a nightmare, but libdebug has you covered. Hijack, and manage signals and syscalls with a simple API.
-
-Looking for previous versions?
-------------------------------
-
-Go to https://docs.libdebug.org/archive/VERSION to find the documentation for a specific version of libdebug.
-
-e.g, for version 0.5.0, go to https://docs.libdebug.org/archive/0.5.0
-
-Supported Architectures
------------------------
-
-libdebug currently supports Linux under the x86_64 and AArch64 architectures.
-
-Other operating systems and architectures are not supported at this time.
-
-Dependencies
-------------
-
-To install libdebug, you first need to have some dependencies that will not be automatically resolved. Depending on your distro, their names may change.
-
-.. tabs::
-
- .. code-tab:: bash
- :caption: Ubuntu
-
- sudo apt install -y python3 python3-dev libdwarf-dev libelf-dev libiberty-dev linux-headers-generic libc6-dbg
-
- .. code-tab:: bash
- :caption: Debian
-
- sudo apt install -y python3 python3-dev libdwarf-dev libelf-dev libiberty-dev linux-headers-generic libc6-dbg
-
- .. code-tab:: bash
- :caption: Arch Linux
-
- sudo pacman -S python libelf libdwarf gcc make debuginfod
-
- .. code-tab:: bash
- :caption: Fedora
-
- sudo dnf install -y python3 python3-devel kernel-devel binutils-devel libdwarf-devel
-
-Installation
-------------
-Installing libdebug once you have dependencies is as simple as running the following command:
-
-.. code-block:: bash
-
- python3 -m pip install libdebug
-
-This will install the stable build of the library. If you don't mind a few hiccups, you can install the latest dev build from the Github repository:
-
-.. code-block:: bash
-
- python3 -m pip install git+https://github.com/libdebug/libdebug.git@dev
-
-If you want to to test your installation when installing from source, we provide a suite of tests that you can run:
-
-.. code-block:: bash
-
- cd test
- python run_suite.py
-
-The test folder includes the Makefile that was used to build the required binaries for transparency. However, the compiled binaries may differ due to scheduling, hardware, and compiler versions. Some tests have hardcoded absolute addresses and will likely fail as a result.
-
-Your first script
------------------
-
-Now that you have libdebug installed, you can start using it in your scripts. Here is a simple example of how to use libdebug to debug a binary:
-
-.. code-block:: python
-
- from libdebug import debugger
-
- d = debugger("./test")
-
- # Start debugging from the entry point
- d.run()
-
- my_breakpoint = d.breakpoint("function")
-
- # Continue the execution until the breakpoint is hit
- d.cont()
-
- # Print RAX
- print(f"RAX is {hex(d.regs.rax)}")
-
- # Kill the process
- d.kill()
-
-The above script will run the binary `test` in the working directory and stop at the function corresponding to the symbol "function". It will then print the value of the RAX register and kill the process.
-
-Conflicts with other Python packages
-------------------------------------
-
-The current version of libdebug is incompatible with https://github.com/Gallopsled/pwntools.
-
-While having both installed in your python environment is not a problem, starting a process with pwntools in a libdebug scripts will cause unexpected behaviors as a result of some race conditions.
-
-Examples of some known issues include:
-
-- ptrace not intercepting SIGTRAP signals when process is run with pwntools.
- This behavior is described in https://github.com/libdebug/libdebug/issues/48.
-- Attaching libdebug to a process that was started with pwntools with ``shell=True`` will cause the process to attach to the shell process instead.
- This behavior is described in https://github.com/libdebug/libdebug/issues/57.
-
-Cite Us
--------
-Need to cite libdebug in your research? Use the following BibTeX entry:
-
-.. code-block:: bibtex
-
- @software{libdebug_2024,
- title = {libdebug: {Build} {Your} {Own} {Debugger}},
- copyright = {MIT Licence},
- url = {https://libdebug.org},
- publisher = {libdebug.org},
- author = {Digregorio, Gabriele and Bertolini, Roberto Alessandro and Panebianco, Francesco and Polino, Mario},
- year = {2024},
- doi = {10.5281/zenodo.13151549},
- }
-
-.. toctree::
- :maxdepth: 2
- :caption: Contents:
-
- basic_features
- breakpoints
- handle_syscalls
- catch_signals
- multithreading
- quality_of_life
- logging
- examples
-
-.. autosummary::
- :toctree: _autosummary
-
-.. toctree::
- :maxdepth: 2
- :caption: Contents:
-
- libdebug
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
diff --git a/docs/source/libdebug.architectures.amd64.rst b/docs/source/libdebug.architectures.amd64.rst
deleted file mode 100644
index 066d5b43..00000000
--- a/docs/source/libdebug.architectures.amd64.rst
+++ /dev/null
@@ -1,53 +0,0 @@
-libdebug.architectures.amd64 package
-====================================
-
-Submodules
-----------
-
-libdebug.architectures.amd64.amd64\_ptrace\_hw\_bp\_helper module
------------------------------------------------------------------
-
-.. automodule:: libdebug.architectures.amd64.amd64_ptrace_hw_bp_helper
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.architectures.amd64.amd64\_ptrace\_register\_holder module
--------------------------------------------------------------------
-
-.. automodule:: libdebug.architectures.amd64.amd64_ptrace_register_holder
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.architectures.amd64.amd64\_registers module
-----------------------------------------------------
-
-.. automodule:: libdebug.architectures.amd64.amd64_registers
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.architectures.amd64.amd64\_stack\_unwinder module
-----------------------------------------------------------
-
-.. automodule:: libdebug.architectures.amd64.amd64_stack_unwinder
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.architectures.amd64.amd64\_syscall\_hijacker module
-------------------------------------------------------------
-
-.. automodule:: libdebug.architectures.amd64.amd64_syscall_hijacker
- :members:
- :undoc-members:
- :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: libdebug.architectures.amd64
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/libdebug.architectures.rst b/docs/source/libdebug.architectures.rst
deleted file mode 100644
index d74edb50..00000000
--- a/docs/source/libdebug.architectures.rst
+++ /dev/null
@@ -1,85 +0,0 @@
-libdebug.architectures package
-==============================
-
-Subpackages
------------
-
-.. toctree::
- :maxdepth: 4
-
- libdebug.architectures.amd64
-
-Submodules
-----------
-
-libdebug.architectures.ptrace\_hardware\_breakpoint\_manager module
--------------------------------------------------------------------
-
-.. automodule:: libdebug.architectures.ptrace_hardware_breakpoint_manager
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.architectures.ptrace\_hardware\_breakpoint\_provider module
---------------------------------------------------------------------
-
-.. automodule:: libdebug.architectures.ptrace_hardware_breakpoint_provider
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.architectures.ptrace\_software\_breakpoint\_patcher module
--------------------------------------------------------------------
-
-.. automodule:: libdebug.architectures.ptrace_software_breakpoint_patcher
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.architectures.register\_helper module
-----------------------------------------------
-
-.. automodule:: libdebug.architectures.register_helper
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.architectures.stack\_unwinding\_manager module
--------------------------------------------------------
-
-.. automodule:: libdebug.architectures.stack_unwinding_manager
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.architectures.stack\_unwinding\_provider module
---------------------------------------------------------
-
-.. automodule:: libdebug.architectures.stack_unwinding_provider
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.architectures.syscall\_hijacking\_manager module
----------------------------------------------------------
-
-.. automodule:: libdebug.architectures.syscall_hijacking_manager
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.architectures.syscall\_hijacking\_provider module
-----------------------------------------------------------
-
-.. automodule:: libdebug.architectures.syscall_hijacking_provider
- :members:
- :undoc-members:
- :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: libdebug.architectures
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/libdebug.builtin.rst b/docs/source/libdebug.builtin.rst
deleted file mode 100644
index 5469f2a5..00000000
--- a/docs/source/libdebug.builtin.rst
+++ /dev/null
@@ -1,29 +0,0 @@
-libdebug.builtin package
-========================
-
-Submodules
-----------
-
-libdebug.builtin.antidebug\_syscall\_handler module
----------------------------------------------------
-
-.. automodule:: libdebug.builtin.antidebug_syscall_handler
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.builtin.pretty\_print\_syscall\_handler module
--------------------------------------------------------
-
-.. automodule:: libdebug.builtin.pretty_print_syscall_handler
- :members:
- :undoc-members:
- :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: libdebug.builtin
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/libdebug.cffi.rst b/docs/source/libdebug.cffi.rst
deleted file mode 100644
index 3db278d7..00000000
--- a/docs/source/libdebug.cffi.rst
+++ /dev/null
@@ -1,45 +0,0 @@
-libdebug.cffi package
-=====================
-
-Submodules
-----------
-
-libdebug.cffi.debug\_sym\_cffi\_build module
---------------------------------------------
-
-.. automodule:: libdebug.cffi.debug_sym_cffi_build
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.cffi.debug\_sym\_cffi\_build\_legacy module
-----------------------------------------------------
-
-.. automodule:: libdebug.cffi.debug_sym_cffi_build_legacy
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.cffi.personality\_cffi\_build module
----------------------------------------------
-
-.. automodule:: libdebug.cffi.personality_cffi_build
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.cffi.ptrace\_cffi\_build module
-----------------------------------------
-
-.. automodule:: libdebug.cffi.ptrace_cffi_build
- :members:
- :undoc-members:
- :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: libdebug.cffi
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/libdebug.data.rst b/docs/source/libdebug.data.rst
deleted file mode 100644
index 209c1ee4..00000000
--- a/docs/source/libdebug.data.rst
+++ /dev/null
@@ -1,69 +0,0 @@
-libdebug.data package
-=====================
-
-Submodules
-----------
-
-libdebug.data.breakpoint module
--------------------------------
-
-.. automodule:: libdebug.data.breakpoint
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.data.memory\_map module
---------------------------------
-
-.. automodule:: libdebug.data.memory_map
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.data.memory\_view module
----------------------------------
-
-.. automodule:: libdebug.data.memory_view
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.data.register\_holder module
--------------------------------------
-
-.. automodule:: libdebug.data.register_holder
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.data.registers module
-------------------------------
-
-.. automodule:: libdebug.data.registers
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.data.signal\_catcher module
-------------------------------------
-
-.. automodule:: libdebug.data.signal_catcher
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.data.syscall\_handler module
--------------------------------------
-
-.. automodule:: libdebug.data.syscall_handler
- :members:
- :undoc-members:
- :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: libdebug.data
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/libdebug.debugger.rst b/docs/source/libdebug.debugger.rst
deleted file mode 100644
index 962906db..00000000
--- a/docs/source/libdebug.debugger.rst
+++ /dev/null
@@ -1,45 +0,0 @@
-libdebug.debugger package
-=========================
-
-Submodules
-----------
-
-libdebug.debugger.debugger module
----------------------------------
-
-.. automodule:: libdebug.debugger.debugger
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.debugger.internal\_debugger module
--------------------------------------------
-
-.. automodule:: libdebug.debugger.internal_debugger
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.debugger.internal\_debugger\_holder module
----------------------------------------------------
-
-.. automodule:: libdebug.debugger.internal_debugger_holder
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.debugger.internal\_debugger\_instance\_manager module
---------------------------------------------------------------
-
-.. automodule:: libdebug.debugger.internal_debugger_instance_manager
- :members:
- :undoc-members:
- :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: libdebug.debugger
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/libdebug.interfaces.rst b/docs/source/libdebug.interfaces.rst
deleted file mode 100644
index 020cad28..00000000
--- a/docs/source/libdebug.interfaces.rst
+++ /dev/null
@@ -1,37 +0,0 @@
-libdebug.interfaces package
-===========================
-
-Submodules
-----------
-
-libdebug.interfaces.debugging\_interface module
------------------------------------------------
-
-.. automodule:: libdebug.interfaces.debugging_interface
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.interfaces.interface\_helper module
---------------------------------------------
-
-.. automodule:: libdebug.interfaces.interface_helper
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.interfaces.interfaces module
--------------------------------------
-
-.. automodule:: libdebug.interfaces.interfaces
- :members:
- :undoc-members:
- :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: libdebug.interfaces
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/libdebug.ptrace.jumpstart.rst b/docs/source/libdebug.ptrace.jumpstart.rst
deleted file mode 100644
index 8e9ec510..00000000
--- a/docs/source/libdebug.ptrace.jumpstart.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-libdebug.ptrace.jumpstart package
-=================================
-
-Module contents
----------------
-
-.. automodule:: libdebug.ptrace.jumpstart
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/libdebug.ptrace.rst b/docs/source/libdebug.ptrace.rst
deleted file mode 100644
index f233c0b7..00000000
--- a/docs/source/libdebug.ptrace.rst
+++ /dev/null
@@ -1,53 +0,0 @@
-libdebug.ptrace package
-=======================
-
-Subpackages
------------
-
-.. toctree::
- :maxdepth: 4
-
- libdebug.ptrace.jumpstart
-
-Submodules
-----------
-
-libdebug.ptrace.ptrace\_constants module
-----------------------------------------
-
-.. automodule:: libdebug.ptrace.ptrace_constants
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.ptrace.ptrace\_interface module
-----------------------------------------
-
-.. automodule:: libdebug.ptrace.ptrace_interface
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.ptrace.ptrace\_register\_holder module
------------------------------------------------
-
-.. automodule:: libdebug.ptrace.ptrace_register_holder
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.ptrace.ptrace\_status\_handler module
-----------------------------------------------
-
-.. automodule:: libdebug.ptrace.ptrace_status_handler
- :members:
- :undoc-members:
- :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: libdebug.ptrace
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/libdebug.rst b/docs/source/libdebug.rst
deleted file mode 100644
index bad1c21e..00000000
--- a/docs/source/libdebug.rst
+++ /dev/null
@@ -1,45 +0,0 @@
-libdebug package
-================
-
-Subpackages
------------
-
-.. toctree::
- :maxdepth: 4
-
- libdebug.architectures
- libdebug.builtin
- libdebug.cffi
- libdebug.data
- libdebug.debugger
- libdebug.interfaces
- libdebug.ptrace
- libdebug.state
- libdebug.utils
-
-Submodules
-----------
-
-libdebug.libdebug module
-------------------------
-
-.. automodule:: libdebug.libdebug
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.liblog module
-----------------------
-
-.. automodule:: libdebug.liblog
- :members:
- :undoc-members:
- :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: libdebug
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/libdebug.state.rst b/docs/source/libdebug.state.rst
deleted file mode 100644
index dc8831e2..00000000
--- a/docs/source/libdebug.state.rst
+++ /dev/null
@@ -1,29 +0,0 @@
-libdebug.state package
-======================
-
-Submodules
-----------
-
-libdebug.state.resume\_context module
--------------------------------------
-
-.. automodule:: libdebug.state.resume_context
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.state.thread\_context module
--------------------------------------
-
-.. automodule:: libdebug.state.thread_context
- :members:
- :undoc-members:
- :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: libdebug.state
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/libdebug.utils.rst b/docs/source/libdebug.utils.rst
deleted file mode 100644
index a4fc0e9d..00000000
--- a/docs/source/libdebug.utils.rst
+++ /dev/null
@@ -1,101 +0,0 @@
-libdebug.utils package
-======================
-
-Submodules
-----------
-
-libdebug.utils.debugger\_wrappers module
-----------------------------------------
-
-.. automodule:: libdebug.utils.debugger_wrappers
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.utils.debugging\_utils module
---------------------------------------
-
-.. automodule:: libdebug.utils.debugging_utils
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.utils.elf\_utils module
---------------------------------
-
-.. automodule:: libdebug.utils.elf_utils
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.utils.gdb module
--------------------------
-
-.. automodule:: libdebug.utils.gdb
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.utils.libcontext module
---------------------------------
-
-.. automodule:: libdebug.utils.libcontext
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.utils.pipe\_manager module
------------------------------------
-
-.. automodule:: libdebug.utils.pipe_manager
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.utils.posix\_spawn module
-----------------------------------
-
-.. automodule:: libdebug.utils.posix_spawn
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.utils.print\_style module
-----------------------------------
-
-.. automodule:: libdebug.utils.print_style
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.utils.process\_utils module
-------------------------------------
-
-.. automodule:: libdebug.utils.process_utils
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.utils.signal\_utils module
------------------------------------
-
-.. automodule:: libdebug.utils.signal_utils
- :members:
- :undoc-members:
- :show-inheritance:
-
-libdebug.utils.syscall\_utils module
-------------------------------------
-
-.. automodule:: libdebug.utils.syscall_utils
- :members:
- :undoc-members:
- :show-inheritance:
-
-Module contents
----------------
-
-.. automodule:: libdebug.utils
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/logging.rst b/docs/source/logging.rst
deleted file mode 100644
index 88b0bce9..00000000
--- a/docs/source/logging.rst
+++ /dev/null
@@ -1,67 +0,0 @@
-Logging
-=======
-Debugging an application with the freedom of a rich API can lead to flows which are hard to unravel. To aid the user in the debugging process, libdebug provides logging, which can be enabled through `argv` parameters.
-
-The available logging levels are:
-
-- ``debugger``
-- ``pipe``
-- ``dbg``
-
-As reported in this documentation, the `argv` parameters are *lowercase*. This choice is made to avoid conflicts with https://github.com/Gallopsled/pwntools, which intercepts uppercase arguments.
-
-Debugger Logging
-----------------
-The `debugger` option displays all logs related to the debugging operations performed on the process by libdebug.
-
-.. image:: https://github.com/libdebug/libdebug/blob/dev/media/debugger_argv.png?raw=true
- :alt: debugger argv option
-
-
-Pipe Logging
-------------
-The `pipe` option, on the other hand, displays all logs related to interactions with the process pipe: bytes received and bytes sent.
-
-.. image:: https://github.com/libdebug/libdebug/blob/dev/media/pipe_argv.png?raw=true
- :alt: pipe argv option
-
-The best of both worlds
------------------------
-The `dbg` option is the combination of the `pipe` and `debugger` options. It displays all logs related to the debugging operations performed on the process by libdebug, as well as interactions with the process pipe: bytes received and bytes sent.
-
-Change logger levels at runtime
--------------------------------
-Logger levels can be changed at runtime using the `libcontext` module. The following example shows how to change the logger levels at runtime.
-
-.. code-block:: python
-
- from libdebug import libcontext
- libcontext.pipe_logger = 'DEBUG'
- libcontext.debugger_logger = 'DEBUG'
- libcontext.general_logger = 'DEBUG'
-
-The `general_logger` refers to the logger used for the general logs, different from the `pipe` and `debugger` logs.
-
-Logger levels can be temporarily enabled at runtime using a `with` statement, as shown in the following example.
-
-.. code-block:: python
-
- from libdebug import libcontext
- with libcontext.tmp(pipe_logger='SILENT', debugger_logger='DEBUG'):
- r.sendline(b'gimme the flag')
-
-In this example, the `pipe_logger` is set to `SILENT`, and the `debugger_logger` is set to `DEBUG`. The logger levels are restored to their previous values when the `with` block is exited.
-
-The supported logger levels are the following:
-
-- ``pipe_logger``: ``DEBUG``, ``SILENT``
-- ``debugger_logger``: ``DEBUG``, ``SILENT``
-- ``general_logger``: ``DEBUG``, ``INFO``, ``WARNING``, ``SILENT``
-
-The default logger levels are:
-
-- ``pipe_logger``: ``SILENT``
-- ``debugger_logger``: ``SILENT``
-- ``general_logger``: ``DEBUG``
-
-The `DEBUG` level is the most verbose, including all logs. The `INFO` level includes all logs except for the `DEBUG` logs. The `WARNING` level includes only the `WARNING` logs. The `SILENT` level disables all logs.
\ No newline at end of file
diff --git a/docs/source/modules.rst b/docs/source/modules.rst
deleted file mode 100644
index b99b2e53..00000000
--- a/docs/source/modules.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-libdebug
-========
-
-.. toctree::
- :maxdepth: 4
-
- libdebug
-
diff --git a/docs/source/multithreading.rst b/docs/source/multithreading.rst
deleted file mode 100644
index b1287335..00000000
--- a/docs/source/multithreading.rst
+++ /dev/null
@@ -1,59 +0,0 @@
-Multithreading
-==============
-.. _multithreading:
-
-libdebug provides a simple way to debug multithreaded programs. Each time the process is cloned, the new thread is automatically traced and registered in the `threads` property of the debugger object.
-
-.. code-block:: python
-
- # Create a debugger object
- d = debugger("./threaded_test")
-
- # Start debugging and continue until the first stopping event
- d.run()
- d.cont()
-
- # Print thread id and program counter value for all threads
- for thread in d.threads:
- print(thread.thread_id, hex(thread.regs.rip))
-
- d.cont()
-
- # Kill all threads
- d.kill()
-
-Objects in the `threads` list are `ThreadContext` objects, which behave similarly to the debugger. Each thread object has a `regs` property that exposes the registers of the thread and a `memory` property for memory access. You can access these properties exactly as you did with the debugger object. See :doc:`basic_features` for more information.
-
-Control Flow Operations
------------------------
-
-Control flow is synchronous between threads: they are either either are all stopped or all running. To this end, the debugger stops all the threads every time a single thread stops. This is a design choice to avoid unexpected behavior as a result of concurrency.
-
-The following is a list of behaviors to keep in mind when using control flow funcions in multithreaded programs.
-
-- `cont` will continue all threads.
-- `step` and `step_until` will step the selected thread.
-- `next` will step on the selected thread or, if a call function is found, continue on all threads until the end of the called function or another stopping event.
-- `finish` will have different behavior depending on the selected heuristic.
- - `backtrace` will continue on all threads but will stop at any breakpoint that any of the threads hit.
- - `step-mode` will step exclusively on the thread that has been specified.
-
-
-Breakpoints
------------
-
-Breakpoints are shared between all threads. This means that if a breakpoint is hit by one thread, all threads will stop. This is a design choice to avoid unexpected behavior as a result of concurrency. This, of course, requires a way for the user to distinguish which thread has hit the breakpoint.
-
-The :class:`libdebug.data.breakpoint.Breakpoint` class contains a function called `hit_on`. Given a thread, it will return whether the breakpoint has been hit on that thread.
-
-.. code-block:: python
-
- # Create a breakpoint at address 0x4005a0
- bp = d.breakpoint(0x15a0)
-
- d.cont()
-
- # Print thread id and program counter value for all threads
- for thread in d.threads:
- if bp.hit_on(thread):
- print("Thread", thread.thread_id, "hit the breakpoint")
\ No newline at end of file
diff --git a/docs/source/quality_of_life.rst b/docs/source/quality_of_life.rst
deleted file mode 100644
index 4e693a3f..00000000
--- a/docs/source/quality_of_life.rst
+++ /dev/null
@@ -1,55 +0,0 @@
-Quality of Life
-===============
-For your convenience, libdebug offers a few functions that will speed up your debugging process.
-
-Automatic Evasion of Anti-Debugging Techniques
-----------------------------------------------
-
-A common anti-debugging technique for Linux ELF binaries is to invoke the `ptrace` syscall with the `PTRACE_TRACEME` argument. The syscall will fail if the binary is currently being traced by a debugger.
-
-Bypassing this technique involves intercepting such syscalls and altering the return value to make the binary believe that it is not being traced. While this can absolutely be performed manually in libdebug, there is also the possibility of passing `escape_antidebug=True` when creating the debugger object. The debugger will take care of the rest.
-
-Syscall Trace Pretty Print
---------------------------
-
-When debugging a binary, it is often much faster to guess what the intended functionality is by looking at the syscalls that are being invoked. libdebug offers a function that will intercept any syscall and print its arguments and return value. This can be done by setting the property `pprint_syscalls = True` in the debugger object.
-
-The output will be printed to the console in color. Handled syscalls with a callback associated with them will be listed as such. Additionally, syscalls hijacked through the libdebug API will be highlighted as striken through, allowing you to monitor both the original behavior and your own changes to the flow.
-
-.. image:: https://github.com/libdebug/libdebug/blob/dev/media/pprint_syscalls.png?raw=true
-
-Symbol Resolution
------------------
-In many of its functions, libdebug accepts ELF symbols as an alternative to actual addresses.
-
-Sometimes, parsing symbol is an expensive operation. Because of this, libdebug offers the possibility of setting the level of symbol resolution.
-
-There are six different levels for symbol resolutions, as follows:
-
-- 0: Symbol resolution is disabled.
-- 1: Parse the ELF symbol table (.symtab) and dynamic symbol table (.dynsym).
-- 2: Parse the ELF DWARF.
-- 3: Follow the external debug file link in the .gnu_debuglink and/or .gnu_debugaltlink sections. If the file is present in the system, read its .symtab and .dynsym.
-- 4: Parse the external debug file DWARF, if the file exists in the system.
-- 5: Download the external debug file using `debuginfod`. The file is cached in the default folder for `debuginfod`.
-
-The default value is level 4 can be modified at runtime in the following way:
-
-.. code-block:: python
-
- from libdebug import libcontext
-
- libcontext.sym_lvl = 5
- d.breakpoint('main')
-
-or also
-
-.. code-block:: python
-
- from libdebug import libcontext
-
- with libcontext.tmp(sym_lvl = 5):
- d.breakpoint('main')
-
-
-Additionally, since reverse-engineering C++ binaries can be a struggle, libdebug automatically demangles C++ symbols.
\ No newline at end of file
diff --git a/docs/source/setup.rst b/docs/source/setup.rst
deleted file mode 100644
index 552eb49d..00000000
--- a/docs/source/setup.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-setup module
-============
-
-.. automodule:: setup
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/stopping_events/breakpoints.md b/docs/stopping_events/breakpoints.md
new file mode 100644
index 00000000..0484308b
--- /dev/null
+++ b/docs/stopping_events/breakpoints.md
@@ -0,0 +1,119 @@
+---
+icon: material/sign-caution
+search:
+ boost: 4
+---
+# :material-sign-caution: Breakpoints
+Breakpoints are the killer feature of any debugger, the fundamental stopping event. They allow you to stop the execution of your code at a specific point and inspect the state of your program to find bugs or understand its design.
+
+!!! WARNING "Multithreading and Breakpoints"
+ **libdebug** breakpoints are shared across all threads. This means that any thread can hit the breakpoint and cause the process to stop. You can use the [`hit_on()`](../debugging_flow/#hit-records) method of a breakpoint object to determine which thread hit the breakpoint (provided that the stop was indeed caused by the breakpoint).
+
+A breakpoint can be inserted at any of two levels: *software* or *hardware*.
+
+### :octicons-code-24: Software Breakpoints
+Software breakpoints in the Linux kernel are implemented by patching the code in memory at runtime. The instruction at the chosen address is replaced with an interrupt instruction that is conventionally used for debugging. For example, in the `i386` and `AMD64` instruction sets, `int3` (0xCC) is reserved for this purpose.
+
+When the `int3` instruction is executed, the CPU raises a `SIGTRAP` signal, which is caught by the debugger. The debugger then stops the process and restores the original instruction to its rightful place.
+
+!!! TIP "Pros and Cons of Software Breakpoints"
+ Software breakpoints are unlimited, but they can break when the program uses self-modifying code. This is because the patched code could be overwritten by the program. On the other hand, software breakpoints are slower than their hardware counterparts on most modern CPUs.
+
+### :octicons-cpu-24: Hardware Breakpoints
+Hardware breakpoints are a more reliable way to set breakpoints. They are made possible by the existence of special registers in the CPU that can be used to monitor memory accesses. Differently from software breakpoints, their hardware counterparts allows the debugger to monitor read and write accesses on top of code execution. This kind of hardware breakpoint is also called a [watchpoint](../watchpoints). More information on watchpoints can be found in the dedicated documentation.
+
+!!! TIP "Pros and Cons of Hardware Breakpoints"
+ Hardware breakpoints are not affected by self-modifying code. They are also usually faster and more flexible. However, hardware breakpoints are limited in number and are hardware-dependent, so their support may vary across different systems.
+
+!!! INFO "Hardware Breakpoint Alignment in AArch64"
+ Hardware breakpoints have to be aligned to 4 bytes (which is the size of an ARM instruction).
+
+
+## **libdebug** API for Breakpoints
+
+The `breakpoint()` function in the [Debugger](../../from_pydoc/generated/debugger/debugger/) object sets a breakpoint at a specific address.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.breakpoint(position, hardware=False, condition='x', length=1, callback=None, file='hybrid')
+ ```
+
+**Parameters**:
+
+| Argument | Type | Description |
+| --- | --- | --- |
+| `position` | `int` \| `str` | The address or symbol where the breakpoint will be set. |
+| `hardware` | `bool` | Set to `True` to set a hardware breakpoint. |
+| `condition` | `str` | The type of access in case of a hardware breakpoint. |
+| `length` | `int` | The size of the word being watched in case of a hardware breakpoint. |
+| `callback` | `Callable` \| `bool` (see callback signature [here](#callback-signature)) | Used to create asyncronous breakpoints (read more on the [debugging flow of stopping events](../debugging_flow)). |
+| `file` | `str` | The backing file for relative addressing. Refer to the [memory access](../../basics/memory_access/#absolute-and-relative-addressing) section for more information on addressing modes. |
+
+**Returns**:
+
+| Return | Type | Description |
+| --- | --- | --- |
+| `Breakpoint` | [Breakpoint](../../from_pydoc/generated/data/breakpoint) | The breakpoint object created. |
+
+!!! WARNING "Limited Hardware Breakpoints"
+ Hardware breakpoints are limited in number. If you exceed the number of hardware breakpoints available on your system, a `RuntimeError` will be raised.
+
+---
+
+!!! ABSTRACT "Usage Example"
+ ```python
+ from libdebug import debugger
+
+ d = debugger("./test_program")
+
+ d.run()
+
+ bp = d.breakpoint(0x10ab, file="binary") # (1)
+ bp1 = d.breakpoint("main", file="binary") # (3)
+ bp2 = d.breakpoint("printf", file="libc") # (4)
+
+ d.cont()
+
+ print(f"RAX: {d.regs.rax} at the breakpoint") # (2)
+ if bp.hit_on(d):
+ print("Breakpoint at 0x10ab was hit")
+ elif bp1.hit_on(d):
+ print("Breakpoint at main was hit")
+ elif bp2.hit_on(d):
+ print("Breakpoint at printf was hit")
+ ```
+
+ 1. Set a software breakpoint at address 0x10ab relative to the program's base address
+ 2. Print the value of the RAX register when the breakpoint is hit
+ 3. Set a software breakpoint at the `main` symbol
+ 4. Set a software breakpoint at the `printf` symbol in the `libc` library
+
+
+### Callback Signature
+If you wish to create an [asynchronous](../debugging_flow) breakpoint, you will have to provide a callback function. If you want to leave the callback empty, you can set callback to `True`.
+
+!!! ABSTRACT "Callback Signature"
+ ```python
+ def callback(t: ThreadContext, bp: Breakpoint):
+ ```
+
+**Parameters**:
+
+| Argument | Type | Description |
+| --- | --- | --- |
+| `t` | [ThreadContext](../../from_pydoc/generated/state/thread_context) | The thread that hit the breakpoint. |
+| `bp` | [Breakpoint](../../from_pydoc/generated/data/breakpoint) | The breakpoint object that triggered the callback. |
+
+---
+
+!!! ABSTRACT "Example usage of asynchronous breakpoints"
+ ```python
+ def on_breakpoint_hit(t, bp):
+ print(f"RAX: {t.regs.rax}")
+
+ if bp.hit_count == 100:
+ print("Hit count reached 100")
+ bp.disable()
+
+ d.breakpoint(0x11f0, callback=on_breakpoint_hit, file="binary")
+ ```
diff --git a/docs/stopping_events/debugging_flow.md b/docs/stopping_events/debugging_flow.md
new file mode 100644
index 00000000..332414f3
--- /dev/null
+++ b/docs/stopping_events/debugging_flow.md
@@ -0,0 +1,81 @@
+---
+icon: octicons/git-pull-request-draft-24
+search:
+ boost: 4
+---
+# :octicons-git-pull-request-draft-24: Debugging Flow of Stopping Events
+Before diving into each **libdebug** stopping event, it's crucial to understand the debugging flow that these events introduce, based on the mode selected by the user.
+
+The flow of all stopping events is similar and adheres to a mostly uniform API structure. Upon placing a stopping event, the user is allowed to specify a callback function for the stopping event. If a callback is passed, the event will trigger **asynchronously**. Otherwise, if the callback is not passed, the event will be **synchronous**. The following flowchart shows the difference between the two flows.
+
+---
+
+
+ { width="90%" }
+ { width="90%" }
+ Flowchart of different handling modes for stopping events
+
+
+When a **synchronous** event is hit, the process will stop, awaiting further commands. When an **asynchronous** event is hit, **libdebug** temporarily stops the process and invokes the user callback. Process execution is automatically resumed right after.
+
+!!! TIP "Tip: Use cases of asynchronous stopping events"
+ The asynchronous mode for stopping events is particularly useful for events being repeated as a result of a loop in the executed code.
+
+ When attempting side-channel reverse engineering, this mode can save a lot of your time.
+
+## :material-format-list-bulleted-type: Types of Stopping Events
+
+**libdebug** supports the following types of stopping events:
+
+| Event Type | Description | Notes |
+|------------|--------------------------------------|--------------------------------------|
+| [Breakpoint](../breakpoints) | Stops the process when a certain address is executed | Can be a software or a hardware breakpoint |
+| [Watchpoint](../watchpoints) | Stops the process when a memory area is read or written | Alias for a hardware breakpoint |
+| [Syscall](../syscalls) | Stops the process when a syscall is made | Two events are supported: syscall start and end |
+| [Signal](../signals) | Stops the process when a signal is received | |
+
+!!! INFO "Multiple callbacks or hijacks"
+ Please note that there can be at most **one** user-defined callback or hijack for each instance of a stopping event (the same syscall, signal or breakpoint address). If a new stopping event is defined for the same thing, the new stopping event will replace the old one, and a warning will be printed.
+
+ Internally, hijacks are considered callbacks, so you cannot have a callback and hijack registered for the same event.
+
+## Common APIs of Stopping Events
+All **libdebug** stopping events share some common attributes that can be employed in debugging scripts.
+
+### :material-power: Enable/Disable
+All stopping events can be enabled or disabled at any time. You can read the `enabled` attribute to check the current state of the event. To enable or disable the event, you can call the `enable()` or `disable()` methods respectively.
+
+### :material-lambda: Callback
+The callback function of the event can be set, changed or removed (set to `None`) at any time. Please be mindful of the event mode resulting from the change on the callback parameter. Additionally, you can set the callback to `True` to register an empty callback.
+
+### :simple-ticktick: Hit Records
+Stopping events have attributes that can help you keep track of hits. For example, the `hit_count` attribute stores the number of times the event has been triggered.
+
+The `hit_on()` function is used to check if the stopping event was the cause of the process stopping. It is particularly useful when debugging multithreaded applications, as it takes a [ThreadContext](../../from_pydoc/generated/state/thread_context) as a parameter. Refer to [multithreading](../../multithreading/multithreading) for more information.
+
+### :material-arrow-decision: Hijacking
+Hijacking is a powerful feature that allows you to change the flow of the process when a stopping event is hit. It is available for both syscalls and signals, but currently not for other stopping events. When registering a hijack for a compatible stopping event, that execution flow will be replaced with another.
+
+
+ { width="90%" }
+ { width="90%" }
+ Example hijacking of a SIGALRM to a SIGUSR1
+
+
+For example, in the case of a signal, you can specify that a received `SIGALRM` signal should be replaced with a `SIGUSR1` signal. This can be useful when you want to prevent a process from executing a certain code path. In fact, you can even use the hijack feature to "NOP" the syscall or signal altogether, avoiding it to be executed / forwarded to the processed. More information on how to use this feature in each stopping event can be found in their respective documentation.
+
+
+### :fontawesome-solid-arrows-rotate: Recursion
+Mixing asynchronous callbacks and hijacking can become messy. Because of this, **libdebug** provides users with the choice of whether to execute the callback for an event that was triggered *by* a callback or hijack.
+
+This behavior is enabled by the parameter `recursive`, available when instantiating a syscall handler, a signal catcher, or their respective hijackers. By default, recursion is disabled.
+
+!!! WARNING "Recursion Loop Detection"
+ When carelessly doing recursive callbacks and hijacking, it could happen that loops are created. **libdebug** automatically performs checks to avoid these situations and raises an exception if an infinite loop is detected.
+
+ For example, the following code raises a `RuntimeError`:
+
+ ```python
+ handler = d.hijack_syscall("read", "write", recursive=True)
+ handler = d.hijack_syscall("write", "read", recursive=True)
+ ```
\ No newline at end of file
diff --git a/docs/stopping_events/signals.md b/docs/stopping_events/signals.md
new file mode 100644
index 00000000..8314054e
--- /dev/null
+++ b/docs/stopping_events/signals.md
@@ -0,0 +1,216 @@
+---
+icon: material/traffic-light-outline
+search:
+ boost: 4
+---
+# :material-traffic-light-outline: Signals
+[Signals](https://man7.org/linux/man-pages/man7/signal.7.html) are a feature of POSIX systems like (e.g., the Linux kernel) that provide a mechanism for asynchronous communication between processes and the operating system. When certain events occur (e.g., hardware interrupts, illegal operations, or termination requests) the kernel can send a signal to a process to notify it of the event. Each signal is identified by a unique integer and corresponds to a specific type of event. For example, `SIGINT` (usually triggered by pressing `Ctrl+C`) is used to interrupt a process, while `SIGKILL` forcefully terminates a process without cleanup.
+
+Processes can handle these signals in different ways: they may catch and define custom behavior for certain signals, ignore them, or allow the default action to occur.
+
+!!! WARNING "Restrictions on Signal Catching"
+ **libdebug** does not support catching `SIGTRAP`, `SIGSTOP`, and `SIGKILL`. While the first is used internally for debugging purposes, the other two cannot be blocked as a result of kernel limitations.
+
+**libdebug** allows you to intercept signals sent to the tracee. Specifically, you can choose to **catch** or **hijack** a specific signal (read more on [hijacking](../stopping_events/#hijacking)).
+
+## :material-bucket-outline: Signal Catchers
+Signal catchers can be created to register [stopping events](../stopping_events/) for when a signal is received.
+
+!!! INFO "Multiple catchers for the same signal"
+ Please note that there can be at most **one** user-defined catcher or hijack for each signal. If a new catcher is defined for a signal that is already caught or hijacked, the new catcher will replace the old one, and a warning will be printed.
+
+## **libdebug** API for Signal Catching
+The `catch_signal()` function in the [Debugger](../../from_pydoc/generated/debugger/debugger/) object registers a catcher for the specified signal.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.catch_signal(signal, callback=None, recursive=False)
+ ```
+
+**Parameters**:
+
+| Argument | Type | Description |
+| --- | --- | --- |
+| `signal` | `int` \| `str` | The signal number or name to catch. If set to `"*"` or `"all"`, all signals will be caught. |
+| `callback` | `Callable` \| `bool` (see callback signature [here](#callback-signature)) | The callback function to be executed when the signal is received. |
+| `recursive` | `bool` | If set to `True`, the catcher's callback will be executed even if the signal was triggered by a hijack. |
+
+**Returns**:
+
+| Return | Type | Description |
+| --- | --- | --- |
+| `SignalCatcher` | [SignalCatcher](../../from_pydoc/generated/data/signal_catcher) | The catcher object created. |
+
+### Callback Signature
+
+!!! ABSTRACT "Callback Signature"
+ ```python
+ def callback(t: ThreadContext, catcher: SignalCatcher):
+ ```
+
+**Parameters**:
+
+| Argument | Type | Description |
+| --- | --- | --- |
+| `t` | [ThreadContext](../../from_pydoc/generated/state/thread_context) | The thread that received the signal. |
+| `catcher` | [SignalCatcher](../../from_pydoc/generated/data/signal_catcher) | The SignalCatcher object that triggered the callback. |
+
+!!! WARNING "Signals in multi-threaded applications"
+ In the Linux kernel, an incoming signal could be delivered to any thread in the process. Please do not assume that the signal will be delivered to a specific thread in your scripts.
+
+---
+
+!!! ABSTRACT "Example usage of asynchronous signal catchers"
+ ```python
+ from libdebug import debugger
+
+ d = debugger("./test_program")
+ d.run()
+
+ # Define the callback function
+ def catcher_SIGUSR1(t, catcher):
+ t.signal = 0x0
+ print("Look mum, I'm catching a signal")
+
+ def catcher_SIGINT(t, catcher):
+ print("Look mum, I'm catching another signal")
+
+ # Register the signal catchers
+ catcher1 = d.catch_signal(10, callback=catcher_SIGUSR1)
+ catcher2 = d.catch_signal('SIGINT', callback=catcher_SIGINT)
+
+ d.cont()
+ d.wait()
+ ```
+
+!!! ABSTRACT "Example of synchronous signal catching"
+ ```python
+ from libdebug import debugger
+
+ d = debugger("./test_program")
+ d.run()
+
+ catcher = d.catch_signal(10)
+ d.cont()
+
+ if catcher.hit_on(d):
+ print("Signal 10 was caught")
+ ```
+
+ The script above will print "Signal 10 was entered".
+
+## :material-arrow-decision: Hijacking
+When hijacking a signal, the user can provide an alternative signal to be executed in place of the original one. Internally, the hijack is implemented by registering a catcher for the signal and replacing the signal number with the new one.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.hijack_signal(original_signal, new_signal, recursive=False)
+ ```
+
+**Parameters**:
+
+| Argument | Type | Description |
+| --- | --- | --- |
+| `original_signal` | `int` \| `str` | The signal number or name to be hijacked. If set to `"*"` or `"all"`, all signals except the restricted ones will be hijacked. |
+| `new_signal` | `int` \| `str` | The signal number or name to be delivered instead. |
+| `recursive` | `bool` | If set to `True`, the catcher's callback will be executed even if the signal was dispached by a hijack. |
+
+**Returns**:
+
+| Return | Type | Description |
+| --- | --- | --- |
+| `SignalCatcher` | [SignalCatcher](../../from_pydoc/generated/data/signal_catcher) | The catcher object created. |
+
+
+!!! ABSTRACT "Example of hijacking a signal"
+
+
+ ```C
+ #include
+ #include
+ #include
+ #include
+
+ // Handler for SIGALRM
+ void handle_sigalrm(int sig) {
+ printf("You failed. Better luck next time\n");
+ exit(1);
+ }
+
+ // Handler for SIGUSR1
+ void handle_sigusr1(int sig) {
+ printf("Congrats: flag{pr1nt_pr0vol4_1s_th3_w4y}\n");
+ exit(0);
+ }
+
+ int main() {
+ // Set up the SIGALRM handler
+ struct sigaction sa_alrm;
+ sa_alrm.sa_handler = handle_sigalrm;
+ sigemptyset(&sa_alrm.sa_mask);
+ sa_alrm.sa_flags = 0;
+ sigaction(SIGALRM, &sa_alrm, NULL);
+
+ // Set up the SIGUSR1 handler
+ struct sigaction sa_usr1;
+ sa_usr1.sa_handler = handle_sigusr1;
+ sigemptyset(&sa_usr1.sa_mask);
+ sa_usr1.sa_flags = 0;
+ sigaction(SIGUSR1, &sa_usr1, NULL);
+
+ // Set an alarm to go off after 10 seconds
+ alarm(10);
+
+ printf("Waiting for a signal...\n");
+
+ // Infinite loop, waiting for signals
+ while (1) {
+ pause(); // Suspend the program until a signal is caught
+ }
+
+ return 0;
+ }
+
+ ```
+
+ ```python
+ from libdebug import debugger
+
+ d = debugger("./test_program")
+ d.run()
+
+ handler = d.hijack_signal("SIGALRM", "SIGUSR1")
+
+ d.cont()
+
+ # Will print "Waiting for a signal..."
+ out = pipe.recvline()
+ print(out.decode())
+
+ d.wait()
+
+ # Will print the flag
+ out = pipe.recvline()
+ print(out.decode())
+ ```
+
+
+
+## :material-filter: Signal Filtering
+Instead of setting a catcher on signals, you might want to filter which signals are not to be forwarded to the debugged process during execution.
+
+!!! ABSTRACT "Example of signal filtering"
+ ```python
+ d.signals_to_block = [10, 15, 'SIGINT', 3, 13]
+ ```
+
+## :material-mail: Arbitrary Signals
+You can also send an arbitrary signal to the process. The signal will be forwarded upon resuming execution. As always, you can specify the signal number or name.
+
+!!! ABSTRACT "Example of sending an arbitrary signal"
+ ```python
+ d.signal = 10
+ d.cont()
+ ```
+
+In [multithreaded](../../multithreading/multithreading) applications, the same syntax applies when using a [ThreadContext](../../from_pydoc/generated/state/thread_context) object instead of the [Debugger](../../from_pydoc/generated/debugger/debugger) object.
\ No newline at end of file
diff --git a/docs/stopping_events/stopping_events.md b/docs/stopping_events/stopping_events.md
new file mode 100644
index 00000000..4899a961
--- /dev/null
+++ b/docs/stopping_events/stopping_events.md
@@ -0,0 +1,28 @@
+---
+icon: octicons/stop-24
+search:
+ boost: 4
+---
+# :octicons-stop-24: Stopping Events
+Debugging a process involves stopping the execution at specific points to inspect the state of the program. **libdebug** provides several ways to stop the execution of a program, such as breakpoints, syscall handling and signal catching. This section covers the different stopping events available in **libdebug**.
+
+## :material-progress-question: Is the process running?
+Before we dive into the different stopping events, it is important to understand how to check if the process is running. The `running` attribute of the [Debugger](../../from_pydoc/generated/debugger/debugger/) object returns `True` if the process is running and `False` otherwise.
+
+!!! ABSTRACT "Example"
+ ```python
+ from libdebug import debugger
+
+ d = debugger("program")
+
+ d.run()
+
+ if d.running:
+ print("The process is running")
+ else:
+ print("The process is not running")
+ ```
+
+ In this example, the script should print `The process is not running`, since the `run()` command gives you control over a stopped process, ready to be debugged.
+
+To know more on how to wait for the process to stop or forcibly cause it to stop, please read about [control flow](../../basics/control_flow_commands/#continuing) commands.
\ No newline at end of file
diff --git a/docs/stopping_events/syscalls.md b/docs/stopping_events/syscalls.md
new file mode 100644
index 00000000..d6d3011e
--- /dev/null
+++ b/docs/stopping_events/syscalls.md
@@ -0,0 +1,232 @@
+---
+icon: fontawesome/solid/terminal
+search:
+ boost: 4
+---
+# :fontawesome-solid-terminal: Syscalls
+System calls (a.k.a. syscalls or software interrupts) are the interface between user space and kernel space. They are used to request services from the kernel, such as reading from a file or creating a new process. **libdebug** allows you to trace syscalls invoked by the debugged program. Specifically, you can choose to **handle** or **hijack** a specific syscall (read more on [hijacking](../stopping_events/#hijacking)).
+
+For extra convenience, the [Debugger](../../from_pydoc/generated/debugger/debugger/) and the [ThreadContext](../../from_pydoc/generated/state/thread_context) objects provide a system-agnostic interface to the arguments and return values of syscalls. Interacting directly with these parameters enables you to create scripts that are independent of the syscall calling convention specific to the target architecture.
+
+| Field | Description |
+| --- | --- |
+| `syscall_number` | The number of the syscall. |
+| `syscall_arg0` | The first argument of the syscall. |
+| `syscall_arg1` | The second argument of the syscall. |
+| `syscall_arg2` | The third argument of the syscall. |
+| `syscall_arg3` | The fourth argument of the syscall. |
+| `syscall_arg4` | The fifth argument of the syscall. |
+| `syscall_arg5` | The sixth argument of the syscall. |
+| `syscall_return` | The return value of the syscall. |
+
+!!! ABSTRACT "Example of Syscall Parameters"
+ ```python
+ [...] # (1)
+
+ binsh_str = d.memory.find(b"/bin/sh\x00", file="libc")[0]
+
+ d.syscall_arg0 = binsh_str
+ d.syscall_arg1 = 0x0
+ d.syscall_arg2 = 0x0
+ d.syscall_number = 0x3b
+
+ d.step() # (2)
+ ```
+
+ 1. The instruction pointer is on a syscall / SVC instruction
+ 2. Now the `execve('/bin/sh', 0, 0)` will be executed in place of the previous syscall.
+
+## :material-format-align-middle: Syscall Handlers
+Syscall handlers can be created to register [stopping events](../stopping_events/) for when a syscall is entered and exited.
+
+!!! QUESTION "Do I have to handle both on enter and on exit?"
+ When using [asynchronous](../debugging_flow) syscall handlers, you can choose to handle both or only one of the two events. However, when using synchronous handlers, both events will stop the process.
+
+## **libdebug** API for Syscall Handlers
+The `handle_syscall()` function in the [Debugger](../../from_pydoc/generated/debugger/debugger/) object registers a handler for the specified syscall.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.handle_syscall(syscall, on_enter=None, on_exit=None, recursive=False)
+ ```
+
+**Parameters**:
+
+| Argument | Type | Description |
+| --- | --- | --- |
+| `syscall` | `int` \| `str` | The syscall number or name to be handled. If set to `"*"` or `"all"` or `"ALL"`, all syscalls will be handled. |
+| `on_enter` | `Callable` \| `bool` (see callback signature [here](#callback-signature)) | The callback function to be executed when the syscall is entered. |
+| `on_exit` | `Callable` \| `bool` (see callback signature [here](#callback-signature)) | The callback function to be executed when the syscall is exited. |
+| `recursive` | `bool` | If set to `True`, the handler's callback will be executed even if the syscall was triggered by a hijack or caused by a callback. |
+
+**Returns**:
+
+| Return | Type | Description |
+| --- | --- | --- |
+| `SyscallHandler` | [SyscallHandler](../../from_pydoc/generated/data/syscall_handler) | The handler object created. |
+
+### Callback Signature
+
+!!! ABSTRACT "Callback Signature"
+ ```python
+ def callback(t: ThreadContext, handler: HandledSyscall) -> None:
+ ```
+
+**Parameters**:
+
+| Argument | Type | Description |
+| --- | --- | --- |
+| `t` | [ThreadContext](../../from_pydoc/generated/state/thread_context) | The thread that hit the syscall. |
+| `handler` | [SyscallHandler](../../from_pydoc/generated/data/syscall_handler) | The SyscallHandler object that triggered the callback. |
+
+!!! INFO "Nuances of Syscall Handling"
+ The syscall handler is the only [stopping event](../stopping_events/) that can be triggered by the same syscall twice in a row. This is because the handler is triggered both when the syscall is entered and when it is exited. As a result the `hit_on()` method of the [SyscallHandler](../../from_pydoc/generated/data/syscall_handler) object will return `True` in both instances.
+
+ You can also use the `hit_on_enter()` and `hit_on_exit()` functions to check if the cause of the process stop was the syscall entering or exiting, respectively.
+
+ As for the `hit_count` attribute, it only stores the number of times the syscall was *exited*.
+
+---
+
+!!! ABSTRACT "Example usage of asynchronous syscall handlers"
+ ```python
+ def on_enter_open(t: ThreadContext, handler: SyscallHandler):
+ print("entering open")
+ t.syscall_arg0 = 0x1
+
+ def on_exit_open(t: ThreadContext, handler: SyscallHandler):
+ print("exiting open")
+ t.syscall_return = 0x0
+
+ handler = d.handle_syscall(syscall="open", on_enter=on_enter_open, on_exit=on_exit_open)
+ ```
+
+!!! ABSTRACT "Example of synchronous syscall handling"
+ ```python
+ from libdebug import debugger
+
+ d = debugger("./test_program")
+ d.run()
+
+ handler = d.handle_syscall(syscall="open")
+ d.cont()
+
+ if handler.hit_on_enter(d):
+ print("open syscall was entered")
+ elif handler.hit_on_exit(d):
+ print("open syscall was exited")
+ ```
+
+ The script above will print "open syscall was entered".
+
+## :octicons-number-24: Resolution of Syscall Numbers
+Syscall handlers can be created with the identifier number of the syscall or by the syscall's common name. In the second case, syscall names are resolved from a definition list for Linux syscalls on the target architecture. The list is fetched from [mebeim's syscall table](https://syscalls.mebeim.net). We thank him for hosting such a precious resource. Once downloaded, the list is cached internally.
+
+## :material-arrow-decision: Hijacking
+When hijacking a syscall, the user can provide an alternative syscall to be executed in place of the original one. Internally, the hijack is implemented by registering a handler for the syscall and replacing the syscall number with the new one.
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.hijack_syscall(original_syscall, new_syscall, recursive=False, **kwargs)
+ ```
+
+**Parameters**:
+
+| Argument | Type | Description |
+| --- | --- | --- |
+| `original_syscall` | `int` \| `str` | The syscall number or name to be hijacked. If set to `"*"` or `"all"` or `"ALL"`, all syscalls will be hijacked. |
+| `new_syscall` | `int` \| `str` | The syscall number or name to be executed instead. |
+| `recursive` | `bool` | If set to `True`, the handler's callback will be executed even if the syscall was triggered by a hijack or caused by a callback. |
+| `**kwargs` | `(int, optional)` | Additional arguments to be passed to the new syscall. |
+
+**Returns**:
+
+| Return | Type | Description |
+| --- | --- | --- |
+| `SyscallHandler` | [SyscallHandler](../../from_pydoc/generated/data/syscall_handler) | The handler object created. |
+
+
+!!! ABSTRACT "Example of hijacking a syscall"
+
+
+ ```C
+ #include
+
+ char secretBuffer[32] = "The password is 12345678";
+
+ int main(int argc, char** argv)
+ {
+ [...]
+
+ read(0, secretBuffer, 31);
+
+ [...]
+ return 0;
+ }
+ ```
+
+ ```python
+ from libdebug import debugger
+
+ d = debugger("./test_program")
+ d.run()
+
+ handler = d.hijack_syscall("read", "write")
+
+ d.cont()
+ d.wait()
+
+ out = pipe.recvline()
+ print(out.decode())
+ ```
+
+
+ In this case, the secret will be leaked to the standard output instead of being overwritten with content from the standard input.
+
+For your convenience, you can also easily provide the syscall parameters to be used when the hijacked syscall is executed:
+
+!!! ABSTRACT "Example of hijacking a syscall with parameters"
+
+
+ ```C
+ #include
+
+ char manufacturerName[32] = "libdebug";
+ char secretKey[32] = "provola";
+
+ int main(int argc, char** argv)
+ {
+ [...]
+
+ read(0, manufacturerName, 31);
+
+ [...]
+ return 0;
+ }
+ ```
+
+ ```python
+ from libdebug import debugger
+
+ d = debugger("./test_program")
+ d.run()
+
+ manufacturerBuffer = ...
+
+ handler = d.hijack_syscall("read", "write",
+ syscall_arg0=0x1,
+ syscall_arg1=manufacturerBuffer,
+ syscall_arg2=0x100
+ )
+
+ d.cont()
+ d.wait()
+
+ out = pipe.recvline()
+ print(out.decode())
+ ```
+
+
+
+ Again, the secret will be leaked to the standard output.
+
diff --git a/docs/stopping_events/watchpoints.md b/docs/stopping_events/watchpoints.md
new file mode 100644
index 00000000..6bb37f34
--- /dev/null
+++ b/docs/stopping_events/watchpoints.md
@@ -0,0 +1,84 @@
+---
+icon: material/track-light
+search:
+ boost: 4
+---
+# :material-track-light: Watchpoints
+Watchpoints are a special type of [hardware breakpoint](../breakpoints#hardware-breakpoints) that triggers when a specific memory location is accessed. You can set a watchpoint to trigger on certain memory access conditions, or upon execution (equivalent to a hardware breakpoint).
+
+Features of watchpoints are shared with breakpoints, so you can set [asynchronous](../debugging_flow) watchpoints and use properties in the same way.
+
+## **libdebug** API for Watchpoints
+The `watchpoint()` function in the [Debugger](../../from_pydoc/generated/debugger/debugger/) object sets a watchpoint at a specific address. While you can also use the [breakpoint API](../breakpoints/#libdebug-api-for-breakpoints) to set up a watchpoint, a specific API is provided for your convenience:
+
+!!! ABSTRACT "Function Signature"
+ ```python
+ d.watchpoint(position, condition='w', length=1, callback=None, file='hybrid')
+ ```
+
+**Parameters**:
+
+| Argument | Type | Description |
+| --- | --- | --- |
+| `position` | `int` \| `str` | The address or symbol where the watchpoint will be set. |
+| `condition` | `str` | The type of access (see [later section](#valid-access-conditions)). |
+| `length` | `int` | The size of the word being watched (see [later section](#valid-word-lengths)). |
+| `callback` | `Callable` \| `bool` (see callback signature [here](#callback-signature)) | Used to create asyncronous watchpoints (read more on the [debugging flow of stopping events](../debugging_flow)). |
+| `file` | `str` | The backing file for relative addressing. Refer to the [memory access](../../basics/memory_access/#absolute-and-relative-addressing) section for more information on addressing modes. |
+
+**Returns**:
+
+| Return | Type | Description |
+| --- | --- | --- |
+| `Breakpoint` | [Breakpoint](../../from_pydoc/generated/data/breakpoint) | The breakpoint object created. |
+
+### Valid Access Conditions
+The `condition` parameter specifies the type of access that triggers the watchpoint. Default is write access.
+
+| Condition | Description | Supported Architectures |
+| --- | --- | --- |
+| `"r"` | Read access | AArch64 |
+| `"w"` | Write access | AMD64, AArch64 |
+| `"rw"` | Read/write access | AMD64, AArch64 |
+| `"x"` | Execute access | AMD64 |
+
+### Valid Word Lengths
+The `length` parameter specifies the size of the word being watched. By default, the watchpoint is set to watch a single byte.
+
+| Architecture | Supported Lengths |
+| --- | --- |
+| AMD64 | 1, 2, 4, 8 |
+| AArch64 | Any length from 1 to 8 bytes |
+
+!!! INFO "Watchpoint alignment in AArch64"
+ The address of the watchpoint on AArch64-based CPUs needs to be aligned to 8 bytes. Instead, basic hardware breakpoints have to be aligned to 4 bytes (which is the size of an ARM instruction).
+
+### Callback Signature
+If you wish to create an [asynchronous](../debugging_flow) watchpoint, you will have to provide a callback function. Since internally watchpoints are implemented as hardware breakpoints, the callback signature is the same as for [breakpoints](../breakpoints#callback-signature). As for breakpoints, if you want to leave the callback empty, you can set callback to `True`.
+
+!!! ABSTRACT "Callback Signature"
+ ```python
+ def callback(t: ThreadContext, bp: Breakpoint):
+ ```
+
+**Parameters**:
+
+| Argument | Type | Description |
+| --- | --- | --- |
+| `t` | [ThreadContext](../../from_pydoc/generated/state/thread_context) | The thread that hit the breakpoint. |
+| `bp` | [Breakpoint](../../from_pydoc/generated/data/breakpoint) | The breakpoint object that triggered the callback. |
+
+---
+
+!!! ABSTRACT "Example usage of asynchronous watchpoints"
+ ```python
+ def on_watchpoint_hit(t, bp):
+ print(f"RAX: {t.regs.rax}")
+
+ if bp.hit_count == 100:
+ print("Hit count reached 100")
+ bp.disable()
+
+ d.watchpoint(0x11f0, condition="rw", length=8, callback=on_watchpoint_hit, file="binary")
+ ```
+
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css
new file mode 100644
index 00000000..1c269653
--- /dev/null
+++ b/docs/stylesheets/extra.css
@@ -0,0 +1,8 @@
+.md-header__button.md-logo {
+ margin: 0.2rem;
+}
+
+.md-header__button.md-logo img, .md-header__button.md-logo svg {
+ height: 2.5rem;
+ width: 2.5rem;
+}
\ No newline at end of file
diff --git a/examples/bof_detection/detect_bof.py b/examples/bof_detection/detect_bof.py
index ef38779c..262bdce4 100644
--- a/examples/bof_detection/detect_bof.py
+++ b/examples/bof_detection/detect_bof.py
@@ -3,13 +3,15 @@
# Copyright (c) 2024 Francesco Panebianco. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
-from libdebug import debugger, libcontext
-from libdebug.utils.debugging_utils import resolve_address_in_maps
-import iced_x86 as iced
import argparse
import os
+
+import iced_x86 as iced
import magic
+from libdebug import debugger, libcontext
+from libdebug.utils.debugging_utils import resolve_address_in_maps
+
libcontext.sym_lvl = 5
###########################################
@@ -29,33 +31,43 @@
# -------- Utility Functions -------- #
#######################################
+
def p64(in_bytes):
- return in_bytes.to_bytes(8, byteorder='little')
+ return in_bytes.to_bytes(8, byteorder="little")
+
def p32(in_bytes):
- return in_bytes.to_bytes(4, byteorder='little')
+ return in_bytes.to_bytes(4, byteorder="little")
+
def p16(in_bytes):
- return in_bytes.to_bytes(2, byteorder='little')
+ return in_bytes.to_bytes(2, byteorder="little")
+
def p8(in_bytes):
- return in_bytes.to_bytes(1, byteorder='little')
+ return in_bytes.to_bytes(1, byteorder="little")
+
def u64(in_bytes):
- return int.from_bytes(in_bytes, byteorder='little')
+ return int.from_bytes(in_bytes, byteorder="little")
+
def u32(in_bytes):
- return int.from_bytes(in_bytes, byteorder='little')
+ return int.from_bytes(in_bytes, byteorder="little")
+
def u16(in_bytes):
- return int.from_bytes(in_bytes, byteorder='little')
+ return int.from_bytes(in_bytes, byteorder="little")
+
def u8(in_bytes):
- return int.from_bytes(in_bytes, byteorder='little')
+ return int.from_bytes(in_bytes, byteorder="little")
+
-def print_color(message, color, end='\n'):
+def print_color(message, color, end="\n"):
print(f"\033[{color}m{message}\033[0m", end=end)
+
has_crashed = False
rip_overwritten = False
rbp_overwritten = False
@@ -77,21 +89,21 @@ def print_color(message, color, end='\n'):
# Different scenarios can easily be handled by changing the input source
# Create the parser
-parser = argparse.ArgumentParser(description='Find vulnerabilities in an AMD64 Linux ELF')
+parser = argparse.ArgumentParser(description="Find vulnerabilities in an AMD64 Linux ELF")
# Add the --maxlen argument
-parser.add_argument('--maxlen', type=int, help='maximum length of the input to test')
+parser.add_argument("--maxlen", type=int, help="maximum length of the input to test")
# Add the positional argument for the file name
-parser.add_argument('filename', type=str, help='the path to the file to process')
+parser.add_argument("filename", type=str, help="the path to the file to process")
# Parse the arguments
args = parser.parse_args()
# Print the arguments
-print(f'File name: {args.filename}')
+print(f"File name: {args.filename}")
if args.maxlen is not None:
- print(f'Max length: {args.maxlen}')
+ print(f"Max length: {args.maxlen}")
# Get the ELF file
ELF_PATH = args.filename
@@ -104,7 +116,7 @@ def print_color(message, color, end='\n'):
elif not os.access(ELF_PATH, os.R_OK):
print(f"File {ELF_PATH} is not readable.")
exit(1)
-elif 'ELF 64-bit LSB pie executable, x86-64' not in magic.from_file(ELF_PATH):
+elif "ELF 64-bit LSB pie executable, x86-64" not in magic.from_file(ELF_PATH):
print(f"File {ELF_PATH} is not a 64-bit ELF file.")
exit(1)
@@ -117,17 +129,17 @@ def print_color(message, color, end='\n'):
d = debugger(ELF_PATH)
- test_payload = b'A' * test_padding_len
+ test_payload = b"A" * test_padding_len
pipe = d.run()
# Break on check stack canary and fortify fail
- check_stack_fail_br = d.breakpoint('__stack_chk_fail', file='libc.so.6')
- check_fortify_fail_br = d.breakpoint('__fortify_fail', file='libc.so.6')
+ check_stack_fail_br = d.breakpoint("__stack_chk_fail", file="libc.so.6")
+ check_fortify_fail_br = d.breakpoint("__fortify_fail", file="libc.so.6")
# Catch SIGSEGV and SIGABRT
- sig1_hdlr = d.catch_signal(signal='SIGSEGV')
- sig2_hdlr = d.catch_signal(signal='SIGABRT')
+ sig1_hdlr = d.catch_signal(signal="SIGSEGV")
+ sig2_hdlr = d.catch_signal(signal="SIGABRT")
d.cont()
@@ -156,12 +168,12 @@ def print_color(message, color, end='\n'):
if has_crashed:
print_color(f"[+] Crash detected with payload length {test_padding_len}", color=LT_COLOR_YELLOW)
- print_color('[+] Post-mortem analysis initiated', color=LT_COLOR_YELLOW)
+ print_color("[+] Post-mortem analysis initiated", color=LT_COLOR_YELLOW)
curr_rip = d.regs.rip
# Check for RIP overwrite
- if '4141' in hex(d.regs.rip):
+ if "4141" in hex(d.regs.rip):
print_color("--> RIP is overwritten with AAAA <--", color=LT_COLOR_RED)
rip_overwritten = True
else:
@@ -169,7 +181,7 @@ def print_color(message, color, end='\n'):
print("Disassembling the instruction at RIP...")
# Dump instruction at rip
- window_from_rip = d.memory[curr_rip, MAX_AMD64_INSTRUCTION_LENGTH, 'absolute']
+ window_from_rip = d.memory[curr_rip, MAX_AMD64_INSTRUCTION_LENGTH, "absolute"]
# Disassemble the instruction (ignoring bytes that are not part of the instruction)
decoder = iced.Decoder(64, window_from_rip)
@@ -178,12 +190,12 @@ def print_color(message, color, end='\n'):
instruction = decoder.decode()
# Get the instruction bytes and convert to hex bytes separated by spaces
- instruction_bytes = window_from_rip[:instruction.len]
+ instruction_bytes = window_from_rip[: instruction.len]
instruction_bytes_str = " ".join(f"{b:02X}" for b in instruction_bytes)
# If the current rip corresponds to a known symbol, print the symbol
try:
- symbol = resolve_address_in_maps(curr_rip, d.maps())
+ symbol = resolve_address_in_maps(curr_rip, d.maps)
if not symbol.startswith("0x"):
print_color(f"<{symbol}> ", color=LT_COLOR_CYAN, end="")
@@ -193,9 +205,9 @@ def print_color(message, color, end='\n'):
# Decode and print each instruction
asm = formatter.format(instruction)
print_color(f"{hex(instruction.ip)}: {instruction_bytes_str.ljust(2*4, ' ')} | {asm}", color=LT_COLOR_CYAN)
-
+
# Check for RBP overwrite
- if not rip_overwritten and '4141' in hex(d.regs.rbp):
+ if not rip_overwritten and "4141" in hex(d.regs.rbp):
print_color("--> RBP is overwritten with AAAA <--", color=LT_COLOR_RED)
print_color("Stack pivot detected", color=LT_COLOR_RED)
rbp_overwritten = True
@@ -203,10 +215,10 @@ def print_color(message, color, end='\n'):
print(f"RBP is at {hex(d.regs.rbp)}")
# Shut up the warnings
- libcontext.general_logger = 'SILENT'
-
+ libcontext.general_logger = "SILENT"
+
# Stack trace
- print_color('\nStack trace:', color=LT_COLOR_RED)
+ print_color("\nStack trace:", color=LT_COLOR_RED)
d.print_backtrace()
d.kill()
@@ -228,7 +240,9 @@ def print_color(message, color, end='\n'):
if fortify_failed:
print_color("[+] Fortify check failed", color=LT_COLOR_YELLOW)
- print_color("[+] This mitigation can prevent a lot of exploits, but some workarounds are possible", color=LT_COLOR_YELLOW)
+ print_color(
+ "[+] This mitigation can prevent a lot of exploits, but some workarounds are possible", color=LT_COLOR_YELLOW
+ )
exit(0)
if not rip_overwritten:
@@ -236,14 +250,16 @@ def print_color(message, color, end='\n'):
if rbp_overwritten:
print_color("[+] However, the analyzer was able to detect a stack pivot", color=LT_COLOR_YELLOW)
- print_color("[+] Given a leak, we could use the stack pivot take control of the execution", color=LT_COLOR_YELLOW)
+ print_color(
+ "[+] Given a leak, we could use the stack pivot take control of the execution", color=LT_COLOR_YELLOW
+ )
exit(0)
# If we reach this point, we have a crash and RIP is overwritten
# We can now proceed to the part where we check which part of the input is used to overwrite RIP
-print('\n-----------------------------------\n\n')
+print("\n-----------------------------------\n\n")
print("[+] A payload length that overwrites RIP has been found.")
print("[+] Starting taint analysis to find the setup.")
@@ -258,24 +274,24 @@ def print_color(message, color, end='\n'):
for taint_start_index in range(0, test_padding_len + 8, 8):
taint_end_index = taint_start_index + 8
- TAINT = p64(0xdeadc0de)
+ TAINT = p64(0xDEADC0DE)
print(f"[+] Analyzing taint from {taint_start_index} to {taint_end_index}...")
- test_payload = b'A' * taint_start_index + TAINT + b'A' * (test_padding_len - taint_end_index)
+ test_payload = b"A" * taint_start_index + TAINT + b"A" * (test_padding_len - taint_end_index)
print(f">> Testing payload: {test_payload}")
- d = debugger('bof')
+ d = debugger("bof")
pipe = d.run()
- sigsegv_checker = d.catch_signal(signal='SIGSEGV')
+ sigsegv_checker = d.catch_signal(signal="SIGSEGV")
d.cont()
pipe.sendline(test_payload)
-
+
d.wait()
if sigsegv_checker.hit_on(d):
@@ -320,22 +336,25 @@ def print_color(message, color, end='\n'):
# Searching memory for the taint
print("Searching memory for the taint...")
- for map in d.maps():
+ for map in d.maps:
# Skip non-writable maps (e.g., vsyscall)
- if 'w' not in map.permissions:
+ if "w" not in map.permissions:
continue
-
+
pid = d.threads[0].process_id
-
- with open(f'/proc/{pid}/mem', 'rb', 0) as mem:
+
+ with open(f"/proc/{pid}/mem", "rb", 0) as mem:
mem.seek(map.start)
memory_content = mem.read(map.end - map.start)
if TAINT in memory_content:
- print_color(f">> Taint found in {map.backing_file} at address {hex(map.start + memory_content.index(TAINT))}", color=LT_COLOR_RED)
+ print_color(
+ f">> Taint found in {map.backing_file} at address {hex(map.start + memory_content.index(TAINT))}",
+ color=LT_COLOR_RED,
+ )
elif d.dead:
print(f">> Program exited with code {d.exit_code} and signal {d.exit_signal}")
-
+
d.kill()
d.terminate()
@@ -346,4 +365,4 @@ def print_color(message, color, end='\n'):
print_color(f"[+] Found setup at offset {taint_offset}", color=LT_COLOR_CYAN)
print_color("[+] This vulnerability can be exploited on systems with Intel CET disabled", color=LT_COLOR_CYAN)
-# Future expansion: proxy the output to check for useful leaks (e.g, libc addresses, stack addresses, etc.)
\ No newline at end of file
+# Future expansion: proxy the output to check for useful leaks (e.g, libc addresses, stack addresses, etc.)
diff --git a/libdebug/architectures/aarch64/aarch64_call_utilities.py b/libdebug/architectures/aarch64/aarch64_call_utilities.py
index cb4f9959..233eb863 100644
--- a/libdebug/architectures/aarch64/aarch64_call_utilities.py
+++ b/libdebug/architectures/aarch64/aarch64_call_utilities.py
@@ -19,10 +19,7 @@ def is_call(self: Aarch64CallUtilities, opcode_window: bytes) -> bool:
return True
# Check for BLR instruction
- if opcode_window[3] == 0xD6 and (opcode_window[2] & 0x3F) == 0x3F:
- return True
-
- return False
+ return bool(opcode_window[3] == 214 and opcode_window[2] & 63 == 63)
def compute_call_skip(self: Aarch64CallUtilities, opcode_window: bytes) -> int:
"""Compute the instruction size of the current call instruction."""
diff --git a/libdebug/architectures/aarch64/aarch64_ptrace_register_holder.py b/libdebug/architectures/aarch64/aarch64_ptrace_register_holder.py
index 6a52ecbf..34af71eb 100644
--- a/libdebug/architectures/aarch64/aarch64_ptrace_register_holder.py
+++ b/libdebug/architectures/aarch64/aarch64_ptrace_register_holder.py
@@ -1,6 +1,6 @@
#
# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
@@ -16,8 +16,9 @@
if TYPE_CHECKING:
from libdebug.state.thread_context import ThreadContext
-AARCH64_GP_REGS = ["x", "w"]
+AARCH64_REGS = [f"x{i}" for i in range(31)] + ["sp", "xzr", "pc"]
+AARCH64_SPECIAL_REGS = ["pstate"]
def _get_property_64(name: str) -> property:
def getter(self: Aarch64Registers) -> int:
@@ -46,22 +47,25 @@ def setter(self: Aarch64Registers, value: int) -> None:
def _get_property_zr(name: str) -> property:
- def getter(_: Aarch64Registers) -> int:
+ def getter(self: Aarch64Registers) -> int:
+ self._internal_debugger._ensure_process_stopped()
return 0
- def setter(_: Aarch64Registers, __: int) -> None:
- pass
+ def setter(self: Aarch64Registers, _: int) -> None:
+ self._internal_debugger._ensure_process_stopped()
return property(getter, setter, None, name)
def _get_property_fp_8(name: str, index: int) -> property:
def getter(self: Aarch64Registers) -> int:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
return int.from_bytes(self._fp_register_file.vregs[index].data, sys.byteorder) & 0xFF
def setter(self: Aarch64Registers, value: int) -> None:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
data = value.to_bytes(1, sys.byteorder)
@@ -73,11 +77,13 @@ def setter(self: Aarch64Registers, value: int) -> None:
def _get_property_fp_16(name: str, index: int) -> property:
def getter(self: Aarch64Registers) -> int:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
return int.from_bytes(self._fp_register_file.vregs[index].data, sys.byteorder) & 0xFFFF
def setter(self: Aarch64Registers, value: int) -> None:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
data = value.to_bytes(2, sys.byteorder)
@@ -89,11 +95,13 @@ def setter(self: Aarch64Registers, value: int) -> None:
def _get_property_fp_32(name: str, index: int) -> property:
def getter(self: Aarch64Registers) -> int:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
return int.from_bytes(self._fp_register_file.vregs[index].data, sys.byteorder) & 0xFFFFFFFF
def setter(self: Aarch64Registers, value: int) -> None:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
data = value.to_bytes(4, sys.byteorder)
@@ -105,11 +113,13 @@ def setter(self: Aarch64Registers, value: int) -> None:
def _get_property_fp_64(name: str, index: int) -> property:
def getter(self: Aarch64Registers) -> int:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
return int.from_bytes(self._fp_register_file.vregs[index].data, sys.byteorder) & 0xFFFFFFFFFFFFFFFF
def setter(self: Aarch64Registers, value: int) -> None:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
data = value.to_bytes(8, sys.byteorder)
@@ -121,11 +131,13 @@ def setter(self: Aarch64Registers, value: int) -> None:
def _get_property_fp_128(name: str, index: int) -> property:
def getter(self: Aarch64Registers) -> int:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
return int.from_bytes(self._fp_register_file.vregs[index].data, sys.byteorder)
def setter(self: Aarch64Registers, value: int) -> None:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
data = value.to_bytes(16, sys.byteorder)
@@ -156,6 +168,18 @@ def provide_regs_class(self: Aarch64PtraceRegisterHolder) -> type:
"""Provide a class to hold the register accessors."""
return Aarch64Registers
+ def provide_regs(self: Aarch64PtraceRegisterHolder) -> list[str]:
+ """Provide the list of registers, excluding the vector and fp registers."""
+ return AARCH64_REGS
+
+ def provide_vector_fp_regs(self: Aarch64PtraceRegisterHolder) -> list[tuple[str]]:
+ """Provide the list of vector and floating point registers."""
+ return self._vector_fp_registers
+
+ def provide_special_regs(self: Aarch64PtraceRegisterHolder) -> list[str]:
+ """Provide the list of special registers, which are not intended for general-purpose use."""
+ return AARCH64_SPECIAL_REGS
+
def apply_on_regs(self: Aarch64PtraceRegisterHolder, target: Aarch64Registers, target_class: type) -> None:
"""Apply the register accessors to the Aarch64Registers class."""
target.register_file = self.register_file
@@ -164,6 +188,8 @@ def apply_on_regs(self: Aarch64PtraceRegisterHolder, target: Aarch64Registers, t
if hasattr(target_class, "w0"):
return
+ self._vector_fp_registers = []
+
for i in range(31):
name_64 = f"x{i}"
name_32 = f"w{i}"
@@ -171,6 +197,9 @@ def apply_on_regs(self: Aarch64PtraceRegisterHolder, target: Aarch64Registers, t
setattr(target_class, name_64, _get_property_64(name_64))
setattr(target_class, name_32, _get_property_32(name_64))
+ for reg in AARCH64_SPECIAL_REGS:
+ setattr(target_class, reg, _get_property_64(reg))
+
# setup the floating point registers
for i in range(32):
name_v = f"v{i}"
@@ -185,6 +214,7 @@ def apply_on_regs(self: Aarch64PtraceRegisterHolder, target: Aarch64Registers, t
setattr(target_class, name_32, _get_property_fp_32(name_32, i))
setattr(target_class, name_16, _get_property_fp_16(name_16, i))
setattr(target_class, name_8, _get_property_fp_8(name_8, i))
+ self._vector_fp_registers.append((name_v, name_128, name_64, name_32, name_16, name_8))
# setup special aarch64 registers
target_class.pc = _get_property_64("pc")
@@ -194,6 +224,8 @@ def apply_on_regs(self: Aarch64PtraceRegisterHolder, target: Aarch64Registers, t
target_class.xzr = _get_property_zr("xzr")
target_class.wzr = _get_property_zr("wzr")
+ Aarch64PtraceRegisterHolder._vector_fp_registers = self._vector_fp_registers
+
def apply_on_thread(self: Aarch64PtraceRegisterHolder, target: ThreadContext, target_class: type) -> None:
"""Apply the register accessors to the thread class."""
target.register_file = self.register_file
diff --git a/libdebug/architectures/aarch64/aarch64_registers.py b/libdebug/architectures/aarch64/aarch64_registers.py
index d2609de1..155f0368 100644
--- a/libdebug/architectures/aarch64/aarch64_registers.py
+++ b/libdebug/architectures/aarch64/aarch64_registers.py
@@ -1,22 +1,13 @@
#
# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
from __future__ import annotations
-from dataclasses import dataclass
-
from libdebug.data.registers import Registers
-from libdebug.debugger.internal_debugger_instance_manager import get_global_internal_debugger
-@dataclass
class Aarch64Registers(Registers):
"""This class holds the state of the architectural-dependent registers of a process."""
-
- def __init__(self: Aarch64Registers, thread_id: int) -> None:
- """Initializes the Registers object."""
- self._internal_debugger = get_global_internal_debugger()
- self._thread_id = thread_id
diff --git a/libdebug/architectures/aarch64/aarch64_stack_unwinder.py b/libdebug/architectures/aarch64/aarch64_stack_unwinder.py
index 7a9b5065..ec9fae47 100644
--- a/libdebug/architectures/aarch64/aarch64_stack_unwinder.py
+++ b/libdebug/architectures/aarch64/aarch64_stack_unwinder.py
@@ -6,6 +6,7 @@
from __future__ import annotations
+import sys
from typing import TYPE_CHECKING
from libdebug.architectures.stack_unwinding_manager import StackUnwindingManager
@@ -13,6 +14,7 @@
if TYPE_CHECKING:
from libdebug.data.memory_map import MemoryMap
+ from libdebug.data.memory_map_list import MemoryMapList
from libdebug.state.thread_context import ThreadContext
@@ -32,7 +34,7 @@ def unwind(self: Aarch64StackUnwinder, target: ThreadContext) -> list:
frame_pointer = target.regs.x29
- vmaps = target._internal_debugger.debugging_interface.maps()
+ vmaps = target._internal_debugger.debugging_interface.get_maps()
initial_link_register = None
try:
@@ -47,10 +49,10 @@ def unwind(self: Aarch64StackUnwinder, target: ThreadContext) -> list:
# Follow the frame chain
while frame_pointer:
try:
- link_register = int.from_bytes(target.memory[frame_pointer + 8, 8, "absolute"], byteorder="little")
- frame_pointer = int.from_bytes(target.memory[frame_pointer, 8, "absolute"], byteorder="little")
+ link_register = int.from_bytes(target.memory[frame_pointer + 8, 8, "absolute"], sys.byteorder)
+ frame_pointer = int.from_bytes(target.memory[frame_pointer, 8, "absolute"], sys.byteorder)
- if not any(vmap.start <= link_register < vmap.end for vmap in vmaps):
+ if not vmaps.filter(link_register):
break
# Leaf functions don't set the previous stack frame pointer
@@ -66,19 +68,23 @@ def unwind(self: Aarch64StackUnwinder, target: ThreadContext) -> list:
return stack_trace
- def get_return_address(self: Aarch64StackUnwinder, target: ThreadContext, vmaps: list[MemoryMap]) -> int:
+ def get_return_address(
+ self: Aarch64StackUnwinder,
+ target: ThreadContext,
+ vmaps: MemoryMapList[MemoryMap],
+ ) -> int:
"""Get the return address of the current function.
Args:
target (ThreadContext): The target ThreadContext.
- vmaps (list[MemoryMap]): The memory maps of the process.
+ vmaps (MemoryMapList[MemoryMap]): The memory maps of the process.
Returns:
int: The return address.
"""
return_address = target.regs.x30
- if not any(vmap.start <= return_address < vmap.end for vmap in vmaps):
+ if not vmaps.filter(return_address):
raise ValueError("Return address not in any valid memory map")
return return_address
diff --git a/libdebug/architectures/amd64/amd64_ptrace_register_holder.py b/libdebug/architectures/amd64/amd64_ptrace_register_holder.py
index 44597dba..96edd7b1 100644
--- a/libdebug/architectures/amd64/amd64_ptrace_register_holder.py
+++ b/libdebug/architectures/amd64/amd64_ptrace_register_holder.py
@@ -6,6 +6,7 @@
from __future__ import annotations
+from ctypes import c_longdouble
from dataclasses import dataclass
from typing import TYPE_CHECKING
@@ -22,33 +23,35 @@
AMD64_EXT_REGS = ["r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"]
AMD64_REGS = [
- "r15",
- "r14",
- "r13",
- "r12",
- "rbp",
- "rbx",
- "r11",
- "r10",
- "r9",
- "r8",
"rax",
+ "rbx",
"rcx",
"rdx",
- "rsi",
"rdi",
- "orig_rax",
+ "rsi",
+ "r8",
+ "r9",
+ "r10",
+ "r11",
+ "r12",
+ "r13",
+ "r14",
+ "r15",
+ "rbp",
+ "rsp",
"rip",
- "cs",
+]
+
+AMD64_SPECIAL_REGS = [
"eflags",
- "rsp",
+ "cs",
"ss",
- "fs_base",
- "gs_base",
"ds",
"es",
"fs",
"gs",
+ "fs_base",
+ "gs_base",
]
@@ -117,11 +120,13 @@ def setter(self: Amd64Registers, value: int) -> None:
def _get_property_fp_xmm0(name: str, index: int) -> property:
def getter(self: Amd64Registers) -> int:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
return int.from_bytes(self._fp_register_file.xmm0[index].data, "little")
def setter(self: Amd64Registers, value: int) -> None:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
data = value.to_bytes(16, "little")
@@ -133,6 +138,7 @@ def setter(self: Amd64Registers, value: int) -> None:
def _get_property_fp_ymm0(name: str, index: int) -> property:
def getter(self: Amd64Registers) -> int:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
xmm0 = int.from_bytes(self._fp_register_file.xmm0[index].data, "little")
@@ -140,6 +146,7 @@ def getter(self: Amd64Registers) -> int:
return (ymm0 << 128) | xmm0
def setter(self: Amd64Registers, value: int) -> None:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
new_xmm0 = value & ((1 << 128) - 1)
@@ -153,6 +160,7 @@ def setter(self: Amd64Registers, value: int) -> None:
def _get_property_fp_zmm0(name: str, index: int) -> property:
def getter(self: Amd64Registers) -> int:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
zmm0 = int.from_bytes(self._fp_register_file.zmm0[index].data, "little")
@@ -161,6 +169,7 @@ def getter(self: Amd64Registers) -> int:
return (zmm0 << 256) | (ymm0 << 128) | xmm0
def setter(self: Amd64Registers, value: int) -> None:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
new_xmm0 = value & ((1 << 128) - 1)
@@ -176,6 +185,7 @@ def setter(self: Amd64Registers, value: int) -> None:
def _get_property_fp_xmm1(name: str, index: int) -> property:
def getter(self: Amd64Registers) -> int:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
zmm1 = int.from_bytes(self._fp_register_file.zmm1[index].data, "little")
@@ -183,6 +193,7 @@ def getter(self: Amd64Registers) -> int:
def setter(self: Amd64Registers, value: int) -> None:
# We do not clear the upper 384 bits of the register
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
previous_value = int.from_bytes(self._fp_register_file.zmm1[index].data, "little")
@@ -196,6 +207,7 @@ def setter(self: Amd64Registers, value: int) -> None:
def _get_property_fp_ymm1(name: str, index: int) -> property:
def getter(self: Amd64Registers) -> int:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
zmm1 = int.from_bytes(self._fp_register_file.zmm1[index].data, "little")
@@ -203,6 +215,7 @@ def getter(self: Amd64Registers) -> int:
def setter(self: Amd64Registers, value: int) -> None:
# We do not clear the upper 256 bits of the register
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
previous_value = self._fp_register_file.zmm1[index]
@@ -216,11 +229,13 @@ def setter(self: Amd64Registers, value: int) -> None:
def _get_property_fp_zmm1(name: str, index: int) -> property:
def getter(self: Amd64Registers) -> int:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
return int.from_bytes(self._fp_register_file.zmm1[index].data, "little")
def setter(self: Amd64Registers, value: int) -> None:
+ self._internal_debugger._ensure_process_stopped()
if not self._fp_register_file.fresh:
self._internal_debugger._fetch_fp_registers(self)
self._fp_register_file.zmm1[index].data = value.to_bytes(64, "little")
@@ -229,6 +244,43 @@ def setter(self: Amd64Registers, value: int) -> None:
return property(getter, setter, None, name)
+def _get_property_fp_mmx(name: str, index: int) -> property:
+ def getter(self: Amd64Registers) -> int:
+ self._internal_debugger._ensure_process_stopped()
+ if not self._fp_register_file.fresh:
+ self._internal_debugger._fetch_fp_registers(self)
+ return int.from_bytes(self._fp_register_file.mmx[index].data, "little") & ((1 << 64) - 1)
+
+ def setter(self: Amd64Registers, value: int) -> None:
+ self._internal_debugger._ensure_process_stopped()
+ if not self._fp_register_file.fresh:
+ self._internal_debugger._fetch_fp_registers(self)
+ self._fp_register_file.mmx[index].data = (value & ((1 << 64) - 1)).to_bytes(16, "little")
+ self._fp_register_file.dirty = True
+
+ return property(getter, setter, None, name)
+
+
+def _get_property_fp_st(name: str, index: int) -> property:
+ # We should be able to expose the long double member from CFFI directly
+ # But their support for long double does not actually allow for value comparison or manipulation
+ # So, ctypes it is
+ def getter(self: Amd64Registers) -> float:
+ self._internal_debugger._ensure_process_stopped()
+ if not self._fp_register_file.fresh:
+ self._internal_debugger._fetch_fp_registers(self)
+ return c_longdouble.from_buffer_copy(bytes(self._fp_register_file.mmx[index].data)).value
+
+ def setter(self: Amd64Registers, value: float) -> None:
+ self._internal_debugger._ensure_process_stopped()
+ if not self._fp_register_file.fresh:
+ self._internal_debugger._fetch_fp_registers(self)
+ self._fp_register_file.mmx[index].data = bytes(c_longdouble(value))
+ self._fp_register_file.dirty = True
+
+ return property(getter, setter, None, name)
+
+
@dataclass
class Amd64PtraceRegisterHolder(PtraceRegisterHolder):
"""A class that provides views and setters for the registers of an x86_64 process."""
@@ -237,6 +289,18 @@ def provide_regs_class(self: Amd64PtraceRegisterHolder) -> type:
"""Provide a class to hold the register accessors."""
return Amd64Registers
+ def provide_regs(self: Amd64PtraceRegisterHolder) -> list[str]:
+ """Provide the list of registers, excluding the vector and fp registers."""
+ return AMD64_REGS
+
+ def provide_vector_fp_regs(self: Amd64PtraceRegisterHolder) -> list[tuple[str]]:
+ """Provide the list of vector and floating point registers."""
+ return self._vector_fp_registers
+
+ def provide_special_regs(self: Amd64PtraceRegisterHolder) -> list[str]:
+ """Provide the list of special registers, which are not intended for general-purpose use."""
+ return AMD64_SPECIAL_REGS
+
def apply_on_regs(self: Amd64PtraceRegisterHolder, target: Amd64Registers, target_class: type) -> None:
"""Apply the register accessors to the Amd64Registers class."""
target.register_file = self.register_file
@@ -246,6 +310,8 @@ def apply_on_regs(self: Amd64PtraceRegisterHolder, target: Amd64Registers, targe
if hasattr(target_class, "rip"):
return
+ self._vector_fp_registers = []
+
# setup accessors
for name in AMD64_GP_REGS:
name_64 = "r" + name + "x"
@@ -282,23 +348,30 @@ def apply_on_regs(self: Amd64PtraceRegisterHolder, target: Amd64Registers, targe
setattr(target_class, name_16, _get_property_16(name_64))
setattr(target_class, name_8l, _get_property_8l(name_64))
+ for name in AMD64_SPECIAL_REGS:
+ setattr(target_class, name, _get_property_64(name))
+
# setup special registers
target_class.rip = _get_property_64("rip")
# setup floating-point registers
# see libdebug/cffi/ptrace_cffi_build.py for the possible values of fp_register_file.type
+ self._handle_fp_legacy(target_class)
+
match self.fp_register_file.type:
case 0:
- self._handle_fp_512(target_class)
+ self._handle_vector_512(target_class)
case 1:
- self._handle_fp_896(target_class)
+ self._handle_vector_896(target_class)
case 2:
- self._handle_fp_2696(target_class)
+ self._handle_vector_2696(target_class)
case _:
raise NotImplementedError(
f"Floating-point register file type {self.fp_register_file.type} not available.",
)
+ Amd64PtraceRegisterHolder._vector_fp_registers = self._vector_fp_registers
+
def apply_on_thread(self: Amd64PtraceRegisterHolder, target: ThreadContext, target_class: type) -> None:
"""Apply the register accessors to the thread class."""
target.register_file = self.register_file
@@ -320,44 +393,57 @@ def apply_on_thread(self: Amd64PtraceRegisterHolder, target: ThreadContext, targ
target_class.syscall_arg4 = _get_property_64("r8")
target_class.syscall_arg5 = _get_property_64("r9")
- def _handle_fp_512(self: Amd64PtraceRegisterHolder, target_class: type) -> None:
+ def _handle_fp_legacy(self: Amd64PtraceRegisterHolder, target_class: type) -> None:
+ """Handle legacy mmx and st registers."""
+ for index in range(8):
+ name_mm = f"mm{index}"
+ setattr(target_class, name_mm, _get_property_fp_mmx(name_mm, index))
+
+ name_st = f"st{index}"
+ setattr(target_class, name_st, _get_property_fp_st(name_st, index))
+
+ self._vector_fp_registers.append((name_mm, name_st))
+
+ def _handle_vector_512(self: Amd64PtraceRegisterHolder, target_class: type) -> None:
"""Handle the case where the xsave area is 512 bytes long, which means we just have the xmm registers."""
for index in range(16):
- name = f"xmm{index}"
- setattr(target_class, name, _get_property_fp_xmm0(name, index))
+ name_xmm = f"xmm{index}"
+ setattr(target_class, name_xmm, _get_property_fp_xmm0(name_xmm, index))
+ self._vector_fp_registers.append((name_xmm,))
- def _handle_fp_896(self: Amd64PtraceRegisterHolder, target_class: type) -> None:
+ def _handle_vector_896(self: Amd64PtraceRegisterHolder, target_class: type) -> None:
"""Handle the case where the xsave area is 896 bytes long, which means we have the xmm and ymm registers."""
for index in range(16):
- name = f"xmm{index}"
- setattr(target_class, name, _get_property_fp_xmm0(name, index))
+ name_xmm = f"xmm{index}"
+ setattr(target_class, name_xmm, _get_property_fp_xmm0(name_xmm, index))
- for index in range(16):
- name = f"ymm{index}"
- setattr(target_class, name, _get_property_fp_ymm0(name, index))
+ name_ymm = f"ymm{index}"
+ setattr(target_class, name_ymm, _get_property_fp_ymm0(name_ymm, index))
+
+ self._vector_fp_registers.append((name_xmm, name_ymm))
- def _handle_fp_2696(self: Amd64PtraceRegisterHolder, target_class: type) -> None:
+ def _handle_vector_2696(self: Amd64PtraceRegisterHolder, target_class: type) -> None:
"""Handle the case where the xsave area is 2696 bytes long, which means we have 32 zmm registers."""
for index in range(16):
- name = f"xmm{index}"
- setattr(target_class, name, _get_property_fp_xmm0(name, index))
+ name_xmm = f"xmm{index}"
+ setattr(target_class, name_xmm, _get_property_fp_xmm0(name_xmm, index))
- for index in range(16):
- name = f"ymm{index}"
- setattr(target_class, name, _get_property_fp_ymm0(name, index))
+ name_ymm = f"ymm{index}"
+ setattr(target_class, name_ymm, _get_property_fp_ymm0(name_ymm, index))
- for index in range(16):
- name = f"zmm{index}"
- setattr(target_class, name, _get_property_fp_zmm0(name, index))
+ name_zmm = f"zmm{index}"
+ setattr(target_class, name_zmm, _get_property_fp_zmm0(name_zmm, index))
- for index in range(16):
- name = f"xmm{index + 16}"
- setattr(target_class, name, _get_property_fp_xmm1(name, index))
+ self._vector_fp_registers.append((name_xmm, name_ymm, name_zmm))
for index in range(16):
- name = f"ymm{index + 16}"
- setattr(target_class, name, _get_property_fp_ymm1(name, index))
+ name_xmm = f"xmm{index + 16}"
+ setattr(target_class, name_xmm, _get_property_fp_xmm1(name_xmm, index))
- for index in range(16):
- name = f"zmm{index + 16}"
- setattr(target_class, name, _get_property_fp_zmm1(name, index))
+ name_ymm = f"ymm{index + 16}"
+ setattr(target_class, name_ymm, _get_property_fp_ymm1(name_ymm, index))
+
+ name_zmm = f"zmm{index + 16}"
+ setattr(target_class, name_zmm, _get_property_fp_zmm1(name_zmm, index))
+
+ self._vector_fp_registers.append((name_xmm, name_ymm, name_zmm))
diff --git a/libdebug/architectures/amd64/amd64_registers.py b/libdebug/architectures/amd64/amd64_registers.py
index a8dbf72a..d36b8586 100644
--- a/libdebug/architectures/amd64/amd64_registers.py
+++ b/libdebug/architectures/amd64/amd64_registers.py
@@ -1,22 +1,13 @@
#
# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
+# Copyright (c) 2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
from __future__ import annotations
-from dataclasses import dataclass
-
from libdebug.data.registers import Registers
-from libdebug.debugger.internal_debugger_instance_manager import get_global_internal_debugger
-@dataclass
class Amd64Registers(Registers):
"""This class holds the state of the architectural-dependent registers of a process."""
-
- def __init__(self: Amd64Registers, thread_id: int) -> None:
- """Initializes the Registers object."""
- self._internal_debugger = get_global_internal_debugger()
- self._thread_id = thread_id
diff --git a/libdebug/architectures/amd64/amd64_stack_unwinder.py b/libdebug/architectures/amd64/amd64_stack_unwinder.py
index 6cec1798..2d9fe88b 100644
--- a/libdebug/architectures/amd64/amd64_stack_unwinder.py
+++ b/libdebug/architectures/amd64/amd64_stack_unwinder.py
@@ -6,6 +6,7 @@
from __future__ import annotations
+import sys
from typing import TYPE_CHECKING
from libdebug.architectures.stack_unwinding_manager import StackUnwindingManager
@@ -13,6 +14,7 @@
if TYPE_CHECKING:
from libdebug.data.memory_map import MemoryMap
+ from libdebug.data.memory_map_list import MemoryMapList
from libdebug.state.thread_context import ThreadContext
@@ -34,18 +36,18 @@ def unwind(self: Amd64StackUnwinder, target: ThreadContext) -> list:
current_rbp = target.regs.rbp
stack_trace = [target.regs.rip]
- vmaps = target._internal_debugger.debugging_interface.maps()
+ vmaps = target._internal_debugger.debugging_interface.get_maps()
while current_rbp:
try:
# Read the return address
- return_address = int.from_bytes(target.memory[current_rbp + 8, 8, "absolute"], byteorder="little")
+ return_address = int.from_bytes(target.memory[current_rbp + 8, 8, "absolute"], sys.byteorder)
if not any(vmap.start <= return_address < vmap.end for vmap in vmaps):
break
# Read the previous rbp and set it as the current one
- current_rbp = int.from_bytes(target.memory[current_rbp, 8, "absolute"], byteorder="little")
+ current_rbp = int.from_bytes(target.memory[current_rbp, 8, "absolute"], sys.byteorder)
stack_trace.append(return_address)
except (OSError, ValueError):
@@ -68,12 +70,12 @@ def unwind(self: Amd64StackUnwinder, target: ThreadContext) -> list:
return stack_trace
- def get_return_address(self: Amd64StackUnwinder, target: ThreadContext, vmaps: list[MemoryMap]) -> int:
+ def get_return_address(self: Amd64StackUnwinder, target: ThreadContext, vmaps: MemoryMapList[MemoryMap]) -> int:
"""Get the return address of the current function.
Args:
target (ThreadContext): The target ThreadContext.
- vmaps (list[MemoryMap]): The memory maps of the process.
+ vmaps (MemoryMapList[MemoryMap]): The memory maps of the process.
Returns:
int: The return address.
@@ -92,8 +94,8 @@ def get_return_address(self: Amd64StackUnwinder, target: ThreadContext, vmaps: l
return_address = int.from_bytes(return_address, byteorder="little")
- if not any(vmap.start <= return_address < vmap.end for vmap in vmaps):
- raise ValueError("Return address not in any valid memory map")
+ if not vmaps.filter(return_address):
+ raise ValueError("Return address not in memory maps.")
return return_address
diff --git a/libdebug/architectures/amd64/compat/__init__.py b/libdebug/architectures/amd64/compat/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/libdebug/architectures/amd64/compat/i386_over_amd64_ptrace_register_holder.py b/libdebug/architectures/amd64/compat/i386_over_amd64_ptrace_register_holder.py
new file mode 100644
index 00000000..ac971c02
--- /dev/null
+++ b/libdebug/architectures/amd64/compat/i386_over_amd64_ptrace_register_holder.py
@@ -0,0 +1,117 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import TYPE_CHECKING
+
+from libdebug.architectures.amd64.amd64_ptrace_register_holder import (
+ _get_property_8h,
+ _get_property_8l,
+ _get_property_16,
+ _get_property_32,
+)
+from libdebug.architectures.amd64.compat.i386_over_amd64_registers import I386OverAMD64Registers
+from libdebug.architectures.i386.i386_ptrace_register_holder import (
+ I386_BASE_REGS,
+ I386_GP_REGS,
+ I386_SPECIAL_REGS,
+ I386PtraceRegisterHolder,
+)
+
+if TYPE_CHECKING:
+ from libdebug.state.thread_context import ThreadContext
+
+
+@dataclass
+class I386OverAMD64PtraceRegisterHolder(I386PtraceRegisterHolder):
+ """A class that provides views and setters for the registers of an x86_64 process."""
+
+ def provide_regs_class(self: I386OverAMD64PtraceRegisterHolder) -> type:
+ """Provide a class to hold the register accessors."""
+ return I386OverAMD64Registers
+
+ def apply_on_regs(
+ self: I386OverAMD64PtraceRegisterHolder,
+ target: I386OverAMD64Registers,
+ target_class: type,
+ ) -> None:
+ """Apply the register accessors to the I386Registers class."""
+ target.register_file = self.register_file
+ target._fp_register_file = self.fp_register_file
+
+ # If the accessors are already defined, we don't need to redefine them
+ if hasattr(target_class, "eip"):
+ return
+
+ self._vector_fp_registers = []
+
+ # setup accessors
+ for name in I386_GP_REGS:
+ name_64 = "r" + name + "x"
+ name_32 = "e" + name + "x"
+ name_16 = name + "x"
+ name_8l = name + "l"
+ name_8h = name + "h"
+
+ setattr(target_class, name_32, _get_property_32(name_64))
+ setattr(target_class, name_16, _get_property_16(name_64))
+ setattr(target_class, name_8l, _get_property_8l(name_64))
+ setattr(target_class, name_8h, _get_property_8h(name_64))
+
+ for name in I386_BASE_REGS:
+ name_64 = "r" + name
+ name_32 = "e" + name
+ name_16 = name
+ name_8l = name + "l"
+
+ setattr(target_class, name_32, _get_property_32(name_64))
+ setattr(target_class, name_16, _get_property_16(name_64))
+ setattr(target_class, name_8l, _get_property_8l(name_64))
+
+ for name in I386_SPECIAL_REGS:
+ setattr(target_class, name, _get_property_32(name))
+
+ # setup special registers
+ target_class.eip = _get_property_32("rip")
+
+ self._handle_fp_legacy(target_class)
+
+ match self.fp_register_file.type:
+ case 0:
+ self._handle_vector_512(target_class)
+ case 1:
+ self._handle_vector_896(target_class)
+ case 2:
+ self._handle_vector_2696(target_class)
+ case _:
+ raise NotImplementedError(
+ f"Floating-point register file type {self.fp_register_file.type} not available.",
+ )
+
+ I386OverAMD64PtraceRegisterHolder._vector_fp_registers = self._vector_fp_registers
+
+ def apply_on_thread(self: I386OverAMD64PtraceRegisterHolder, target: ThreadContext, target_class: type) -> None:
+ """Apply the register accessors to the thread class."""
+ target.register_file = self.register_file
+
+ # If the accessors are already defined, we don't need to redefine them
+ if hasattr(target_class, "instruction_pointer"):
+ return
+
+ # setup generic "instruction_pointer" property
+ target_class.instruction_pointer = _get_property_32("rip")
+
+ # setup generic syscall properties
+ target_class.syscall_number = _get_property_32("orig_rax")
+ target_class.syscall_return = _get_property_32("rax")
+ target_class.syscall_arg0 = _get_property_32("rbx")
+ target_class.syscall_arg1 = _get_property_32("rcx")
+ target_class.syscall_arg2 = _get_property_32("rdx")
+ target_class.syscall_arg3 = _get_property_32("rsi")
+ target_class.syscall_arg4 = _get_property_32("rdi")
+ target_class.syscall_arg5 = _get_property_32("rbp")
diff --git a/libdebug/architectures/amd64/compat/i386_over_amd64_registers.py b/libdebug/architectures/amd64/compat/i386_over_amd64_registers.py
new file mode 100644
index 00000000..681672a7
--- /dev/null
+++ b/libdebug/architectures/amd64/compat/i386_over_amd64_registers.py
@@ -0,0 +1,13 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from __future__ import annotations
+
+from libdebug.data.registers import Registers
+
+
+class I386OverAMD64Registers(Registers):
+ """This class holds the state of the architectural-dependent registers of a process."""
diff --git a/libdebug/architectures/breakpoint_validator.py b/libdebug/architectures/breakpoint_validator.py
index 6804f497..7a43285e 100644
--- a/libdebug/architectures/breakpoint_validator.py
+++ b/libdebug/architectures/breakpoint_validator.py
@@ -10,6 +10,7 @@
from libdebug.architectures.aarch64.aarch64_breakpoint_validator import validate_breakpoint_aarch64
from libdebug.architectures.amd64.amd64_breakpoint_validator import validate_breakpoint_amd64
+from libdebug.architectures.i386.i386_breakpoint_validator import validate_breakpoint_i386
if TYPE_CHECKING:
from libdebug.data.breakpoint import Breakpoint
@@ -20,5 +21,7 @@ def validate_hardware_breakpoint(arch: str, bp: Breakpoint) -> None:
validate_breakpoint_aarch64(bp)
elif arch == "amd64":
validate_breakpoint_amd64(bp)
+ elif arch == "i386":
+ validate_breakpoint_i386(bp)
else:
raise ValueError(f"Architecture {arch} not supported")
diff --git a/libdebug/architectures/call_utilities_manager.py b/libdebug/architectures/call_utilities_manager.py
index 09d26b9c..b9a2040c 100644
--- a/libdebug/architectures/call_utilities_manager.py
+++ b/libdebug/architectures/call_utilities_manager.py
@@ -8,6 +8,7 @@
from abc import ABC, abstractmethod
+
class CallUtilitiesManager(ABC):
"""An architecture-independent interface for call instruction utilities."""
@@ -21,4 +22,4 @@ def compute_call_skip(self: CallUtilitiesManager, opcode_window: bytes) -> int:
@abstractmethod
def get_call_and_skip_amount(self, opcode_window: bytes) -> tuple[bool, int]:
- """Check if the current instruction is a call instruction and compute the instruction size."""
\ No newline at end of file
+ """Check if the current instruction is a call instruction and compute the instruction size."""
diff --git a/libdebug/architectures/call_utilities_provider.py b/libdebug/architectures/call_utilities_provider.py
index 4bee02f8..fa0031ae 100644
--- a/libdebug/architectures/call_utilities_provider.py
+++ b/libdebug/architectures/call_utilities_provider.py
@@ -11,9 +11,13 @@
Amd64CallUtilities,
)
from libdebug.architectures.call_utilities_manager import CallUtilitiesManager
+from libdebug.architectures.i386.i386_call_utilities import (
+ I386CallUtilities,
+)
_aarch64_call_utilities = Aarch64CallUtilities()
_amd64_call_utilities = Amd64CallUtilities()
+_i386_call_utilities = I386CallUtilities()
def call_utilities_provider(architecture: str) -> CallUtilitiesManager:
@@ -23,5 +27,7 @@ def call_utilities_provider(architecture: str) -> CallUtilitiesManager:
return _amd64_call_utilities
case "aarch64":
return _aarch64_call_utilities
+ case "i386":
+ return _i386_call_utilities
case _:
raise NotImplementedError(f"Architecture {architecture} not available.")
diff --git a/libdebug/architectures/i386/__init__.py b/libdebug/architectures/i386/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/libdebug/architectures/i386/i386_breakpoint_validator.py b/libdebug/architectures/i386/i386_breakpoint_validator.py
new file mode 100644
index 00000000..fb509517
--- /dev/null
+++ b/libdebug/architectures/i386/i386_breakpoint_validator.py
@@ -0,0 +1,21 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from libdebug.data.breakpoint import Breakpoint
+
+
+def validate_breakpoint_i386(bp: Breakpoint) -> None:
+ """Validate a hardware breakpoint for the i386 architecture."""
+ if bp.condition not in ["w", "rw", "x"]:
+ raise ValueError("Invalid condition for watchpoints. Supported conditions are 'w', 'rw', 'x'.")
+
+ if bp.length not in [1, 2, 4]:
+ raise ValueError("Invalid length for watchpoints. Supported lengths are 1, 2, 4.")
diff --git a/libdebug/architectures/i386/i386_call_utilities.py b/libdebug/architectures/i386/i386_call_utilities.py
new file mode 100644
index 00000000..267be494
--- /dev/null
+++ b/libdebug/architectures/i386/i386_call_utilities.py
@@ -0,0 +1,63 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Roberto Alessandro Bertolini, Francesco Panebianco. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from __future__ import annotations
+
+from libdebug.architectures.call_utilities_manager import CallUtilitiesManager
+
+
+class I386CallUtilities(CallUtilitiesManager):
+ """Class that provides call utilities for the i386 architecture."""
+
+ def is_call(self: I386CallUtilities, opcode_window: bytes) -> bool:
+ """Check if the current instruction is a call instruction."""
+ # Check for direct CALL (E8 xx xx xx xx)
+ if opcode_window[0] == 0xE8:
+ return True
+
+ # Check for indirect CALL using ModR/M (FF /2)
+ if opcode_window[0] == 0xFF:
+ # Extract ModR/M byte
+ modRM = opcode_window[1]
+ reg = (modRM >> 3) & 0x07
+
+ if reg == 2:
+ return True
+
+ return False
+
+ def compute_call_skip(self: I386CallUtilities, opcode_window: bytes) -> int:
+ """Compute the instruction size of the current call instruction."""
+ # Check for direct CALL (E8 xx xx xx xx)
+ if opcode_window[0] == 0xE8:
+ return 5
+
+ # Check for indirect CALL using ModR/M (FF /2)
+ if opcode_window[0] == 0xFF:
+ # Extract ModR/M byte
+ modRM = opcode_window[1]
+ mod = (modRM >> 6) & 0x03
+ reg = (modRM >> 3) & 0x07
+
+ if reg == 2:
+ if mod == 0:
+ if (modRM & 0x07) == 4:
+ return 3 + (4 if opcode_window[2] == 0x25 else 0)
+ elif (modRM & 0x07) == 5:
+ return 6
+ return 2
+ elif mod == 1:
+ return 3
+ elif mod == 2:
+ return 6
+ elif mod == 3:
+ return 2
+
+ return 0
+
+ def get_call_and_skip_amount(self: I386CallUtilities, opcode_window: bytes) -> tuple[bool, int]:
+ skip = self.compute_call_skip(opcode_window)
+ return skip != 0, skip
diff --git a/libdebug/architectures/i386/i386_ptrace_register_holder.py b/libdebug/architectures/i386/i386_ptrace_register_holder.py
new file mode 100644
index 00000000..3e6d49bb
--- /dev/null
+++ b/libdebug/architectures/i386/i386_ptrace_register_holder.py
@@ -0,0 +1,195 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import TYPE_CHECKING
+
+from libdebug.architectures.amd64.amd64_ptrace_register_holder import (
+ _get_property_8h,
+ _get_property_8l,
+ _get_property_16,
+ _get_property_32,
+ _get_property_fp_mmx,
+ _get_property_fp_st,
+ _get_property_fp_xmm0,
+ _get_property_fp_ymm0,
+ _get_property_fp_zmm0,
+)
+from libdebug.architectures.i386.i386_registers import I386Registers
+from libdebug.ptrace.ptrace_register_holder import PtraceRegisterHolder
+
+if TYPE_CHECKING:
+ from libdebug.state.thread_context import ThreadContext
+
+I386_GP_REGS = ["a", "b", "c", "d"]
+
+I386_BASE_REGS = ["bp", "sp", "si", "di"]
+
+I386_REGS = [
+ "eax",
+ "ebx",
+ "ecx",
+ "edx",
+ "esi",
+ "edi",
+ "ebp",
+ "esp",
+ "eip",
+]
+
+I386_SPECIAL_REGS = [
+ "eflags",
+ "cs",
+ "ss",
+ "ds",
+ "es",
+ "fs",
+ "gs",
+]
+
+
+@dataclass
+class I386PtraceRegisterHolder(PtraceRegisterHolder):
+ """A class that provides views and setters for the registers of an i386 process."""
+
+ def provide_regs_class(self: I386PtraceRegisterHolder) -> type:
+ """Provide a class to hold the register accessors."""
+ return I386Registers
+
+ def provide_regs(self: I386PtraceRegisterHolder) -> list[str]:
+ """Provide the list of registers, excluding the vector and fp registers."""
+ return I386_REGS
+
+ def provide_vector_fp_regs(self: I386PtraceRegisterHolder) -> list[str]:
+ """Provide the list of vector and floating point registers."""
+ return self._vector_fp_registers
+
+ def provide_special_regs(self: I386PtraceRegisterHolder) -> list[str]:
+ """Provide the list of special registers, which are not intended for general-purpose use."""
+ return I386_SPECIAL_REGS
+
+ def apply_on_regs(self: I386PtraceRegisterHolder, target: I386Registers, target_class: type) -> None:
+ """Apply the register accessors to the I386Registers class."""
+ target.register_file = self.register_file
+ target._fp_register_file = self.fp_register_file
+
+ # If the accessors are already defined, we don't need to redefine them
+ if hasattr(target_class, "eip"):
+ return
+
+ self._vector_fp_registers = []
+
+ # setup accessors
+ for name in I386_GP_REGS:
+ name_32 = "e" + name + "x"
+ name_16 = name + "x"
+ name_8l = name + "l"
+ name_8h = name + "h"
+
+ setattr(target_class, name_32, _get_property_32(name_32))
+ setattr(target_class, name_16, _get_property_16(name_32))
+ setattr(target_class, name_8l, _get_property_8l(name_32))
+ setattr(target_class, name_8h, _get_property_8h(name_32))
+
+ for name in I386_BASE_REGS:
+ name_32 = "e" + name
+ name_16 = name
+ name_8l = name + "l"
+
+ setattr(target_class, name_32, _get_property_32(name_32))
+ setattr(target_class, name_16, _get_property_16(name_32))
+ setattr(target_class, name_8l, _get_property_8l(name_32))
+
+ for name in I386_SPECIAL_REGS:
+ setattr(target_class, name, _get_property_32(name))
+
+ # setup special registers
+ target_class.eip = _get_property_32("eip")
+
+ self._handle_fp_legacy(target_class)
+
+ match self.fp_register_file.type:
+ case 0:
+ self._handle_vector_512(target_class)
+ case 1:
+ self._handle_vector_896(target_class)
+ case 2:
+ self._handle_vector_2696(target_class)
+ case _:
+ raise NotImplementedError(
+ f"Floating-point register file type {self.fp_register_file.type} not available.",
+ )
+
+ I386PtraceRegisterHolder._vector_fp_registers = self._vector_fp_registers
+
+ def apply_on_thread(self: I386PtraceRegisterHolder, target: ThreadContext, target_class: type) -> None:
+ """Apply the register accessors to the thread class."""
+ target.register_file = self.register_file
+
+ # If the accessors are already defined, we don't need to redefine them
+ if hasattr(target_class, "instruction_pointer"):
+ return
+
+ # setup generic "instruction_pointer" property
+ target_class.instruction_pointer = _get_property_32("eip")
+
+ # setup generic syscall properties
+ target_class.syscall_number = _get_property_32("orig_eax")
+ target_class.syscall_return = _get_property_32("eax")
+ target_class.syscall_arg0 = _get_property_32("ebx")
+ target_class.syscall_arg1 = _get_property_32("ecx")
+ target_class.syscall_arg2 = _get_property_32("edx")
+ target_class.syscall_arg3 = _get_property_32("esi")
+ target_class.syscall_arg4 = _get_property_32("edi")
+ target_class.syscall_arg5 = _get_property_32("ebp")
+
+ def _handle_fp_legacy(self: I386PtraceRegisterHolder, target_class: type) -> None:
+ """Handle legacy mmx and st registers."""
+ for index in range(8):
+ name_mm = f"mm{index}"
+ setattr(target_class, name_mm, _get_property_fp_mmx(name_mm, index))
+
+ name_st = f"st{index}"
+ setattr(target_class, name_st, _get_property_fp_st(name_st, index))
+
+ self._vector_fp_registers.append((name_mm, name_st))
+
+ def _handle_vector_512(self: I386PtraceRegisterHolder, target_class: type) -> None:
+ """Handle the case where the xsave area is 512 bytes long, which means we just have the xmm registers."""
+ # i386 only gets 8 registers
+ for index in range(8):
+ name_xmm = f"xmm{index}"
+ setattr(target_class, name_xmm, _get_property_fp_xmm0(name_xmm, index))
+ self._vector_fp_registers.append((name_xmm,))
+
+ def _handle_vector_896(self: I386PtraceRegisterHolder, target_class: type) -> None:
+ """Handle the case where the xsave area is 896 bytes long, which means we have the xmm and ymm registers."""
+ # i386 only gets 8 registers
+ for index in range(8):
+ name_xmm = f"xmm{index}"
+ setattr(target_class, name_xmm, _get_property_fp_xmm0(name_xmm, index))
+
+ name_ymm = f"ymm{index}"
+ setattr(target_class, name_ymm, _get_property_fp_ymm0(name_ymm, index))
+
+ self._vector_fp_registers.append((name_xmm, name_ymm))
+
+ def _handle_vector_2696(self: I386PtraceRegisterHolder, target_class: type) -> None:
+ """Handle the case where the xsave area is 2696 bytes long, which means we have 32 zmm registers."""
+ # i386 only gets 8 registers
+ for index in range(8):
+ name_xmm = f"xmm{index}"
+ setattr(target_class, name_xmm, _get_property_fp_xmm0(name_xmm, index))
+
+ name_ymm = f"ymm{index}"
+ setattr(target_class, name_ymm, _get_property_fp_ymm0(name_ymm, index))
+
+ name_zmm = f"zmm{index}"
+ setattr(target_class, name_zmm, _get_property_fp_zmm0(name_zmm, index))
+
+ self._vector_fp_registers.append((name_xmm, name_ymm, name_zmm))
diff --git a/libdebug/architectures/i386/i386_registers.py b/libdebug/architectures/i386/i386_registers.py
new file mode 100644
index 00000000..94c455b7
--- /dev/null
+++ b/libdebug/architectures/i386/i386_registers.py
@@ -0,0 +1,13 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from __future__ import annotations
+
+from libdebug.data.registers import Registers
+
+
+class I386Registers(Registers):
+ """This class holds the state of the architectural-dependent registers of a process."""
diff --git a/libdebug/architectures/i386/i386_stack_unwinder.py b/libdebug/architectures/i386/i386_stack_unwinder.py
new file mode 100644
index 00000000..32002c30
--- /dev/null
+++ b/libdebug/architectures/i386/i386_stack_unwinder.py
@@ -0,0 +1,120 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini, Francesco Panebianco. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from libdebug.architectures.stack_unwinding_manager import StackUnwindingManager
+from libdebug.liblog import liblog
+
+if TYPE_CHECKING:
+ from libdebug.data.memory_map import MemoryMap
+ from libdebug.data.memory_map_list import MemoryMapList
+ from libdebug.state.thread_context import ThreadContext
+
+
+class I386StackUnwinder(StackUnwindingManager):
+ """Class that provides stack unwinding for the i386 architecture."""
+
+ def unwind(self: I386StackUnwinder, target: ThreadContext) -> list:
+ """Unwind the stack of a process.
+
+ Args:
+ target (ThreadContext): The target ThreadContext.
+
+ Returns:
+ list: A list of return addresses.
+ """
+ assert hasattr(target.regs, "eip")
+ assert hasattr(target.regs, "ebp")
+
+ current_ebp = target.regs.ebp
+ stack_trace = [target.regs.eip]
+
+ vmaps = target._internal_debugger.debugging_interface.get_maps()
+
+ while current_ebp:
+ try:
+ # Read the return address
+ return_address = int.from_bytes(target.memory[current_ebp + 4, 4], byteorder="little")
+
+ if not any(vmap.start <= return_address < vmap.end for vmap in vmaps):
+ break
+
+ # Read the previous ebp and set it as the current one
+ current_ebp = int.from_bytes(target.memory[current_ebp, 4], byteorder="little")
+
+ stack_trace.append(return_address)
+ except (OSError, ValueError):
+ break
+
+ # If we are in the prologue of a function, we need to get the return address from the stack
+ # using a slightly more complex method
+ try:
+ first_return_address = self.get_return_address(target, vmaps)
+
+ if len(stack_trace) > 1:
+ if first_return_address != stack_trace[1]:
+ stack_trace.insert(1, first_return_address)
+ else:
+ stack_trace.append(first_return_address)
+ except (OSError, ValueError):
+ liblog.WARNING(
+ "Failed to get the return address from the stack. Check stack frame registers (e.g., base pointer). The stack trace may be incomplete.",
+ )
+
+ return stack_trace
+
+ def get_return_address(self: I386StackUnwinder, target: ThreadContext, vmaps: MemoryMapList[MemoryMap]) -> int:
+ """Get the return address of the current function.
+
+ Args:
+ target (ThreadContext): The target ThreadContext.
+ vmaps (list[MemoryMap]): The memory maps of the process.
+
+ Returns:
+ int: The return address.
+ """
+ instruction_window = target.memory[target.regs.eip, 4]
+
+ # Check if the instruction window is a function preamble and handle each case
+ return_address = None
+
+ if self._preamble_state(instruction_window) == 0:
+ return_address = target.memory[target.regs.ebp + 4, 4]
+ elif self._preamble_state(instruction_window) == 1:
+ return_address = target.memory[target.regs.esp, 4]
+ else:
+ return_address = target.memory[target.regs.esp + 4, 4]
+
+ return_address = int.from_bytes(return_address, byteorder="little")
+
+ if not vmaps.filter(return_address):
+ raise ValueError("Return address is not in any memory map.")
+
+ return return_address
+
+ def _preamble_state(self: I386StackUnwinder, instruction_window: bytes) -> int:
+ """Check if the instruction window is a function preamble and, if so, at what stage.
+
+ Args:
+ instruction_window (bytes): The instruction window.
+
+ Returns:
+ int: 0 if not a preamble, 1 if ebp has not been pushed yet, 2 otherwise
+ """
+ preamble_state = 0
+
+ # endbr32 and push ebp
+ if b"\xf3\x0f\x1e\xfb" in instruction_window or b"\x55" in instruction_window:
+ preamble_state = 1
+
+ # mov ebp, esp
+ elif b"\x89\xe5" in instruction_window:
+ preamble_state = 2
+
+ return preamble_state
diff --git a/libdebug/architectures/register_helper.py b/libdebug/architectures/register_helper.py
index 95e20048..345d8f20 100644
--- a/libdebug/architectures/register_helper.py
+++ b/libdebug/architectures/register_helper.py
@@ -10,7 +10,14 @@
from libdebug.architectures.amd64.amd64_ptrace_register_holder import (
Amd64PtraceRegisterHolder,
)
+from libdebug.architectures.amd64.compat.i386_over_amd64_ptrace_register_holder import (
+ I386OverAMD64PtraceRegisterHolder,
+)
+from libdebug.architectures.i386.i386_ptrace_register_holder import (
+ I386PtraceRegisterHolder,
+)
from libdebug.data.register_holder import RegisterHolder
+from libdebug.utils.libcontext import libcontext
def register_holder_provider(
@@ -20,9 +27,14 @@ def register_holder_provider(
) -> RegisterHolder:
"""Returns an instance of the register holder to be used by the `_InternalDebugger` class."""
match architecture:
- case "amd64" | "i386":
+ case "amd64":
return Amd64PtraceRegisterHolder(register_file, fp_register_file)
case "aarch64":
return Aarch64PtraceRegisterHolder(register_file, fp_register_file)
+ case "i386":
+ if libcontext.platform == "amd64":
+ return I386OverAMD64PtraceRegisterHolder(register_file, fp_register_file)
+ else:
+ return I386PtraceRegisterHolder(register_file, fp_register_file)
case _:
raise NotImplementedError(f"Architecture {architecture} not available.")
diff --git a/libdebug/architectures/stack_unwinding_provider.py b/libdebug/architectures/stack_unwinding_provider.py
index 51778dec..0a21fa56 100644
--- a/libdebug/architectures/stack_unwinding_provider.py
+++ b/libdebug/architectures/stack_unwinding_provider.py
@@ -10,10 +10,14 @@
from libdebug.architectures.amd64.amd64_stack_unwinder import (
Amd64StackUnwinder,
)
+from libdebug.architectures.i386.i386_stack_unwinder import (
+ I386StackUnwinder,
+)
from libdebug.architectures.stack_unwinding_manager import StackUnwindingManager
_aarch64_stack_unwinder = Aarch64StackUnwinder()
_amd64_stack_unwinder = Amd64StackUnwinder()
+_i386_stack_unwinder = I386StackUnwinder()
def stack_unwinding_provider(architecture: str) -> StackUnwindingManager:
@@ -23,5 +27,7 @@ def stack_unwinding_provider(architecture: str) -> StackUnwindingManager:
return _amd64_stack_unwinder
case "aarch64":
return _aarch64_stack_unwinder
+ case "i386":
+ return _i386_stack_unwinder
case _:
raise NotImplementedError(f"Architecture {architecture} not available.")
diff --git a/libdebug/builtin/pretty_print_syscall_handler.py b/libdebug/builtin/pretty_print_syscall_handler.py
index 4dca9e41..dfd23d22 100644
--- a/libdebug/builtin/pretty_print_syscall_handler.py
+++ b/libdebug/builtin/pretty_print_syscall_handler.py
@@ -8,7 +8,7 @@
from typing import TYPE_CHECKING
-from libdebug.utils.print_style import PrintStyle
+from libdebug.utils.ansi_escape_codes import ANSIColors
from libdebug.utils.syscall_utils import (
resolve_syscall_arguments,
resolve_syscall_name,
@@ -18,56 +18,65 @@
from libdebug.state.thread_context import ThreadContext
-def pprint_on_enter(d: ThreadContext, syscall_number: int, **kwargs: int) -> None:
+def pprint_on_enter(t: ThreadContext, syscall_number: int, **kwargs: int) -> None:
"""Function that will be called when a syscall is entered in pretty print mode.
Args:
- d (ThreadContext): the thread context.
+ t (ThreadContext): the thread context.
syscall_number (int): the syscall number.
**kwargs (bool): the keyword arguments.
"""
- syscall_name = resolve_syscall_name(d._internal_debugger.arch, syscall_number)
- syscall_args = resolve_syscall_arguments(d._internal_debugger.arch, syscall_number)
+ syscall_name = resolve_syscall_name(t._internal_debugger.arch, syscall_number)
+ syscall_args = resolve_syscall_arguments(t._internal_debugger.arch, syscall_number)
values = [
- d.syscall_arg0,
- d.syscall_arg1,
- d.syscall_arg2,
- d.syscall_arg3,
- d.syscall_arg4,
- d.syscall_arg5,
+ t.syscall_arg0,
+ t.syscall_arg1,
+ t.syscall_arg2,
+ t.syscall_arg3,
+ t.syscall_arg4,
+ t.syscall_arg5,
]
+ # Print the thread id
+ header = f"{ANSIColors.BOLD}{t.tid}{ANSIColors.RESET} "
+
if "old_args" in kwargs:
old_args = kwargs["old_args"]
entries = [
- f"{arg} = {PrintStyle.BRIGHT_YELLOW}0x{value:x}{PrintStyle.DEFAULT_COLOR}"
+ f"{arg} = {ANSIColors.BRIGHT_YELLOW}0x{value:x}{ANSIColors.DEFAULT_COLOR}"
if old_value == value
- else f"{arg} = {PrintStyle.BRIGHT_YELLOW}0x{old_value:x} -> {PrintStyle.BRIGHT_YELLOW}0x{value:x}{PrintStyle.DEFAULT_COLOR}"
+ else f"{arg} = {ANSIColors.STRIKE}{ANSIColors.BRIGHT_YELLOW}0x{old_value:x}{ANSIColors.RESET} {ANSIColors.BRIGHT_YELLOW}0x{value:x}{ANSIColors.DEFAULT_COLOR}"
for arg, value, old_value in zip(syscall_args, values, old_args, strict=False)
if arg is not None
]
else:
entries = [
- f"{arg} = {PrintStyle.BRIGHT_YELLOW}0x{value:x}{PrintStyle.DEFAULT_COLOR}"
+ f"{arg} = {ANSIColors.BRIGHT_YELLOW}0x{value:x}{ANSIColors.DEFAULT_COLOR}"
for arg, value in zip(syscall_args, values, strict=False)
if arg is not None
]
hijacked = kwargs.get("hijacked", False)
user_handled = kwargs.get("callback", False)
+ hijacker = kwargs.get("hijacker", None)
if hijacked:
print(
- f"{PrintStyle.RED}(user hijacked) {PrintStyle.STRIKE}{PrintStyle.BLUE}{syscall_name}{PrintStyle.DEFAULT_COLOR}({', '.join(entries)}){PrintStyle.RESET}",
+ f"{header}{ANSIColors.RED}(hijacked) {ANSIColors.STRIKE}{ANSIColors.BLUE}{syscall_name}{ANSIColors.DEFAULT_COLOR}({', '.join(entries)}){ANSIColors.RESET}",
)
elif user_handled:
print(
- f"{PrintStyle.RED}(callback) {PrintStyle.BLUE}{syscall_name}{PrintStyle.DEFAULT_COLOR}({', '.join(entries)}) = ",
+ f"{header}{ANSIColors.RED}(callback) {ANSIColors.BLUE}{syscall_name}{ANSIColors.DEFAULT_COLOR}({', '.join(entries)}) = ",
+ end="",
+ )
+ elif hijacker:
+ print(
+ f"{header}{ANSIColors.RED}(executed) {ANSIColors.BLUE}{syscall_name}{ANSIColors.DEFAULT_COLOR}({', '.join(entries)}) = ",
end="",
)
else:
print(
- f"{PrintStyle.BLUE}{syscall_name}{PrintStyle.DEFAULT_COLOR}({', '.join(entries)}) = ",
+ f"{header}{ANSIColors.BLUE}{syscall_name}{ANSIColors.DEFAULT_COLOR}({', '.join(entries)}) = ",
end="",
)
@@ -80,7 +89,7 @@ def pprint_on_exit(syscall_return: int | tuple[int, int]) -> None:
"""
if isinstance(syscall_return, tuple):
print(
- f"{PrintStyle.YELLOW}{PrintStyle.STRIKE}0x{syscall_return[0]:x}{PrintStyle.RESET} {PrintStyle.YELLOW}0x{syscall_return[1]:x}{PrintStyle.RESET}",
+ f"{ANSIColors.YELLOW}{ANSIColors.STRIKE}0x{syscall_return[0]:x}{ANSIColors.RESET} {ANSIColors.YELLOW}0x{syscall_return[1]:x}{ANSIColors.RESET}",
)
else:
- print(f"{PrintStyle.YELLOW}0x{syscall_return:x}{PrintStyle.RESET}")
+ print(f"{ANSIColors.YELLOW}0x{syscall_return:x}{ANSIColors.RESET}")
diff --git a/libdebug/cffi/ptrace_cffi_build.py b/libdebug/cffi/ptrace_cffi_build.py
index 101044ae..fbd634b2 100644
--- a/libdebug/cffi/ptrace_cffi_build.py
+++ b/libdebug/cffi/ptrace_cffi_build.py
@@ -12,7 +12,7 @@
architecture = platform.machine()
-if architecture == "x86_64":
+def parse_fp_regs_x86():
# We need to determine if we have AVX, AVX2, AVX512, etc.
path = Path("/proc/cpuinfo")
@@ -50,7 +50,7 @@
_Bool fresh; // true if the registers have already been fetched for this state
unsigned char bool_padding[6];
unsigned char padding0[32];
- struct reg_128 st[8];
+ struct reg_128 mmx[8];
struct reg_128 xmm0[16];
unsigned char padding1[96];
// end of the 512 byte legacy region
@@ -62,7 +62,8 @@
struct reg_256 zmm0[16];
// zmm1 starts at offset 1664
struct reg_512 zmm1[16];
- unsigned char padding4[8];
+ unsigned int pkru;
+ unsigned char padding7[60];
};
#pragma pack(pop)
"""
@@ -88,14 +89,20 @@
_Bool fresh; // true if the registers have already been fetched for this state
unsigned char bool_padding[6];
unsigned char padding0[32];
- struct reg_128 st[8];
+ struct reg_128 mmx[8];
struct reg_128 xmm0[16];
unsigned char padding1[96];
// end of the 512 byte legacy region
unsigned char padding2[64];
// ymm0 starts at offset 576
struct reg_128 ymm0[16];
- unsigned char padding3[64];
+ unsigned char padding3[128];
+ struct reg_128 bndregs[4];
+ struct reg_128 bndcfg;
+ unsigned char padding4[48];
+ unsigned char padding6[1600];
+ unsigned int pkru;
+ unsigned char padding7[60];
};
#pragma pack(pop)
"""
@@ -121,7 +128,7 @@
_Bool fresh; // true if the registers have already been fetched for this state
unsigned char bool_padding[6];
unsigned char padding0[32];
- struct reg_128 st[8];
+ struct reg_128 mmx[8];
struct reg_128 xmm0[16];
unsigned char padding1[96];
};
@@ -144,6 +151,11 @@
#define XSAVE 1
"""
+ return fp_regs_struct, fpregs_define
+
+if architecture == "x86_64":
+ fp_regs_struct, fpregs_define = parse_fp_regs_x86()
+
ptrace_regs_struct = """
struct ptrace_regs_struct
{
@@ -308,6 +320,68 @@
return 0; // Not a CALL
}
"""
+elif architecture == "i686":
+ fp_regs_struct, fpregs_define = parse_fp_regs_x86()
+
+ ptrace_regs_struct = """
+ struct ptrace_regs_struct
+ {
+ unsigned long ebx;
+ unsigned long ecx;
+ unsigned long edx;
+ unsigned long esi;
+ unsigned long edi;
+ unsigned long ebp;
+ unsigned long eax;
+ unsigned long ds;
+ unsigned long es;
+ unsigned long fs;
+ unsigned long gs;
+ unsigned long orig_eax;
+ unsigned long eip;
+ unsigned long cs;
+ unsigned long eflags;
+ unsigned long esp;
+ unsigned long ss;
+ };
+ """
+
+ arch_define = """
+ #define ARCH_I386
+ """
+
+ breakpoint_define = """
+ #define INSTRUCTION_POINTER(regs) (regs.eip)
+ #define INSTALL_BREAKPOINT(instruction) ((instruction & 0xFFFFFF00) | 0xCC)
+ #define BREAKPOINT_SIZE 1
+ #define IS_SW_BREAKPOINT(instruction) (instruction == 0xCC)
+ """
+
+ control_flow_define = """
+ // i686 Architecture specific
+ #define IS_RET_INSTRUCTION(instruction) (instruction == 0xC3 || instruction == 0xCB || instruction == 0xC2 || instruction == 0xCA)
+
+ int IS_CALL_INSTRUCTION(uint8_t* instr)
+ {
+ // Check for direct CALL (E8 xx xx xx xx)
+ if (instr[0] == (uint8_t)0xE8) {
+ return 1; // It's a CALL
+ }
+
+ // Check for indirect CALL using ModR/M (FF /2)
+ if (instr[0] == (uint8_t)0xFF) {
+ // Extract ModR/M byte
+ uint8_t modRM = (uint8_t)instr[1];
+ uint8_t reg = (modRM >> 3) & 7; // Middle three bits
+
+ if (reg == 2) {
+ return 1; // It's a CALL
+ }
+ }
+
+ return 0; // Not a CALL
+ }
+ """
else:
raise NotImplementedError(f"Architecture {platform.machine()} not available.")
@@ -318,21 +392,21 @@
ffibuilder.cdef("""
struct ptrace_hit_bp {
int pid;
- uint64_t addr;
- uint64_t bp_instruction;
- uint64_t prev_instruction;
+ unsigned long addr;
+ unsigned long bp_instruction;
+ unsigned long prev_instruction;
};
struct software_breakpoint {
- uint64_t addr;
- uint64_t instruction;
- uint64_t patched_instruction;
+ unsigned long addr;
+ unsigned long instruction;
+ unsigned long patched_instruction;
char enabled;
struct software_breakpoint *next;
};
struct hardware_breakpoint {
- uint64_t addr;
+ unsigned long addr;
int tid;
char enabled;
char type[2];
@@ -371,21 +445,21 @@
void ptrace_reattach_from_gdb(struct global_state *state, int pid);
void ptrace_set_options(int pid);
- uint64_t ptrace_peekdata(int pid, uint64_t addr);
- uint64_t ptrace_pokedata(int pid, uint64_t addr, uint64_t data);
+ unsigned long ptrace_peekdata(int pid, unsigned long addr);
+ unsigned long ptrace_pokedata(int pid, unsigned long addr, unsigned long data);
struct fp_regs_struct *get_thread_fp_regs(struct global_state *state, int tid);
void get_fp_regs(int tid, struct fp_regs_struct *fpregs);
void set_fp_regs(int tid, struct fp_regs_struct *fpregs);
- uint64_t ptrace_geteventmsg(int pid);
+ unsigned long ptrace_geteventmsg(int pid);
long singlestep(struct global_state *state, int tid);
- int step_until(struct global_state *state, int tid, uint64_t addr, int max_steps);
+ int step_until(struct global_state *state, int tid, unsigned long addr, int max_steps);
int cont_all_and_set_bps(struct global_state *state, int pid);
- int stepping_finish(struct global_state *state, int tid);
+ int stepping_finish(struct global_state *state, int tid, _Bool use_trampoline_heuristic);
struct thread_status *wait_all_and_update_regs(struct global_state *state, int pid);
void free_thread_status_list(struct thread_status *head);
@@ -394,15 +468,15 @@
void unregister_thread(struct global_state *state, int tid);
void free_thread_list(struct global_state *state);
- void register_breakpoint(struct global_state *state, int pid, uint64_t address);
- void unregister_breakpoint(struct global_state *state, uint64_t address);
- void enable_breakpoint(struct global_state *state, uint64_t address);
- void disable_breakpoint(struct global_state *state, uint64_t address);
+ void register_breakpoint(struct global_state *state, int pid, unsigned long address);
+ void unregister_breakpoint(struct global_state *state, unsigned long address);
+ void enable_breakpoint(struct global_state *state, unsigned long address);
+ void disable_breakpoint(struct global_state *state, unsigned long address);
- void register_hw_breakpoint(struct global_state *state, int tid, uint64_t address, char type[2], char len);
- void unregister_hw_breakpoint(struct global_state *state, int tid, uint64_t address);
- void enable_hw_breakpoint(struct global_state *state, int tid, uint64_t address);
- void disable_hw_breakpoint(struct global_state *state, int tid, uint64_t address);
+ void register_hw_breakpoint(struct global_state *state, int tid, unsigned long address, char type[2], char len);
+ void unregister_hw_breakpoint(struct global_state *state, int tid, unsigned long address);
+ void enable_hw_breakpoint(struct global_state *state, int tid, unsigned long address);
+ void disable_hw_breakpoint(struct global_state *state, int tid, unsigned long address);
unsigned long get_hit_hw_breakpoint(struct global_state *state, int tid);
int get_remaining_hw_breakpoint_count(struct global_state *state, int tid);
int get_remaining_hw_watchpoint_count(struct global_state *state, int tid);
diff --git a/libdebug/cffi/ptrace_cffi_source.c b/libdebug/cffi/ptrace_cffi_source.c
index bfd42780..22bf979b 100644
--- a/libdebug/cffi/ptrace_cffi_source.c
+++ b/libdebug/cffi/ptrace_cffi_source.c
@@ -17,7 +17,7 @@
#include
// Run some static assertions to ensure that the fp types are correct
-#ifdef ARCH_AMD64
+#if defined ARCH_AMD64 || defined ARCH_I386
#ifndef FPREGS_AVX
#error "FPREGS_AVX must be defined"
#endif
@@ -29,9 +29,9 @@
#if (FPREGS_AVX == 0)
_Static_assert((sizeof(struct fp_regs_struct) - offsetof(struct fp_regs_struct, padding0)) == 512, "user_fpregs_struct size is not 512 bytes");
#elif (FPREGS_AVX == 1)
- _Static_assert((sizeof(struct fp_regs_struct) - offsetof(struct fp_regs_struct, padding0)) == 896, "user_fpregs_struct size is not 896 bytes");
+ _Static_assert((sizeof(struct fp_regs_struct) - offsetof(struct fp_regs_struct, padding0)) == 2752, "user_fpregs_struct size is not 2752 bytes");
#elif (FPREGS_AVX == 2)
- _Static_assert((sizeof(struct fp_regs_struct) - offsetof(struct fp_regs_struct, padding0)) == 2696, "user_fpregs_struct size is not 2696 bytes");
+ _Static_assert((sizeof(struct fp_regs_struct) - offsetof(struct fp_regs_struct, padding0)) == 2752, "user_fpregs_struct size is not 2752 bytes");
#else
#error "FPREGS_AVX must be 0, 1 or 2"
#endif
@@ -39,21 +39,21 @@
struct ptrace_hit_bp {
int pid;
- uint64_t addr;
- uint64_t bp_instruction;
- uint64_t prev_instruction;
+ unsigned long addr;
+ unsigned long bp_instruction;
+ unsigned long prev_instruction;
};
struct software_breakpoint {
- uint64_t addr;
- uint64_t instruction;
- uint64_t patched_instruction;
+ unsigned long addr;
+ unsigned long instruction;
+ unsigned long patched_instruction;
char enabled;
struct software_breakpoint *next;
};
struct hardware_breakpoint {
- uint64_t addr;
+ unsigned long addr;
int tid;
char enabled;
char type[2];
@@ -83,7 +83,7 @@ struct global_state {
_Bool handle_syscall_enabled;
};
-#ifdef ARCH_AMD64
+#if defined ARCH_AMD64 || defined ARCH_I386
int getregs(int tid, struct ptrace_regs_struct *regs)
{
return ptrace(PTRACE_GETREGS, tid, NULL, regs);
@@ -123,10 +123,10 @@ int setregs(int tid, struct ptrace_regs_struct *regs)
}
#endif
-#ifdef ARCH_AMD64
+#if defined ARCH_AMD64 || defined ARCH_I386
-#define DR_BASE 0x350
-#define DR_SIZE 0x8
+#define DR_BASE offsetof(struct user, u_debugreg[0])
+#define DR_SIZE sizeof(unsigned long)
#define CTRL_LOCAL(x) (1 << (2 * x))
#define CTRL_COND(x) (16 + (4 * x))
#define CTRL_COND_VAL(x) (x == 'x' ? 0 : (x == 'w' ? 1 : 3))
@@ -404,7 +404,7 @@ struct fp_regs_struct *get_thread_fp_regs(struct global_state *state, int tid)
return NULL;
}
-#ifdef ARCH_AMD64
+#if defined ARCH_AMD64 || defined ARCH_I386
void get_fp_regs(int tid, struct fp_regs_struct *fpregs)
{
#if (XSAVE == 0)
@@ -496,7 +496,7 @@ struct ptrace_regs_struct *register_thread(struct global_state *state, int tid)
t->tid = tid;
t->signal_to_forward = 0;
-#ifdef ARCH_AMD64
+#if defined ARCH_AMD64 || defined ARCH_I386
t->fpregs.type = FPREGS_AVX;
#endif
t->fpregs.dirty = 0;
@@ -525,11 +525,28 @@ void unregister_thread(struct global_state *state, int tid)
// Add the thread to the dead list
t->next = state->dead_t_HEAD;
state->dead_t_HEAD = t;
- return;
+ break;
}
prev = t;
t = t->next;
}
+
+ struct hardware_breakpoint *hw_b = state->hw_b_HEAD;
+ struct hardware_breakpoint *hw_b_prev = NULL;
+
+ while (hw_b != NULL) {
+ if (hw_b->tid == tid) {
+ if (hw_b_prev == NULL) {
+ state->hw_b_HEAD = hw_b->next;
+ } else {
+ hw_b_prev->next = hw_b->next;
+ }
+ free(hw_b);
+ return;
+ }
+ hw_b_prev = hw_b;
+ hw_b = hw_b->next;
+ }
}
void free_thread_list(struct global_state *state)
@@ -660,7 +677,7 @@ void ptrace_set_options(int pid)
ptrace(PTRACE_SETOPTIONS, pid, NULL, options);
}
-uint64_t ptrace_peekdata(int pid, uint64_t addr)
+unsigned long ptrace_peekdata(int pid, unsigned long addr)
{
// Since the value returned by a successful PTRACE_PEEK*
// request may be -1, the caller must clear errno before the call,
@@ -669,14 +686,14 @@ uint64_t ptrace_peekdata(int pid, uint64_t addr)
return ptrace(PTRACE_PEEKDATA, pid, (void *)addr, NULL);
}
-uint64_t ptrace_pokedata(int pid, uint64_t addr, uint64_t data)
+unsigned long ptrace_pokedata(int pid, unsigned long addr, unsigned long data)
{
return ptrace(PTRACE_POKEDATA, pid, (void *)addr, data);
}
-uint64_t ptrace_geteventmsg(int pid)
+unsigned long ptrace_geteventmsg(int pid)
{
- uint64_t data = 0;
+ unsigned long data = 0;
ptrace(PTRACE_GETEVENTMSG, pid, NULL, &data);
@@ -701,7 +718,7 @@ long singlestep(struct global_state *state, int tid)
t = t->next;
}
-#ifdef ARCH_AMD64
+#if defined ARCH_AMD64 || defined ARCH_I386
return ptrace(PTRACE_SINGLESTEP, tid, NULL, signal_to_forward);
#endif
@@ -724,7 +741,7 @@ long singlestep(struct global_state *state, int tid)
#endif
}
-int step_until(struct global_state *state, int tid, uint64_t addr, int max_steps)
+int step_until(struct global_state *state, int tid, unsigned long addr, int max_steps)
{
// flush any register changes
struct thread *t = state->t_HEAD, *stepping_thread = NULL;
@@ -741,7 +758,7 @@ int step_until(struct global_state *state, int tid, uint64_t addr, int max_steps
}
int count = 0, status = 0;
- uint64_t previous_ip;
+ unsigned long previous_ip;
if (!stepping_thread) {
perror("Thread not found");
@@ -815,7 +832,7 @@ int prepare_for_run(struct global_state *state, int pid)
while (t != NULL) {
t_hit = 0;
- uint64_t ip = INSTRUCTION_POINTER(t->regs);
+ unsigned long ip = INSTRUCTION_POINTER(t->regs);
b = state->sw_b_HEAD;
while (b != NULL && !t_hit) {
@@ -995,9 +1012,9 @@ void free_thread_status_list(struct thread_status *head)
}
}
-void register_breakpoint(struct global_state *state, int pid, uint64_t address)
+void register_breakpoint(struct global_state *state, int pid, unsigned long address)
{
- uint64_t instruction, patched_instruction;
+ unsigned long instruction, patched_instruction;
instruction = ptrace(PTRACE_PEEKDATA, pid, (void *)address, NULL);
@@ -1041,7 +1058,7 @@ void register_breakpoint(struct global_state *state, int pid, uint64_t address)
}
}
-void unregister_breakpoint(struct global_state *state, uint64_t address)
+void unregister_breakpoint(struct global_state *state, unsigned long address)
{
struct software_breakpoint *b = state->sw_b_HEAD;
struct software_breakpoint *prev = NULL;
@@ -1061,7 +1078,7 @@ void unregister_breakpoint(struct global_state *state, uint64_t address)
}
}
-void enable_breakpoint(struct global_state *state, uint64_t address)
+void enable_breakpoint(struct global_state *state, unsigned long address)
{
struct software_breakpoint *b = state->sw_b_HEAD;
@@ -1079,7 +1096,7 @@ void enable_breakpoint(struct global_state *state, uint64_t address)
}
}
-void disable_breakpoint(struct global_state *state, uint64_t address)
+void disable_breakpoint(struct global_state *state, unsigned long address)
{
struct software_breakpoint *b = state->sw_b_HEAD;
@@ -1122,7 +1139,83 @@ void free_breakpoints(struct global_state *state)
state->hw_b_HEAD = NULL;
}
-int stepping_finish(struct global_state *state, int tid)
+#if defined ARCH_AMD64 || defined ARCH_I386
+int check_if_dl_trampoline(struct global_state *state, unsigned long instruction_pointer)
+{
+ // https://codebrowser.dev/glibc/glibc/sysdeps/i386/dl-trampoline.S.html
+ // 0xf7fdaf80 <_dl_runtime_resolve+16>: pop edx
+ // 0xf7fdaf81 <_dl_runtime_resolve+17>: mov ecx,DWORD PTR [esp]
+ // 0xf7fdaf84 <_dl_runtime_resolve+20>: mov DWORD PTR [esp],eax
+ // 0xf7fdaf87 <_dl_runtime_resolve+23>: mov eax,DWORD PTR [esp+0x4]
+ // => 0xf7fdaf8b <_dl_runtime_resolve+27>: ret 0xc
+ // 0xf7fdaf8e: xchg ax,ax
+ // 0xf7fdaf90 <_dl_runtime_profile>: push esp
+ // 0xf7fdaf91 <_dl_runtime_profile+1>: add DWORD PTR [esp],0x8
+ // 0xf7fdaf95 <_dl_runtime_profile+5>: push ebp
+ // 0xf7fdaf96 <_dl_runtime_profile+6>: push eax
+ // 0xf7fdaf97 <_dl_runtime_profile+7>: push ecx
+ // 0xf7fdaf98 <_dl_runtime_profile+8>: push edx
+
+ // https://elixir.bootlin.com/glibc/glibc-2.35/source/sysdeps/i386/dl-trampoline.S
+ // 0xf7fd9004 <_dl_runtime_resolve+20>: pop edx
+ // 0xf7fd9005 <_dl_runtime_resolve+21>: mov ecx,DWORD PTR [esp]
+ // 0xf7fd9008 <_dl_runtime_resolve+24>: mov DWORD PTR [esp],eax
+ // 0xf7fd900b <_dl_runtime_resolve+27>: mov eax,DWORD PTR [esp+0x4]
+ // => 0xf7fd900f <_dl_runtime_resolve+31>: ret 0xc
+ // 0xf7fd9012: lea esi,[esi+eiz*1+0x0]
+ // 0xf7fd9019: lea esi,[esi+eiz*1+0x0]
+ // 0xf7fd9020 <_dl_runtime_resolve_shstk>: endbr32
+ // 0xf7fd9024 <_dl_runtime_resolve_shstk+4>: push eax
+ // 0xf7fd9025 <_dl_runtime_resolve_shstk+5>: push edx
+
+ unsigned long data;
+
+ // if ((instruction_pointer & 0xf) != 0xb) {
+ // return 0;
+ // }
+ // breaks if libc is compiled with CET
+
+ instruction_pointer -= 0xb;
+
+ data = ptrace(PTRACE_PEEKDATA, state->t_HEAD->tid, (void *)instruction_pointer, NULL);
+ data = data & 0xFFFFFFFF; // on i386 we get 4 bytes from the ptrace call, while on amd64 we get 8 bytes
+
+ if (data != 0x240c8b5a) {
+ return 0;
+ }
+
+ instruction_pointer += 0x4;
+
+ data = ptrace(PTRACE_PEEKDATA, state->t_HEAD->tid, (void *)instruction_pointer, NULL);
+ data = data & 0xFFFFFFFF;
+
+ if (data != 0x8b240489) {
+ return 0;
+ }
+
+ instruction_pointer += 0x4;
+
+ data = ptrace(PTRACE_PEEKDATA, state->t_HEAD->tid, (void *)instruction_pointer, NULL);
+ data = data & 0xFFFFFFFF;
+
+ if (data != 0xc2042444) {
+ return 0;
+ }
+
+ instruction_pointer += 0x4;
+
+ data = ptrace(PTRACE_PEEKDATA, state->t_HEAD->tid, (void *)instruction_pointer, NULL);
+ data = data & 0xFFFF;
+
+ if (data != 0x000c) {
+ return 0;
+ }
+
+ return 1;
+}
+#endif
+
+int stepping_finish(struct global_state *state, int tid, _Bool use_trampoline_heuristic)
{
int status = prepare_for_run(state, tid);
@@ -1140,8 +1233,9 @@ int stepping_finish(struct global_state *state, int tid)
return -1;
}
- uint64_t previous_ip, current_ip;
- uint64_t opcode_window, opcode;
+ unsigned long previous_ip, current_ip;
+ unsigned long opcode_window, opcode;
+ struct software_breakpoint *b = state->sw_b_HEAD;
// We need to keep track of the nested calls
int nested_call_counter = 1;
@@ -1162,7 +1256,7 @@ int stepping_finish(struct global_state *state, int tid)
// Get value at current instruction pointer
opcode_window = ptrace(PTRACE_PEEKDATA, tid, (void *)current_ip, NULL);
-#ifdef ARCH_AMD64
+#if defined ARCH_AMD64 || defined ARCH_I386
// on amd64 we care only about the first byte
opcode = opcode_window & 0xFF;
#endif
@@ -1183,6 +1277,18 @@ int stepping_finish(struct global_state *state, int tid)
else if (IS_RET_INSTRUCTION(opcode))
nested_call_counter--;
+#if defined ARCH_AMD64 || defined ARCH_I386
+ // On i386, dl-trampoline.S ends with a ret instruction to the resolved address
+ // While, on amd64, it ends with a jmp to the resolved address
+ // On i386, this decrements the nested_call_counter even if it shouldn't
+ // So we need to heuristically check if we are in a dl-trampoline.S
+ // And if so, we need to re-increment the nested_call_counter
+ // https://codebrowser.dev/glibc/glibc/sysdeps/i386/dl-trampoline.S.html
+ if (use_trampoline_heuristic && check_if_dl_trampoline(state, current_ip)) {
+ nested_call_counter++;
+ }
+#endif
+
} while (nested_call_counter > 0);
// We are in a return instruction, do the last step
@@ -1196,7 +1302,6 @@ int stepping_finish(struct global_state *state, int tid)
cleanup:
// remove any installed breakpoint
- struct software_breakpoint *b = state->sw_b_HEAD;
while (b != NULL) {
if (b->enabled) {
ptrace(PTRACE_POKEDATA, tid, (void *)b->addr, b->instruction);
@@ -1207,7 +1312,7 @@ int stepping_finish(struct global_state *state, int tid)
return 0;
}
-void register_hw_breakpoint(struct global_state *state, int tid, uint64_t address, char type[2], char len)
+void register_hw_breakpoint(struct global_state *state, int tid, unsigned long address, char type[2], char len)
{
struct hardware_breakpoint *b = state->hw_b_HEAD;
@@ -1233,7 +1338,7 @@ void register_hw_breakpoint(struct global_state *state, int tid, uint64_t addres
install_hardware_breakpoint(b);
}
-void unregister_hw_breakpoint(struct global_state *state, int tid, uint64_t address)
+void unregister_hw_breakpoint(struct global_state *state, int tid, unsigned long address)
{
struct hardware_breakpoint *b = state->hw_b_HEAD;
struct hardware_breakpoint *prev = NULL;
@@ -1258,7 +1363,7 @@ void unregister_hw_breakpoint(struct global_state *state, int tid, uint64_t addr
}
}
-void enable_hw_breakpoint(struct global_state *state, int tid, uint64_t address)
+void enable_hw_breakpoint(struct global_state *state, int tid, unsigned long address)
{
struct hardware_breakpoint *b = state->hw_b_HEAD;
@@ -1274,7 +1379,7 @@ void enable_hw_breakpoint(struct global_state *state, int tid, uint64_t address)
}
}
-void disable_hw_breakpoint(struct global_state *state, int tid, uint64_t address)
+void disable_hw_breakpoint(struct global_state *state, int tid, unsigned long address)
{
struct hardware_breakpoint *b = state->hw_b_HEAD;
diff --git a/libdebug/commlink/__init__.py b/libdebug/commlink/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/libdebug/commlink/buffer_data.py b/libdebug/commlink/buffer_data.py
new file mode 100644
index 00000000..5c3a0b89
--- /dev/null
+++ b/libdebug/commlink/buffer_data.py
@@ -0,0 +1,47 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from __future__ import annotations
+
+
+class BufferData:
+ """Class that represents a buffer to store data coming from stdout and stderr."""
+
+ def __init__(self: BufferData, data: bytes) -> None:
+ """Initializes the BufferData object."""
+ self.data = data
+
+ def clear(self: BufferData) -> None:
+ """Clears the buffer."""
+ self.data = b""
+
+ def get_data(self: BufferData) -> bytes:
+ """Returns the data stored in the buffer."""
+ return self.data
+
+ def append(self, data: bytes) -> None:
+ """Appends data to the buffer."""
+ self.data += data
+
+ def overwrite(self, data: bytes) -> None:
+ """Overwrites the buffer with the given data."""
+ self.data = data
+
+ def find(self: BufferData, pattern: bytes) -> int:
+ """Finds the first occurrence of the given pattern in the buffer."""
+ return self.data.find(pattern)
+
+ def __len__(self: BufferData) -> int:
+ """Returns the length of the buffer."""
+ return len(self.data)
+
+ def __repr__(self: BufferData) -> str:
+ """Returns a string representation of the buffer."""
+ return self.data.__repr__()
+
+ def __getitem__(self: BufferData, key: int) -> bytes:
+ """Returns the item at the given index."""
+ return self.data[key]
diff --git a/libdebug/commlink/libterminal.py b/libdebug/commlink/libterminal.py
new file mode 100644
index 00000000..1275d14f
--- /dev/null
+++ b/libdebug/commlink/libterminal.py
@@ -0,0 +1,238 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from __future__ import annotations
+
+import sys
+import threading
+from logging import StreamHandler
+from pathlib import Path
+from queue import Queue
+from termios import TCSANOW, tcgetattr, tcsetattr
+from threading import Event
+from typing import TYPE_CHECKING
+
+from prompt_toolkit.application import Application, run_in_terminal
+from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
+from prompt_toolkit.history import FileHistory
+from prompt_toolkit.key_binding import KeyBindings
+from prompt_toolkit.layout import Layout
+from prompt_toolkit.widgets import TextArea
+
+from libdebug.commlink.std_wrapper import StdWrapper
+from libdebug.debugger.internal_debugger_instance_manager import provide_internal_debugger
+from libdebug.liblog import liblog
+
+if TYPE_CHECKING:
+ from prompt_toolkit.application.application import KeyPressEvent
+
+PATH_HISTORY = Path.home() / ".cache" / "libdebug" / "history"
+
+
+class LibTerminal:
+ """Class that represents a terminal to interact with the child process."""
+
+ def __init__(
+ self: LibTerminal,
+ prompt: str,
+ sendline: callable,
+ end_interactive_event: Event,
+ auto_quit: bool,
+ ) -> None:
+ """Initializes the LibTerminal object."""
+ # Provide the internal debugger instance
+ self._internal_debugger = provide_internal_debugger(self)
+
+ # Function to send a line to the child process
+ self._sendline: callable = sendline
+
+ # Event to signal the end of the interactive session
+ self.__end_interactive_event: Event = end_interactive_event
+
+ # Flag to indicate if the terminal should automatically quit when the debugged process stops
+ self._auto_quit: bool = auto_quit
+
+ # Initialize the message queue for the prompt_toolkit application
+ self._app_message_queue: Queue = Queue()
+
+ # Initialize the thread reference for the prompt_toolkit application
+ self._app_thread: threading.Thread | None = None
+
+ # Flag to indicate if the terminal has warned the user about the stop of the debugged process
+ self._has_warned_stop: bool = False
+
+ # Backup the original stdout and stderr
+ self._stdout_backup: object = sys.stdout
+ self._stderr_backup: object = sys.stderr
+
+ # Redirect stdout and stderr to the terminal
+ sys.stdout = StdWrapper(self._stdout_backup, self)
+ sys.stderr = StdWrapper(self._stderr_backup, self)
+
+ # Redirect the loggers to the terminal
+ for handler in liblog.general_logger.handlers:
+ if isinstance(handler, StreamHandler):
+ handler.stream = sys.stderr
+
+ for handler in liblog.pipe_logger.handlers:
+ if isinstance(handler, StreamHandler):
+ handler.stream = sys.stderr
+
+ for handler in liblog.debugger_logger.handlers:
+ if isinstance(handler, StreamHandler):
+ handler.stream = sys.stderr
+
+ # Save the original stdin settings, if needed. Just in case
+ if not self._internal_debugger.stdin_settings_backup:
+ self._internal_debugger.stdin_settings_backup = tcgetattr(sys.stdin.fileno())
+
+ # Create the history file, if it does not exist
+ if not PATH_HISTORY.exists():
+ PATH_HISTORY.parent.mkdir(parents=True, exist_ok=True)
+ PATH_HISTORY.touch()
+
+ self._run_prompt(prompt)
+
+ def _run_prompt(self: LibTerminal, prompt: str) -> None:
+ """Run the prompt_toolkit application."""
+ input_field = TextArea(
+ height=3,
+ prompt=prompt,
+ style="class:input-field",
+ history=FileHistory(str(PATH_HISTORY)),
+ auto_suggest=AutoSuggestFromHistory(),
+ )
+
+ kb = KeyBindings()
+
+ @kb.add("enter")
+ def on_enter(event: KeyPressEvent) -> None:
+ """Send the user input to the child process."""
+ buffer = event.app.current_buffer
+ cmd = buffer.text
+ if cmd:
+ try:
+ self._sendline(cmd.encode("utf-8"))
+ buffer.history.append_string(cmd)
+ except RuntimeError:
+ liblog.warning("The stdin pipe of the child process is not available anymore")
+ finally:
+ buffer.reset()
+
+ @kb.add("c-c")
+ @kb.add("c-d")
+ def app_exit(event: KeyPressEvent) -> None:
+ """Manage the key bindings for the exit of the application."""
+ # Flush the output field
+ update_output(event.app)
+ # Signal the end of the interactive session
+ self.__end_interactive_event.set()
+ while self.__end_interactive_event.is_set():
+ # Wait to be sure that the other thread is not polling from the child process's
+ # stderr and stdout pipes anymore
+ pass
+ event.app.exit()
+
+ @kb.add("tab")
+ def accept_suggestion(event: KeyPressEvent) -> None:
+ """Accept the auto-suggestion."""
+ buffer = event.current_buffer
+ suggestion = buffer.suggestion
+ if suggestion:
+ buffer.insert_text(suggestion.text)
+
+ layout = Layout(input_field)
+
+ # Note: The refresh_interval is set to 0.5 seconds is an arbitrary trade-off between the
+ # responsiveness of the terminal and the CPU usage. Little values also cause difficulties
+ # in the management of the copy-paste. We might consider to change the value in the future or
+ # to make it dynamic/configurable.
+ app = Application(
+ layout=layout,
+ key_bindings=kb,
+ full_screen=False,
+ refresh_interval=0.5,
+ )
+
+ def update_output(app: Application) -> None:
+ """Update the output field with the messages in the queue."""
+ if (
+ not self._internal_debugger.running
+ and (event_type := self._internal_debugger.resume_context.get_event_type())
+ and not self._has_warned_stop
+ ):
+ liblog.warning(
+ f"The debugged process has stopped due to the following event(s). {event_type}",
+ )
+ self._has_warned_stop = True
+ if self._auto_quit:
+ # Flush the output field and exit the application
+ self.__end_interactive_event.set()
+
+ while self.__end_interactive_event.is_set():
+ # Wait to be sure that the other thread is not polling from the child process
+ # stderr and stdout pipes anymore
+ pass
+
+ # Update the output field with the messages in the queue
+ msg = b""
+ if not self._app_message_queue.empty():
+ msg += self._app_message_queue.get()
+
+ if msg:
+ if not msg.endswith(b"\n"):
+ # Add a newline character at the end of the message
+ # to avoid the prompt_toolkit bug that causes the last line to be
+ # overwritten by the prompt
+ msg += b"\n"
+ run_in_terminal(lambda: sys.stdout.buffer.write(msg))
+ run_in_terminal(lambda: sys.stdout.buffer.flush())
+
+ if self._has_warned_stop and self._auto_quit:
+ app.exit()
+
+ # Add the update_output function to the event loop
+ app.on_invalidate.add_handler(update_output)
+
+ # Run in another thread
+ self._app_thread = threading.Thread(target=app.run, daemon=True)
+ self._app_thread.start()
+
+ def _write_manager(self, payload: bytes) -> int:
+ """Put the payload in the message queue for the prompt_toolkit application."""
+ if isinstance(payload, bytes):
+ # We want the special characters to be displayed correctly
+ self._app_message_queue.put(payload.decode("utf-8", errors="backslashreplace").encode("utf-8"))
+ else:
+ # We need to encode the payload to bytes
+ self._app_message_queue.put(payload.encode("utf-8"))
+
+ def reset(self: LibTerminal) -> None:
+ """Reset the terminal to its original state."""
+ # Wait for the prompt_toolkit application to finish
+ # This (included the timeout) is necessary to avoid race conditions and deadlocks
+ while self._app_thread.join(0.1):
+ pass
+
+ # Restore the original stdout and stderr
+ sys.stdout = self._stdout_backup
+ sys.stderr = self._stderr_backup
+
+ # Restore the loggers
+ for handler in liblog.general_logger.handlers:
+ if isinstance(handler, StreamHandler):
+ handler.stream = sys.stderr
+
+ for handler in liblog.pipe_logger.handlers:
+ if isinstance(handler, StreamHandler):
+ handler.stream = sys.stderr
+
+ for handler in liblog.debugger_logger.handlers:
+ if isinstance(handler, StreamHandler):
+ handler.stream = sys.stderr
+
+ # Restore the original stdin settings
+ tcsetattr(sys.stdin.fileno(), TCSANOW, self._internal_debugger.stdin_settings_backup)
diff --git a/libdebug/commlink/pipe_manager.py b/libdebug/commlink/pipe_manager.py
new file mode 100644
index 00000000..a0149cbf
--- /dev/null
+++ b/libdebug/commlink/pipe_manager.py
@@ -0,0 +1,645 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from __future__ import annotations
+
+import os
+import select
+import sys
+import time
+from errno import EAGAIN
+from threading import Event
+from typing import TYPE_CHECKING
+
+from libdebug.commlink.buffer_data import BufferData
+from libdebug.commlink.libterminal import LibTerminal
+from libdebug.debugger.internal_debugger_instance_manager import extend_internal_debugger, provide_internal_debugger
+from libdebug.liblog import liblog
+
+if TYPE_CHECKING:
+ from libdebug.debugger.internal_debugger import InternalDebugger
+
+
+class PipeManager:
+ """Class for managing pipes of the child process."""
+
+ timeout_default: int = 2
+ prompt_default: str = "$ "
+
+ def __init__(self: PipeManager, stdin_write: int, stdout_read: int, stderr_read: int) -> None:
+ """Initializes the PipeManager class.
+
+ Args:
+ stdin_write (int): file descriptor for stdin write.
+ stdout_read (int): file descriptor for stdout read.
+ stderr_read (int): file descriptor for stderr read.
+ """
+ self._stdin_write: int = stdin_write
+ self._stdout_read: int = stdout_read
+ self._stderr_read: int = stderr_read
+ self._stderr_is_open: bool = True
+ self._stdout_is_open: bool = True
+ self._internal_debugger: InternalDebugger = provide_internal_debugger(self)
+
+ self.__stdout_buffer: BufferData = BufferData(b"")
+ self.__stderr_buffer: BufferData = BufferData(b"")
+
+ self.__end_interactive_event: Event = Event()
+
+ def _raw_recv(
+ self: PipeManager,
+ numb: int | None = None,
+ timeout: float | None = None,
+ stderr: bool = False,
+ ) -> int:
+ """Receives at most numb bytes from the child process.
+
+ Args:
+ numb (int | None, optional): number of bytes to receive. Defaults to None.
+ timeout (float, optional): timeout in seconds. Defaults to None.
+ stderr (bool, optional): receive from stderr. Defaults to False.
+
+ Returns:
+ int: number of bytes received.
+ """
+ pipe_read: int = self._stderr_read if stderr else self._stdout_read
+
+ if not pipe_read:
+ raise RuntimeError("No pipe of the child process")
+
+ data_buffer = self.__stderr_buffer if stderr else self.__stdout_buffer
+
+ received_numb = 0
+
+ if numb is not None and timeout is not None:
+ # Checking the numb
+ if numb < 0:
+ raise ValueError("The number of bytes to receive must be positive")
+
+ # Setting the alarm
+ end_time = time.time() + timeout
+
+ while numb > received_numb:
+ if (remaining_time := max(0, end_time - time.time())) == 0:
+ # Timeout reached
+ break
+
+ try:
+ ready, _, _ = select.select([pipe_read], [], [], remaining_time)
+ if ready:
+ data = os.read(pipe_read, 4096)
+ received_numb += len(data)
+ data_buffer.append(data)
+ else:
+ # No more data available in the pipe at the moment
+ break
+ except OSError as e:
+ if e.errno != EAGAIN:
+ if stderr:
+ self._stderr_is_open = False
+ else:
+ self._stdout_is_open = False
+ elif timeout is not None:
+ try:
+ ready, _, _ = select.select([pipe_read], [], [], timeout)
+ if ready:
+ data = os.read(pipe_read, 4096)
+ received_numb += len(data)
+ data_buffer.append(data)
+ except OSError as e:
+ if e.errno != EAGAIN:
+ if stderr:
+ self._stderr_is_open = False
+ else:
+ self._stdout_is_open = False
+ else:
+ try:
+ data = os.read(pipe_read, 4096)
+ if data:
+ received_numb += len(data)
+ data_buffer.append(data)
+ except OSError as e:
+ if e.errno != EAGAIN:
+ if stderr:
+ self._stderr_is_open = False
+ else:
+ self._stdout_is_open = False
+
+ if received_numb:
+ liblog.pipe(f"{'stderr' if stderr else 'stdout'} {received_numb}B: {data_buffer[:received_numb]!r}")
+ return received_numb
+
+ def close(self: PipeManager) -> None:
+ """Closes all the pipes of the child process."""
+ os.close(self._stdin_write)
+ os.close(self._stdout_read)
+ os.close(self._stderr_read)
+
+ def _buffered_recv(self: PipeManager, numb: int, timeout: int, stderr: bool) -> bytes:
+ """Receives at most numb bytes from the child process stdout or stderr.
+
+ Args:
+ numb (int): number of bytes to receive.
+ timeout (int): timeout in seconds.
+ stderr (bool): receive from stderr.
+
+ Returns:
+ bytes: received bytes from the child process stdout or stderr.
+ """
+ data_buffer = self.__stderr_buffer if stderr else self.__stdout_buffer
+ open_flag = self._stderr_is_open if stderr else self._stdout_is_open
+
+ data_buffer_len = len(data_buffer)
+
+ if data_buffer_len >= numb:
+ # We have enough data in the buffer
+ received = data_buffer[:numb]
+ data_buffer.overwrite(data_buffer[numb:])
+ elif open_flag:
+ # We can receive more data
+ remaining = numb - data_buffer_len
+ self._raw_recv(numb=remaining, timeout=timeout, stderr=stderr)
+ received = data_buffer[:numb]
+ data_buffer.overwrite(data_buffer[numb:])
+ elif data_buffer_len != 0:
+ # The pipe is not available but we have some data in the buffer. We will return just that
+ received = data_buffer.get_data()
+ data_buffer.clear()
+ else:
+ # The pipe is not available and no data is buffered
+ raise RuntimeError(f"Broken {'stderr' if stderr else 'stdout'} pipe. Is the child process still alive?")
+ return received
+
+ def recv(
+ self: PipeManager,
+ numb: int = 4096,
+ timeout: int = timeout_default,
+ ) -> bytes:
+ """Receives at most numb bytes from the child process stdout.
+
+ Args:
+ numb (int, optional): number of bytes to receive. Defaults to 4096.
+ timeout (int, optional): timeout in seconds. Defaults to timeout_default.
+
+ Returns:
+ bytes: received bytes from the child process stdout.
+ """
+ return self._buffered_recv(numb=numb, timeout=timeout, stderr=False)
+
+ def recverr(
+ self: PipeManager,
+ numb: int = 4096,
+ timeout: int = timeout_default,
+ ) -> bytes:
+ """Receives at most numb bytes from the child process stderr.
+
+ Args:
+ numb (int, optional): number of bytes to receive. Defaults to 4096.
+ timeout (int, optional): timeout in seconds. Defaults to timeout_default.
+
+ Returns:
+ bytes: received bytes from the child process stderr.
+ """
+ return self._buffered_recv(numb=numb, timeout=timeout, stderr=True)
+
+ def _recvonceuntil(
+ self: PipeManager,
+ delims: bytes,
+ drop: bool = False,
+ timeout: float = timeout_default,
+ stderr: bool = False,
+ optional: bool = False,
+ ) -> bytes:
+ """Receives data from the child process until the delimiters are found.
+
+ Args:
+ delims (bytes): delimiters where to stop.
+ drop (bool, optional): drop the delimiter. Defaults to False.
+ timeout (float, optional): timeout in seconds. Defaults to timeout_default.
+ stderr (bool, optional): receive from stderr. Defaults to False.
+ optional (bool, optional): whether to ignore the wait for the received input if the command is executed when the process is stopped. Defaults to False.
+
+ Returns:
+ bytes: received data from the child process stdout.
+ """
+ if isinstance(delims, str):
+ liblog.warning("The delimiters are a string, converting to bytes")
+ delims = delims.encode()
+
+ # Buffer for the received data
+ data_buffer = self.__stderr_buffer if stderr else self.__stdout_buffer
+
+ # Setting the alarm
+ end_time = time.time() + timeout
+ while True:
+ open_flag = self._stderr_is_open if stderr else self._stdout_is_open
+
+ if (until := data_buffer.find(delims)) != -1:
+ break
+
+ if (remaining_time := max(0, end_time - time.time())) == 0:
+ raise TimeoutError("Timeout reached")
+
+ if not open_flag:
+ # The delimiters are not in the buffer and the pipe is not available
+ raise RuntimeError(f"Broken {'stderr' if stderr else 'stdout'} pipe. Is the child process still alive?")
+
+ received_numb = self._raw_recv(stderr=stderr, timeout=remaining_time)
+
+ if (
+ received_numb == 0
+ and not self._internal_debugger.running
+ and self._internal_debugger.is_debugging
+ and (event := self._internal_debugger.resume_context.get_event_type())
+ ):
+ # We will not receive more data, the child process is not running
+ if optional:
+ return b""
+ event = self._internal_debugger.resume_context.get_event_type()
+ raise RuntimeError(
+ f"Receive until error. The debugged process has stopped due to the following event(s). {event}",
+ )
+ received_data = data_buffer[:until]
+ if not drop:
+ # Include the delimiters in the received data
+ received_data += data_buffer[until : until + len(delims)]
+ remaining_data = data_buffer[until + len(delims) :]
+ data_buffer.overwrite(remaining_data)
+ return received_data
+
+ def _recvuntil(
+ self: PipeManager,
+ delims: bytes,
+ occurences: int = 1,
+ drop: bool = False,
+ timeout: float = timeout_default,
+ stderr: bool = False,
+ optional: bool = False,
+ ) -> bytes:
+ """Receives data from the child process until the delimiters are found occurences time.
+
+ Args:
+ delims (bytes): delimiters where to stop.
+ occurences (int, optional): number of delimiters to find. Defaults to 1.
+ drop (bool, optional): drop the delimiter. Defaults to False.
+ timeout (float, optional): timeout in seconds. Defaults to timeout_default.
+ stderr (bool, optional): receive from stderr. Defaults to False.
+ optional (bool, optional): whether to ignore the wait for the received input if the command is executed when the process is stopped. Defaults to False.
+
+ Returns:
+ bytes: received data from the child process stdout.
+ """
+ if occurences <= 0:
+ raise ValueError("The number of occurences to receive must be positive")
+
+ # Buffer for the received data
+ data_buffer = b""
+
+ # Setting the alarm
+ end_time = time.time() + timeout
+
+ for _ in range(occurences):
+ # Adjust the timeout for select to the remaining time
+ remaining_time = None if end_time is None else max(0, end_time - time.time())
+
+ data_buffer += self._recvonceuntil(
+ delims=delims,
+ drop=drop,
+ timeout=remaining_time,
+ stderr=stderr,
+ optional=optional,
+ )
+
+ return data_buffer
+
+ def recvuntil(
+ self: PipeManager,
+ delims: bytes,
+ occurences: int = 1,
+ drop: bool = False,
+ timeout: int = timeout_default,
+ optional: bool = False,
+ ) -> bytes:
+ """Receives data from the child process stdout until the delimiters are found.
+
+ Args:
+ delims (bytes): delimiters where to stop.
+ occurences (int, optional): number of delimiters to find. Defaults to 1.
+ drop (bool, optional): drop the delimiter. Defaults to False.
+ timeout (int, optional): timeout in seconds. Defaults to timeout_default.
+ optional (bool, optional): whether to ignore the wait for the received input if the command is executed when the process is stopped. Defaults to False.
+
+ Returns:
+ bytes: received data from the child process stdout.
+ """
+ return self._recvuntil(
+ delims=delims,
+ occurences=occurences,
+ drop=drop,
+ timeout=timeout,
+ stderr=False,
+ optional=optional,
+ )
+
+ def recverruntil(
+ self: PipeManager,
+ delims: bytes,
+ occurences: int = 1,
+ drop: bool = False,
+ timeout: int = timeout_default,
+ optional: bool = False,
+ ) -> bytes:
+ """Receives data from the child process stderr until the delimiters are found.
+
+ Args:
+ delims (bytes): delimiters where to stop.
+ occurences (int, optional): number of delimiters to find. Defaults to 1.
+ drop (bool, optional): drop the delimiter. Defaults to False.
+ timeout (int, optional): timeout in seconds. Defaults to timeout_default.
+ optional (bool, optional): whether to ignore the wait for the received input if the command is executed when the process is stopped. Defaults to False.
+
+ Returns:
+ bytes: received data from the child process stderr.
+ """
+ return self._recvuntil(
+ delims=delims,
+ occurences=occurences,
+ drop=drop,
+ timeout=timeout,
+ stderr=True,
+ optional=optional,
+ )
+
+ def recvline(
+ self: PipeManager,
+ numlines: int = 1,
+ drop: bool = True,
+ timeout: int = timeout_default,
+ optional: bool = False,
+ ) -> bytes:
+ """Receives numlines lines from the child process stdout.
+
+ Args:
+ numlines (int, optional): number of lines to receive. Defaults to 1.
+ drop (bool, optional): drop the line ending. Defaults to True.
+ timeout (int, optional): timeout in seconds. Defaults to timeout_default.
+ optional (bool, optional): whether to ignore the wait for the received input if the command is executed when the process is stopped. Defaults to False.
+
+ Returns:
+ bytes: received lines from the child process stdout.
+ """
+ return self.recvuntil(delims=b"\n", occurences=numlines, drop=drop, timeout=timeout, optional=optional)
+
+ def recverrline(
+ self: PipeManager,
+ numlines: int = 1,
+ drop: bool = True,
+ timeout: int = timeout_default,
+ optional: bool = False,
+ ) -> bytes:
+ """Receives numlines lines from the child process stderr.
+
+ Args:
+ numlines (int, optional): number of lines to receive. Defaults to 1.
+ drop (bool, optional): drop the line ending. Defaults to True.
+ timeout (int, optional): timeout in seconds. Defaults to timeout_default.
+ optional (bool, optional): whether to ignore the wait for the received input if the command is executed when the process is stopped. Defaults to False.
+
+ Returns:
+ bytes: received lines from the child process stdout.
+ """
+ return self.recverruntil(delims=b"\n", occurences=numlines, drop=drop, timeout=timeout, optional=optional)
+
+ def send(self: PipeManager, data: bytes) -> int:
+ """Sends data to the child process stdin.
+
+ Args:
+ data (bytes): data to send.
+
+ Returns:
+ int: number of bytes sent.
+
+ Raises:
+ RuntimeError: no stdin pipe of the child process.
+ """
+ if not self._stdin_write:
+ raise RuntimeError("No stdin pipe of the child process")
+
+ liblog.pipe(f"Sending {len(data)} bytes to the child process: {data!r}")
+
+ if isinstance(data, str):
+ liblog.warning("The input data is a string, converting to bytes")
+ data = data.encode()
+
+ try:
+ number_bytes = os.write(self._stdin_write, data)
+ except OSError as e:
+ raise RuntimeError("Broken pipe. Is the child process still running?") from e
+
+ return number_bytes
+
+ def sendline(self: PipeManager, data: bytes) -> int:
+ """Sends data to the child process stdin and append a newline.
+
+ Args:
+ data (bytes): data to send.
+
+ Returns:
+ int: number of bytes sent.
+ """
+ if isinstance(data, str):
+ liblog.warning("The input data is a string, converting to bytes")
+ data = data.encode()
+ return self.send(data=data + b"\n")
+
+ def sendafter(
+ self: PipeManager,
+ delims: bytes,
+ data: bytes,
+ occurences: int = 1,
+ drop: bool = False,
+ timeout: int = timeout_default,
+ optional: bool = False,
+ ) -> tuple[bytes, int]:
+ """Sends data to the child process stdin after the delimiters are found in the stdout.
+
+ Args:
+ delims (bytes): delimiters where to stop.
+ data (bytes): data to send.
+ occurences (int, optional): number of delimiters to find. Defaults to 1.
+ drop (bool, optional): drop the delimiter. Defaults to False.
+ timeout (int, optional): timeout in seconds. Defaults to timeout_default.
+ optional (bool, optional): whether to ignore the wait for the received input if the command is executed when the process is stopped. Defaults to False.
+
+ Returns:
+ bytes: received data from the child process stdout.
+ int: number of bytes sent.
+ """
+ received = self.recvuntil(delims=delims, occurences=occurences, drop=drop, timeout=timeout, optional=optional)
+ sent = self.send(data)
+ return (received, sent)
+
+ def sendaftererr(
+ self: PipeManager,
+ delims: bytes,
+ data: bytes,
+ occurences: int = 1,
+ drop: bool = False,
+ timeout: int = timeout_default,
+ optional: bool = False,
+ ) -> tuple[bytes, int]:
+ """Sends data to the child process stdin after the delimiters are found in stderr.
+
+ Args:
+ delims (bytes): delimiters where to stop.
+ data (bytes): data to send.
+ occurences (int, optional): number of delimiters to find. Defaults to 1.
+ drop (bool, optional): drop the delimiter. Defaults to False.
+ timeout (int, optional): timeout in seconds. Defaults to timeout_default.
+ optional (bool, optional): whether to ignore the wait for the received input if the command is executed when the process is stopped. Defaults to False.
+
+ Returns:
+ bytes: received data from the child process stderr.
+ int: number of bytes sent.
+ """
+ received = self.recverruntil(
+ delims=delims,
+ occurences=occurences,
+ drop=drop,
+ timeout=timeout,
+ optional=optional,
+ )
+ sent = self.send(data)
+ return (received, sent)
+
+ def sendlineafter(
+ self: PipeManager,
+ delims: bytes,
+ data: bytes,
+ occurences: int = 1,
+ drop: bool = False,
+ timeout: int = timeout_default,
+ optional: bool = False,
+ ) -> tuple[bytes, int]:
+ """Sends line to the child process stdin after the delimiters are found in the stdout.
+
+ Args:
+ delims (bytes): delimiters where to stop.
+ data (bytes): data to send.
+ occurences (int, optional): number of delimiters to find. Defaults to 1.
+ drop (bool, optional): drop the delimiter. Defaults to False.
+ timeout (int, optional): timeout in seconds. Defaults to timeout_default.
+ optional (bool, optional): whether to ignore the wait for the received input if the command is executed when the process is stopped. Defaults to False.
+
+ Returns:
+ bytes: received data from the child process stdout.
+ int: number of bytes sent.
+ """
+ received = self.recvuntil(delims=delims, occurences=occurences, drop=drop, timeout=timeout, optional=optional)
+ sent = self.sendline(data)
+ return (received, sent)
+
+ def sendlineaftererr(
+ self: PipeManager,
+ delims: bytes,
+ data: bytes,
+ occurences: int = 1,
+ drop: bool = False,
+ timeout: int = timeout_default,
+ optional: bool = False,
+ ) -> tuple[bytes, int]:
+ """Sends line to the child process stdin after the delimiters are found in the stderr.
+
+ Args:
+ delims (bytes): delimiters where to stop.
+ data (bytes): data to send.
+ occurences (int, optional): number of delimiters to find. Defaults to 1.
+ drop (bool, optional): drop the delimiter. Defaults to False.
+ timeout (int, optional): timeout in seconds. Defaults to timeout_default.
+ optional (bool, optional): whether to ignore the wait for the received input if the command is executed when the process is stopped. Defaults to False.
+
+ Returns:
+ bytes: received data from the child process stderr.
+ int: number of bytes sent.
+ """
+ received = self.recverruntil(
+ delims=delims,
+ occurences=occurences,
+ drop=drop,
+ timeout=timeout,
+ optional=optional,
+ )
+ sent = self.sendline(data)
+ return (received, sent)
+
+ def _recv_for_interactive(self: PipeManager) -> None:
+ """Receives data from the child process."""
+ stdout_has_warned = False
+ stderr_has_warned = False
+
+ while not (self.__end_interactive_event.is_set() or (stdout_has_warned and stderr_has_warned)):
+ # We can afford to treat stdout and stderr sequentially. This approach should also prevent
+ # messing up the order of the information printed by the child process.
+ # To avoid starvation, we switch between pipes upon receiving a bunch of data from one of them.
+ if self._stdout_is_open:
+ while True:
+ new_recv = self._raw_recv()
+ payload = self.__stdout_buffer.get_data()
+
+ if not (new_recv or payload):
+ # No more data available in the stdout pipe at the moment
+ break
+
+ sys.stdout.write(payload)
+ self.__stdout_buffer.clear()
+ elif not stdout_has_warned:
+ # The child process has closed the stdout pipe and we have to print the warning message
+ liblog.warning("The stdout pipe of the child process is not available anymore")
+ stdout_has_warned = True
+ if self._stderr_is_open:
+ while True:
+ new_recv = self._raw_recv(stderr=True)
+ payload = self.__stderr_buffer.get_data()
+
+ if not (new_recv or payload):
+ # No more data available in the stderr pipe at the moment
+ break
+
+ sys.stderr.write(payload)
+ self.__stderr_buffer.clear()
+ elif not stderr_has_warned:
+ # The child process has closed the stderr pipe
+ liblog.warning("The stderr pipe of the child process is not available anymore")
+ stderr_has_warned = True
+
+ def interactive(self: PipeManager, prompt: str = prompt_default, auto_quit: bool = False) -> None:
+ """Manually interact with the child process.
+
+ Args:
+ prompt (str, optional): prompt for the interactive mode. Defaults to "$ " (prompt_default).
+ auto_quit (bool, optional): whether to automatically quit the interactive mode when the child process is not running. Defaults to False.
+ """
+ liblog.info("Calling interactive mode")
+
+ # Set up and run the terminal
+ with extend_internal_debugger(self):
+ libterminal = LibTerminal(prompt, self.sendline, self.__end_interactive_event, auto_quit)
+
+ # Receive data from the child process's stdout and stderr pipes
+ self._recv_for_interactive()
+
+ # Be sure that the interactive mode has ended
+ # If the the stderr and stdout pipes are closed, the interactive mode will continue until the user manually
+ # stops it
+ self.__end_interactive_event.wait()
+
+ # Unset the interactive mode event
+ self.__end_interactive_event.clear()
+
+ # Reset the terminal
+ libterminal.reset()
+
+ liblog.info("Exiting interactive mode")
diff --git a/libdebug/commlink/std_wrapper.py b/libdebug/commlink/std_wrapper.py
new file mode 100644
index 00000000..04056abb
--- /dev/null
+++ b/libdebug/commlink/std_wrapper.py
@@ -0,0 +1,29 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from libdebug.commlink.libterminal import LibTerminal
+
+
+class StdWrapper:
+ """Wrapper around stderr/stdout to allow for custom write method."""
+
+ def __init__(self: StdWrapper, fd: object, terminal: LibTerminal) -> None:
+ """Initializes the StderrWrapper object."""
+ self._fd: object = fd
+ self._terminal: LibTerminal = terminal
+
+ def write(self, payload: bytes | str) -> int:
+ """Overloads the write method to allow for custom behavior."""
+ return self._terminal._write_manager(payload)
+
+ def __getattr__(self, k: any) -> any:
+ """Ensure that all other attributes are forwarded to the original file descriptor."""
+ return getattr(self._fd, k)
diff --git a/libdebug/data/breakpoint.py b/libdebug/data/breakpoint.py
index c4a618e9..89999167 100644
--- a/libdebug/data/breakpoint.py
+++ b/libdebug/data/breakpoint.py
@@ -61,8 +61,17 @@ def disable(self: Breakpoint) -> None:
def hit_on(self: Breakpoint, thread_context: ThreadContext) -> bool:
"""Returns whether the breakpoint has been hit on the given thread context."""
- return self.enabled and thread_context.instruction_pointer == self.address
+ if not self.enabled:
+ return False
+
+ internal_debugger = provide_internal_debugger(self)
+ internal_debugger._ensure_process_stopped()
+ return internal_debugger.resume_context.event_hit_ref.get(thread_context.thread_id) == self
def __hash__(self: Breakpoint) -> int:
"""Hash the breakpoint by its address, so that it can be used in sets and maps correctly."""
return hash(self.address)
+
+ def __eq__(self: Breakpoint, other: object) -> bool:
+ """Check if two breakpoints are equal."""
+ return id(self) == id(other)
diff --git a/libdebug/data/gdb_resume_event.py b/libdebug/data/gdb_resume_event.py
new file mode 100644
index 00000000..6210ea43
--- /dev/null
+++ b/libdebug/data/gdb_resume_event.py
@@ -0,0 +1,40 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from libdebug.debugger.internal_debugger import InternalDebugger
+
+
+class GdbResumeEvent:
+ """This class handles the actions needed to resume the debugging session, after returning from GDB."""
+
+ def __init__(
+ self: GdbResumeEvent,
+ internal_debugger: InternalDebugger,
+ lambda_function: callable[[], None],
+ ) -> None:
+ """Initializes the GdbResumeEvent.
+
+ Args:
+ internal_debugger (InternalDebugger): The internal debugger instance.
+ lambda_function (callable[[], None]): The blocking lambda function to wait on.
+ """
+ self._internal_debugger = internal_debugger
+ self._lambda_function = lambda_function
+ self._joined = False
+
+ def join(self: GdbResumeEvent) -> None:
+ """Resumes the debugging session, blocking the script until GDB terminate and libdebug reattaches."""
+ if self._joined:
+ raise RuntimeError("GdbResumeEvent already joined")
+
+ self._lambda_function()
+ self._internal_debugger._resume_from_gdb()
+ self._joined = True
diff --git a/libdebug/data/memory_map.py b/libdebug/data/memory_map.py
index 0a74b395..828f742d 100644
--- a/libdebug/data/memory_map.py
+++ b/libdebug/data/memory_map.py
@@ -1,6 +1,6 @@
#
# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Roberto Alessandro Bertolini. All rights reserved.
+# Copyright (c) 2023-2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
@@ -14,7 +14,7 @@ class MemoryMap:
"""A memory map of the target process.
Attributes:
- start (int): The start address of the memory map.
+ start (int): The start address of the memory map. You can access it also with the 'base' attribute.
end (int): The end address of the memory map.
permissions (str): The permissions of the memory map.
size (int): The size of the memory map.
@@ -59,6 +59,11 @@ def parse(vmap: str) -> MemoryMap:
return MemoryMap(start, end, permissions, size, int_offset, backing_file)
+ @property
+ def base(self: MemoryMap) -> int:
+ """Alias for the start address of the memory map."""
+ return self.start
+
def __repr__(self: MemoryMap) -> str:
"""Return the string representation of the memory map."""
return f"MemoryMap(start={hex(self.start)}, end={hex(self.end)}, permissions={self.permissions}, size={hex(self.size)}, offset={hex(self.offset)}, backing_file={self.backing_file})"
diff --git a/libdebug/data/memory_map_list.py b/libdebug/data/memory_map_list.py
new file mode 100644
index 00000000..c5fb8d62
--- /dev/null
+++ b/libdebug/data/memory_map_list.py
@@ -0,0 +1,83 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from libdebug.debugger.internal_debugger_instance_manager import extend_internal_debugger, provide_internal_debugger
+from libdebug.liblog import liblog
+
+if TYPE_CHECKING:
+ from libdebug.data.memory_map import MemoryMap
+
+
+class MemoryMapList(list):
+ """A list of memory maps of the target process."""
+
+ def __init__(self: MemoryMapList, memory_maps: list[MemoryMap]) -> None:
+ """Initializes the MemoryMapList."""
+ super().__init__(memory_maps)
+ self._internal_debugger = provide_internal_debugger(self)
+
+ def _search_by_address(self: MemoryMapList, address: int) -> list[MemoryMap]:
+ for vmap in self:
+ if vmap.start <= address < vmap.end:
+ return [vmap]
+ return []
+
+ def _search_by_backing_file(self: MemoryMapList, backing_file: str) -> list[MemoryMap]:
+ if backing_file in ["binary", self._internal_debugger._process_name]:
+ backing_file = self._internal_debugger._process_full_path
+
+ filtered_maps = []
+ unique_files = set()
+
+ for vmap in self:
+ if backing_file in vmap.backing_file:
+ filtered_maps.append(vmap)
+ unique_files.add(vmap.backing_file)
+
+ if len(unique_files) > 1:
+ liblog.warning(
+ f"The substring {backing_file} is present in multiple, different backing files. The address resolution cannot be accurate. The matching backing files are: {', '.join(unique_files)}.",
+ )
+
+ return filtered_maps
+
+ def filter(self: MemoryMapList, value: int | str) -> MemoryMapList[MemoryMap]:
+ """Filters the memory maps according to the specified value.
+
+ If the value is an integer, it is treated as an address.
+ If the value is a string, it is treated as a backing file.
+
+ Args:
+ value (int | str): The value to search for.
+
+ Returns:
+ MemoryMapList[MemoryMap]: The memory maps matching the specified value.
+ """
+ if isinstance(value, int):
+ filtered_maps = self._search_by_address(value)
+ elif isinstance(value, str):
+ filtered_maps = self._search_by_backing_file(value)
+ else:
+ raise TypeError("The value must be an integer or a string.")
+
+ with extend_internal_debugger(self._internal_debugger):
+ return MemoryMapList(filtered_maps)
+
+ def __hash__(self) -> int:
+ """Return the hash of the memory map list."""
+ return hash(id(self))
+
+ def __eq__(self, other: object) -> bool:
+ """Check if the memory map list is equal to another object."""
+ return super().__eq__(other)
+
+ def __repr__(self) -> str:
+ """Return the string representation of the memory map list."""
+ return f"MemoryMapList({super().__repr__()})"
diff --git a/libdebug/data/register_holder.py b/libdebug/data/register_holder.py
index bba817dc..fb248ff0 100644
--- a/libdebug/data/register_holder.py
+++ b/libdebug/data/register_holder.py
@@ -49,3 +49,15 @@ def flush(self: RegisterHolder, source: ThreadContext) -> None:
Args:
source (ThreadContext): The object from which the register values should be flushed.
"""
+
+ @abstractmethod
+ def provide_regs(self: RegisterHolder) -> list[str]:
+ """Provide the list of registers, excluding the vector and fp registers."""
+
+ @abstractmethod
+ def provide_vector_fp_regs(self: RegisterHolder) -> list[tuple[str]]:
+ """Provide the list of vector and floating point registers."""
+
+ @abstractmethod
+ def provide_special_regs(self: RegisterHolder) -> list[str]:
+ """Provide the list of special registers, which are not intended for general-purpose use."""
diff --git a/libdebug/data/registers.py b/libdebug/data/registers.py
index dae54ddc..9d04854a 100644
--- a/libdebug/data/registers.py
+++ b/libdebug/data/registers.py
@@ -1,19 +1,45 @@
#
# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
+# Copyright (c) 2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
from __future__ import annotations
-from abc import ABC, abstractmethod
from dataclasses import dataclass
+from libdebug.debugger.internal_debugger_instance_manager import get_global_internal_debugger
+
@dataclass
-class Registers(ABC):
+class Registers:
"""Abtract class that holds the state of the architectural-dependent registers of a process."""
- @abstractmethod
- def __init__(self: Registers) -> None:
+ def __init__(self: Registers, thread_id: int, generic_regs: list[str]) -> None:
"""Initializes the Registers object."""
+ self._internal_debugger = get_global_internal_debugger()
+ self._thread_id = thread_id
+ self._generic_regs = generic_regs
+
+ def __repr__(self: Registers) -> str:
+ """Returns a string representation of the object."""
+ repr_str = f"Registers(thread_id={self._thread_id})"
+
+ attributes = self._generic_regs
+ max_len = max(len(attr) for attr in attributes) + 1
+
+ repr_str += "".join(f"\n {attr + ':':<{max_len}} {getattr(self, attr):#x}" for attr in attributes)
+
+ return repr_str
+
+ def filter(self: Registers, value: float) -> list[str]:
+ """Filters the registers by value.
+
+ Args:
+ value (float): The value to search for.
+
+ Returns:
+ list[str]: A list of names of the registers containing the value.
+ """
+ attributes = self.__class__.__dict__
+ return [attr for attr in attributes if getattr(self, attr) == value]
diff --git a/libdebug/data/signal_catcher.py b/libdebug/data/signal_catcher.py
index 026be5b4..a08f1399 100644
--- a/libdebug/data/signal_catcher.py
+++ b/libdebug/data/signal_catcher.py
@@ -23,10 +23,8 @@ class SignalCatcher:
Attributes:
signal_number (int): The signal number to catch.
- callback (Callable[[ThreadContext, CaughtSignal], None]): The callback defined by the user to execute when the
- signal is caught.
- recursive (bool): Whether, when the signal is hijacked with another one, the signal catcher associated with the
- new signal should be considered as well. Defaults to False.
+ callback (Callable[[ThreadContext, CaughtSignal], None]): The callback defined by the user to execute when the signal is caught.
+ recursive (bool): Whether, when the signal is hijacked with another one, the signal catcher associated with the new signal should be considered as well. Defaults to False.
enabled (bool): Whether the signal will be caught or not.
hit_count (int): The number of times the signal has been caught.
"""
@@ -49,7 +47,7 @@ def disable(self: SignalCatcher) -> None:
def hit_on(self: SignalCatcher, thread_context: ThreadContext) -> bool:
"""Returns whether the signal catcher has been hit on the given thread context."""
- return self.enabled and thread_context._signal_number == self.signal_number
+ return self.enabled and thread_context.signal_number == self.signal_number
def __hash__(self: SignalCatcher) -> int:
"""Return the hash of the signal catcher, based just on the signal number."""
diff --git a/libdebug/data/symbol.py b/libdebug/data/symbol.py
new file mode 100644
index 00000000..fa37d50f
--- /dev/null
+++ b/libdebug/data/symbol.py
@@ -0,0 +1,33 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+
+@dataclass
+class Symbol:
+ """A symbol in the target process.
+
+ start (int): The start address of the symbol in the target process.
+ end (int): The end address of the symbol in the target process.
+ name (str): The name of the symbol in the target process.
+ backing_file (str): The backing file of the symbol in the target process.
+ """
+
+ start: int
+ end: int
+ name: str
+ backing_file: str
+
+ def __hash__(self: Symbol) -> int:
+ """Returns the hash of the symbol."""
+ return hash((self.start, self.end, self.name, self.backing_file))
+
+ def __repr__(self: Symbol) -> str:
+ """Returns the string representation of the symbol."""
+ return f"Symbol(start={self.start:#x}, end={self.end:#x}, name={self.name}, backing_file={self.backing_file})"
diff --git a/libdebug/data/symbol_list.py b/libdebug/data/symbol_list.py
new file mode 100644
index 00000000..3c169820
--- /dev/null
+++ b/libdebug/data/symbol_list.py
@@ -0,0 +1,99 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from libdebug.debugger.internal_debugger_instance_manager import get_global_internal_debugger
+
+if TYPE_CHECKING:
+ from libdebug.data.symbol import Symbol
+
+
+class SymbolList(list):
+ """A list of symbols in the target process."""
+
+ def __init__(self: SymbolList, symbols: list[Symbol]) -> None:
+ """Initializes the SymbolDict."""
+ super().__init__(symbols)
+
+ def _search_by_address(self: SymbolList, address: int) -> list[Symbol]:
+ """Searches for a symbol by address.
+
+ Args:
+ address (int): The address of the symbol to search for.
+
+ Returns:
+ list[Symbol]: The list of symbols that match the specified address.
+ """
+ # Find the memory map that contains the address
+ if maps := get_global_internal_debugger().maps.filter(address):
+ address -= maps[0].start
+ else:
+ raise ValueError(
+ f"Address {address:#x} does not belong to any memory map. You must specify an absolute address."
+ )
+ return [symbol for symbol in self if symbol.start <= address < symbol.end]
+
+ def _search_by_name(self: SymbolList, name: str) -> list[Symbol]:
+ """Searches for a symbol by name.
+
+ Args:
+ name (str): The name of the symbol to search for.
+
+ Returns:
+ list[Symbol]: The list of symbols that match the specified name.
+ """
+ exact_match = []
+ no_exact_match = []
+ # We first want to list the symbols that exactly match the name
+ for symbol in self:
+ if symbol.name == name:
+ exact_match.append(symbol)
+ elif name in symbol.name:
+ no_exact_match.append(symbol)
+ return exact_match + no_exact_match
+
+ def filter(self: SymbolList, value: int | str) -> SymbolList[Symbol]:
+ """Filters the symbols according to the specified value.
+
+ If the value is an integer, it is treated as an address.
+ If the value is a string, it is treated as a symbol name.
+
+ Args:
+ value (int | str): The address or name of the symbol to find.
+
+ Returns:
+ SymbolList[Symbol]: The symbols matching the specified value.
+ """
+ if isinstance(value, int):
+ filtered_symbols = self._search_by_address(value)
+ elif isinstance(value, str):
+ filtered_symbols = self._search_by_name(value)
+ else:
+ raise TypeError("The value must be an integer or a string.")
+
+ return SymbolList(filtered_symbols)
+
+ def __getitem__(self: SymbolList, key: str) -> Symbol:
+ """Returns the symbol with the specified name."""
+ symbols = [symbol for symbol in self if symbol.name == key]
+ if not symbols:
+ raise KeyError(f"Symbol '{key}' not found.")
+ return symbols
+
+ def __hash__(self) -> int:
+ """Return the hash of the symbol list."""
+ return hash(id(self))
+
+ def __eq__(self, other: object) -> bool:
+ """Check if the symbol list is equal to another object."""
+ return super().__eq__(other)
+
+ def __repr__(self: SymbolList) -> str:
+ """Returns the string representation of the SymbolDict without the default factory."""
+ return f"SymbolList({super().__repr__()})"
diff --git a/libdebug/data/syscall_handler.py b/libdebug/data/syscall_handler.py
index abcb3c30..bf9d94bc 100644
--- a/libdebug/data/syscall_handler.py
+++ b/libdebug/data/syscall_handler.py
@@ -23,16 +23,11 @@ class SyscallHandler:
Attributes:
syscall_number (int): The syscall number to handle.
- on_enter_user (Callable[[ThreadContext, int], None]): The callback defined by the user to execute when the
- syscall is entered.
- on_exit_user (Callable[[ThreadContext, int], None]): The callback defined by the user to execute when the
- syscall is exited.
- on_enter_pprint (Callable[[ThreadContext, int], None]): The callback defined by the pretty print to execute when
- the syscall is entered.
- on_exit_pprint (Callable[[ThreadContext, int], None]): The callback defined by the pretty print to execute when
- the syscall is exited.
- recursive (bool): Whether, when the syscall is hijacked with another one, the syscall handler associated with
- the new syscall should be considered as well. Defaults to False.
+ on_enter_user (Callable[[ThreadContext, int], None]): The callback defined by the user to execute when the syscall is entered.
+ on_exit_user (Callable[[ThreadContext, int], None]): The callback defined by the user to execute when the syscall is exited.
+ on_enter_pprint (Callable[[ThreadContext, int], None]): The callback defined by the pretty print to execute when the syscall is entered.
+ on_exit_pprint (Callable[[ThreadContext, int], None]): The callback defined by the pretty print to execute when the syscall is exited.
+ recursive (bool): Whether, when the syscall is hijacked with another one, the syscall handler associated with the new syscall should be considered as well. Defaults to False.
enabled (bool): Whether the syscall will be handled or not.
hit_count (int): The number of times the syscall has been handled.
"""
@@ -61,6 +56,10 @@ def disable(self: SyscallHandler) -> None:
self.enabled = False
self._has_entered = False
+ def hit_on(self: SyscallHandler, thread_context: ThreadContext) -> bool:
+ """Returns whether the syscall handler has been hit on the given thread context."""
+ return self.enabled and thread_context.syscall_number == self.syscall_number
+
def hit_on_enter(self: SyscallHandler, thread_context: ThreadContext) -> bool:
"""Returns whether the syscall handler has been hit during the syscall entry on the given thread context."""
return self.enabled and thread_context.syscall_number == self.syscall_number and self._has_entered
diff --git a/libdebug/data/terminals.py b/libdebug/data/terminals.py
new file mode 100644
index 00000000..113c01dd
--- /dev/null
+++ b/libdebug/data/terminals.py
@@ -0,0 +1,41 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from dataclasses import dataclass
+from typing import ClassVar
+
+
+@dataclass
+class TerminalTypes:
+ """Terminal class for launching terminal emulators with predefined commands."""
+
+ terminals: ClassVar[dict[str, list[str]]] = {
+ "gnome-terminal-server": ["gnome-terminal", "--tab", "--"],
+ "konsole": ["konsole", "--new-tab", "-e"],
+ "xterm": ["xterm", "-e"],
+ "lxterminal": ["lxterminal", "-e"],
+ "mate-terminal": ["mate-terminal", "--tab", "-e"],
+ "tilix": ["tilix", "--action=app-new-session", "-e"],
+ "kgx": ["kgx", "--tab", "-e"],
+ "alacritty": ["alacritty", "-e"],
+ "kitty": ["kitty", "-e"],
+ "urxvt": ["urxvt", "-e"],
+ "tmux: server": ["tmux", "split-window", "-h"],
+ "xfce4-terminal": ["xfce4-terminal", "--tab", "-e"],
+ "terminator": ["terminator", "--new-tab", "-e"],
+ }
+
+ @staticmethod
+ def get_command(terminal_name: str) -> list[str]:
+ """Retrieve the command list for a given terminal emulator name.
+
+ Args:
+ terminal_name (str): the name of the terminal emulator.
+
+ Returns:
+ list[str]: the command list for the terminal emulator, or an empty list if not found.
+ """
+ return TerminalTypes.terminals.get(terminal_name, [])
diff --git a/libdebug/debugger/debugger.py b/libdebug/debugger/debugger.py
index 33f36ecb..f6dbacbe 100644
--- a/libdebug/debugger/debugger.py
+++ b/libdebug/debugger/debugger.py
@@ -1,4 +1,5 @@
#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini, Francesco Panebianco. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
@@ -8,6 +9,7 @@
from contextlib import contextmanager
from typing import TYPE_CHECKING
+from libdebug.liblog import liblog
from libdebug.utils.arch_mappings import map_arch
from libdebug.utils.signal_utils import (
get_all_signal_numbers,
@@ -22,11 +24,18 @@
if TYPE_CHECKING:
from collections.abc import Callable
+ from libdebug.commlink.pipe_manager import PipeManager
from libdebug.data.breakpoint import Breakpoint
+ from libdebug.data.gdb_resume_event import GdbResumeEvent
from libdebug.data.memory_map import MemoryMap
+ from libdebug.data.memory_map_list import MemoryMapList
+ from libdebug.data.registers import Registers
from libdebug.data.signal_catcher import SignalCatcher
+ from libdebug.data.symbol import Symbol
+ from libdebug.data.symbol_list import SymbolList
from libdebug.data.syscall_handler import SyscallHandler
from libdebug.debugger.internal_debugger import InternalDebugger
+ from libdebug.memory.abstract_memory_view import AbstractMemoryView
from libdebug.state.thread_context import ThreadContext
@@ -36,7 +45,7 @@ class Debugger:
_sentinel: object = object()
"""A sentinel object."""
- _internal_debugger: InternalDebugger | None = None
+ _internal_debugger: InternalDebugger
"""The internal debugger object."""
def __init__(self: Debugger) -> None:
@@ -47,9 +56,13 @@ def post_init_(self: Debugger, internal_debugger: InternalDebugger) -> None:
self._internal_debugger = internal_debugger
self._internal_debugger.start_up()
- def run(self: Debugger) -> None:
- """Starts the process and waits for it to stop."""
- return self._internal_debugger.run()
+ def run(self: Debugger, redirect_pipes: bool = True) -> PipeManager | None:
+ """Starts the process and waits for it to stop.
+
+ Args:
+ redirect_pipes (bool): Whether to hook and redirect the pipes of the process to a PipeManager.
+ """
+ return self._internal_debugger.run(redirect_pipes)
def attach(self: Debugger, pid: int) -> None:
"""Attaches to an existing process."""
@@ -83,13 +96,31 @@ def wait(self: Debugger) -> None:
"""Waits for the process to stop."""
self._internal_debugger.wait()
- def maps(self: Debugger) -> list[MemoryMap]:
- """Returns the memory maps of the process."""
- return self._internal_debugger.maps()
-
def print_maps(self: Debugger) -> None:
"""Prints the memory maps of the process."""
- self._internal_debugger.print_maps()
+ liblog.warning("The `print_maps` method is deprecated. Use `d.pprint_maps` instead.")
+ self._internal_debugger.pprint_maps()
+
+ def pprint_maps(self: Debugger) -> None:
+ """Prints the memory maps of the process."""
+ self._internal_debugger.pprint_maps()
+
+ def resolve_symbol(self: Debugger, symbol: str, file: str = "binary") -> int:
+ """Resolves the address of the specified symbol.
+
+ Args:
+ symbol (str): The symbol to resolve.
+ file (str): The backing file to resolve the symbol in. Defaults to "binary"
+
+ Returns:
+ int: The address of the symbol.
+ """
+ return self._internal_debugger.resolve_symbol(symbol, file)
+
+ @property
+ def symbols(self: Debugger) -> SymbolList[Symbol]:
+ """Get the symbols of the process."""
+ return self._internal_debugger.symbols
def breakpoint(
self: Debugger,
@@ -97,22 +128,18 @@ def breakpoint(
hardware: bool = False,
condition: str = "x",
length: int = 1,
- callback: None | Callable[[ThreadContext, Breakpoint], None] = None,
+ callback: None | bool | Callable[[ThreadContext, Breakpoint], None] = None,
file: str = "hybrid",
) -> Breakpoint:
"""Sets a breakpoint at the specified location.
Args:
position (int | bytes): The location of the breakpoint.
- hardware (bool, optional): Whether the breakpoint should be hardware-assisted or purely software.
- Defaults to False.
+ hardware (bool, optional): Whether the breakpoint should be hardware-assisted or purely software. Defaults to False.
condition (str, optional): The trigger condition for the breakpoint. Defaults to None.
length (int, optional): The length of the breakpoint. Only for watchpoints. Defaults to 1.
- callback (Callable[[ThreadContext, Breakpoint], None], optional): A callback to be called when the
- breakpoint is hit. Defaults to None.
- file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid"
- (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t.
- the "binary" map file).
+ callback (None | bool | Callable[[ThreadContext, Breakpoint], None], optional): A callback to be called when the breakpoint is hit. If True, an empty callback will be set. Defaults to None.
+ file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file).
"""
return self._internal_debugger.breakpoint(position, hardware, condition, length, callback, file)
@@ -121,21 +148,17 @@ def watchpoint(
position: int | str,
condition: str = "w",
length: int = 1,
- callback: None | Callable[[ThreadContext, Breakpoint], None] = None,
+ callback: None | bool | Callable[[ThreadContext, Breakpoint], None] = None,
file: str = "hybrid",
) -> Breakpoint:
"""Sets a watchpoint at the specified location. Internally, watchpoints are implemented as breakpoints.
Args:
position (int | bytes): The location of the breakpoint.
- condition (str, optional): The trigger condition for the watchpoint (either "w", "rw" or "x").
- Defaults to "w".
+ condition (str, optional): The trigger condition for the watchpoint (either "w", "rw" or "x"). Defaults to "w".
length (int, optional): The size of the word in being watched (1, 2, 4 or 8). Defaults to 1.
- callback (Callable[[ThreadContext, Breakpoint], None], optional): A callback to be called when the
- watchpoint is hit. Defaults to None.
- file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid"
- (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t.
- the "binary" map file).
+ callback (None | bool | Callable[[ThreadContext, Breakpoint], None], optional): A callback to be called when the watchpoint is hit. If True, an empty callback will be set. Defaults to None.
+ file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file).
"""
return self._internal_debugger.breakpoint(
position,
@@ -149,20 +172,18 @@ def watchpoint(
def catch_signal(
self: Debugger,
signal: int | str,
- callback: None | Callable[[ThreadContext, SignalCatcher], None] = None,
+ callback: None | bool | Callable[[ThreadContext, SignalCatcher], None] = None,
recursive: bool = False,
) -> SignalCatcher:
"""Catch a signal in the target process.
Args:
- signal (int | str): The signal to catch.
- callback (Callable[[ThreadContext, CaughtSignal], None], optional): A callback to be called when the signal
- is caught. Defaults to None.
- recursive (bool, optional): Whether, when the signal is hijacked with another one, the signal catcher
- associated with the new signal should be considered as well. Defaults to False.
+ signal (int | str): The signal to catch. If "*", "ALL", "all" or -1 is passed, all signals will be caught.
+ callback (None | bool | Callable[[ThreadContext, SignalCatcher], None], optional): A callback to be called when the signal is caught. If True, an empty callback will be set. Defaults to None.
+ recursive (bool, optional): Whether, when the signal is hijacked with another one, the signal catcher associated with the new signal should be considered as well. Defaults to False.
Returns:
- CaughtSignal: The CaughtSignal object.
+ SignalCatcher: The SignalCatcher object.
"""
return self._internal_debugger.catch_signal(signal, callback, recursive)
@@ -175,36 +196,32 @@ def hijack_signal(
"""Hijack a signal in the target process.
Args:
- original_signal (int | str): The signal to hijack.
+ original_signal (int | str): The signal to hijack. If "*", "ALL", "all" or -1 is passed, all signals will be hijacked.
new_signal (int | str): The signal to hijack the original signal with.
- recursive (bool, optional): Whether, when the signal is hijacked with another one, the signal catcher
- associated with the new signal should be considered as well. Defaults to False.
+ recursive (bool, optional): Whether, when the signal is hijacked with another one, the signal catcher associated with the new signal should be considered as well. Defaults to False.
Returns:
- CaughtSignal: The CaughtSignal object.
+ SignalCatcher: The SignalCatcher object.
"""
return self._internal_debugger.hijack_signal(original_signal, new_signal, recursive)
def handle_syscall(
self: Debugger,
syscall: int | str,
- on_enter: Callable[[ThreadContext, SyscallHandler], None] | None = None,
- on_exit: Callable[[ThreadContext, SyscallHandler], None] | None = None,
+ on_enter: None | bool | Callable[[ThreadContext, SyscallHandler], None] = None,
+ on_exit: None | bool | Callable[[ThreadContext, SyscallHandler], None] = None,
recursive: bool = False,
) -> SyscallHandler:
"""Handle a syscall in the target process.
Args:
- syscall (int | str): The syscall name or number to handle.
- on_enter (Callable[[ThreadContext, HandledSyscall], None], optional): The callback to execute when the
- syscall is entered. Defaults to None.
- on_exit (Callable[[ThreadContext, HandledSyscall], None], optional): The callback to execute when the
- syscall is exited. Defaults to None.
- recursive (bool, optional): Whether, when the syscall is hijacked with another one, the syscall handler
- associated with the new syscall should be considered as well. Defaults to False.
+ syscall (int | str): The syscall name or number to handle. If "*", "ALL", "all" or -1 is passed, all syscalls will be handled.
+ on_enter (None | bool |Callable[[ThreadContext, SyscallHandler], None], optional): The callback to execute when the syscall is entered. If True, an empty callback will be set. Defaults to None.
+ on_exit (None | bool | Callable[[ThreadContext, SyscallHandler], None], optional): The callback to execute when the syscall is exited. If True, an empty callback will be set. Defaults to None.
+ recursive (bool, optional): Whether, when the syscall is hijacked with another one, the syscall handler associated with the new syscall should be considered as well. Defaults to False.
Returns:
- HandledSyscall: The HandledSyscall object.
+ SyscallHandler: The SyscallHandler object.
"""
return self._internal_debugger.handle_syscall(syscall, on_enter, on_exit, recursive)
@@ -218,27 +235,40 @@ def hijack_syscall(
"""Hijacks a syscall in the target process.
Args:
- original_syscall (int | str): The syscall name or number to hijack.
+ original_syscall (int | str): The syscall name or number to hijack. If "*", "ALL", "all" or -1 is passed, all syscalls will be hijacked.
new_syscall (int | str): The syscall name or number to hijack the original syscall with.
- recursive (bool, optional): Whether, when the syscall is hijacked with another one, the syscall handler
- associated with the new syscall should be considered as well. Defaults to False.
+ recursive (bool, optional): Whether, when the syscall is hijacked with another one, the syscall handler associated with the new syscall should be considered as well. Defaults to False.
**kwargs: (int, optional): The arguments to pass to the new syscall.
Returns:
- HandledSyscall: The HandledSyscall object.
+ SyscallHandler: The SyscallHandler object.
"""
return self._internal_debugger.hijack_syscall(original_syscall, new_syscall, recursive, **kwargs)
- def gdb(self: Debugger, open_in_new_process: bool = True) -> None:
- """Migrates the current debugging session to GDB."""
- self._internal_debugger.gdb(open_in_new_process)
+ def gdb(
+ self: Debugger,
+ migrate_breakpoints: bool = True,
+ open_in_new_process: bool = True,
+ blocking: bool = True,
+ ) -> GdbResumeEvent:
+ """Migrates the current debugging session to GDB.
+
+ Args:
+ migrate_breakpoints (bool): Whether to migrate over the breakpoints set in libdebug to GDB.
+ open_in_new_process (bool): Whether to attempt to open GDB in a new process instead of the current one.
+ blocking (bool): Whether to block the script until GDB is closed.
+ """
+ return self._internal_debugger.gdb(migrate_breakpoints, open_in_new_process, blocking)
- def r(self: Debugger) -> None:
+ def r(self: Debugger, redirect_pipes: bool = True) -> PipeManager | None:
"""Alias for the `run` method.
Starts the process and waits for it to stop.
+
+ Args:
+ redirect_pipes (bool): Whether to hook and redirect the pipes of the process to a PipeManager.
"""
- self._internal_debugger.run()
+ return self._internal_debugger.run(redirect_pipes)
def c(self: Debugger) -> None:
"""Alias for the `cont` method.
@@ -274,15 +304,11 @@ def bp(
Args:
position (int | bytes): The location of the breakpoint.
- hardware (bool, optional): Whether the breakpoint should be hardware-assisted or purely software.
- Defaults to False.
+ hardware (bool, optional): Whether the breakpoint should be hardware-assisted or purely software. Defaults to False.
condition (str, optional): The trigger condition for the breakpoint. Defaults to None.
length (int, optional): The length of the breakpoint. Only for watchpoints. Defaults to 1.
- callback (Callable[[ThreadContext, Breakpoint], None], optional): A callback to be called when the
- breakpoint is hit. Defaults to None.
- file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid"
- (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t.
- the "binary" map file).
+ callback (Callable[[ThreadContext, Breakpoint], None], optional): A callback to be called when the breakpoint is hit. Defaults to None.
+ file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file).
"""
return self._internal_debugger.breakpoint(position, hardware, condition, length, callback, file)
@@ -300,14 +326,10 @@ def wp(
Args:
position (int | bytes): The location of the breakpoint.
- condition (str, optional): The trigger condition for the watchpoint (either "w", "rw" or "x").
- Defaults to "w".
+ condition (str, optional): The trigger condition for the watchpoint (either "w", "rw" or "x"). Defaults to "w".
length (int, optional): The size of the word in being watched (1, 2, 4 or 8). Defaults to 1.
- callback (Callable[[ThreadContext, Breakpoint], None], optional): A callback to be called when the
- watchpoint is hit. Defaults to None.
- file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid"
- (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t.
- the "binary" map file).
+ callback (Callable[[ThreadContext, Breakpoint], None], optional): A callback to be called when the watchpoint is hit. Defaults to None.
+ file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file).
"""
return self._internal_debugger.breakpoint(
position,
@@ -355,7 +377,7 @@ def handled_syscalls(self: InternalDebugger) -> dict[int, SyscallHandler]:
"""Get the handled syscalls dictionary.
Returns:
- dict[int, HandledSyscall]: the handled syscalls dictionary.
+ dict[int, SyscallHandler]: the handled syscalls dictionary.
"""
return self._internal_debugger.handled_syscalls
@@ -364,10 +386,15 @@ def caught_signals(self: InternalDebugger) -> dict[int, SignalCatcher]:
"""Get the caught signals dictionary.
Returns:
- dict[int, CaughtSignal]: the caught signals dictionary.
+ dict[int, SignalCatcher]: the caught signals dictionary.
"""
return self._internal_debugger.caught_signals
+ @property
+ def maps(self: Debugger) -> MemoryMapList[MemoryMap]:
+ """Get the memory maps of the process."""
+ return self._internal_debugger.maps
+
@property
def pprint_syscalls(self: Debugger) -> bool:
"""Get the state of the pprint_syscalls flag.
@@ -399,9 +426,6 @@ def pprint_syscalls_context(self: Debugger, value: bool) -> ...:
Args:
value (bool): the value to set.
-
- Yields:
- None
"""
old_value = self.pprint_syscalls
self.pprint_syscalls = value
@@ -528,30 +552,363 @@ def fast_memory(self: Debugger, value: bool) -> None:
raise TypeError("fast_memory must be a boolean")
self._internal_debugger.fast_memory = value
- def __getattr__(self: Debugger, name: str) -> object:
- """This function is called when an attribute is not found in the `Debugger` object.
+ @property
+ def instruction_pointer(self: Debugger) -> int:
+ """Get the main thread's instruction pointer."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].instruction_pointer
+
+ @instruction_pointer.setter
+ def instruction_pointer(self: Debugger, value: int) -> None:
+ """Set the main thread's instruction pointer."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ self.threads[0].instruction_pointer = value
+
+ @property
+ def syscall_arg0(self: Debugger) -> int:
+ """Get the main thread's syscall argument 0."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].syscall_arg0
+
+ @syscall_arg0.setter
+ def syscall_arg0(self: Debugger, value: int) -> None:
+ """Set the main thread's syscall argument 0."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ self.threads[0].syscall_arg0 = value
+
+ @property
+ def syscall_arg1(self: Debugger) -> int:
+ """Get the main thread's syscall argument 1."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].syscall_arg1
+
+ @syscall_arg1.setter
+ def syscall_arg1(self: Debugger, value: int) -> None:
+ """Set the main thread's syscall argument 1."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ self.threads[0].syscall_arg1 = value
+
+ @property
+ def syscall_arg2(self: Debugger) -> int:
+ """Get the main thread's syscall argument 2."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].syscall_arg2
+
+ @syscall_arg2.setter
+ def syscall_arg2(self: Debugger, value: int) -> None:
+ """Set the main thread's syscall argument 2."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ self.threads[0].syscall_arg2 = value
+
+ @property
+ def syscall_arg3(self: Debugger) -> int:
+ """Get the main thread's syscall argument 3."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].syscall_arg3
+
+ @syscall_arg3.setter
+ def syscall_arg3(self: Debugger, value: int) -> None:
+ """Set the main thread's syscall argument 3."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ self.threads[0].syscall_arg3 = value
+
+ @property
+ def syscall_arg4(self: Debugger) -> int:
+ """Get the main thread's syscall argument 4."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].syscall_arg4
+
+ @syscall_arg4.setter
+ def syscall_arg4(self: Debugger, value: int) -> None:
+ """Set the main thread's syscall argument 4."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ self.threads[0].syscall_arg4 = value
+
+ @property
+ def syscall_arg5(self: Debugger) -> int:
+ """Get the main thread's syscall argument 5."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].syscall_arg5
+
+ @syscall_arg5.setter
+ def syscall_arg5(self: Debugger, value: int) -> None:
+ """Set the main thread's syscall argument 5."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ self.threads[0].syscall_arg5 = value
+
+ @property
+ def syscall_number(self: Debugger) -> int:
+ """Get the main thread's syscall number."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].syscall_number
+
+ @syscall_number.setter
+ def syscall_number(self: Debugger, value: int) -> None:
+ """Set the main thread's syscall number."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ self.threads[0].syscall_number = value
+
+ @property
+ def syscall_return(self: Debugger) -> int:
+ """Get the main thread's syscall return value."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].syscall_return
+
+ @syscall_return.setter
+ def syscall_return(self: Debugger, value: int) -> None:
+ """Set the main thread's syscall return value."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ self.threads[0].syscall_return = value
+
+ @property
+ def regs(self: Debugger) -> Registers:
+ """Get the main thread's registers."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].regs
+
+ @property
+ def dead(self: Debugger) -> bool:
+ """Whether the process is dead."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].dead
+
+ @property
+ def memory(self: Debugger) -> AbstractMemoryView:
+ """The memory view of the process."""
+ return self._internal_debugger.memory
+
+ @property
+ def mem(self: Debugger) -> AbstractMemoryView:
+ """Alias for the `memory` property.
+
+ Get the memory view of the process.
+ """
+ return self._internal_debugger.memory
- It is used to forward the call to the first `ThreadContext` object.
+ @property
+ def process_id(self: Debugger) -> int:
+ """The process ID."""
+ return self._internal_debugger.process_id
+
+ @property
+ def pid(self: Debugger) -> int:
+ """Alias for `process_id` property.
+
+ The process ID.
"""
+ return self._internal_debugger.process_id
+
+ @property
+ def thread_id(self: Debugger) -> int:
+ """The thread ID of the main thread."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].tid
+
+ @property
+ def tid(self: Debugger) -> int:
+ """Alias for `thread_id` property.
+
+ The thread ID of the main thread.
+ """
+ return self._thread_id
+
+ @property
+ def running(self: Debugger) -> bool:
+ """Whether the process is running."""
+ return self._internal_debugger.running
+
+ @property
+ def saved_ip(self: Debugger) -> int:
+ """Get the saved instruction pointer of the main thread."""
if not self.threads:
- raise AttributeError(f"'debugger has no attribute '{name}'")
+ raise ValueError("No threads available.")
+ return self.threads[0].saved_ip
- thread_context = self.threads[0]
+ @property
+ def exit_code(self: Debugger) -> int | None:
+ """The main thread's exit code."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].exit_code
+
+ @property
+ def exit_signal(self: Debugger) -> str | None:
+ """The main thread's exit signal."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].exit_signal
- # hasattr internally calls getattr, so we use this to avoid double access to the attribute
- # do not use None as default value, as it is a valid value
- if (attr := getattr(thread_context, name, self._sentinel)) == self._sentinel:
- raise AttributeError(f"'Debugger has no attribute '{name}'")
- return attr
+ @property
+ def signal(self: Debugger) -> str | None:
+ """The signal to be forwarded to the main thread."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].signal
- def __setattr__(self: Debugger, name: str, value: object) -> None:
- """This function is called when an attribute is set in the `Debugger` object.
+ @signal.setter
+ def signal(self: Debugger, signal: str | int) -> None:
+ """Set the signal to forward to the main thread."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ self.threads[0].signal = signal
- It is used to forward the call to the first `ThreadContext` object.
+ @property
+ def signal_number(self: Debugger) -> int | None:
+ """The signal number to be forwarded to the main thread."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].signal_number
+
+ def backtrace(self: Debugger, as_symbols: bool = False) -> list:
+ """Returns the current backtrace of the main thread.
+
+ Args:
+ as_symbols (bool, optional): Whether to return the backtrace as symbols
"""
- # First we check if the attribute is available in the `Debugger` object
- if hasattr(Debugger, name):
- super().__setattr__(name, value)
- else:
- thread_context = self.threads[0]
- setattr(thread_context, name, value)
+ if not self.threads:
+ raise ValueError("No threads available.")
+ return self.threads[0].backtrace(as_symbols)
+
+ def pprint_backtrace(self: Debugger) -> None:
+ """Pretty pints the current backtrace of the main thread."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ self.threads[0].pprint_backtrace()
+
+ def pprint_registers(self: Debugger) -> None:
+ """Pretty prints the main thread's registers."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ self.threads[0].pprint_registers()
+
+ def pprint_regs(self: Debugger) -> None:
+ """Alias for the `pprint_registers` method.
+
+ Pretty prints the main thread's registers.
+ """
+ self.pprint_registers()
+
+ def pprint_registers_all(self: Debugger) -> None:
+ """Pretty prints all the main thread's registers."""
+ if not self.threads:
+ raise ValueError("No threads available.")
+ self.threads[0].pprint_registers_all()
+
+ def pprint_regs_all(self: Debugger) -> None:
+ """Alias for the `pprint_registers_all` method.
+
+ Pretty prints all the main thread's registers.
+ """
+ self.pprint_registers_all()
+
+ def step(self: Debugger) -> None:
+ """Executes a single instruction of the process."""
+ self._internal_debugger.step(self)
+
+ def step_until(
+ self: Debugger,
+ position: int | str,
+ max_steps: int = -1,
+ file: str = "hybrid",
+ ) -> None:
+ """Executes instructions of the process until the specified location is reached.
+
+ Args:
+ position (int | bytes): The location to reach.
+ max_steps (int, optional): The maximum number of steps to execute. Defaults to -1.
+ file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file).
+ """
+ self._internal_debugger.step_until(self, position, max_steps, file)
+
+ def finish(self: Debugger, heuristic: str = "backtrace") -> None:
+ """Continues execution until the current function returns or the process stops.
+
+ The command requires a heuristic to determine the end of the function. The available heuristics are:
+ - `backtrace`: The debugger will place a breakpoint on the saved return address found on the stack and continue execution on all threads.
+ - `step-mode`: The debugger will step on the specified thread until the current function returns. This will be slower.
+
+ Args:
+ heuristic (str, optional): The heuristic to use. Defaults to "backtrace".
+ """
+ self._internal_debugger.finish(self, heuristic=heuristic)
+
+ def next(self: Debugger) -> None:
+ """Executes the next instruction of the process. If the instruction is a call, the debugger will continue until the called function returns."""
+ self._internal_debugger.next(self)
+
+ def si(self: Debugger) -> None:
+ """Alias for the `step` method.
+
+ Executes a single instruction of the process.
+ """
+ self._internal_debugger.step(self)
+
+ def su(
+ self: Debugger,
+ position: int | str,
+ max_steps: int = -1,
+ ) -> None:
+ """Alias for the `step_until` method.
+
+ Executes instructions of the process until the specified location is reached.
+
+ Args:
+ position (int | bytes): The location to reach.
+ max_steps (int, optional): The maximum number of steps to execute. Defaults to -1.
+ """
+ self._internal_debugger.step_until(self, position, max_steps)
+
+ def fin(self: Debugger, heuristic: str = "backtrace") -> None:
+ """Alias for the `finish` method. Continues execution until the current function returns or the process stops.
+
+ The command requires a heuristic to determine the end of the function. The available heuristics are:
+ - `backtrace`: The debugger will place a breakpoint on the saved return address found on the stack and continue execution on all threads.
+ - `step-mode`: The debugger will step on the specified thread until the current function returns. This will be slower.
+
+ Args:
+ heuristic (str, optional): The heuristic to use. Defaults to "backtrace".
+ """
+ self._internal_debugger.finish(self, heuristic)
+
+ def ni(self: Debugger) -> None:
+ """Alias for the `next` method. Executes the next instruction of the process. If the instruction is a call, the debugger will continue until the called function returns."""
+ self._internal_debugger.next(self)
+
+ def __repr__(self: Debugger) -> str:
+ """Return the string representation of the `Debugger` object."""
+ repr_str = "Debugger("
+ repr_str += f"argv = {self._internal_debugger.argv}, "
+ repr_str += f"aslr = {self._internal_debugger.aslr_enabled}, "
+ repr_str += f"env = {self._internal_debugger.env}, "
+ repr_str += f"escape_antidebug = {self._internal_debugger.escape_antidebug}, "
+ repr_str += f"continue_to_binary_entrypoint = {self._internal_debugger.autoreach_entrypoint}, "
+ repr_str += f"auto_interrupt_on_command = {self._internal_debugger.auto_interrupt_on_command}, "
+ repr_str += f"fast_memory = {self._internal_debugger.fast_memory}, "
+ repr_str += f"kill_on_exit = {self._internal_debugger.kill_on_exit})\n"
+ repr_str += f" Architecture: {self.arch}\n"
+ repr_str += " Threads:"
+ for thread in self.threads:
+ repr_str += f"\n ({thread.tid}, {'dead' if thread.dead else 'alive'}) "
+ repr_str += f"ip: {thread.instruction_pointer:#x}"
+ return repr_str
diff --git a/libdebug/debugger/internal_debugger.py b/libdebug/debugger/internal_debugger.py
index f49a9f71..a3d71972 100644
--- a/libdebug/debugger/internal_debugger.py
+++ b/libdebug/debugger/internal_debugger.py
@@ -15,18 +15,24 @@
from queue import Queue
from signal import SIGKILL, SIGSTOP, SIGTRAP
from subprocess import Popen
+from tempfile import NamedTemporaryFile
from threading import Thread, current_thread
from typing import TYPE_CHECKING
-import psutil
+from psutil import STATUS_ZOMBIE, Error, Process, ZombieProcess, process_iter
from libdebug.architectures.breakpoint_validator import validate_hardware_breakpoint
from libdebug.architectures.syscall_hijacker import SyscallHijacker
from libdebug.builtin.antidebug_syscall_handler import on_enter_ptrace, on_exit_ptrace
-from libdebug.builtin.pretty_print_syscall_handler import pprint_on_enter, pprint_on_exit
+from libdebug.builtin.pretty_print_syscall_handler import (
+ pprint_on_enter,
+ pprint_on_exit,
+)
from libdebug.data.breakpoint import Breakpoint
+from libdebug.data.gdb_resume_event import GdbResumeEvent
from libdebug.data.signal_catcher import SignalCatcher
from libdebug.data.syscall_handler import SyscallHandler
+from libdebug.data.terminals import TerminalTypes
from libdebug.debugger.internal_debugger_instance_manager import (
extend_internal_debugger,
link_to_internal_debugger,
@@ -37,6 +43,7 @@
from libdebug.memory.direct_memory_view import DirectMemoryView
from libdebug.memory.process_memory_manager import ProcessMemoryManager
from libdebug.state.resume_context import ResumeContext
+from libdebug.utils.ansi_escape_codes import ANSIColors
from libdebug.utils.arch_mappings import map_arch
from libdebug.utils.debugger_wrappers import (
background_alias,
@@ -44,13 +51,12 @@
change_state_function_thread,
)
from libdebug.utils.debugging_utils import (
- check_absolute_address,
normalize_and_validate_address,
resolve_symbol_in_maps,
)
+from libdebug.utils.elf_utils import get_all_symbols
from libdebug.utils.libcontext import libcontext
from libdebug.utils.platform_utils import get_platform_register_size
-from libdebug.utils.print_style import PrintStyle
from libdebug.utils.signal_utils import (
resolve_signal_name,
resolve_signal_number,
@@ -63,13 +69,18 @@
if TYPE_CHECKING:
from collections.abc import Callable
+ from typing import Any
+ from libdebug.commlink.pipe_manager import PipeManager
from libdebug.data.memory_map import MemoryMap
+ from libdebug.data.memory_map_list import MemoryMapList
from libdebug.data.registers import Registers
+ from libdebug.data.symbol import Symbol
+ from libdebug.data.symbol_list import SymbolList
+ from libdebug.debugger import Debugger
from libdebug.interfaces.debugging_interface import DebuggingInterface
from libdebug.memory.abstract_memory_view import AbstractMemoryView
from libdebug.state.thread_context import ThreadContext
- from libdebug.utils.pipe_manager import PipeManager
THREAD_TERMINATE = -1
GDB_GOBACK_LOCATION = str((Path(__file__).parent.parent / "utils" / "gdb.py").resolve())
@@ -103,16 +114,13 @@ class InternalDebugger:
"""A flag that indicates if the debugger should automatically interrupt the debugged process when a command is issued."""
breakpoints: dict[int, Breakpoint]
- """A dictionary of all the breakpoints set on the process.
- Key: the address of the breakpoint."""
+ """A dictionary of all the breakpoints set on the process. Key: the address of the breakpoint."""
handled_syscalls: dict[int, SyscallHandler]
- """A dictionary of all the syscall handled in the process.
- Key: the syscall number."""
+ """A dictionary of all the syscall handled in the process. Key: the syscall number."""
caught_signals: dict[int, SignalCatcher]
- """A dictionary of all the signals caught in the process.
- Key: the signal number."""
+ """A dictionary of all the signals caught in the process. Key: the signal number."""
signals_to_block: list[int]
"""The signals to not forward to the process."""
@@ -133,7 +141,7 @@ class InternalDebugger:
"""The PID of the debugged process."""
pipe_manager: PipeManager
- """The pipe manager used to communicate with the debugged process."""
+ """The PipeManager used to communicate with the debugged process."""
memory: AbstractMemoryView
"""The memory view of the debugged process."""
@@ -144,12 +152,21 @@ class InternalDebugger:
instanced: bool = False
"""Whether the process was started and has not been killed yet."""
+ is_debugging: bool = False
+ """Whether the debugger is currently debugging a process."""
+
pprint_syscalls: bool
"""A flag that indicates if the debugger should pretty print syscalls."""
resume_context: ResumeContext
"""Context that indicates if the debugger should resume the debugged process."""
+ debugger: Debugger
+ """The debugger object."""
+
+ stdin_settings_backup: list[Any]
+ """The backup of the stdin settings. Used to restore the original settings after possible conflicts due to the pipe manager interacactive mode."""
+
__polling_thread: Thread | None
"""The background thread used to poll the process for state change."""
@@ -162,6 +179,9 @@ class InternalDebugger:
_is_running: bool
"""The overall state of the debugged process. True if the process is running, False otherwise."""
+ _is_migrated_to_gdb: bool
+ """A flag that indicates if the debuggee was migrated to GDB."""
+
_fast_memory: DirectMemoryView
"""The memory view of the debugged process using the fast memory access method."""
@@ -187,8 +207,11 @@ def __init__(self: InternalDebugger) -> None:
self.process_id = 0
self.threads = []
self.instanced = False
+ self.is_debugging = False
self._is_running = False
+ self._is_migrated_to_gdb = False
self.resume_context = ResumeContext()
+ self.stdin_settings_backup = []
self.arch = map_arch(libcontext.platform)
self.kill_on_exit = True
self._process_memory_manager = ProcessMemoryManager()
@@ -210,6 +233,7 @@ def clear(self: InternalDebugger) -> None:
self.process_id = 0
self.threads.clear()
self.instanced = False
+ self.is_debugging = False
self._is_running = False
self.resume_context.clear()
@@ -242,8 +266,12 @@ def _background_invalid_call(self: InternalDebugger, *_: ..., **__: ...) -> None
"""Raises an error when an invalid call is made in background mode."""
raise RuntimeError("This method is not available in a callback.")
- def run(self: InternalDebugger) -> None:
- """Starts the process and waits for it to stop."""
+ def run(self: InternalDebugger, redirect_pipes: bool = True) -> PipeManager | None:
+ """Starts the process and waits for it to stop.
+
+ Args:
+ redirect_pipes (bool): Whether to hook and redirect the pipes of the process to a PipeManager.
+ """
if not self.argv:
raise RuntimeError("No binary file specified.")
@@ -255,7 +283,7 @@ def run(self: InternalDebugger) -> None:
f"File {self.argv[0]} is not executable.",
)
- if self.instanced:
+ if self.is_debugging:
liblog.debugger("Process already running, stopping it before restarting.")
self.kill()
if self.threads:
@@ -263,11 +291,12 @@ def run(self: InternalDebugger) -> None:
self.debugging_interface.reset()
self.instanced = True
+ self.is_debugging = True
if not self.__polling_thread_command_queue.empty():
raise RuntimeError("Polling thread command queue not empty.")
- self.__polling_thread_command_queue.put((self.__threaded_run, ()))
+ self.__polling_thread_command_queue.put((self.__threaded_run, (redirect_pipes,)))
self._join_and_check_status()
@@ -275,7 +304,7 @@ def run(self: InternalDebugger) -> None:
liblog.debugger("Enabling anti-debugging escape mechanism.")
self._enable_antidebug_escaping()
- if not self.pipe_manager:
+ if redirect_pipes and not self.pipe_manager:
raise RuntimeError("Something went wrong during pipe initialization.")
self._process_memory_manager.open(self.process_id)
@@ -284,7 +313,7 @@ def run(self: InternalDebugger) -> None:
def attach(self: InternalDebugger, pid: int) -> None:
"""Attaches to an existing process."""
- if self.instanced:
+ if self.is_debugging:
liblog.debugger("Process already running, stopping it before restarting.")
self.kill()
if self.threads:
@@ -292,25 +321,28 @@ def attach(self: InternalDebugger, pid: int) -> None:
self.debugging_interface.reset()
self.instanced = True
+ self.is_debugging = True
if not self.__polling_thread_command_queue.empty():
raise RuntimeError("Polling thread command queue not empty.")
self.__polling_thread_command_queue.put((self.__threaded_attach, (pid,)))
- self._process_memory_manager.open(self.process_id)
-
self._join_and_check_status()
+ self._process_memory_manager.open(self.process_id)
+
def detach(self: InternalDebugger) -> None:
"""Detaches from the process."""
- if not self.instanced:
+ if not self.is_debugging:
raise RuntimeError("Process not running, cannot detach.")
self._ensure_process_stopped()
self.__polling_thread_command_queue.put((self.__threaded_detach, ()))
+ self.is_debugging = False
+
self._join_and_check_status()
self._process_memory_manager.close()
@@ -318,6 +350,8 @@ def detach(self: InternalDebugger) -> None:
@background_alias(_background_invalid_call)
def kill(self: InternalDebugger) -> None:
"""Kills the process."""
+ if not self.is_debugging:
+ raise RuntimeError("No process currently debugged, cannot kill.")
try:
self._ensure_process_stopped()
except (OSError, RuntimeError):
@@ -329,6 +363,7 @@ def kill(self: InternalDebugger) -> None:
self.__polling_thread_command_queue.put((self.__threaded_kill, ()))
self.instanced = False
+ self.is_debugging = False
if self.pipe_manager:
self.pipe_manager.close()
@@ -342,12 +377,21 @@ def terminate(self: InternalDebugger) -> None:
This method should only be called to free up resources when the debugger object is no longer needed.
"""
if self.instanced and self.running:
- self.interrupt()
+ try:
+ self.interrupt()
+ except ProcessLookupError:
+ # The process has already been killed by someone or something else
+ liblog.debugger("Interrupting process failed: already terminated")
- if self.instanced:
- self.kill()
+ if self.instanced and self.is_debugging:
+ try:
+ self.kill()
+ except ProcessLookupError:
+ # The process has already been killed by someone or something else
+ liblog.debugger("Killing process failed: already terminated")
self.instanced = False
+ self.is_debugging = False
if self.__polling_thread is not None:
self.__polling_thread_command_queue.put((THREAD_TERMINATE, ()))
@@ -372,7 +416,7 @@ def cont(self: InternalDebugger) -> None:
@background_alias(_background_invalid_call)
def interrupt(self: InternalDebugger) -> None:
"""Interrupts the process."""
- if not self.instanced:
+ if not self.is_debugging:
raise RuntimeError("Process not running, cannot interrupt.")
# We have to ensure that at least one thread is alive before executing the method
@@ -390,7 +434,7 @@ def interrupt(self: InternalDebugger) -> None:
@background_alias(_background_invalid_call)
def wait(self: InternalDebugger) -> None:
"""Waits for the process to stop."""
- if not self.instanced:
+ if not self.is_debugging:
raise RuntimeError("Process not running, cannot wait.")
self._join_and_check_status()
@@ -404,29 +448,48 @@ def wait(self: InternalDebugger) -> None:
self._join_and_check_status()
- def maps(self: InternalDebugger) -> list[MemoryMap]:
+ @property
+ def maps(self: InternalDebugger) -> MemoryMapList[MemoryMap]:
"""Returns the memory maps of the process."""
self._ensure_process_stopped()
- return self.debugging_interface.maps()
+ return self.debugging_interface.get_maps()
@property
def memory(self: InternalDebugger) -> AbstractMemoryView:
"""The memory view of the debugged process."""
return self._fast_memory if self.fast_memory else self._slow_memory
- def print_maps(self: InternalDebugger) -> None:
+ def pprint_maps(self: InternalDebugger) -> None:
"""Prints the memory maps of the process."""
self._ensure_process_stopped()
- maps = self.maps()
- for memory_map in maps:
- if "x" in memory_map.permissions:
- print(f"{PrintStyle.RED}{memory_map}{PrintStyle.RESET}")
+ header = (
+ f"{'start':>18} "
+ f"{'end':>18} "
+ f"{'perm':>6} "
+ f"{'size':>8} "
+ f"{'offset':>8} "
+ f"{'backing_file':<20}"
+ )
+ print(header)
+ for memory_map in self.maps:
+ info = (
+ f"{memory_map.start:#18x} "
+ f"{memory_map.end:#18x} "
+ f"{memory_map.permissions:>6} "
+ f"{memory_map.size:#8x} "
+ f"{memory_map.offset:#8x} "
+ f"{memory_map.backing_file}"
+ )
+ if "rwx" in memory_map.permissions:
+ print(f"{ANSIColors.RED}{ANSIColors.UNDERLINE}{info}{ANSIColors.RESET}")
+ elif "x" in memory_map.permissions:
+ print(f"{ANSIColors.RED}{info}{ANSIColors.RESET}")
elif "w" in memory_map.permissions:
- print(f"{PrintStyle.YELLOW}{memory_map}{PrintStyle.RESET}")
+ print(f"{ANSIColors.YELLOW}{info}{ANSIColors.RESET}")
elif "r" in memory_map.permissions:
- print(f"{PrintStyle.GREEN}{memory_map}{PrintStyle.RESET}")
+ print(f"{ANSIColors.GREEN}{info}{ANSIColors.RESET}")
else:
- print(memory_map)
+ print(info)
@background_alias(_background_invalid_call)
@change_state_function_process
@@ -436,22 +499,18 @@ def breakpoint(
hardware: bool = False,
condition: str = "x",
length: int = 1,
- callback: None | Callable[[ThreadContext, Breakpoint], None] = None,
+ callback: None | bool | Callable[[ThreadContext, Breakpoint], None] = None,
file: str = "hybrid",
) -> Breakpoint:
"""Sets a breakpoint at the specified location.
Args:
position (int | bytes): The location of the breakpoint.
- hardware (bool, optional): Whether the breakpoint should be hardware-assisted or purely software.
- Defaults to False.
+ hardware (bool, optional): Whether the breakpoint should be hardware-assisted or purely software. Defaults to False.
condition (str, optional): The trigger condition for the breakpoint. Defaults to None.
length (int, optional): The length of the breakpoint. Only for watchpoints. Defaults to 1.
- callback (Callable[[ThreadContext, Breakpoint], None], optional): A callback to be called when the
- breakpoint is hit. Defaults to None.
- file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid"
- (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t.
- the "binary" map file).
+ callback (None | bool | Callable[[ThreadContext, Breakpoint], None], optional): A callback to be called when the breakpoint is hit. If True, an empty callback will be set. Defaults to None.
+ file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file).
"""
if isinstance(position, str):
address = self.resolve_symbol(position, file)
@@ -462,6 +521,11 @@ def breakpoint(
if condition != "x" and not hardware:
raise ValueError("Breakpoint condition is supported only for hardware watchpoints.")
+ if callback is True:
+
+ def callback(_: ThreadContext, __: Breakpoint) -> None:
+ pass
+
bp = Breakpoint(address, position, 0, hardware, callback, condition.lower(), length)
if hardware:
@@ -484,20 +548,18 @@ def breakpoint(
def catch_signal(
self: InternalDebugger,
signal: int | str,
- callback: None | Callable[[ThreadContext, SignalCatcher], None] = None,
+ callback: None | bool | Callable[[ThreadContext, SignalCatcher], None] = None,
recursive: bool = False,
) -> SignalCatcher:
"""Catch a signal in the target process.
Args:
- signal (int | str): The signal to catch.
- callback (Callable[[ThreadContext, CaughtSignal], None], optional): A callback to be called when the signal is
- caught. Defaults to None.
- recursive (bool, optional): Whether, when the signal is hijacked with another one, the signal catcher
- associated with the new signal should be considered as well. Defaults to False.
+ signal (int | str): The signal to catch. If "*", "ALL", "all" or -1 is passed, all signals will be caught.
+ callback (None | bool | Callable[[ThreadContext, SignalCatcher], None], optional): A callback to be called when the signal is caught. If True, an empty callback will be set. Defaults to None.
+ recursive (bool, optional): Whether, when the signal is hijacked with another one, the signal catcher associated with the new signal should be considered as well. Defaults to False.
Returns:
- CaughtSignal: The CaughtSignal object.
+ SignalCatcher: The SignalCatcher object.
"""
if isinstance(signal, str):
signal_number = resolve_signal_number(signal)
@@ -528,6 +590,11 @@ def catch_signal(
if not isinstance(recursive, bool):
raise TypeError("recursive must be a boolean")
+ if callback is True:
+
+ def callback(_: ThreadContext, __: SignalCatcher) -> None:
+ pass
+
catcher = SignalCatcher(signal_number, callback, recursive)
link_to_internal_debugger(catcher, self)
@@ -549,13 +616,12 @@ def hijack_signal(
"""Hijack a signal in the target process.
Args:
- original_signal (int | str): The signal to hijack.
+ original_signal (int | str): The signal to hijack. If "*", "ALL", "all" or -1 is passed, all signals will be hijacked.
new_signal (int | str): The signal to hijack the original signal with.
- recursive (bool, optional): Whether, when the signal is hijacked with another one, the signal catcher
- associated with the new signal should be considered as well. Defaults to False.
+ recursive (bool, optional): Whether, when the signal is hijacked with another one, the signal catcher associated with the new signal should be considered as well. Defaults to False.
Returns:
- CaughtSignal: The CaughtSignal object.
+ SignalCatcher: The SignalCatcher object.
"""
if isinstance(original_signal, str):
original_signal_number = resolve_signal_number(original_signal)
@@ -564,6 +630,9 @@ def hijack_signal(
new_signal_number = resolve_signal_number(new_signal) if isinstance(new_signal, str) else new_signal
+ if new_signal_number == -1:
+ raise ValueError("Cannot hijack a signal with the 'ALL' signal.")
+
if original_signal_number == new_signal_number:
raise ValueError(
"The original signal and the new signal must be different during hijacking.",
@@ -587,22 +656,29 @@ def handle_syscall(
"""Handle a syscall in the target process.
Args:
- syscall (int | str): The syscall name or number to handle.
- on_enter (Callable[[ThreadContext, HandledSyscall], None], optional): The callback to execute when the
- syscall is entered. Defaults to None.
- on_exit (Callable[[ThreadContext, HandledSyscall], None], optional): The callback to execute when the
- syscall is exited. Defaults to None.
- recursive (bool, optional): Whether, when the syscall is hijacked with another one, the syscall handler
- associated with the new syscall should be considered as well. Defaults to False.
+ syscall (int | str): The syscall name or number to handle. If "*", "ALL", "all", or -1 is passed, all syscalls will be handled.
+ on_enter (None | bool |Callable[[ThreadContext, SyscallHandler], None], optional): The callback to execute when the syscall is entered. If True, an empty callback will be set. Defaults to None.
+ on_exit (None | bool | Callable[[ThreadContext, SyscallHandler], None], optional): The callback to execute when the syscall is exited. If True, an empty callback will be set. Defaults to None.
+ recursive (bool, optional): Whether, when the syscall is hijacked with another one, the syscall handler associated with the new syscall should be considered as well. Defaults to False.
Returns:
- HandledSyscall: The HandledSyscall object.
+ SyscallHandler: The SyscallHandler object.
"""
syscall_number = resolve_syscall_number(self.arch, syscall) if isinstance(syscall, str) else syscall
if not isinstance(recursive, bool):
raise TypeError("recursive must be a boolean")
+ if on_enter is True:
+
+ def on_enter(_: ThreadContext, __: SyscallHandler) -> None:
+ pass
+
+ if on_exit is True:
+
+ def on_exit(_: ThreadContext, __: SyscallHandler) -> None:
+ pass
+
# Check if the syscall is already handled (by the user or by the pretty print handler)
if syscall_number in self.handled_syscalls:
handler = self.handled_syscalls[syscall_number]
@@ -646,14 +722,13 @@ def hijack_syscall(
"""Hijacks a syscall in the target process.
Args:
- original_syscall (int | str): The syscall name or number to hijack.
+ original_syscall (int | str): The syscall name or number to hijack. If "*", "ALL", "all" or -1 is passed, all syscalls will be hijacked.
new_syscall (int | str): The syscall name or number to hijack the original syscall with.
- recursive (bool, optional): Whether, when the syscall is hijacked with another one, the syscall handler
- associated with the new syscall should be considered as well. Defaults to False.
+ recursive (bool, optional): Whether, when the syscall is hijacked with another one, the syscall handler associated with the new syscall should be considered as well. Defaults to False.
**kwargs: (int, optional): The arguments to pass to the new syscall.
Returns:
- HandledSyscall: The HandledSyscall object.
+ SyscallHandler: The SyscallHandler object.
"""
if set(kwargs) - SyscallHijacker.allowed_args:
raise ValueError("Invalid keyword arguments in syscall hijack")
@@ -667,6 +742,9 @@ def hijack_syscall(
resolve_syscall_number(self.arch, new_syscall) if isinstance(new_syscall, str) else new_syscall
)
+ if new_syscall_number == -1:
+ raise ValueError("Cannot hijack a syscall with the 'ALL' syscall.")
+
if original_syscall_number == new_syscall_number:
raise ValueError(
"The original syscall and the new syscall must be different during hijacking.",
@@ -710,7 +788,12 @@ def hijack_syscall(
@background_alias(_background_invalid_call)
@change_state_function_process
- def gdb(self: InternalDebugger, open_in_new_process: bool = True) -> None:
+ def gdb(
+ self: InternalDebugger,
+ migrate_breakpoints: bool = True,
+ open_in_new_process: bool = True,
+ blocking: bool = True,
+ ) -> GdbResumeEvent:
"""Migrates the current debugging session to GDB."""
# TODO: not needed?
self.interrupt()
@@ -719,99 +802,205 @@ def gdb(self: InternalDebugger, open_in_new_process: bool = True) -> None:
self._join_and_check_status()
+ # Create the command file
+ command_file = self._craft_gdb_migration_file(migrate_breakpoints)
+
if open_in_new_process and libcontext.terminal:
- self._open_gdb_in_new_process()
- else:
- if open_in_new_process:
+ lambda_fun = self._open_gdb_in_new_process(command_file)
+ elif open_in_new_process:
+ self._auto_detect_terminal()
+ if not libcontext.terminal:
liblog.warning(
- "Cannot open in a new process. Please configure the terminal in libcontext.terminal.",
+ "Cannot auto-detect terminal. Please configure the terminal in libcontext.terminal. Opening gdb in the current shell.",
)
- self._open_gdb_in_shell()
+ lambda_fun = self._open_gdb_in_shell(command_file)
+ else:
+ lambda_fun = self._open_gdb_in_new_process(command_file)
+ else:
+ lambda_fun = self._open_gdb_in_shell(command_file)
- self.__polling_thread_command_queue.put((self.__threaded_migrate_from_gdb, ()))
+ resume_event = GdbResumeEvent(self, lambda_fun)
- self._join_and_check_status()
+ self._is_migrated_to_gdb = True
+
+ if blocking:
+ resume_event.join()
+ return None
+ else:
+ return resume_event
+
+ def _auto_detect_terminal(self: InternalDebugger) -> None:
+ """Auto-detects the terminal."""
+ try:
+ process = Process(self.process_id)
+ while process:
+ pname = process.name().lower()
+ if terminal_command := TerminalTypes.get_command(pname):
+ libcontext.terminal = terminal_command
+ liblog.debugger(f"Auto-detected terminal: {libcontext.terminal}")
+ process = process.parent()
+ except Error:
+ pass
+
+ def _craft_gdb_migration_command(self: InternalDebugger, migrate_breakpoints: bool) -> str:
+ """Crafts the command to migrate to GDB.
+
+ Args:
+ migrate_breakpoints (bool): Whether to migrate the breakpoints.
+
+ Returns:
+ str: The command to migrate to GDB.
+ """
+ gdb_command = f'/bin/gdb -q --pid {self.process_id} -ex "source {GDB_GOBACK_LOCATION} " -ex "ni" -ex "ni"'
+
+ if not migrate_breakpoints:
+ return gdb_command
- def _craft_gdb_migration_command(self: InternalDebugger) -> list[str]:
- """Crafts the command to migrate to GDB."""
- gdb_command = [
- "/bin/gdb",
- "-q",
- "--pid",
- str(self.process_id),
- "-ex",
- "source " + GDB_GOBACK_LOCATION,
- "-ex",
- "ni",
- "-ex",
- "ni",
- ]
-
- bp_args = []
for bp in self.breakpoints.values():
if bp.enabled:
- bp_args.append("-ex")
-
if bp.hardware and bp.condition == "rw":
- bp_args.append(f"awatch *(int{bp.length * 8}_t *) {bp.address:0x}")
+ gdb_command += f' -ex "awatch *(int{bp.length * 8}_t *) {bp.address:#x}"'
elif bp.hardware and bp.condition == "w":
- bp_args.append(f"watch *(int{bp.length * 8}_t *) {bp.address:0x}")
+ gdb_command += f' -ex "watch *(int{bp.length * 8}_t *) {bp.address:#x}"'
elif bp.hardware:
- bp_args.append("hb *" + hex(bp.address))
+ gdb_command += f' -ex "hb *{bp.address:#x}"'
else:
- bp_args.append("b *" + hex(bp.address))
+ gdb_command += f' -ex "b *{bp.address:#x}"'
if self.threads[0].instruction_pointer == bp.address and not bp.hardware:
# We have to enqueue an additional continue
- bp_args.append("-ex")
- bp_args.append("ni")
+ gdb_command += ' -ex "ni"'
- return gdb_command + bp_args
+ return gdb_command
- def _open_gdb_in_new_process(self: InternalDebugger) -> None:
- """Opens GDB in a new process following the configuration in libcontext.terminal."""
- args = self._craft_gdb_migration_command()
+ def _craft_gdb_migration_file(self: InternalDebugger, migrate_breakpoints: bool) -> str:
+ """Crafts the file to migrate to GDB.
- initial_pid = Popen(libcontext.terminal + args).pid
+ Args:
+ migrate_breakpoints (bool): Whether to migrate the breakpoints.
- os.waitpid(initial_pid, 0)
+ Returns:
+ str: The path to the file.
+ """
+ # Different terminals accept what to run in different ways. To make this work with all terminals, we need to
+ # create a temporary script that will run the command. This script will be executed by the terminal.
+ command = self._craft_gdb_migration_command(migrate_breakpoints)
+ with NamedTemporaryFile(delete=False, mode="w", suffix=".sh") as temp_file:
+ temp_file.write("#!/bin/bash\n")
+ temp_file.write(command)
+ script_path = temp_file.name
- liblog.debugger("Waiting for GDB process to terminate...")
+ # Make the script executable
+ Path.chmod(Path(script_path), 0o755)
+ return script_path
- for proc in psutil.process_iter():
- try:
- cmdline = proc.cmdline()
- except psutil.ZombieProcess:
- # This is a zombie process, which psutil tracks but we cannot interact with
- continue
+ def _open_gdb_in_new_process(self: InternalDebugger, script_path: str) -> None:
+ """Opens GDB in a new process following the configuration in libcontext.terminal.
- if args == cmdline:
- gdb_process = proc
- break
- else:
- raise RuntimeError("GDB process not found.")
+ Args:
+ script_path (str): The path to the script to run in the terminal.
+ """
+ # Create the command to open the terminal and run the script
+ command = [*libcontext.terminal, script_path]
- while gdb_process.is_running() and gdb_process.status() != psutil.STATUS_ZOMBIE:
- # As the GDB process is in a different group, we do not have the authority to wait on it
- # So we must keep polling it until it is no longer running
- pass
+ # Open GDB in a new terminal
+ terminal_pid = Popen(command).pid
+
+ # This is the command line that we are looking for
+ cmdline_target = ["/bin/bash", script_path]
+
+ self._wait_for_gdb(terminal_pid, cmdline_target)
- def _open_gdb_in_shell(self: InternalDebugger) -> None:
- """Open GDB in the current shell."""
+ def wait_for_termination() -> None:
+ liblog.debugger("Waiting for GDB process to terminate...")
+
+ for proc in process_iter():
+ try:
+ cmdline = proc.cmdline()
+ except ZombieProcess:
+ # This is a zombie process, which psutil tracks but we cannot interact with
+ continue
+
+ if cmdline_target == cmdline:
+ gdb_process = proc
+ break
+ else:
+ raise RuntimeError("GDB process not found.")
+
+ while gdb_process.is_running() and gdb_process.status() != STATUS_ZOMBIE:
+ # As the GDB process is in a different group, we do not have the authority to wait on it
+ # So we must keep polling it until it is no longer running
+ pass
+
+ return wait_for_termination
+
+ def _open_gdb_in_shell(self: InternalDebugger, script_path: str) -> None:
+ """Open GDB in the current shell.
+
+ Args:
+ script_path (str): The path to the script to run in the terminal.
+ """
gdb_pid = os.fork()
+
if gdb_pid == 0: # This is the child process.
- args = self._craft_gdb_migration_command()
- os.execv("/bin/gdb", args)
- else: # This is the parent process.
- # Parent ignores SIGINT, so only GDB (child) receives it
- signal.signal(signal.SIGINT, signal.SIG_IGN)
+ os.execv("/bin/bash", ["/bin/bash", script_path])
+ raise RuntimeError("Failed to execute GDB.")
+
+ # This is the parent process.
+ # Parent ignores SIGINT, so only GDB (child) receives it
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
+ def wait_for_termination() -> None:
# Wait for the child process to finish
os.waitpid(gdb_pid, 0)
# Reset the SIGINT behavior to default handling after child exits
signal.signal(signal.SIGINT, signal.SIG_DFL)
+ return wait_for_termination
+
+ def _wait_for_gdb(self: InternalDebugger, terminal_pid: int, cmdline_target: list[str]) -> None:
+ """Waits for GDB to open in the terminal.
+
+ Args:
+ terminal_pid (int): The PID of the terminal process.
+ cmdline_target (list[str]): The command line that we are looking for.
+ """
+ # We need to wait for GDB to open in the terminal. However, different terminals have different behaviors
+ # so we need to manually check if the terminal is still alive and if GDB has opened
+ waiting_for_gdb = True
+ terminal_alive = False
+ scan_after_terminal_death = 0
+ scan_after_terminal_death_max = 3
+ while waiting_for_gdb:
+ terminal_alive = False
+ for proc in process_iter():
+ try:
+ cmdline = proc.cmdline()
+ if cmdline == cmdline_target:
+ waiting_for_gdb = False
+ elif proc.pid == terminal_pid:
+ terminal_alive = True
+ except ZombieProcess:
+ # This is a zombie process, which psutil tracks but we cannot interact with
+ continue
+ if not terminal_alive and waiting_for_gdb and scan_after_terminal_death < scan_after_terminal_death_max:
+ # If the terminal has died, we need to wait a bit before we can be sure that GDB will not open.
+ # Indeed, some terminals take different steps to open GDB. We must be sure to refresh the list
+ # of processes. One extra iteration should be enough, but we will iterate more just to be sure.
+ scan_after_terminal_death += 1
+ elif not terminal_alive and waiting_for_gdb:
+ # If the terminal has died and GDB has not opened, we are sure that GDB will not open
+ raise RuntimeError("Failed to open GDB in terminal.")
+
+ def _resume_from_gdb(self: InternalDebugger) -> None:
+ """Resumes the process after migrating from GDB."""
+ self.__polling_thread_command_queue.put((self.__threaded_migrate_from_gdb, ()))
+
+ self._join_and_check_status()
+
+ self._is_migrated_to_gdb = False
+
def _background_step(self: InternalDebugger, thread: ThreadContext) -> None:
"""Executes a single instruction of the process.
@@ -847,9 +1036,7 @@ def _background_step_until(
thread (ThreadContext): The thread to step. Defaults to None.
position (int | bytes): The location to reach.
max_steps (int, optional): The maximum number of steps to execute. Defaults to -1.
- file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid"
- (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t.
- the "binary" map file).
+ file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file).
"""
if isinstance(position, str):
address = self.resolve_symbol(position, file)
@@ -873,9 +1060,7 @@ def step_until(
thread (ThreadContext): The thread to step. Defaults to None.
position (int | bytes): The location to reach.
max_steps (int, optional): The maximum number of steps to execute. Defaults to -1.
- file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid"
- (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t.
- the "binary" map file).
+ file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file).
"""
if isinstance(position, str):
address = self.resolve_symbol(position, file)
@@ -1071,10 +1256,10 @@ def resolve_address(
if skip_absolute_address_validation and backing_file == "absolute":
return address
- maps = self.debugging_interface.maps()
+ maps = self.maps
if backing_file in ["hybrid", "absolute"]:
- if check_absolute_address(address, maps):
+ if maps.filter(address):
# If the address is absolute, we can return it directly
return address
elif backing_file == "absolute":
@@ -1089,29 +1274,9 @@ def resolve_address(
liblog.warning(
f"No backing file specified and no corresponding absolute address found for {hex(address)}. Assuming {backing_file}.",
)
- elif backing_file == (full_backing_path := self._process_full_path) or backing_file in [
- "binary",
- self._process_name,
- ]:
- backing_file = full_backing_path
-
- filtered_maps = []
- unique_files = set()
- for vmap in maps:
- if backing_file in vmap.backing_file:
- filtered_maps.append(vmap)
- unique_files.add(vmap.backing_file)
+ filtered_maps = maps.filter(backing_file)
- if len(unique_files) > 1:
- raise ValueError(
- f"The substring {backing_file} is present in multiple, different backing files. The address resolution cannot be accurate. The matching backing files are: {', '.join(unique_files)}.",
- )
-
- if not filtered_maps:
- raise ValueError(
- f"The specified string {backing_file} does not correspond to any backing file. The available backing files are: {', '.join(set(vmap.backing_file for vmap in maps))}.",
- )
return normalize_and_validate_address(address, filtered_maps)
def resolve_symbol(self: InternalDebugger, symbol: str, backing_file: str) -> int:
@@ -1124,8 +1289,6 @@ def resolve_symbol(self: InternalDebugger, symbol: str, backing_file: str) -> in
Returns:
int: The address of the symbol.
"""
- maps = self.debugging_interface.maps()
-
if backing_file == "absolute":
raise ValueError("Cannot use `absolute` backing file with symbols.")
@@ -1133,39 +1296,33 @@ def resolve_symbol(self: InternalDebugger, symbol: str, backing_file: str) -> in
# If no explicit backing file is specified, we have to assume it is in the main map
backing_file = self._process_full_path
liblog.debugger(f"No backing file specified for the symbol {symbol}. Assuming {backing_file}.")
- elif backing_file == (full_backing_path := self._process_full_path) or backing_file in [
- "binary",
- self._process_name,
- ]:
- backing_file = full_backing_path
-
- filtered_maps = []
- unique_files = set()
-
- for vmap in maps:
- if backing_file in vmap.backing_file:
- filtered_maps.append(vmap)
- unique_files.add(vmap.backing_file)
-
- if len(unique_files) > 1:
- raise ValueError(
- f"The substring {backing_file} is present in multiple, different backing files. The address resolution cannot be accurate. The matching backing files are: {', '.join(unique_files)}.",
- )
+ elif backing_file in ["binary", self._process_name]:
+ backing_file = self._process_full_path
- if not filtered_maps:
- raise ValueError(
- f"The specified string {backing_file} does not correspond to any backing file. The available backing files are: {', '.join(set(vmap.backing_file for vmap in maps))}.",
- )
+ filtered_maps = self.maps.filter(backing_file)
return resolve_symbol_in_maps(symbol, filtered_maps)
+ @property
+ def symbols(self: InternalDebugger) -> SymbolList[Symbol]:
+ """Get the symbols of the process."""
+ self._ensure_process_stopped()
+ backing_files = {vmap.backing_file for vmap in self.maps}
+ with extend_internal_debugger(self):
+ return get_all_symbols(backing_files)
+
def _background_ensure_process_stopped(self: InternalDebugger) -> None:
"""Validates the state of the process."""
- # In background mode, there shouldn't be anything to do here
+ # There is no case where this should ever happen, but...
+ if self._is_migrated_to_gdb:
+ raise RuntimeError("Cannot execute this command after migrating to GDB.")
@background_alias(_background_ensure_process_stopped)
def _ensure_process_stopped(self: InternalDebugger) -> None:
"""Validates the state of the process."""
+ if self._is_migrated_to_gdb:
+ raise RuntimeError("Cannot execute this command after migrating to GDB.")
+
if not self.running:
return
@@ -1234,9 +1391,9 @@ def _process_name(self: InternalDebugger) -> str:
with Path(f"/proc/{self.process_id}/comm").open() as f:
return f.read().strip()
- def __threaded_run(self: InternalDebugger) -> None:
+ def __threaded_run(self: InternalDebugger, redirect_pipes: bool) -> None:
liblog.debugger("Starting process %s.", self.argv[0])
- self.debugging_interface.run()
+ self.debugging_interface.run(redirect_pipes)
self.set_stopped()
@@ -1368,7 +1525,7 @@ def __threaded_flush_fp_registers(self: InternalDebugger, registers: Registers)
@background_alias(__threaded_peek_memory)
def _peek_memory(self: InternalDebugger, address: int) -> bytes:
"""Reads memory from the process."""
- if not self.instanced:
+ if not self.is_debugging:
raise RuntimeError("Process not running, cannot access memory.")
if self.running:
@@ -1397,7 +1554,7 @@ def _peek_memory(self: InternalDebugger, address: int) -> bytes:
def _fast_read_memory(self: InternalDebugger, address: int, size: int) -> bytes:
"""Reads memory from the process."""
- if not self.instanced:
+ if not self.is_debugging:
raise RuntimeError("Process not running, cannot access memory.")
if self.running:
@@ -1414,7 +1571,7 @@ def _fast_read_memory(self: InternalDebugger, address: int, size: int) -> bytes:
@background_alias(__threaded_poke_memory)
def _poke_memory(self: InternalDebugger, address: int, data: bytes) -> None:
"""Writes memory to the process."""
- if not self.instanced:
+ if not self.is_debugging:
raise RuntimeError("Process not running, cannot access memory.")
if self.running:
@@ -1434,7 +1591,7 @@ def _poke_memory(self: InternalDebugger, address: int, data: bytes) -> None:
def _fast_write_memory(self: InternalDebugger, address: int, data: bytes) -> None:
"""Writes memory to the process."""
- if not self.instanced:
+ if not self.is_debugging:
raise RuntimeError("Process not running, cannot access memory.")
if self.running:
@@ -1451,7 +1608,7 @@ def _fast_write_memory(self: InternalDebugger, address: int, data: bytes) -> Non
@background_alias(__threaded_fetch_fp_registers)
def _fetch_fp_registers(self: InternalDebugger, registers: Registers) -> None:
"""Fetches the floating point registers of a thread."""
- if not self.instanced:
+ if not self.is_debugging:
raise RuntimeError("Process not running, cannot read floating-point registers.")
self._ensure_process_stopped()
@@ -1465,7 +1622,7 @@ def _fetch_fp_registers(self: InternalDebugger, registers: Registers) -> None:
@background_alias(__threaded_flush_fp_registers)
def _flush_fp_registers(self: InternalDebugger, registers: Registers) -> None:
"""Flushes the floating point registers of a thread."""
- if not self.instanced:
+ if not self.is_debugging:
raise RuntimeError("Process not running, cannot write floating-point registers.")
self._ensure_process_stopped()
diff --git a/libdebug/debugger/internal_debugger_holder.py b/libdebug/debugger/internal_debugger_holder.py
index 8a2d6da7..52cd49c3 100644
--- a/libdebug/debugger/internal_debugger_holder.py
+++ b/libdebug/debugger/internal_debugger_holder.py
@@ -7,7 +7,9 @@
from __future__ import annotations
import atexit
+import sys
from dataclasses import dataclass, field
+from termios import TCSANOW, tcsetattr
from threading import Lock
from typing import TYPE_CHECKING
from weakref import WeakKeyDictionary
@@ -35,6 +37,13 @@ def _cleanup_internal_debugger() -> None:
for debugger in set(internal_debugger_holder.internal_debuggers.values()):
debugger: InternalDebugger
+ # Restore the original stdin settings, just in case
+ try:
+ if debugger.stdin_settings_backup:
+ tcsetattr(sys.stdin.fileno(), TCSANOW, debugger.stdin_settings_backup)
+ except Exception as e:
+ liblog.debugger(f"Error while restoring the original stdin settings: {e}")
+
if debugger.instanced and debugger.kill_on_exit:
try:
debugger.interrupt()
diff --git a/libdebug/interfaces/debugging_interface.py b/libdebug/interfaces/debugging_interface.py
index 4159032e..6b3c5bce 100644
--- a/libdebug/interfaces/debugging_interface.py
+++ b/libdebug/interfaces/debugging_interface.py
@@ -12,6 +12,7 @@
if TYPE_CHECKING:
from libdebug.data.breakpoint import Breakpoint
from libdebug.data.memory_map import MemoryMap
+ from libdebug.data.memory_map_list import MemoryMapList
from libdebug.data.registers import Registers
from libdebug.data.signal_catcher import SignalCatcher
from libdebug.data.syscall_handler import SyscallHandler
@@ -30,8 +31,12 @@ def reset(self: DebuggingInterface) -> None:
"""Resets the state of the interface."""
@abstractmethod
- def run(self: DebuggingInterface) -> None:
- """Runs the specified process."""
+ def run(self: DebuggingInterface, redirect_pipes: bool) -> None:
+ """Runs the specified process.
+
+ Args:
+ redirect_pipes (bool): Whether to hook and redirect the pipes of the process to a PipeManager.
+ """
@abstractmethod
def attach(self: DebuggingInterface, pid: int) -> None:
@@ -96,12 +101,12 @@ def finish(self: DebuggingInterface, thread: ThreadContext, heuristic: str) -> N
heuristic (str, optional): The heuristic to use. Defaults to "backtrace".
"""
+ @abstractmethod
def next(self: DebuggingInterface, thread: ThreadContext) -> None:
- """Executes the next instruction of the process. If the instruction is a call, the debugger will continue until the called function returns.
- """
+ """Executes the next instruction of the process. If the instruction is a call, the debugger will continue until the called function returns."""
@abstractmethod
- def maps(self: DebuggingInterface) -> list[MemoryMap]:
+ def get_maps(self: DebuggingInterface) -> MemoryMapList[MemoryMap]:
"""Returns the memory maps of the process."""
@abstractmethod
diff --git a/libdebug/libdebug.py b/libdebug/libdebug.py
index 5d5bcf88..18ec6e8b 100644
--- a/libdebug/libdebug.py
+++ b/libdebug/libdebug.py
@@ -7,12 +7,12 @@
from libdebug.debugger.debugger import Debugger
from libdebug.debugger.internal_debugger import InternalDebugger
-from libdebug.utils.elf_utils import elf_architecture
+from libdebug.utils.elf_utils import elf_architecture, resolve_argv_path
def debugger(
argv: str | list[str] = [],
- aslr: bool = False,
+ aslr: bool = True,
env: dict[str, str] | None = None,
escape_antidebug: bool = False,
continue_to_binary_entrypoint: bool = True,
@@ -23,8 +23,8 @@ def debugger(
"""This function is used to create a new `Debugger` object. It returns a `Debugger` object.
Args:
- argv (str | list[str], optional): The location of the binary to debug, and any additional arguments to pass to it.
- aslr (bool, optional): Whether to enable ASLR. Defaults to False.
+ argv (str | list[str], optional): The location of the binary to debug and any arguments to pass to it.
+ aslr (bool, optional): Whether to enable ASLR. Defaults to True.
env (dict[str, str], optional): The environment variables to use. Defaults to the same environment of the debugging script.
escape_antidebug (bool): Whether to automatically attempt to patch antidebugger detectors based on the ptrace syscall.
continue_to_binary_entrypoint (bool, optional): Whether to automatically continue to the binary entrypoint. Defaults to True.
@@ -36,7 +36,9 @@ def debugger(
Debugger: The `Debugger` object.
"""
if isinstance(argv, str):
- argv = [argv]
+ argv = [resolve_argv_path(argv)]
+ elif argv:
+ argv[0] = resolve_argv_path(argv[0])
internal_debugger = InternalDebugger()
internal_debugger.argv = argv
@@ -51,6 +53,8 @@ def debugger(
debugger = Debugger()
debugger.post_init_(internal_debugger)
+ internal_debugger.debugger = debugger
+
# If we are attaching, we assume the architecture is the same as the current platform
if argv:
debugger.arch = elf_architecture(argv[0])
diff --git a/libdebug/liblog.py b/libdebug/liblog.py
index 0e377047..da764e27 100644
--- a/libdebug/liblog.py
+++ b/libdebug/liblog.py
@@ -8,7 +8,7 @@
import logging
-from libdebug.utils.print_style import PrintStyle
+from libdebug.utils.ansi_escape_codes import ANSIColors
class LibLog:
@@ -72,7 +72,7 @@ def debugger(self: LibLog, message: str, *args: str, **kwargs: str) -> None:
*args: positional arguments to pass to the logger.
**kwargs: keyword arguments to pass to the logger.
"""
- header = f"[{PrintStyle.RED}DEBUGGER{PrintStyle.DEFAULT_COLOR}]"
+ header = f"[{ANSIColors.RED}DEBUGGER{ANSIColors.DEFAULT_COLOR}]"
self.debugger_logger.debug(f"{header} {message}", *args, **kwargs)
def pipe(self: LibLog, message: str, *args: str, **kwargs: str) -> None:
@@ -83,7 +83,7 @@ def pipe(self: LibLog, message: str, *args: str, **kwargs: str) -> None:
*args: positional arguments to pass to the logger.
**kwargs: keyword arguments to pass to the logger.
"""
- header = f"[{PrintStyle.BLUE}PIPE{PrintStyle.DEFAULT_COLOR}]"
+ header = f"[{ANSIColors.BLUE}PIPE{ANSIColors.DEFAULT_COLOR}]"
self.pipe_logger.debug(f"{header} {message}", *args, **kwargs)
def info(self: LibLog, message: str, *args: str, **kwargs: str) -> None:
@@ -94,7 +94,7 @@ def info(self: LibLog, message: str, *args: str, **kwargs: str) -> None:
*args: positional arguments to pass to the logger.
**kwargs: keyword arguments to pass to the logger.
"""
- header = f"[{PrintStyle.GREEN}INFO{PrintStyle.DEFAULT_COLOR}]"
+ header = f"[{ANSIColors.GREEN}INFO{ANSIColors.DEFAULT_COLOR}]"
self.general_logger.info(f"{header} {message}", *args, **kwargs)
def warning(self: LibLog, message: str, *args: str, **kwargs: str) -> None:
@@ -105,7 +105,7 @@ def warning(self: LibLog, message: str, *args: str, **kwargs: str) -> None:
*args: positional arguments to pass to the logger.
**kwargs: keyword arguments to pass to the logger.
"""
- header = f"[{PrintStyle.BRIGHT_YELLOW}WARNING{PrintStyle.DEFAULT_COLOR}]"
+ header = f"[{ANSIColors.BRIGHT_YELLOW}WARNING{ANSIColors.DEFAULT_COLOR}]"
self.general_logger.warning(f"{header} {message}", *args, **kwargs)
def error(self: LibLog, message: str, *args: str, **kwargs: str) -> None:
@@ -116,7 +116,7 @@ def error(self: LibLog, message: str, *args: str, **kwargs: str) -> None:
*args: positional arguments to pass to the logger.
**kwargs: keyword arguments to pass to the logger.
"""
- header = f"[{PrintStyle.RED}ERROR{PrintStyle.DEFAULT_COLOR}]"
+ header = f"[{ANSIColors.RED}ERROR{ANSIColors.DEFAULT_COLOR}]"
self.general_logger.error(f"{header} {message}", *args, **kwargs)
diff --git a/libdebug/memory/abstract_memory_view.py b/libdebug/memory/abstract_memory_view.py
index e9d66b21..2fae64e8 100644
--- a/libdebug/memory/abstract_memory_view.py
+++ b/libdebug/memory/abstract_memory_view.py
@@ -6,11 +6,13 @@
from __future__ import annotations
+import sys
from abc import ABC, abstractmethod
from collections.abc import MutableSequence
from libdebug.debugger.internal_debugger_instance_manager import provide_internal_debugger
from libdebug.liblog import liblog
+from libdebug.utils.search_utils import find_all_overlapping_occurrences
class AbstractMemoryView(MutableSequence, ABC):
@@ -22,7 +24,6 @@ class AbstractMemoryView(MutableSequence, ABC):
def __init__(self: AbstractMemoryView) -> None:
"""Initializes the MemoryView."""
self._internal_debugger = provide_internal_debugger(self)
- self.maps_provider = self._internal_debugger.debugging_interface.maps
@abstractmethod
def read(self: AbstractMemoryView, address: int, size: int) -> bytes:
@@ -45,6 +46,83 @@ def write(self: AbstractMemoryView, address: int, data: bytes) -> None:
data (bytes): The data to write.
"""
+ def find(
+ self: AbstractMemoryView,
+ value: bytes | str | int,
+ file: str = "all",
+ start: int | None = None,
+ end: int | None = None,
+ ) -> list[int]:
+ """Searches for the given value in the specified memory maps of the process.
+
+ The start and end addresses can be used to limit the search to a specific range.
+ If not specified, the search will be performed on the whole memory map.
+
+ Args:
+ value (bytes | str | int): The value to search for.
+ file (str): The backing file to search the value in. Defaults to "all", which means all memory.
+ start (int | None): The start address of the search. Defaults to None.
+ end (int | None): The end address of the search. Defaults to None.
+
+ Returns:
+ list[int]: A list of offset where the value was found.
+ """
+ if isinstance(value, str):
+ value = value.encode()
+ elif isinstance(value, int):
+ value = value.to_bytes(1, sys.byteorder)
+
+ occurrences = []
+ if file == "all" and start is None and end is None:
+ for vmap in self._internal_debugger.maps:
+ liblog.debugger(f"Searching in {vmap.backing_file}...")
+ try:
+ memory_content = self.read(vmap.start, vmap.end - vmap.start)
+ except (OSError, OverflowError):
+ # There are some memory regions that cannot be read, such as [vvar], [vdso], etc.
+ continue
+ occurrences += find_all_overlapping_occurrences(value, memory_content, vmap.start)
+ elif file == "all" and start is not None and end is None:
+ for vmap in self._internal_debugger.maps:
+ if vmap.end > start:
+ liblog.debugger(f"Searching in {vmap.backing_file}...")
+ read_start = max(vmap.start, start)
+ try:
+ memory_content = self.read(read_start, vmap.end - read_start)
+ except (OSError, OverflowError):
+ # There are some memory regions that cannot be read, such as [vvar], [vdso], etc.
+ continue
+ occurrences += find_all_overlapping_occurrences(value, memory_content, read_start)
+ elif file == "all" and start is None and end is not None:
+ for vmap in self._internal_debugger.maps:
+ if vmap.start < end:
+ liblog.debugger(f"Searching in {vmap.backing_file}...")
+ read_end = min(vmap.end, end)
+ try:
+ memory_content = self.read(vmap.start, read_end - vmap.start)
+ except (OSError, OverflowError):
+ # There are some memory regions that cannot be read, such as [vvar], [vdso], etc.
+ continue
+ occurrences += find_all_overlapping_occurrences(value, memory_content, vmap.start)
+ elif file == "all" and start is not None and end is not None:
+ # Search in the specified range, hybrid mode
+ start = self._internal_debugger.resolve_address(start, "hybrid", True)
+ end = self._internal_debugger.resolve_address(end, "hybrid", True)
+ liblog.debugger(f"Searching in the range {start:#x}-{end:#x}...")
+ memory_content = self.read(start, end - start)
+ occurrences = find_all_overlapping_occurrences(value, memory_content, start)
+ else:
+ maps = self._internal_debugger.maps.filter(file)
+ start = self._internal_debugger.resolve_address(start, file, True) if start is not None else maps[0].start
+ end = self._internal_debugger.resolve_address(end, file, True) if end is not None else maps[-1].end - 1
+
+ liblog.debugger(f"Searching in the range {start:#x}-{end:#x}...")
+ memory_content = self.read(start, end - start)
+
+ occurrences = find_all_overlapping_occurrences(value, memory_content, start)
+
+ return occurrences
+
def __getitem__(self: AbstractMemoryView, key: int | slice | str | tuple) -> bytes:
"""Read from memory, either a single byte or a byte string.
@@ -73,9 +151,7 @@ def _manage_memory_read_type(
Args:
key (int | slice | str | tuple): The key to read from memory.
- file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid"
- (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t.
- the "binary" map file).
+ file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file).
"""
if isinstance(key, int):
address = self._internal_debugger.resolve_address(key, file, skip_absolute_address_validation=True)
@@ -160,9 +236,7 @@ def _manage_memory_write_type(
Args:
key (int | slice | str | tuple): The key to read from memory.
value (bytes): The value to write.
- file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid"
- (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t.
- the "binary" map file).
+ file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file).
"""
if isinstance(key, int):
address = self._internal_debugger.resolve_address(key, file, skip_absolute_address_validation=True)
diff --git a/libdebug/ptrace/ptrace_interface.py b/libdebug/ptrace/ptrace_interface.py
index f4246f82..4eb5fa77 100644
--- a/libdebug/ptrace/ptrace_interface.py
+++ b/libdebug/ptrace/ptrace_interface.py
@@ -10,12 +10,14 @@
import os
import pty
import tty
+from fcntl import F_GETFL, F_SETFL, fcntl
from pathlib import Path
from typing import TYPE_CHECKING
-from libdebug.architectures.register_helper import register_holder_provider
from libdebug.architectures.call_utilities_provider import call_utilities_provider
+from libdebug.architectures.register_helper import register_holder_provider
from libdebug.cffi import _ptrace_cffi
+from libdebug.commlink.pipe_manager import PipeManager
from libdebug.data.breakpoint import Breakpoint
from libdebug.debugger.internal_debugger_instance_manager import (
extend_internal_debugger,
@@ -27,10 +29,10 @@
from libdebug.state.thread_context import ThreadContext
from libdebug.utils.debugging_utils import normalize_and_validate_address
from libdebug.utils.elf_utils import get_entry_point
-from libdebug.utils.pipe_manager import PipeManager
from libdebug.utils.process_utils import (
disable_self_aslr,
get_process_maps,
+ get_process_tasks,
invalidate_process_cache,
)
@@ -49,6 +51,7 @@
if TYPE_CHECKING:
from libdebug.data.memory_map import MemoryMap
+ from libdebug.data.memory_map_list import MemoryMapList
from libdebug.data.registers import Registers
from libdebug.data.signal_catcher import SignalCatcher
from libdebug.data.syscall_handler import SyscallHandler
@@ -97,7 +100,7 @@ def _set_options(self: PtraceInterface) -> None:
"""Sets the tracer options."""
self.lib_trace.ptrace_set_options(self.process_id)
- def run(self: PtraceInterface) -> None:
+ def run(self: PtraceInterface, redirect_pipes: bool) -> None:
"""Runs the specified process."""
if not self._disabled_aslr and not self._internal_debugger.aslr_enabled:
disable_self_aslr()
@@ -112,15 +115,38 @@ def run(self: PtraceInterface) -> None:
with extend_internal_debugger(self):
self.status_handler = PtraceStatusHandler()
- # Creating pipes for stdin, stdout, stderr
- self.stdin_read, self.stdin_write = os.pipe()
- self.stdout_read, self.stdout_write = pty.openpty()
- self.stderr_read, self.stderr_write = pty.openpty()
-
- # Setting stdout, stderr to raw mode to avoid terminal control codes interfering with the
- # output
- tty.setraw(self.stdout_read)
- tty.setraw(self.stderr_read)
+ file_actions = []
+
+ if redirect_pipes:
+ # Creating pipes for stdin, stdout, stderr
+ self.stdin_read, self.stdin_write = os.pipe()
+ self.stdout_read, self.stdout_write = pty.openpty()
+ self.stderr_read, self.stderr_write = pty.openpty()
+
+ # Setting stdout, stderr to raw mode to avoid terminal control codes interfering with the
+ # output
+ tty.setraw(self.stdout_read)
+ tty.setraw(self.stderr_read)
+
+ flags = fcntl(self.stdout_read, F_GETFL)
+ fcntl(self.stdout_read, F_SETFL, flags | os.O_NONBLOCK)
+
+ flags = fcntl(self.stderr_read, F_GETFL)
+ fcntl(self.stderr_read, F_SETFL, flags | os.O_NONBLOCK)
+
+ file_actions.extend(
+ [
+ (POSIX_SPAWN_CLOSE, self.stdin_write),
+ (POSIX_SPAWN_CLOSE, self.stdout_read),
+ (POSIX_SPAWN_CLOSE, self.stderr_read),
+ (POSIX_SPAWN_DUP2, self.stdin_read, 0),
+ (POSIX_SPAWN_DUP2, self.stdout_write, 1),
+ (POSIX_SPAWN_DUP2, self.stderr_write, 2),
+ (POSIX_SPAWN_CLOSE, self.stdin_read),
+ (POSIX_SPAWN_CLOSE, self.stdout_write),
+ (POSIX_SPAWN_CLOSE, self.stderr_write),
+ ]
+ )
# argv[1] is the length of the custom environment variables
# argv[2:2 + env_len] is the custom environment variables
@@ -144,17 +170,7 @@ def run(self: PtraceInterface) -> None:
JUMPSTART_LOCATION,
argv,
os.environ,
- file_actions=[
- (POSIX_SPAWN_CLOSE, self.stdin_write),
- (POSIX_SPAWN_CLOSE, self.stdout_read),
- (POSIX_SPAWN_CLOSE, self.stderr_read),
- (POSIX_SPAWN_DUP2, self.stdin_read, 0),
- (POSIX_SPAWN_DUP2, self.stdout_write, 1),
- (POSIX_SPAWN_DUP2, self.stderr_write, 2),
- (POSIX_SPAWN_CLOSE, self.stdin_read),
- (POSIX_SPAWN_CLOSE, self.stdout_write),
- (POSIX_SPAWN_CLOSE, self.stderr_write),
- ],
+ file_actions=file_actions,
setpgroup=0,
)
@@ -164,7 +180,19 @@ def run(self: PtraceInterface) -> None:
self.register_new_thread(child_pid)
continue_to_entry_point = self._internal_debugger.autoreach_entrypoint
self._setup_parent(continue_to_entry_point)
- self._internal_debugger.pipe_manager = self._setup_pipe()
+
+ if redirect_pipes:
+ self._internal_debugger.pipe_manager = self._setup_pipe()
+ else:
+ self._internal_debugger.pipe_manager = None
+
+ # https://stackoverflow.com/questions/58918188/why-is-stdin-not-propagated-to-child-process-of-different-process-group
+ # We need to set the foreground process group to the child process group, otherwise the child process
+ # will not receive the input from the terminal
+ try:
+ os.tcsetpgrp(0, child_pid)
+ except OSError as e:
+ liblog.debugger("Failed to set the foreground process group: %r", e)
def attach(self: PtraceInterface, pid: int) -> None:
"""Attaches to the specified process.
@@ -176,19 +204,32 @@ def attach(self: PtraceInterface, pid: int) -> None:
with extend_internal_debugger(self):
self.status_handler = PtraceStatusHandler()
- res = self.lib_trace.ptrace_attach(pid)
- if res == -1:
- errno_val = self.ffi.errno
- raise OSError(errno_val, errno.errorcode[errno_val])
+ # Attach to all the tasks of the process
+ self._attach_to_all_tasks(pid)
self.process_id = pid
self.detached = False
self._internal_debugger.process_id = pid
- self.register_new_thread(pid)
# If we are attaching to a process, we don't want to continue to the entry point
# which we have probably already passed
self._setup_parent(False)
+ def _attach_to_all_tasks(self: PtraceStatusHandler, pid: int) -> None:
+ """Attach to all the tasks of the process."""
+ tids = get_process_tasks(pid)
+ for tid in tids:
+ res = self.lib_trace.ptrace_attach(tid)
+ if res == -1:
+ errno_val = self.ffi.errno
+ if errno_val == errno.EPERM:
+ raise PermissionError(
+ errno_val,
+ errno.errorcode[errno_val],
+ "You don't have permission to attach to the process. Did you check the ptrace_scope?",
+ )
+ raise OSError(errno_val, errno.errorcode[errno_val])
+ self.register_new_thread(tid)
+
def detach(self: PtraceInterface) -> None:
"""Detaches from the process."""
# We must disable all breakpoints before detaching
@@ -200,6 +241,12 @@ def detach(self: PtraceInterface) -> None:
self.detached = True
+ # Reset the event type
+ self._internal_debugger.resume_context.event_type.clear()
+
+ # Reset the breakpoint hit
+ self._internal_debugger.resume_context.event_hit_ref.clear()
+
def kill(self: PtraceInterface) -> None:
"""Instantly terminates the process."""
if not self.detached:
@@ -238,6 +285,12 @@ def cont(self: PtraceInterface) -> None:
else:
self._global_state.handle_syscall_enabled = False
+ # Reset the event type
+ self._internal_debugger.resume_context.event_type.clear()
+
+ # Reset the breakpoint hit
+ self._internal_debugger.resume_context.event_hit_ref.clear()
+
result = self.lib_trace.cont_all_and_set_bps(
self._global_state,
self.process_id,
@@ -257,6 +310,12 @@ def step(self: PtraceInterface, thread: ThreadContext) -> None:
for bp in self._internal_debugger.breakpoints.values():
bp._disabled_for_step = True
+ # Reset the event type
+ self._internal_debugger.resume_context.event_type.clear()
+
+ # Reset the breakpoint hit
+ self._internal_debugger.resume_context.event_hit_ref.clear()
+
result = self.lib_trace.singlestep(self._global_state, thread.thread_id)
if result == -1:
errno_val = self.ffi.errno
@@ -276,6 +335,12 @@ def step_until(self: PtraceInterface, thread: ThreadContext, address: int, max_s
for bp in self._internal_debugger.breakpoints.values():
bp._disabled_for_step = True
+ # Reset the event type
+ self._internal_debugger.resume_context.event_type.clear()
+
+ # Reset the breakpoint hit
+ self._internal_debugger.resume_context.event_hit_ref.clear()
+
result = self.lib_trace.step_until(
self._global_state,
thread.thread_id,
@@ -296,10 +361,17 @@ def finish(self: PtraceInterface, thread: ThreadContext, heuristic: str) -> None
thread (ThreadContext): The thread to step.
heuristic (str): The heuristic to use.
"""
+ # Reset the event type
+ self._internal_debugger.resume_context.event_type.clear()
+
+ # Reset the breakpoint hit
+ self._internal_debugger.resume_context.event_hit_ref.clear()
+
if heuristic == "step-mode":
result = self.lib_trace.stepping_finish(
self._global_state,
thread.thread_id,
+ self._internal_debugger.arch == "i386",
)
if result == -1:
@@ -353,6 +425,11 @@ def finish(self: PtraceInterface, thread: ThreadContext, heuristic: str) -> None
def next(self: PtraceInterface, thread: ThreadContext) -> None:
"""Executes the next instruction of the process. If the instruction is a call, the debugger will continue until the called function returns."""
+ # Reset the event type
+ self._internal_debugger.resume_context.event_type.clear()
+
+ # Reset the breakpoint hit
+ self._internal_debugger.resume_context.event_hit_ref.clear()
opcode_window = thread.memory.read(thread.instruction_pointer, 8)
@@ -411,9 +488,9 @@ def _setup_pipe(self: PtraceInterface) -> None:
os.close(self.stdout_write)
os.close(self.stderr_write)
except Exception as e:
- # TODO: custom exception
raise Exception("Closing fds failed: %r", e) from e
- return PipeManager(self.stdin_write, self.stdout_read, self.stderr_read)
+ with extend_internal_debugger(self):
+ return PipeManager(self.stdin_write, self.stdout_read, self.stderr_read)
def _setup_parent(self: PtraceInterface, continue_to_entry_point: bool) -> None:
"""Sets up the parent process after the child process has been created or attached to."""
@@ -430,7 +507,7 @@ def _setup_parent(self: PtraceInterface, continue_to_entry_point: bool) -> None:
entry_point = get_entry_point(self._internal_debugger.argv[0])
# For PIE binaries, the entry point is a relative address
- entry_point = normalize_and_validate_address(entry_point, self.maps())
+ entry_point = normalize_and_validate_address(entry_point, self.get_maps())
bp = Breakpoint(entry_point, hardware=True)
self.set_breakpoint(bp)
@@ -741,9 +818,10 @@ def _get_event_msg(self: PtraceInterface, thread_id: int) -> int:
"""Returns the event message."""
return self.lib_trace.ptrace_geteventmsg(thread_id)
- def maps(self: PtraceInterface) -> list[MemoryMap]:
+ def get_maps(self: PtraceInterface) -> MemoryMapList[MemoryMap]:
"""Returns the memory maps of the process."""
- return get_process_maps(self.process_id)
+ with extend_internal_debugger(self._internal_debugger):
+ return get_process_maps(self.process_id)
def get_hit_watchpoint(self: PtraceInterface, thread_id: int) -> Breakpoint:
"""Returns the watchpoint that has been hit."""
diff --git a/libdebug/ptrace/ptrace_status_handler.py b/libdebug/ptrace/ptrace_status_handler.py
index 8ad42c47..aa0fe4be 100644
--- a/libdebug/ptrace/ptrace_status_handler.py
+++ b/libdebug/ptrace/ptrace_status_handler.py
@@ -8,7 +8,6 @@
import os
import signal
-from pathlib import Path
from typing import TYPE_CHECKING
from libdebug.architectures.ptrace_software_breakpoint_patcher import (
@@ -17,6 +16,8 @@
from libdebug.debugger.internal_debugger_instance_manager import provide_internal_debugger
from libdebug.liblog import liblog
from libdebug.ptrace.ptrace_constants import SYSCALL_SIGTRAP, StopEvents
+from libdebug.state.resume_context import EventType
+from libdebug.utils.process_utils import get_process_tasks
from libdebug.utils.signal_utils import resolve_signal_name
if TYPE_CHECKING:
@@ -69,6 +70,7 @@ def _handle_breakpoints(self: PtraceStatusHandler, thread_id: int) -> bool:
if not hasattr(thread, "instruction_pointer"):
# This is a signal trap hit on process startup
# Do not resume the process until the user decides to do so
+ self.internal_debugger.resume_context.event_type[thread_id] = EventType.STARTUP
self.internal_debugger.resume_context.resume = False
self.forward_signal = False
return
@@ -107,6 +109,7 @@ def _handle_breakpoints(self: PtraceStatusHandler, thread_id: int) -> bool:
liblog.debugger("Watchpoint hit at 0x%x", bp.address)
if bp:
+ self.internal_debugger.resume_context.event_hit_ref[thread_id] = bp
self.forward_signal = False
bp.hit_count += 1
@@ -114,6 +117,7 @@ def _handle_breakpoints(self: PtraceStatusHandler, thread_id: int) -> bool:
bp.callback(thread, bp)
else:
# If the breakpoint has no callback, we need to stop the process despite the other signals
+ self.internal_debugger.resume_context.event_type[thread_id] = EventType.BREAKPOINT
self.internal_debugger.resume_context.resume = False
def _manage_syscall_on_enter(
@@ -140,6 +144,7 @@ def _manage_syscall_on_enter(
syscall_number_after_callback = thread.syscall_number
if syscall_number_after_callback != syscall_number:
+ # The syscall number has changed
# Pretty print the syscall number before the callback
if handler.on_enter_pprint:
handler.on_enter_pprint(
@@ -148,7 +153,6 @@ def _manage_syscall_on_enter(
hijacked=True,
old_args=old_args,
)
- # The syscall number has changed
if syscall_number_after_callback in self.internal_debugger.handled_syscalls:
callback_hijack = self.internal_debugger.handled_syscalls[syscall_number_after_callback]
@@ -171,7 +175,7 @@ def _manage_syscall_on_enter(
)
elif callback_hijack.on_enter_pprint:
# Pretty print the syscall number
- callback_hijack.on_enter_pprint(thread, syscall_number_after_callback)
+ callback_hijack.on_enter_pprint(thread, syscall_number_after_callback, hijacker=True)
callback_hijack._has_entered = True
callback_hijack._skip_exit = True
else:
@@ -180,19 +184,20 @@ def _manage_syscall_on_enter(
callback_hijack._skip_exit = True
elif handler.on_enter_pprint:
# Pretty print the syscall number
- handler.on_enter_pprint(thread, syscall_number, callback=True)
+ handler.on_enter_pprint(thread, syscall_number, callback=True, old_args=old_args)
handler._has_entered = True
else:
handler._has_entered = True
elif handler.on_enter_pprint:
# Pretty print the syscall number
- handler.on_enter_pprint(thread, syscall_number)
+ handler.on_enter_pprint(thread, syscall_number, callback=(handler.on_exit_user is not None))
handler._has_entered = True
elif handler.on_exit_pprint or handler.on_exit_user:
# The syscall has been entered but the user did not define an on_enter callback
handler._has_entered = True
if not handler.on_enter_user and not handler.on_exit_user and handler.enabled:
# If the syscall has no callback, we need to stop the process despite the other signals
+ self.internal_debugger.resume_context.event_type[thread.thread_id] = EventType.SYSCALL
handler._has_entered = True
self.internal_debugger.resume_context.resume = False
@@ -205,12 +210,17 @@ def _handle_syscall(self: PtraceStatusHandler, thread_id: int) -> bool:
syscall_number = thread.syscall_number
- if syscall_number not in self.internal_debugger.handled_syscalls:
+ if syscall_number in self.internal_debugger.handled_syscalls:
+ handler = self.internal_debugger.handled_syscalls[syscall_number]
+ elif -1 in self.internal_debugger.handled_syscalls:
+ # Handle all syscalls is enabled
+ handler = self.internal_debugger.handled_syscalls[-1]
+ else:
# This is a syscall we don't care about
# Resume the execution
return
- handler = self.internal_debugger.handled_syscalls[syscall_number]
+ self.internal_debugger.resume_context.event_hit_ref[thread_id] = handler
if not handler._has_entered:
# The syscall is being entered
@@ -257,6 +267,7 @@ def _handle_syscall(self: PtraceStatusHandler, thread_id: int) -> bool:
handler._skip_exit = False
if not handler.on_enter_user and not handler.on_exit_user and handler.enabled:
# If the syscall has no callback, we need to stop the process despite the other signals
+ self.internal_debugger.resume_context.event_type[thread_id] = EventType.SYSCALL
self.internal_debugger.resume_context.resume = False
def _manage_caught_signal(
@@ -308,6 +319,7 @@ def _manage_caught_signal(
)
else:
# If the caught signal has no callback, we need to stop the process despite the other signals
+ self.internal_debugger.resume_context.event = EventType.SIGNAL
self.internal_debugger.resume_context.resume = False
def _handle_signal(self: PtraceStatusHandler, thread: ThreadContext) -> bool:
@@ -317,6 +329,17 @@ def _handle_signal(self: PtraceStatusHandler, thread: ThreadContext) -> bool:
if signal_number in self.internal_debugger.caught_signals:
catcher = self.internal_debugger.caught_signals[signal_number]
+ self._manage_caught_signal(catcher, thread, signal_number, {signal_number})
+ elif -1 in self.internal_debugger.caught_signals and signal_number not in (
+ signal.SIGSTOP,
+ signal.SIGTRAP,
+ signal.SIGKILL,
+ ):
+ # Handle all signals is enabled
+ catcher = self.internal_debugger.caught_signals[-1]
+
+ self.internal_debugger.resume_context.event_hit_ref[thread.thread_id] = catcher
+
self._manage_caught_signal(catcher, thread, signal_number, {signal_number})
def _internal_signal_handler(
@@ -339,6 +362,7 @@ def _internal_signal_handler(
pid,
resolve_signal_name(signum),
)
+ self.internal_debugger.resume_context.event_type[pid] = EventType.USER_INTERRUPT
self.internal_debugger.resume_context.resume = False
self.internal_debugger.resume_context.force_interrupt = False
self.forward_signal = False
@@ -349,6 +373,7 @@ def _internal_signal_handler(
if self.internal_debugger.resume_context.is_a_step:
# The process is stepping, we need to stop the execution
+ self.internal_debugger.resume_context.event_type[pid] = EventType.STEP
self.internal_debugger.resume_context.resume = False
self.internal_debugger.resume_context.is_a_step = False
self.forward_signal = False
@@ -442,10 +467,7 @@ def manage_change(self: PtraceStatusHandler, result: list[tuple]) -> None:
def check_for_new_threads(self: PtraceStatusHandler, pid: int) -> None:
"""Check for new threads in the process and register them."""
- if not Path(f"/proc/{pid}/task").exists():
- return
-
- tids = [int(x) for x in os.listdir(f"/proc/{pid}/task")]
+ tids = get_process_tasks(pid)
for tid in tids:
if not self.internal_debugger.get_thread_by_id(tid):
self.ptrace_interface.register_new_thread(tid, self.internal_debugger)
diff --git a/libdebug/state/resume_context.py b/libdebug/state/resume_context.py
index 57a255db..ef992ef2 100644
--- a/libdebug/state/resume_context.py
+++ b/libdebug/state/resume_context.py
@@ -6,6 +6,11 @@
from __future__ import annotations
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from libdebug.data.breakpoint import Breakpoint
+
class ResumeContext:
"""A class representing the context of the resume decision."""
@@ -18,6 +23,8 @@ def __init__(self: ResumeContext) -> None:
self.is_startup: bool = False
self.block_on_signal: bool = False
self.threads_with_signals_to_forward: list[int] = []
+ self.event_type: dict[int, EventType] = {}
+ self.event_hit_ref: dict[int, Breakpoint] = {}
def clear(self: ResumeContext) -> None:
"""Clears the context."""
@@ -27,3 +34,41 @@ def clear(self: ResumeContext) -> None:
self.is_startup = False
self.block_on_signal = False
self.threads_with_signals_to_forward.clear()
+ self.event_type.clear()
+ self.event_hit_ref.clear()
+
+ def get_event_type(self: ResumeContext) -> str:
+ """Returns the event type to be printed."""
+ event_str = ""
+ if self.event_type:
+ for tid, event in self.event_type.items():
+ if event == EventType.BREAKPOINT:
+ hit_ref = self.event_hit_ref[tid]
+ if hit_ref.condition != "x":
+ event_str += (
+ f"Watchpoint at {hit_ref.address:#x} with condition {hit_ref.condition} on thread {tid}."
+ )
+ else:
+ event_str += f"Breakpoint at {hit_ref.address:#x} on thread {tid}."
+ elif event == EventType.SYSCALL:
+ hit_ref = self.event_hit_ref[tid]
+ event_str += f"Syscall {hit_ref.syscall_number} on thread {tid}."
+ elif event == EventType.SIGNAL:
+ hit_ref = self.event_hit_ref[tid]
+ event_str += f"Signal {hit_ref.signal} on thread {tid}."
+ else:
+ event_str += f"{event} on thread {tid}."
+
+ return event_str
+
+
+class EventType:
+ """A class representing the type of event that caused the resume decision."""
+
+ UNKNOWN = "Unknown Event"
+ BREAKPOINT = "Breakpoint"
+ SYSCALL = "Syscall"
+ SIGNAL = "Signal"
+ USER_INTERRUPT = "User Interrupt"
+ STEP = "Step"
+ STARTUP = "Process Startup"
diff --git a/libdebug/state/thread_context.py b/libdebug/state/thread_context.py
index 6b165a34..26eee55f 100644
--- a/libdebug/state/thread_context.py
+++ b/libdebug/state/thread_context.py
@@ -12,13 +12,14 @@
provide_internal_debugger,
)
from libdebug.liblog import liblog
+from libdebug.utils.ansi_escape_codes import ANSIColors
from libdebug.utils.debugging_utils import resolve_address_in_maps
-from libdebug.utils.print_style import PrintStyle
from libdebug.utils.signal_utils import resolve_signal_name, resolve_signal_number
if TYPE_CHECKING:
from libdebug.data.register_holder import RegisterHolder
from libdebug.data.registers import Registers
+ from libdebug.debugger.debugger import Debugger
from libdebug.debugger.internal_debugger import InternalDebugger
from libdebug.memory.abstract_memory_view import AbstractMemoryView
@@ -59,6 +60,9 @@ class ThreadContext:
_internal_debugger: InternalDebugger | None = None
"""The debugging context this thread belongs to."""
+ _register_holder: RegisterHolder | None = None
+ """The register holder object."""
+
_dead: bool = False
"""Whether the thread is dead."""
@@ -78,15 +82,21 @@ def __init__(self: ThreadContext, thread_id: int, registers: RegisterHolder) ->
"""Initializes the Thread Context."""
self._internal_debugger = provide_internal_debugger(self)
self._thread_id = thread_id
- regs_class = registers.provide_regs_class()
- self.regs = regs_class(thread_id)
- registers.apply_on_regs(self.regs, regs_class)
- registers.apply_on_thread(self, ThreadContext)
+ self._register_holder = registers
+ regs_class = self._register_holder.provide_regs_class()
+ self.regs = regs_class(thread_id, self._register_holder.provide_regs())
+ self._register_holder.apply_on_regs(self.regs, regs_class)
+ self._register_holder.apply_on_thread(self, ThreadContext)
def set_as_dead(self: ThreadContext) -> None:
"""Set the thread as dead."""
self._dead = True
+ @property
+ def debugger(self: ThreadContext) -> Debugger:
+ """The debugging context this thread belongs to."""
+ return self._internal_debugger.debugger
+
@property
def dead(self: ThreadContext) -> bool:
"""Whether the thread is dead."""
@@ -107,12 +117,15 @@ def mem(self: ThreadContext) -> AbstractMemoryView:
@property
def process_id(self: ThreadContext) -> int:
- """The process ID of the thread."""
+ """The process ID."""
return self._internal_debugger.process_id
@property
def pid(self: ThreadContext) -> int:
- """The process ID of the thread."""
+ """Alias for `process_id` property.
+
+ The process ID.
+ """
return self._internal_debugger.process_id
@property
@@ -130,6 +143,21 @@ def running(self: ThreadContext) -> bool:
"""Whether the process is running."""
return self._internal_debugger.running
+ @property
+ def saved_ip(self: ThreadContext) -> int:
+ """The return address of the current function."""
+ self._internal_debugger._ensure_process_stopped()
+ stack_unwinder = stack_unwinding_provider(self._internal_debugger.arch)
+
+ try:
+ return_address = stack_unwinder.get_return_address(self, self._internal_debugger.maps)
+ except (OSError, ValueError) as e:
+ raise ValueError(
+ "Failed to get the return address. Check stack frame registers (e.g., base pointer).",
+ ) from e
+
+ return return_address
+
@property
def exit_code(self: ThreadContext) -> int | None:
"""The thread's exit code."""
@@ -174,6 +202,11 @@ def signal(self: ThreadContext, signal: str | int) -> None:
self._signal_number = signal
self._internal_debugger.resume_context.threads_with_signals_to_forward.append(self.thread_id)
+ @property
+ def signal_number(self: ThreadContext) -> int:
+ """The signal number to forward to the thread."""
+ return self._signal_number
+
def backtrace(self: ThreadContext, as_symbols: bool = False) -> list:
"""Returns the current backtrace of the thread.
@@ -184,37 +217,91 @@ def backtrace(self: ThreadContext, as_symbols: bool = False) -> list:
stack_unwinder = stack_unwinding_provider(self._internal_debugger.arch)
backtrace = stack_unwinder.unwind(self)
if as_symbols:
- maps = self._internal_debugger.debugging_interface.maps()
+ maps = self._internal_debugger.debugging_interface.get_maps()
backtrace = [resolve_address_in_maps(x, maps) for x in backtrace]
return backtrace
- def print_backtrace(self: ThreadContext) -> None:
- """Prints the current backtrace of the thread."""
+ def pprint_backtrace(self: ThreadContext) -> None:
+ """Pretty prints the current backtrace of the thread."""
self._internal_debugger._ensure_process_stopped()
stack_unwinder = stack_unwinding_provider(self._internal_debugger.arch)
backtrace = stack_unwinder.unwind(self)
- maps = self._internal_debugger.debugging_interface.maps()
+ maps = self._internal_debugger.debugging_interface.get_maps()
for return_address in backtrace:
- return_address_symbol = resolve_address_in_maps(return_address, maps)
+ filtered_maps = maps.filter(return_address)
+ return_address_symbol = resolve_address_in_maps(return_address, filtered_maps)
+ permissions = filtered_maps[0].permissions
+ if "rwx" in permissions:
+ style = f"{ANSIColors.UNDERLINE}{ANSIColors.RED}"
+ elif "x" in permissions:
+ style = f"{ANSIColors.RED}"
+ elif "w" in permissions:
+ # This should not happen, but it's here for completeness
+ style = f"{ANSIColors.YELLOW}"
+ elif "r" in permissions:
+ # This should not happen, but it's here for completeness
+ style = f"{ANSIColors.GREEN}"
if return_address_symbol[:2] == "0x":
- print(f"{PrintStyle.RED}{return_address:#x} {PrintStyle.RESET}")
+ print(f"{style}{return_address:#x} {ANSIColors.RESET}")
else:
- print(f"{PrintStyle.RED}{return_address:#x} <{return_address_symbol}> {PrintStyle.RESET}")
+ print(f"{style}{return_address:#x} <{return_address_symbol}> {ANSIColors.RESET}")
+
+ def _pprint_reg(self: ThreadContext, register: str) -> None:
+ attr = getattr(self.regs, register)
+ color = ""
+ style = ""
+ formatted_attr = f"{attr:#x}"
+
+ if maps := self._internal_debugger.maps.filter(attr):
+ permissions = maps[0].permissions
+ if "rwx" in permissions:
+ color = ANSIColors.RED
+ style = ANSIColors.UNDERLINE
+ elif "x" in permissions:
+ color = ANSIColors.RED
+ elif "w" in permissions:
+ color = ANSIColors.YELLOW
+ elif "r" in permissions:
+ color = ANSIColors.GREEN
+
+ if color or style:
+ formatted_attr = f"{color}{style}{attr:#x}{ANSIColors.RESET}"
+ print(f"{ANSIColors.RED}{register}{ANSIColors.RESET}\t{formatted_attr}")
+
+ def pprint_registers(self: ThreadContext) -> None:
+ """Pretty prints the thread's registers."""
+ for register in self._register_holder.provide_regs():
+ self._pprint_reg(register)
+
+ def pprint_regs(self: ThreadContext) -> None:
+ """Alias for the `pprint_registers` method.
+
+ Pretty prints the thread's registers.
+ """
+ self.pprint_registers()
- @property
- def saved_ip(self: ThreadContext) -> int:
- """The return address of the current function."""
- self._internal_debugger._ensure_process_stopped()
- stack_unwinder = stack_unwinding_provider(self._internal_debugger.arch)
+ def pprint_registers_all(self: ThreadContext) -> None:
+ """Pretty prints all the thread's registers."""
+ self.pprint_registers()
- try:
- return_address = stack_unwinder.get_return_address(self, self._internal_debugger.debugging_interface.maps())
- except (OSError, ValueError) as e:
- raise ValueError(
- "Failed to get the return address. Check stack frame registers (e.g., base pointer).",
- ) from e
+ for t in self._register_holder.provide_special_regs():
+ self._pprint_reg(t)
- return return_address
+ for t in self._register_holder.provide_vector_fp_regs():
+ print(f"{ANSIColors.BLUE}" + "{" + f"{ANSIColors.RESET}")
+ for register in t:
+ value = getattr(self.regs, register)
+ formatted_value = f"{value:#x}" if isinstance(value, int) else str(value)
+ print(f" {ANSIColors.RED}{register}{ANSIColors.RESET}\t{formatted_value}")
+
+ print(f"{ANSIColors.BLUE}" + "}" + f"{ANSIColors.RESET}")
+
+ def pprint_regs_all(self: ThreadContext) -> None:
+ """Alias for the `pprint_registers_all` method.
+
+ Pretty prints all the thread's registers.
+ """
+ self.pprint_registers_all()
def step(self: ThreadContext) -> None:
"""Executes a single instruction of the process."""
@@ -231,9 +318,7 @@ def step_until(
Args:
position (int | bytes): The location to reach.
max_steps (int, optional): The maximum number of steps to execute. Defaults to -1.
- file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid"
- (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t.
- the "binary" map file).
+ file (str, optional): The user-defined backing file to resolve the address in. Defaults to "hybrid" (libdebug will first try to solve the address as an absolute address, then as a relative address w.r.t. the "binary" map file).
"""
self._internal_debugger.step_until(self, position, max_steps, file)
@@ -290,3 +375,12 @@ def fin(self: ThreadContext, heuristic: str = "backtrace") -> None:
def ni(self: ThreadContext) -> None:
"""Alias for the `next` method. Executes the next instruction of the process. If the instruction is a call, the debugger will continue until the called function returns."""
self._internal_debugger.next(self)
+
+ def __repr__(self: ThreadContext) -> str:
+ """Returns a string representation of the object."""
+ repr_str = "ThreadContext()\n"
+ repr_str += f" Thread ID: {self.thread_id}\n"
+ repr_str += f" Process ID: {self.process_id}\n"
+ repr_str += f" Instruction Pointer: {self.instruction_pointer:#x}\n"
+ repr_str += f" Dead: {self.dead}"
+ return repr_str
diff --git a/libdebug/utils/print_style.py b/libdebug/utils/ansi_escape_codes.py
similarity index 91%
rename from libdebug/utils/print_style.py
rename to libdebug/utils/ansi_escape_codes.py
index 48dca068..2d739e0f 100644
--- a/libdebug/utils/print_style.py
+++ b/libdebug/utils/ansi_escape_codes.py
@@ -4,8 +4,10 @@
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
+from __future__ import annotations
-class PrintStyle:
+
+class ANSIColors:
"""Class to define colors for the terminal."""
RED = "\033[91m"
diff --git a/libdebug/utils/arch_mappings.py b/libdebug/utils/arch_mappings.py
index bc7a7694..ae70ea33 100644
--- a/libdebug/utils/arch_mappings.py
+++ b/libdebug/utils/arch_mappings.py
@@ -5,6 +5,7 @@
#
ARCH_MAPPING = {
+ "i686": "i386",
"x86": "i386",
"x86_64": "amd64",
"x64": "amd64",
diff --git a/libdebug/utils/debugger_wrappers.py b/libdebug/utils/debugger_wrappers.py
index f56b7cb4..cd599032 100644
--- a/libdebug/utils/debugger_wrappers.py
+++ b/libdebug/utils/debugger_wrappers.py
@@ -55,7 +55,7 @@ def wrapper(
# We have to ensure that at least one thread is alive before executing the method
if thread.dead:
- raise RuntimeError("The threads is dead.")
+ raise RuntimeError("The thread is dead.")
return method(self, thread, *args, **kwargs)
return wrapper
diff --git a/libdebug/utils/debugging_utils.py b/libdebug/utils/debugging_utils.py
index 173e83b7..9a5e9440 100644
--- a/libdebug/utils/debugging_utils.py
+++ b/libdebug/utils/debugging_utils.py
@@ -5,22 +5,18 @@
#
from libdebug.data.memory_map import MemoryMap
+from libdebug.data.memory_map_list import MemoryMapList
from libdebug.liblog import liblog
from libdebug.utils.elf_utils import is_pie, resolve_address, resolve_symbol
-def check_absolute_address(address: int, maps: list[MemoryMap]) -> bool:
- """Checks if the specified address is an absolute address.
-
- Returns:
- bool: True if the specified address is an absolute address, False otherwise.
- """
- return any(vmap.start <= address < vmap.end for vmap in maps)
-
-
-def normalize_and_validate_address(address: int, maps: list[MemoryMap]) -> int:
+def normalize_and_validate_address(address: int, maps: MemoryMapList[MemoryMap]) -> int:
"""Normalizes and validates the specified address.
+ Args:
+ address (int): The address to normalize and validate.
+ maps (MemoryMapList[MemoryMap]): The memory maps.
+
Returns:
int: The normalized address.
@@ -38,12 +34,12 @@ def normalize_and_validate_address(address: int, maps: list[MemoryMap]) -> int:
raise ValueError(f"Address {hex(address)} does not belong to any memory map.")
-def resolve_symbol_in_maps(symbol: str, maps: list[MemoryMap]) -> int:
+def resolve_symbol_in_maps(symbol: str, maps: MemoryMapList[MemoryMap]) -> int:
"""Returns the address of the specified symbol in the specified memory maps.
Args:
symbol (str): The symbol whose address should be returned.
- maps (list[MemoryMap]): The memory maps.
+ maps (MemoryMapList[MemoryMap]): The memory maps.
Returns:
int: The address of the specified symbol in the specified memory maps.
@@ -79,12 +75,12 @@ def resolve_symbol_in_maps(symbol: str, maps: list[MemoryMap]) -> int:
raise ValueError(f"Symbol {symbol} not found in the specified mapped file. Please specify a valid symbol.")
-def resolve_address_in_maps(address: int, maps: list[MemoryMap]) -> str:
+def resolve_address_in_maps(address: int, maps: MemoryMapList[MemoryMap]) -> str:
"""Returns the symbol corresponding to the specified address in the specified memory maps.
Args:
address (int): The address whose symbol should be returned.
- maps (list[MemoryMap]): The memory maps.
+ maps (MemoryMapList[MemoryMap]): The memory maps.
Returns:
str: The symbol corresponding to the specified address in the specified memory maps.
diff --git a/libdebug/utils/elf_utils.py b/libdebug/utils/elf_utils.py
index 7a5212bb..62ccccf1 100644
--- a/libdebug/utils/elf_utils.py
+++ b/libdebug/utils/elf_utils.py
@@ -5,6 +5,7 @@
#
import functools
+import shutil
from pathlib import Path
import requests
@@ -12,6 +13,8 @@
from libdebug.cffi.debug_sym_cffi import ffi
from libdebug.cffi.debug_sym_cffi import lib as lib_sym
+from libdebug.data.symbol import Symbol
+from libdebug.data.symbol_list import SymbolList
from libdebug.liblog import liblog
from libdebug.utils.libcontext import libcontext
@@ -33,9 +36,10 @@ def _download_debuginfod(buildid: str, debuginfod_path: Path) -> None:
if r.ok:
debuginfod_path.parent.mkdir(parents=True, exist_ok=True)
-
with debuginfod_path.open("wb") as f:
f.write(r.content)
+ else:
+ liblog.error(f"Failed to download debuginfo file. Error code: {r.status_code}")
except Exception as e:
liblog.debugger(f"Exception {e} occurred while downloading debuginfod symbols")
@@ -60,16 +64,16 @@ def _debuginfod(buildid: str) -> Path:
@functools.cache
-def _collect_external_info(path: str) -> dict[str, tuple[int, int]]:
+def _collect_external_info(path: str) -> SymbolList[Symbol]:
"""Returns a dictionary containing the symbols taken from the external debuginfo file.
Args:
path (str): The path to the ELF file.
Returns:
- symbols (dict): A dictionary containing the symbols of the specified external debuginfo file.
+ SymbolList[Symbol]: A list containing the symbols taken from the external debuginfo file.
"""
- symbols = {}
+ symbols = []
c_file_path = ffi.new("char[]", path.encode("utf-8"))
head = lib_sym.collect_external_symbols(c_file_path, libcontext.sym_lvl)
@@ -79,16 +83,16 @@ def _collect_external_info(path: str) -> dict[str, tuple[int, int]]:
while cursor != ffi.NULL:
symbol_name = ffi.string(cursor.name).decode("utf-8")
- symbols[symbol_name] = (cursor.low_pc, cursor.high_pc)
+ symbols.append(Symbol(cursor.low_pc, cursor.high_pc, symbol_name, path))
cursor = cursor.next
lib_sym.free_symbol_info(head)
- return symbols
+ return SymbolList(symbols)
@functools.cache
-def _parse_elf_file(path: str, debug_info_level: int) -> tuple[dict[str, tuple[int, int]], str | None, str | None]:
+def _parse_elf_file(path: str, debug_info_level: int) -> tuple[SymbolList[Symbol], str | None, str | None]:
"""Returns a dictionary containing the symbols of the specified ELF file and the buildid.
Args:
@@ -96,11 +100,11 @@ def _parse_elf_file(path: str, debug_info_level: int) -> tuple[dict[str, tuple[i
debug_info_level (int): The debug info level.
Returns:
- symbols (dict): A dictionary containing the symbols of the specified ELF file.
+ symbols (SymbolList[Symbol): A list containing the symbols of the specified ELF file.
buildid (str): The buildid of the specified ELF file.
debug_file_path (str): The path to the external debuginfo file corresponding.
"""
- symbols = {}
+ symbols = []
buildid = None
debug_file_path = None
@@ -112,7 +116,7 @@ def _parse_elf_file(path: str, debug_info_level: int) -> tuple[dict[str, tuple[i
while cursor != ffi.NULL:
symbol_name = ffi.string(cursor.name).decode("utf-8")
- symbols[symbol_name] = (cursor.low_pc, cursor.high_pc)
+ symbols.append(Symbol(cursor.low_pc, cursor.high_pc, symbol_name, path))
cursor = cursor.next
lib_sym.free_symbol_info(head)
@@ -124,7 +128,7 @@ def _parse_elf_file(path: str, debug_info_level: int) -> tuple[dict[str, tuple[i
debug_file_path = lib_sym.get_debug_file()
debug_file_path = ffi.string(debug_file_path).decode("utf-8") if debug_file_path != ffi.NULL else None
- return symbols, buildid, debug_file_path
+ return SymbolList(symbols), buildid, debug_file_path
@functools.cache
@@ -145,29 +149,68 @@ def resolve_symbol(path: str, symbol: str) -> int:
# Retrieve the symbols from the SymbolTableSection
symbols, buildid, debug_file = _parse_elf_file(path, libcontext.sym_lvl)
- if symbol in symbols:
- return symbols[symbol][0]
+ symbols = [sym for sym in symbols if sym.name == symbol]
+ if symbols:
+ return symbols[0].start
# Retrieve the symbols from the external debuginfo file
if buildid and debug_file and libcontext.sym_lvl > 2:
folder = buildid[:2]
absolute_debug_path_str = str((LOCAL_DEBUG_PATH / folder / debug_file).resolve())
symbols = _collect_external_info(absolute_debug_path_str)
- if symbol in symbols:
- return symbols[symbol][0]
+ symbols = [sym for sym in symbols if sym.name == symbol]
+ if symbols:
+ return symbols[0].start
# Retrieve the symbols from debuginfod
if buildid and libcontext.sym_lvl > 4:
absolute_debug_path = _debuginfod(buildid)
if absolute_debug_path.exists():
symbols = _collect_external_info(str(absolute_debug_path))
- if symbol in symbols:
- return symbols[symbol][0]
+ symbols = [sym for sym in symbols if sym.name == symbol]
+ if symbols:
+ return symbols[0].start
# Symbol not found
raise ValueError(f"Symbol {symbol} not found in {path}. Please specify a valid symbol.")
+def get_all_symbols(backing_files: set[str]) -> SymbolList[Symbol]:
+ """Returns a list of all the symbols in the target process.
+
+ Args:
+ backing_files (set[str]): The set of backing files.
+
+ Returns:
+ SymbolList[Symbol]: A list of all the symbols in the target process.
+ """
+ symbols = SymbolList([])
+
+ if libcontext.sym_lvl == 0:
+ raise Exception(
+ "Symbol resolution is disabled. Please enable it by setting the sym_lvl libcontext parameter to a value greater than 0.",
+ )
+
+ for file in backing_files:
+ # Retrieve the symbols from the SymbolTableSection
+ new_symbols, buildid, debug_file = _parse_elf_file(file, libcontext.sym_lvl)
+ symbols += new_symbols
+
+ # Retrieve the symbols from the external debuginfo file
+ if buildid and debug_file and libcontext.sym_lvl > 2:
+ folder = buildid[:2]
+ absolute_debug_path_str = str((LOCAL_DEBUG_PATH / folder / debug_file).resolve())
+ symbols += _collect_external_info(absolute_debug_path_str)
+
+ # Retrieve the symbols from debuginfod
+ if buildid and libcontext.sym_lvl > 4:
+ absolute_debug_path = _debuginfod(buildid)
+ if absolute_debug_path.exists():
+ symbols += _collect_external_info(str(absolute_debug_path))
+
+ return symbols
+
+
@functools.cache
def resolve_address(path: str, address: int) -> str:
"""Returns the symbol corresponding to the specified address in the specified ELF file.
@@ -184,27 +227,30 @@ def resolve_address(path: str, address: int) -> str:
# Retrieve the symbols from the SymbolTableSection
symbols, buildid, debug_file = _parse_elf_file(path, libcontext.sym_lvl)
- for symbol, (symbol_start, symbol_end) in symbols.items():
- if symbol_start <= address < symbol_end:
- return f"{symbol}+{address-symbol_start:x}"
+ symbols = [symbol for symbol in symbols if symbol.start <= address < symbol.end]
+ if symbols:
+ symbol = symbols[0]
+ return f"{symbol.name}+{address-symbol.start:x}"
# Retrieve the symbols from the external debuginfo file
if buildid and debug_file and libcontext.sym_lvl > 2:
folder = buildid[:2]
absolute_debug_path_str = str((LOCAL_DEBUG_PATH / folder / debug_file).resolve())
symbols = _collect_external_info(absolute_debug_path_str)
- for symbol, (symbol_start, symbol_end) in symbols.items():
- if symbol_start <= address < symbol_end:
- return f"{symbol}+{address-symbol_start:x}"
+ symbols = [symbol for symbol in symbols if symbol.start <= address < symbol.end]
+ if symbols:
+ symbol = symbols[0]
+ return f"{symbol.name}+{address-symbol.start:x}"
# Retrieve the symbols from debuginfod
if buildid and libcontext.sym_lvl > 4:
absolute_debug_path = _debuginfod(buildid)
if absolute_debug_path.exists():
symbols = _collect_external_info(str(absolute_debug_path))
- for symbol, (symbol_start, symbol_end) in symbols.items():
- if symbol_start <= address < symbol_end:
- return f"{symbol}+{address-symbol_start:x}"
+ symbols = [symbol for symbol in symbols if symbol.start <= address < symbol.end]
+ if symbols:
+ symbol = symbols[0]
+ return f"{symbol.name}+{address-symbol.start:x}"
# Address not found
raise ValueError(f"Address {hex(address)} not found in {path}. Please specify a valid address.")
@@ -264,3 +310,24 @@ def elf_architecture(path: str) -> str:
str: The architecture of the specified ELF file.
"""
return parse_elf_characteristics(path)[2]
+
+
+def resolve_argv_path(argv_path: str) -> str:
+ """Resolve the path of the binary to debug.
+
+ Args:
+ argv_path (str): The provided path of the binary to debug.
+
+ Returns:
+ str: The resolved path of the binary to debug.
+ """
+ argv_path_expanded = Path(argv_path).expanduser()
+
+ # Check if the path is absolute after expansion
+ if argv_path_expanded.is_absolute():
+ # It's an absolute path, return it as is
+ resolved_path = argv_path_expanded
+ else:
+ # It's a relative path, try to resolve it
+ resolved_path = abs_path if (abs_path := shutil.which(argv_path_expanded)) else argv_path_expanded
+ return str(resolved_path)
diff --git a/libdebug/utils/libcontext.py b/libdebug/utils/libcontext.py
index e1167bf7..1c86d6b4 100644
--- a/libdebug/utils/libcontext.py
+++ b/libdebug/utils/libcontext.py
@@ -42,7 +42,7 @@ def __init__(self: LibContext) -> None:
self._pipe_logger_levels = ["DEBUG", "SILENT"]
self._debugger_logger_levels = ["DEBUG", "SILENT"]
self._general_logger_levels = ["DEBUG", "INFO", "WARNING", "SILENT"]
- self._sym_lvl = 3
+ self._sym_lvl = 5
self._debugger_logger = "SILENT"
self._pipe_logger = "SILENT"
diff --git a/libdebug/utils/pipe_manager.py b/libdebug/utils/pipe_manager.py
deleted file mode 100644
index 6cf71e70..00000000
--- a/libdebug/utils/pipe_manager.py
+++ /dev/null
@@ -1,386 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-from __future__ import annotations
-
-import os
-import time
-from select import select
-
-from libdebug.liblog import liblog
-
-
-class PipeManager:
- """Class for managing pipes of the child process."""
-
- _instance = None
- timeout_default: int = 2
-
- def __init__(self: PipeManager, stdin_write: int, stdout_read: int, stderr_read: int) -> None:
- """Initializes the PipeManager class.
-
- Args:
- stdin_write (int): file descriptor for stdin write.
- stdout_read (int): file descriptor for stdout read.
- stderr_read (int): file descriptor for stderr read.
- """
- self.stdin_write: int = stdin_write
- self.stdout_read: int = stdout_read
- self.stderr_read: int = stderr_read
-
- def _recv(
- self: PipeManager,
- numb: int | None = None,
- timeout: float = timeout_default,
- stderr: bool = False,
- ) -> bytes:
- """Receives at most numb bytes from the child process.
-
- Args:
- numb (int, optional): number of bytes to receive. Defaults to None.
- timeout (float, optional): timeout in seconds. Defaults to timeout_default.
- stderr (bool, optional): receive from stderr. Defaults to False.
-
- Returns:
- bytes: received bytes from the child process stdout.
-
- Raises:
- ValueError: numb is negative.
- RuntimeError: no stdout pipe of the child process.
- """
- pipe_read: int = self.stderr_read if stderr else self.stdout_read
-
- if not pipe_read:
- raise RuntimeError("No pipe of the child process")
-
- # Buffer for the received data
- data_buffer = b""
-
- if numb:
- # Checking the numb
- if numb < 0:
- raise ValueError("The number of bytes to receive must be positive")
-
- # Setting the alarm
- end_time = time.time() + timeout
- while numb > 0:
- if end_time is not None and time.time() > end_time:
- # Timeout reached
- break
-
- # Adjust the timeout for select to the remaining time
- remaining_time = None if end_time is None else max(0, end_time - time.time())
- ready, _, _ = select([pipe_read], [], [], remaining_time)
-
- if not ready:
- # No data ready within the remaining timeout
- break
-
- try:
- data = os.read(pipe_read, numb)
- except OSError as e:
- raise RuntimeError("Broken pipe. Is the child process still running?") from e
-
- if not data:
- # No more data available
- break
-
- numb -= len(data)
- data_buffer += data
- else:
- ready, _, _ = select([pipe_read], [], [], timeout)
-
- if ready:
- # Read all available bytes up to 4096
- data = os.read(pipe_read, 4096)
- data_buffer += data
-
- liblog.pipe(f"Received {len(data_buffer)} bytes from the child process: {data_buffer!r}")
- return data_buffer
-
- def close(self: PipeManager) -> None:
- """Closes all the pipes of the child process."""
- os.close(self.stdin_write)
- os.close(self.stdout_read)
- os.close(self.stderr_read)
-
- def recv(self: PipeManager, numb: int | None = None, timeout: int = timeout_default) -> bytes:
- """Receives at most numb bytes from the child process stdout.
-
- Args:
- numb (int, optional): number of bytes to receive. Defaults to None.
- timeout (int, optional): timeout in seconds. Defaults to timeout_default.
-
- Returns:
- bytes: received bytes from the child process stdout.
- """
- return self._recv(numb=numb, timeout=timeout, stderr=False)
-
- def recverr(self: PipeManager, numb: int | None = None, timeout: int = timeout_default) -> bytes:
- """Receives at most numb bytes from the child process stderr.
-
- Args:
- numb (int, optional): number of bytes to receive. Defaults to None.
- timeout (int, optional): timeout in seconds. Defaults to timeout_default.
-
- Returns:
- bytes: received bytes from the child process stderr.
- """
- return self._recv(numb=numb, timeout=timeout, stderr=True)
-
- def _recvonceuntil(
- self: PipeManager,
- delims: bytes,
- drop: bool = False,
- timeout: float = timeout_default,
- stderr: bool = False,
- ) -> bytes:
- """Receives data from the child process until the delimiters are found.
-
- Args:
- delims (bytes): delimiters where to stop.
- drop (bool, optional): drop the delimiter. Defaults to False.
- timeout (float, optional): timeout in seconds. Defaults to timeout_default.
- stderr (bool, optional): receive from stderr. Defaults to False.
-
- Returns:
- bytes: received data from the child process stdout.
-
- Raises:
- RuntimeError: no stdout pipe of the child process.
- TimeoutError: timeout reached.
- """
- if isinstance(delims, str):
- liblog.warning("The delimiters are a string, converting to bytes")
- delims = delims.encode()
-
- # Buffer for the received data
- data_buffer = b""
-
- # Setting the alarm
- end_time = time.time() + timeout
- while True:
- if end_time is not None and time.time() > end_time:
- # Timeout reached
- raise TimeoutError("Timeout reached")
-
- # Adjust the timeout for select to the remaining time
- remaining_time = None if end_time is None else max(0, end_time - time.time())
-
- data = self._recv(numb=1, timeout=remaining_time, stderr=stderr)
-
- data_buffer += data
-
- if delims in data_buffer:
- # Delims reached
- if drop:
- data_buffer = data_buffer[: -len(delims)]
- break
-
- return data_buffer
-
- def _recvuntil(
- self: PipeManager,
- delims: bytes,
- occurences: int = 1,
- drop: bool = False,
- timeout: float = timeout_default,
- stderr: bool = False,
- ) -> bytes:
- """Receives data from the child process until the delimiters are found occurences time.
-
- Args:
- delims (bytes): delimiters where to stop.
- occurences (int, optional): number of delimiters to find. Defaults to 1.
- drop (bool, optional): drop the delimiter. Defaults to False.
- timeout (float, optional): timeout in seconds. Defaults to timeout_default.
- stderr (bool, optional): receive from stderr. Defaults to False.
-
- Returns:
- bytes: received data from the child process stdout.
- """
- if occurences <= 0:
- raise ValueError("The number of occurences to receive must be positive")
-
- # Buffer for the received data
- data_buffer = b""
-
- # Setting the alarm
- end_time = time.time() + timeout
-
- for _ in range(occurences):
- # Adjust the timeout for select to the remaining time
- remaining_time = None if end_time is None else max(0, end_time - time.time())
-
- data_buffer += self._recvonceuntil(delims=delims, drop=drop, timeout=remaining_time, stderr=stderr)
-
- return data_buffer
-
- def recvuntil(
- self: PipeManager,
- delims: bytes,
- occurences: int = 1,
- drop: bool = False,
- timeout: int = timeout_default,
- ) -> bytes:
- """Receives data from the child process stdout until the delimiters are found.
-
- Args:
- delims (bytes): delimiters where to stop.
- occurences (int, optional): number of delimiters to find. Defaults to 1.
- drop (bool, optional): drop the delimiter. Defaults to False.
- timeout (int, optional): timeout in seconds. Defaults to timeout_default.
-
- Returns:
- bytes: received data from the child process stdout.
- """
- return self._recvuntil(
- delims=delims,
- occurences=occurences,
- drop=drop,
- timeout=timeout,
- stderr=False,
- )
-
- def recverruntil(
- self: PipeManager,
- delims: bytes,
- occurences: int = 1,
- drop: bool = False,
- timeout: int = timeout_default,
- ) -> bytes:
- """Receives data from the child process stderr until the delimiters are found.
-
- Args:
- delims (bytes): delimiters where to stop.
- occurences (int, optional): number of delimiters to find. Defaults to 1.
- drop (bool, optional): drop the delimiter. Defaults to False.
- timeout (int, optional): timeout in seconds. Defaults to timeout_default.
-
- Returns:
- bytes: received data from the child process stderr.
- """
- return self._recvuntil(
- delims=delims,
- occurences=occurences,
- drop=drop,
- timeout=timeout,
- stderr=True,
- )
-
- def recvline(self: PipeManager, numlines: int = 1, drop: bool = True, timeout: int = timeout_default) -> bytes:
- """Receives numlines lines from the child process stdout.
-
- Args:
- numlines (int, optional): number of lines to receive. Defaults to 1.
- drop (bool, optional): drop the line ending. Defaults to True.
- timeout (int, optional): timeout in seconds. Defaults to timeout_default.
-
- Returns:
- bytes: received lines from the child process stdout.
- """
- return self.recvuntil(delims=b"\n", occurences=numlines, drop=drop, timeout=timeout)
-
- def recverrline(self: PipeManager, numlines: int = 1, drop: bool = True, timeout: int = timeout_default) -> bytes:
- """Receives numlines lines from the child process stderr.
-
- Args:
- numlines (int, optional): number of lines to receive. Defaults to 1.
- drop (bool, optional): drop the line ending. Defaults to True.
- timeout (int, optional): timeout in seconds. Defaults to timeout_default.
-
- Returns:
- bytes: received lines from the child process stdout.
- """
- return self.recverruntil(delims=b"\n", occurences=numlines, drop=drop, timeout=timeout)
-
- def send(self: PipeManager, data: bytes) -> int:
- """Sends data to the child process stdin.
-
- Args:
- data (bytes): data to send.
-
- Returns:
- int: number of bytes sent.
-
- Raises:
- RuntimeError: no stdin pipe of the child process.
- """
- if not self.stdin_write:
- raise RuntimeError("No stdin pipe of the child process")
-
- liblog.pipe(f"Sending {len(data)} bytes to the child process: {data!r}")
-
- if isinstance(data, str):
- liblog.warning("The input data is a string, converting to bytes")
- data = data.encode()
-
- return os.write(self.stdin_write, data)
-
- def sendline(self: PipeManager, data: bytes) -> int:
- """Sends data to the child process stdin and append a newline.
-
- Args:
- data (bytes): data to send.
-
- Returns:
- int: number of bytes sent.
- """
- if isinstance(data, str):
- liblog.warning("The input data is a string, converting to bytes")
- data = data.encode()
-
- return self.send(data=data + b"\n")
-
- def sendafter(
- self: PipeManager,
- delims: bytes,
- data: bytes,
- occurences: int = 1,
- drop: bool = False,
- timeout: int = timeout_default,
- ) -> tuple[bytes, int]:
- """Sends data to the child process stdin after the delimiters are found.
-
- Args:
- delims (bytes): delimiters where to stop.
- data (bytes): data to send.
- occurences (int, optional): number of delimiters to find. Defaults to 1.
- drop (bool, optional): drop the delimiter. Defaults to False.
- timeout (int, optional): timeout in seconds. Defaults to timeout_default.
-
- Returns:
- bytes: received data from the child process stdout.
- int: number of bytes sent.
- """
- received = self.recvuntil(delims=delims, occurences=occurences, drop=drop, timeout=timeout)
- sent = self.send(data)
- return (received, sent)
-
- def sendlineafter(
- self: PipeManager,
- delims: bytes,
- data: bytes,
- occurences: int = 1,
- drop: bool = False,
- timeout: int = timeout_default,
- ) -> tuple[bytes, int]:
- """Sends line to the child process stdin after the delimiters are found.
-
- Args:
- delims (bytes): delimiters where to stop.
- data (bytes): data to send.
- occurences (int, optional): number of delimiters to find. Defaults to 1.
- drop (bool, optional): drop the delimiter. Defaults to False.
- timeout (int, optional): timeout in seconds. Defaults to timeout_default.
-
- Returns:
- bytes: received data from the child process stdout.
- int: number of bytes sent.
- """
- received = self.recvuntil(delims=delims, occurences=occurences, drop=drop, timeout=timeout)
- sent = self.sendline(data)
- return (received, sent)
diff --git a/libdebug/utils/platform_utils.py b/libdebug/utils/platform_utils.py
index f83b7e88..324d3b7b 100644
--- a/libdebug/utils/platform_utils.py
+++ b/libdebug/utils/platform_utils.py
@@ -19,5 +19,7 @@ def get_platform_register_size(arch: str) -> int:
return 8
case "aarch64":
return 8
+ case "i386":
+ return 4
case _:
raise ValueError(f"Architecture {arch} not supported.")
diff --git a/libdebug/utils/process_utils.py b/libdebug/utils/process_utils.py
index 94f03e3a..fd51b086 100644
--- a/libdebug/utils/process_utils.py
+++ b/libdebug/utils/process_utils.py
@@ -1,6 +1,6 @@
#
# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Roberto Alessandro Bertolini. All rights reserved.
+# Copyright (c) 2023-2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
@@ -10,10 +10,11 @@
from libdebug.cffi._personality_cffi import lib as lib_personality
from libdebug.data.memory_map import MemoryMap
+from libdebug.data.memory_map_list import MemoryMapList
@functools.cache
-def get_process_maps(process_id: int) -> list[MemoryMap]:
+def get_process_maps(process_id: int) -> MemoryMapList[MemoryMap]:
"""Returns the memory maps of the specified process.
Args:
@@ -25,7 +26,7 @@ def get_process_maps(process_id: int) -> list[MemoryMap]:
with Path(f"/proc/{process_id}/maps").open() as maps_file:
maps = maps_file.readlines()
- return [MemoryMap.parse(vmap) for vmap in maps]
+ return MemoryMapList([MemoryMap.parse(vmap) for vmap in maps])
@functools.cache
@@ -41,6 +42,21 @@ def get_open_fds(process_id: int) -> list[int]:
return [int(fd) for fd in os.listdir(f"/proc/{process_id}/fd")]
+def get_process_tasks(process_id: int) -> list[int]:
+ """Returns the tasks of the specified process.
+
+ Args:
+ process_id (int): The PID of the process whose tasks should be returned.
+
+ Returns:
+ list: A list of integers, each representing a task of the specified process.
+ """
+ tids = []
+ if Path(f"/proc/{process_id}/task").exists():
+ tids = [int(task) for task in os.listdir(f"/proc/{process_id}/task")]
+ return tids
+
+
def invalidate_process_cache() -> None:
"""Invalidates the cache of the functions in this module. Must be executed any time the process executes code."""
get_process_maps.cache_clear()
diff --git a/libdebug/utils/search_utils.py b/libdebug/utils/search_utils.py
new file mode 100644
index 00000000..2f6a1a30
--- /dev/null
+++ b/libdebug/utils/search_utils.py
@@ -0,0 +1,20 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+
+def find_all_overlapping_occurrences(pattern: bytes, data: bytes, abs_address: int = 0) -> list[int]:
+ """Find all overlapping occurrences of a pattern in a data."""
+ start = 0
+ occurrences = []
+ while True:
+ start = data.find(pattern, start)
+ if start == -1:
+ # No more occurrences
+ break
+ occurrences.append(start + abs_address)
+ # Increment start to find overlapping matches
+ start += 1
+ return occurrences
diff --git a/libdebug/utils/signal_utils.py b/libdebug/utils/signal_utils.py
index d0c99b88..9957b4fb 100644
--- a/libdebug/utils/signal_utils.py
+++ b/libdebug/utils/signal_utils.py
@@ -20,6 +20,13 @@ def create_signal_mappings() -> tuple[dict, dict]:
signal_to_number[name] = number
number_to_signal[number] = name
+ # RT signals have a different convention
+ for i in range(1, signal.SIGRTMAX - signal.SIGRTMIN):
+ name = f"SIGRTMIN+{i}"
+ number = signal.SIGRTMIN + i
+ signal_to_number[name] = number
+ number_to_signal[number] = name
+
return signal_to_number, number_to_signal
@@ -33,6 +40,9 @@ def resolve_signal_number(name: str) -> int:
Returns:
int: the signal number.
"""
+ if name in ["ALL", "all", "*", "pkm"]:
+ return -1
+
signal_to_number, _ = create_signal_mappings()
try:
@@ -51,6 +61,9 @@ def resolve_signal_name(number: int) -> str:
Returns:
str: the signal name.
"""
+ if number == -1:
+ return "ALL"
+
_, number_to_signal = create_signal_mappings()
try:
diff --git a/libdebug/utils/syscall_utils.py b/libdebug/utils/syscall_utils.py
index f96cb93e..1aa7c00e 100644
--- a/libdebug/utils/syscall_utils.py
+++ b/libdebug/utils/syscall_utils.py
@@ -21,6 +21,8 @@ def get_remote_definition_url(arch: str) -> str:
return f"{SYSCALLS_REMOTE}/x86/64/x64/latest/table.json"
case "aarch64":
return f"{SYSCALLS_REMOTE}/arm64/64/aarch64/latest/table.json"
+ case "i386":
+ return f"{SYSCALLS_REMOTE}/x86/32/ia32/latest/table.json"
case _:
raise ValueError(f"Architecture {arch} not supported")
@@ -59,6 +61,9 @@ def resolve_syscall_number(architecture: str, name: str) -> int:
"""Resolve a syscall name to its number."""
definitions = get_syscall_definitions(architecture)
+ if name in ["all", "*", "ALL", "pkm"]:
+ return -1
+
for syscall in definitions["syscalls"]:
if syscall["name"] == name:
return syscall["number"]
@@ -71,6 +76,9 @@ def resolve_syscall_name(architecture: str, number: int) -> str:
"""Resolve a syscall number to its name."""
definitions = get_syscall_definitions(architecture)
+ if number == -1:
+ return "all"
+
for syscall in definitions["syscalls"]:
if syscall["number"] == number:
return syscall["name"]
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 00000000..3fd60911
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,105 @@
+site_name: Docs / libdebug
+site_url: http://localhost:8000
+repo_url: https://github.com/libdebug/libdebug
+repo_name: libdebug
+theme:
+ name: material
+ logo: assets/libdebug_logo.webp
+ favicon: assets/favicon.ico
+ custom_dir: ./docs/overrides
+ palette:
+ # Palette toggle for light mode
+ - media: "(prefers-color-scheme: light)"
+ primary: green
+ accent: teal
+ toggle:
+ icon: material/lightbulb
+ name: Switch to dark mode
+
+ # Palette toggle for dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: teal
+ accent: dark-green
+ toggle:
+ icon: material/lightbulb-outline
+ name: Switch to light mode
+ font:
+ text: Nunito
+ code: Hack
+ icon:
+ repo: fontawesome/brands/github
+ features:
+ - navigation.instant
+ - navigation.instant.prefetch
+ - navigation.tabs
+ - navigation.tabs.sticky
+ - navigation.path
+ - navigation.footer
+ - search.suggest
+ - search.highlight
+ - content.code.copy
+ - content.code.annotate
+plugins:
+ - search
+ - blog
+ - mkdocstrings
+nav:
+ - index.md
+ - Code Examples:
+ - Examples Index: code_examples/examples_index.md
+ - nlinks - DEF CON Quals 2023: code_examples/example_nlinks.md
+ - CyberChallenge 24 Workshop: code_examples/example_cc24.md
+ - The Basics:
+ - libdebug 101: basics/libdebug101.md
+ - Running an Executable: basics/running_an_executable.md
+ - Process Death (and afterlife): basics/kill_and_post_mortem.md
+ - Default VS ASAP Mode: basics/command_queue.md
+ - Register Access: basics/register_access.md
+ - Memory Access: basics/memory_access.md
+ - Control Flow: basics/control_flow_commands.md
+ - Detaching and GDB Migration: basics/detach_and_gdb.md
+ - Supported Systems: basics/supported_systems.md
+ - Stopping Events:
+ - Stopping Events: stopping_events/stopping_events.md
+ - Debugging Flow of Stopping Events: stopping_events/debugging_flow.md
+ - Breakpoints: stopping_events/breakpoints.md
+ - Watchpoints: stopping_events/watchpoints.md
+ - Syscalls: stopping_events/syscalls.md
+ - Signals: stopping_events/signals.md
+ - Multithreading:
+ - Multithreaded Applications: multithreading/multithreading.md
+ - Logging:
+ - Logging: logging/liblog.md
+ - Quality of Life:
+ - Quality of Life: quality_of_life/quality_of_life.md
+ - Pretty Printing: quality_of_life/pretty_printing.md
+ - Symbol Resolution: quality_of_life/symbols.md
+ - Memory Maps: quality_of_life/memory_maps.md
+ - Stack Frame Utils: quality_of_life/stack_frame_utils.md
+ - Evasion of Anti-Debugging: quality_of_life/anti_debugging.md
+ - Blog:
+ - blog/index.md
+
+extra:
+ version:
+ provider: mike
+markdown_extensions:
+ - admonition
+ - pymdownx.superfences
+ - pymdownx.tabbed:
+ alternate_style: true
+ - pymdownx.highlight:
+ anchor_linenums: true
+ line_spans: __span
+ pygments_lang_class: true
+ - pymdownx.inlinehilite
+ - pymdownx.snippets
+ - attr_list
+ - md_in_html
+ - pymdownx.emoji:
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
+
+extra_css:
+ - stylesheets/extra.css
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index f36dc557..7e51d20c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -47,6 +47,7 @@ dependencies = [
"psutil",
"pyelftools",
"requests",
+ "prompt-toolkit",
]
[project.optional-dependencies]
diff --git a/setup.py b/setup.py
index a337f121..7f77da03 100644
--- a/setup.py
+++ b/setup.py
@@ -18,6 +18,7 @@
os.path.isfile("/usr/include/sys/ptrace.h")
or os.path.isfile("/usr/include/x86_64-linux-gnu/sys/ptrace.h")
or os.path.isfile("/usr/include/aarch64-linux-gnu/sys/ptrace.h")
+ or os.path.isfile("/usr/include/i386-linux-gnu/sys/ptrace.h")
):
print("Required C libraries not found. Please install ptrace or kernel headers")
exit(1)
@@ -69,11 +70,11 @@ def get_outputs(self):
setup(
name="libdebug",
- version="0.6.0",
+ version="0.7.0",
author="JinBlack, Io_no, MrIndeciso, Frank01001",
description="A library to debug binary programs",
packages=find_packages(include=["libdebug", "libdebug.*"]),
- install_requires=["capstone", "pyelftools", "cffi", "requests", "psutil"],
+ install_requires=["capstone", "pyelftools", "cffi", "requests", "psutil", "prompt-toolkit"],
setup_requires=["cffi"],
cffi_modules=[
"./libdebug/cffi/ptrace_cffi_build.py:ffibuilder",
diff --git a/test/aarch64/run_suite.py b/test/aarch64/run_suite.py
deleted file mode 100644
index ebf8be5d..00000000
--- a/test/aarch64/run_suite.py
+++ /dev/null
@@ -1,78 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import sys
-from unittest import TestLoader, TestSuite, TextTestRunner
-
-from scripts.attach_detach_test import AttachDetachTest
-from scripts.auto_waiting_test import AutoWaitingTest
-from scripts.backtrace_test import BacktraceTest
-from scripts.basic_test import BasicTest
-from scripts.basic_test_pie import BasicTestPie
-from scripts.basic_test_hw import BasicTestHw
-from scripts.breakpoint_test import BreakpointTest
-from scripts.brute_test import BruteTest
-from scripts.builtin_handler_test import BuiltinHandlerTest
-from scripts.callback_test import CallbackTest
-from scripts.catch_signal_test import CatchSignalTest
-from scripts.control_flow_test import ControlFlowTest
-from scripts.death_test import DeathTest
-from scripts.finish_test import FinishTest
-from scripts.floating_point_test import FloatingPointTest
-from scripts.handle_syscall_test import HandleSyscallTest
-from scripts.hijack_syscall_test import HijackSyscallTest
-from scripts.jumpstart_test import JumpstartTest
-from scripts.memory_test import MemoryTest
-from scripts.next_test import NextTest
-from scripts.signals_multithread_test import SignalMultithreadTest
-from scripts.speed_test import SpeedTest
-from scripts.thread_test_complex import ThreadTestComplex
-from scripts.thread_test import ThreadTest
-from scripts.watchpoint_test import WatchpointTest
-
-def fast_suite():
- suite = TestSuite()
-
- suite.addTest(TestLoader().loadTestsFromTestCase(AttachDetachTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(AutoWaitingTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(BacktraceTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(BasicTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(BasicTestPie))
- suite.addTest(TestLoader().loadTestsFromTestCase(BasicTestHw))
- suite.addTest(TestLoader().loadTestsFromTestCase(BreakpointTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(BruteTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(BuiltinHandlerTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(CallbackTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(CatchSignalTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(ControlFlowTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(DeathTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(FinishTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(FloatingPointTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(HandleSyscallTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(HijackSyscallTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(JumpstartTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(MemoryTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(NextTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(SignalMultithreadTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(SpeedTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(ThreadTestComplex))
- suite.addTest(TestLoader().loadTestsFromTestCase(ThreadTest))
- suite.addTest(TestLoader().loadTestsFromTestCase(WatchpointTest))
-
- return suite
-
-
-if __name__ == "__main__":
- if sys.version_info >= (3, 12):
- runner = TextTestRunner(verbosity=2, durations=3)
- else:
- runner = TextTestRunner(verbosity=2)
-
- suite = fast_suite()
-
- runner.run(suite)
-
- sys.exit(0)
diff --git a/test/aarch64/scripts/attach_detach_test.py b/test/aarch64/scripts/attach_detach_test.py
deleted file mode 100644
index 97311f40..00000000
--- a/test/aarch64/scripts/attach_detach_test.py
+++ /dev/null
@@ -1,92 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import logging
-import unittest
-
-from pwn import process
-
-from libdebug import debugger
-
-logging.getLogger("pwnlib").setLevel(logging.ERROR)
-
-
-class AttachDetachTest(unittest.TestCase):
- def setUp(self):
- pass
-
- def test_attach(self):
- r = process("binaries/attach_test")
-
- d = debugger()
- d.attach(r.pid)
- bp = d.breakpoint("printName", hardware=True)
- d.cont()
-
- r.recvuntil(b"name:")
- r.sendline(b"Io_no")
-
- self.assertTrue(d.regs.pc == bp.address)
-
- d.cont()
-
- d.kill()
-
- def test_attach_and_detach_1(self):
- r = process("binaries/attach_test")
-
- d = debugger()
-
- # Attach to the process
- d.attach(r.pid)
- d.detach()
-
- # Validate that, after detaching, the process is still running
- r.recvuntil(b"name:", timeout=1)
- r.sendline(b"Io_no")
-
- r.kill()
-
- def test_attach_and_detach_2(self):
- d = debugger("binaries/attach_test")
-
- # Run the process
- r = d.run()
- d.detach()
-
- # Validate that, after detaching, the process is still running
- r.recvuntil(b"name:", timeout=1)
- r.sendline(b"Io_no")
-
- d.kill()
-
- def test_attach_and_detach_3(self):
- d = debugger("binaries/attach_test")
-
- r = d.run()
-
- # We must ensure that any breakpoint is unset before detaching
- d.breakpoint(0xa04, file="binary")
- d.breakpoint(0xa08, hardware=True, file="binary")
-
- d.detach()
-
- # Validate that, after detaching, the process is still running
- r.recvuntil(b"name:", timeout=1)
- r.sendline(b"Io_no")
-
- d.kill()
-
- def test_attach_and_detach_4(self):
- r = process("binaries/attach_test")
-
- d = debugger()
- d.attach(r.pid)
- d.detach()
- d.kill()
-
- # Validate that, after detaching and killing, the process is effectively terminated
- self.assertRaises(EOFError, r.sendline, b"provola")
\ No newline at end of file
diff --git a/test/aarch64/scripts/backtrace_test.py b/test/aarch64/scripts/backtrace_test.py
deleted file mode 100644
index 7c497e13..00000000
--- a/test/aarch64/scripts/backtrace_test.py
+++ /dev/null
@@ -1,108 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class BacktraceTest(unittest.TestCase):
- def setUp(self):
- self.d = debugger("binaries/backtrace_test")
-
- def test_backtrace(self):
- d = self.d
-
- d.run()
-
- bp0 = d.breakpoint("main+8")
- bp1 = d.breakpoint("function1+8")
- bp2 = d.breakpoint("function2+8")
- bp3 = d.breakpoint("function3+8")
- bp4 = d.breakpoint("function4+8")
- bp5 = d.breakpoint("function5+8")
- bp6 = d.breakpoint("function6+8")
-
- d.cont()
-
- self.assertTrue(d.regs.pc == bp0.address)
- backtrace = d.backtrace(as_symbols=True)
- self.assertIn("_start", backtrace.pop())
- self.assertEqual(backtrace[:1], ["main+8"])
-
- d.cont()
-
- self.assertTrue(d.regs.pc == bp1.address)
- backtrace = d.backtrace(as_symbols=True)
- self.assertIn("_start", backtrace.pop())
- self.assertEqual(backtrace[:2], ["function1+8", "main+c"])
-
- d.cont()
-
- self.assertTrue(d.regs.pc == bp2.address)
- backtrace = d.backtrace(as_symbols=True)
- self.assertIn("_start", backtrace.pop())
- self.assertEqual(backtrace[:3], ["function2+8", "function1+10", "main+c"])
-
- d.cont()
-
- self.assertTrue(d.regs.pc == bp3.address)
- backtrace = d.backtrace(as_symbols=True)
- self.assertIn("_start", backtrace.pop())
- self.assertEqual(
- backtrace[:4], ["function3+8", "function2+18", "function1+10", "main+c"]
- )
-
- d.cont()
-
- self.assertTrue(d.regs.pc == bp4.address)
- backtrace = d.backtrace(as_symbols=True)
- self.assertIn("_start", backtrace.pop())
- self.assertEqual(
- backtrace[:5],
- ["function4+8", "function3+18", "function2+18", "function1+10", "main+c"],
- )
-
- d.cont()
-
- self.assertTrue(d.regs.pc == bp5.address)
- backtrace = d.backtrace(as_symbols=True)
- self.assertIn("_start", backtrace.pop())
- self.assertEqual(
- backtrace[:6],
- [
- "function5+8",
- "function4+18",
- "function3+18",
- "function2+18",
- "function1+10",
- "main+c",
- ],
- )
-
- d.cont()
-
- self.assertTrue(d.regs.pc == bp6.address)
- backtrace = d.backtrace(as_symbols=True)
- self.assertIn("_start", backtrace.pop())
- self.assertEqual(
- backtrace[:7],
- [
- "function6+8",
- "function5+18",
- "function4+18",
- "function3+18",
- "function2+18",
- "function1+10",
- "main+c",
- ],
- )
-
- d.kill()
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/test/aarch64/scripts/basic_test.py b/test/aarch64/scripts/basic_test.py
deleted file mode 100644
index 758ffb94..00000000
--- a/test/aarch64/scripts/basic_test.py
+++ /dev/null
@@ -1,135 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class BasicTest(unittest.TestCase):
-
- def test_basic(self):
- d = debugger("binaries/basic_test")
- d.run()
- bp = d.breakpoint("register_test")
- d.cont()
- assert bp.address == d.regs.pc
- d.cont()
- d.kill()
- d.terminate()
-
- def test_registers(self):
- d = debugger("binaries/basic_test")
- d.run()
-
- bp = d.breakpoint(0x4008a4)
-
- d.cont()
-
- assert d.regs.pc == bp.address
-
- assert d.regs.x0 == 0x4444333322221111
- assert d.regs.x1 == 0x8888777766665555
- assert d.regs.x2 == 0xccccbbbbaaaa9999
- assert d.regs.x3 == 0x1111ffffeeeedddd
- assert d.regs.x4 == 0x5555444433332222
- assert d.regs.x5 == 0x9999888877776666
- assert d.regs.x6 == 0xddddccccbbbbaaaa
- assert d.regs.x7 == 0x22221111ffffeeee
- assert d.regs.x8 == 0x6666555544443333
- assert d.regs.x9 == 0xaaaa999988887777
- assert d.regs.x10 == 0xeeeeddddccccbbbb
- assert d.regs.x11 == 0x333322221111ffff
- assert d.regs.x12 == 0x7777666655554444
- assert d.regs.x13 == 0xbbbbaaaa99998888
- assert d.regs.x14 == 0xffffeeeeddddcccc
- assert d.regs.x15 == 0x4444333322221111
- assert d.regs.x16 == 0x8888777766665555
- assert d.regs.x17 == 0xccccbbbbaaaa9999
- assert d.regs.x18 == 0x1111ffffeeeedddd
- assert d.regs.x19 == 0x5555444433332222
- assert d.regs.x20 == 0x9999888877776666
- assert d.regs.x21 == 0xddddccccbbbbaaaa
- assert d.regs.x22 == 0x22221111ffffeeee
- assert d.regs.x23 == 0x6666555544443333
- assert d.regs.x24 == 0xaaaa999988887777
- assert d.regs.x25 == 0xeeeeddddccccbbbb
- assert d.regs.x26 == 0x333322221111ffff
- assert d.regs.x27 == 0x7777666655554444
- assert d.regs.x28 == 0xbbbbaaaa99998888
- assert d.regs.x29 == 0xffffeeeeddddcccc
- assert d.regs.x30 == 0x4444333322221111
-
- assert d.regs.lr == 0x4444333322221111
- assert d.regs.fp == 0xffffeeeeddddcccc
- assert d.regs.xzr == 0
- assert d.regs.wzr == 0
-
- d.regs.xzr = 0x123456789abcdef0
- d.regs.wzr = 0x12345678
-
- assert d.regs.xzr == 0
- assert d.regs.wzr == 0
-
- assert d.regs.w0 == 0x22221111
- assert d.regs.w1 == 0x66665555
- assert d.regs.w2 == 0xaaaa9999
- assert d.regs.w3 == 0xeeeedddd
- assert d.regs.w4 == 0x33332222
- assert d.regs.w5 == 0x77776666
- assert d.regs.w6 == 0xbbbbaaaa
- assert d.regs.w7 == 0xffffeeee
- assert d.regs.w8 == 0x44443333
- assert d.regs.w9 == 0x88887777
- assert d.regs.w10 == 0xccccbbbb
- assert d.regs.w11 == 0x1111ffff
- assert d.regs.w12 == 0x55554444
- assert d.regs.w13 == 0x99998888
- assert d.regs.w14 == 0xddddcccc
- assert d.regs.w15 == 0x22221111
- assert d.regs.w16 == 0x66665555
- assert d.regs.w17 == 0xaaaa9999
- assert d.regs.w18 == 0xeeeedddd
- assert d.regs.w19 == 0x33332222
- assert d.regs.w20 == 0x77776666
- assert d.regs.w21 == 0xbbbbaaaa
- assert d.regs.w22 == 0xffffeeee
- assert d.regs.w23 == 0x44443333
- assert d.regs.w24 == 0x88887777
- assert d.regs.w25 == 0xccccbbbb
- assert d.regs.w26 == 0x1111ffff
- assert d.regs.w27 == 0x55554444
- assert d.regs.w28 == 0x99998888
- assert d.regs.w29 == 0xddddcccc
- assert d.regs.w30 == 0x22221111
-
- d.cont()
-
- d.kill()
- d.terminate()
-
- def test_step(self):
- d = debugger("binaries/basic_test")
-
- d.run()
- bp = d.breakpoint("register_test")
- d.cont()
-
- assert bp.address == d.regs.pc
- assert bp.hit_count == 1
-
- d.step()
-
- assert (bp.address + 4) == d.regs.pc
- assert bp.hit_count == 1
-
- d.step()
-
- assert (bp.address + 8) == d.regs.pc
- assert bp.hit_count == 1
-
- d.kill()
- d.terminate()
diff --git a/test/aarch64/scripts/basic_test_hw.py b/test/aarch64/scripts/basic_test_hw.py
deleted file mode 100644
index e0320b84..00000000
--- a/test/aarch64/scripts/basic_test_hw.py
+++ /dev/null
@@ -1,123 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class BasicTestHw(unittest.TestCase):
- def test_basic(self):
- d = debugger("binaries/basic_test")
- d.run()
- bp = d.breakpoint("register_test", hardware=True)
- d.cont()
- assert bp.address == d.regs.pc
- d.cont()
- d.kill()
- d.terminate()
-
- def test_registers(self):
- d = debugger("binaries/basic_test")
- d.run()
-
- bp = d.breakpoint(0x4008a4, hardware=True)
-
- d.cont()
-
- assert d.regs.pc == bp.address
-
- assert d.regs.x0 == 0x4444333322221111
- assert d.regs.x1 == 0x8888777766665555
- assert d.regs.x2 == 0xccccbbbbaaaa9999
- assert d.regs.x3 == 0x1111ffffeeeedddd
- assert d.regs.x4 == 0x5555444433332222
- assert d.regs.x5 == 0x9999888877776666
- assert d.regs.x6 == 0xddddccccbbbbaaaa
- assert d.regs.x7 == 0x22221111ffffeeee
- assert d.regs.x8 == 0x6666555544443333
- assert d.regs.x9 == 0xaaaa999988887777
- assert d.regs.x10 == 0xeeeeddddccccbbbb
- assert d.regs.x11 == 0x333322221111ffff
- assert d.regs.x12 == 0x7777666655554444
- assert d.regs.x13 == 0xbbbbaaaa99998888
- assert d.regs.x14 == 0xffffeeeeddddcccc
- assert d.regs.x15 == 0x4444333322221111
- assert d.regs.x16 == 0x8888777766665555
- assert d.regs.x17 == 0xccccbbbbaaaa9999
- assert d.regs.x18 == 0x1111ffffeeeedddd
- assert d.regs.x19 == 0x5555444433332222
- assert d.regs.x20 == 0x9999888877776666
- assert d.regs.x21 == 0xddddccccbbbbaaaa
- assert d.regs.x22 == 0x22221111ffffeeee
- assert d.regs.x23 == 0x6666555544443333
- assert d.regs.x24 == 0xaaaa999988887777
- assert d.regs.x25 == 0xeeeeddddccccbbbb
- assert d.regs.x26 == 0x333322221111ffff
- assert d.regs.x27 == 0x7777666655554444
- assert d.regs.x28 == 0xbbbbaaaa99998888
- assert d.regs.x29 == 0xffffeeeeddddcccc
- assert d.regs.x30 == 0x4444333322221111
-
- assert d.regs.w0 == 0x22221111
- assert d.regs.w1 == 0x66665555
- assert d.regs.w2 == 0xaaaa9999
- assert d.regs.w3 == 0xeeeedddd
- assert d.regs.w4 == 0x33332222
- assert d.regs.w5 == 0x77776666
- assert d.regs.w6 == 0xbbbbaaaa
- assert d.regs.w7 == 0xffffeeee
- assert d.regs.w8 == 0x44443333
- assert d.regs.w9 == 0x88887777
- assert d.regs.w10 == 0xccccbbbb
- assert d.regs.w11 == 0x1111ffff
- assert d.regs.w12 == 0x55554444
- assert d.regs.w13 == 0x99998888
- assert d.regs.w14 == 0xddddcccc
- assert d.regs.w15 == 0x22221111
- assert d.regs.w16 == 0x66665555
- assert d.regs.w17 == 0xaaaa9999
- assert d.regs.w18 == 0xeeeedddd
- assert d.regs.w19 == 0x33332222
- assert d.regs.w20 == 0x77776666
- assert d.regs.w21 == 0xbbbbaaaa
- assert d.regs.w22 == 0xffffeeee
- assert d.regs.w23 == 0x44443333
- assert d.regs.w24 == 0x88887777
- assert d.regs.w25 == 0xccccbbbb
- assert d.regs.w26 == 0x1111ffff
- assert d.regs.w27 == 0x55554444
- assert d.regs.w28 == 0x99998888
- assert d.regs.w29 == 0xddddcccc
- assert d.regs.w30 == 0x22221111
-
- d.cont()
-
- d.kill()
- d.terminate()
-
- def test_step(self):
- d = debugger("binaries/basic_test")
-
- d.run()
- bp = d.breakpoint("register_test", hardware=True)
- d.cont()
-
- assert bp.address == d.regs.pc
- assert bp.hit_count == 1
-
- d.step()
-
- assert (bp.address + 4) == d.regs.pc
- assert bp.hit_count == 1
-
- d.step()
-
- assert (bp.address + 8) == d.regs.pc
- assert bp.hit_count == 1
-
- d.kill()
- d.terminate()
\ No newline at end of file
diff --git a/test/aarch64/scripts/basic_test_pie.py b/test/aarch64/scripts/basic_test_pie.py
deleted file mode 100644
index 1ff08c44..00000000
--- a/test/aarch64/scripts/basic_test_pie.py
+++ /dev/null
@@ -1,24 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-class BasicTestPie(unittest.TestCase):
- def test_basic(self):
- d = debugger("binaries/basic_test_pie")
-
- d.run()
- bp = d.breakpoint(0x780, file="binary")
- d.cont()
-
- assert bp.address == d.regs.pc
- assert d.regs.x0 == 0xaabbccdd11223344
-
- d.cont()
- d.kill()
- d.terminate()
\ No newline at end of file
diff --git a/test/aarch64/scripts/breakpoint_test.py b/test/aarch64/scripts/breakpoint_test.py
deleted file mode 100644
index b95b1c50..00000000
--- a/test/aarch64/scripts/breakpoint_test.py
+++ /dev/null
@@ -1,375 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import io
-import logging
-import unittest
-
-from libdebug import debugger
-
-
-class BreakpointTest(unittest.TestCase):
- def setUp(self):
- # Redirect logging to a string buffer
- self.log_capture_string = io.StringIO()
- self.log_handler = logging.StreamHandler(self.log_capture_string)
- self.log_handler.setLevel(logging.WARNING)
-
- self.logger = logging.getLogger("libdebug")
- self.original_handlers = self.logger.handlers
- self.logger.handlers = []
- self.logger.addHandler(self.log_handler)
- self.logger.setLevel(logging.WARNING)
-
- def test_bps(self):
- d = debugger("binaries/breakpoint_test")
-
- d.run()
-
- bp1 = d.breakpoint("random_function")
- bp2 = d.breakpoint(0x7fc, file="binary")
- bp3 = d.breakpoint(0x820, file="binary")
-
- counter = 1
-
- d.cont()
-
- while True:
- if d.regs.pc == bp1.address:
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- elif d.regs.pc == bp2.address:
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- counter += 1
- elif d.regs.pc == bp3.address:
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.x1 == 45)
- self.assertTrue(d.regs.w1 == 45)
- self.assertTrue(bp3.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- break
-
- d.cont()
-
- assert bp2.hit_count == 10
-
- d.kill()
- d.terminate()
-
- def test_bp_disable(self):
- d = debugger("binaries/breakpoint_test")
-
- d.run()
-
- bp1 = d.breakpoint("random_function")
- bp2 = d.breakpoint(0x7fc, file="binary")
- bp3 = d.breakpoint(0x820, file="binary")
-
- counter = 1
-
- d.cont()
-
- while True:
- if d.regs.pc == bp1.address:
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- elif d.regs.pc == bp2.address:
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- bp2.disable()
- elif d.regs.pc == bp3.address:
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.w1 == 45)
- self.assertTrue(d.regs.x1 == 45)
- self.assertTrue(bp3.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- break
-
- d.cont()
-
- assert bp2.hit_count == 1
-
- d.kill()
- d.terminate()
-
- def test_bp_disable_hardware(self):
- d = debugger("binaries/breakpoint_test")
-
- d.run()
-
- bp1 = d.breakpoint("random_function")
- bp2 = d.breakpoint(0x7fc, file="binary", hardware=True)
- bp3 = d.breakpoint(0x820, file="binary")
-
- counter = 1
-
- d.cont()
-
- while True:
- if d.regs.pc == bp1.address:
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- elif d.regs.pc == bp2.address:
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- bp2.disable()
- elif d.regs.pc == bp3.address:
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.w1 == 45)
- self.assertTrue(d.regs.x1 == 45)
- self.assertTrue(bp3.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- break
-
- d.cont()
-
- assert bp2.hit_count == 1
-
- d.kill()
- d.terminate()
-
- def test_bp_disable_reenable(self):
- d = debugger("binaries/breakpoint_test")
-
- d.run()
-
- bp1 = d.breakpoint("random_function")
- bp2 = d.breakpoint(0x7fc, file="binary")
- bp4 = d.breakpoint(0x814, file="binary")
- bp3 = d.breakpoint(0x820, file="binary")
-
- counter = 1
-
- d.cont()
-
- while True:
- if d.regs.pc == bp1.address:
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- elif d.regs.pc == bp2.address:
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- if bp4.enabled:
- bp4.disable()
- else:
- bp4.enable()
- counter += 1
- elif d.regs.pc == bp3.address:
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.w1 == 45)
- self.assertTrue(d.regs.x1 == 45)
- self.assertTrue(bp3.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- break
- elif bp4.hit_on(d):
- pass
-
- d.cont()
-
- assert bp3.hit_count == 1
- assert bp4.hit_count == (bp2.hit_count // 2 + 1)
-
- d.kill()
- d.terminate()
-
- def test_bp_disable_reenable_hardware(self):
- d = debugger("binaries/breakpoint_test")
-
- d.run()
-
- bp1 = d.breakpoint("random_function", hardware=True)
- bp2 = d.breakpoint(0x7fc, file="binary", hardware=True)
- bp4 = d.breakpoint(0x810, file="binary", hardware=True)
- bp3 = d.breakpoint(0x820, file="binary", hardware=True)
-
- counter = 1
-
- d.cont()
-
- for _ in range(20):
- if d.regs.pc == bp1.address:
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- elif d.regs.pc == bp2.address:
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- if bp4.enabled:
- bp4.disable()
- else:
- bp4.enable()
- counter += 1
- elif d.regs.pc == bp3.address:
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.w1 == 45)
- self.assertTrue(d.regs.x1 == 45)
- self.assertTrue(bp3.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- break
- elif bp4.hit_on(d):
- pass
-
- d.cont()
-
- assert bp4.hit_count == (bp2.hit_count // 2 + 1)
-
- d.kill()
- d.terminate()
-
- def test_bps_running(self):
- d = debugger("binaries/breakpoint_test")
-
- d.run()
-
- bp1 = d.breakpoint("random_function")
- bp2 = d.breakpoint(0x7fc, file="binary")
- bp3 = d.breakpoint(0x820, file="binary")
-
- counter = 1
-
- d.cont()
-
- while True:
- if d.running:
- pass
- if d.regs.pc == bp1.address:
- self.assertFalse(d.running)
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- elif d.regs.pc == bp2.address:
- self.assertFalse(d.running)
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- counter += 1
- elif d.regs.pc == bp3.address:
- self.assertFalse(d.running)
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.w1 == 45)
- self.assertTrue(d.regs.x1 == 45)
- self.assertTrue(bp3.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- break
-
- d.cont()
-
- assert bp2.hit_count == 10
-
- d.kill()
- d.terminate()
-
- def test_bp_backing_file(self):
- d = debugger("binaries/executable_section_test")
-
- d.run()
-
- bp1 = d.breakpoint(0x968, file="binary")
-
- d.cont()
-
- d.wait()
-
- if bp1.hit_on(d):
- for vmap in d.maps():
- if "x" in vmap.permissions and "anon" in vmap.backing_file:
- section = vmap.backing_file
- bp2 = d.breakpoint(0x10, file=section)
- d.cont()
-
- d.wait()
-
- if bp2.hit_on(d):
- self.assertEqual(d.memory[d.regs.pc, 4], bytes.fromhex("ff430091"))
- self.assertEqual(d.regs.w0, 9)
-
- d.kill()
-
- self.assertEqual(bp1.hit_count, 1)
- self.assertEqual(bp2.hit_count, 1)
-
- d.run()
-
- bp1 = d.breakpoint(0x968, file="executable_section_test")
-
- d.cont()
-
- d.wait()
-
- if bp1.hit_on(d):
- for vmap in d.maps():
- if "x" in vmap.permissions and "anon" in vmap.backing_file:
- section = vmap.backing_file
- bp2 = d.breakpoint(0x10, file=section)
- d.cont()
-
- d.wait()
-
- if bp2.hit_on(d):
- self.assertEqual(d.memory[d.regs.pc, 4], bytes.fromhex("ff430091"))
- self.assertEqual(d.regs.w0, 9)
-
- d.run()
-
- bp1 = d.breakpoint(0x968, file="hybrid")
-
- d.cont()
-
- d.wait()
-
- if bp1.hit_on(d):
- for vmap in d.maps():
- if "x" in vmap.permissions and "anon" in vmap.backing_file:
- section = vmap.backing_file
- bp2 = d.breakpoint(0x10, file=section)
- d.cont()
-
- d.wait()
-
- if bp2.hit_on(d):
- self.assertEqual(d.memory[d.regs.pc, 4], bytes.fromhex("ff430091"))
- self.assertEqual(d.regs.w0, 9)
-
- d.kill()
-
- self.assertEqual(bp1.hit_count, 1)
- self.assertEqual(bp2.hit_count, 1)
-
- d.run()
-
- with self.assertRaises(ValueError):
- d.breakpoint(0x968, file="absolute")
-
- d.kill()
- d.terminate()
diff --git a/test/aarch64/scripts/brute_test.py b/test/aarch64/scripts/brute_test.py
deleted file mode 100644
index 0b4e0c82..00000000
--- a/test/aarch64/scripts/brute_test.py
+++ /dev/null
@@ -1,48 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio, Francesco Panebianco. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import string
-import unittest
-
-from libdebug import debugger
-
-
-class BruteTest(unittest.TestCase):
- def setUp(self):
- pass
-
- def test_bruteforce(self):
- flag = ""
- counter = 1
-
- d = debugger("binaries/brute_test")
-
- while not flag or flag != "BRUTINOBRUTONE":
- for c in string.printable:
- r = d.run()
- bp = d.breakpoint(0x974, hardware=True, file="binary")
- d.cont()
-
- r.sendlineafter(b"chars\n", (flag + c).encode())
-
- while bp.address == d.regs.pc:
- d.cont()
-
- if bp.hit_count > counter:
- flag += c
- counter = bp.hit_count
- d.kill()
- break
-
- message = r.recvline()
-
- d.kill()
-
- if message == b"Giusto!":
- flag += c
- break
-
- self.assertEqual(flag, "BRUTINOBRUTONE")
diff --git a/test/aarch64/scripts/catch_signal_test.py b/test/aarch64/scripts/catch_signal_test.py
deleted file mode 100644
index 82e16d08..00000000
--- a/test/aarch64/scripts/catch_signal_test.py
+++ /dev/null
@@ -1,1266 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import io
-import logging
-import unittest
-
-from libdebug import debugger
-
-
-class CatchSignalTest(unittest.TestCase):
- def setUp(self):
- # Redirect logging to a string buffer
- self.log_capture_string = io.StringIO()
- self.log_handler = logging.StreamHandler(self.log_capture_string)
- self.log_handler.setLevel(logging.WARNING)
-
- self.logger = logging.getLogger("libdebug")
- self.original_handlers = self.logger.handlers
- self.logger.handlers = []
- self.logger.addHandler(self.log_handler)
- self.logger.setLevel(logging.WARNING)
-
- def tearDown(self):
- self.logger.removeHandler(self.log_handler)
- self.logger.handlers = self.original_handlers
- self.log_handler.close()
-
- def test_signal_catch_signal_block(self):
- SIGUSR1_count = 0
- SIGINT_count = 0
- SIGQUIT_count = 0
- SIGTERM_count = 0
- SIGPIPE_count = 0
-
- def catcher_SIGUSR1(t, sc):
- nonlocal SIGUSR1_count
-
- SIGUSR1_count += 1
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
-
- SIGTERM_count += 1
-
- def catcher_SIGINT(t, sc):
- nonlocal SIGINT_count
-
- SIGINT_count += 1
-
- def catcher_SIGQUIT(t, sc):
- nonlocal SIGQUIT_count
-
- SIGQUIT_count += 1
-
- def catcher_SIGPIPE(t, sc):
- nonlocal SIGPIPE_count
-
- SIGPIPE_count += 1
-
- d = debugger("binaries/catch_signal_test")
-
- d.signals_to_block = ["SIGUSR1", 15, "SIGINT", 3, 13]
-
- d.run()
-
- catcher1 = d.catch_signal(10, callback=catcher_SIGUSR1)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
- catcher3 = d.catch_signal(2, callback=catcher_SIGINT)
- catcher4 = d.catch_signal("SIGQUIT", callback=catcher_SIGQUIT)
- catcher5 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 2)
- self.assertEqual(SIGTERM_count, 2)
- self.assertEqual(SIGINT_count, 2)
- self.assertEqual(SIGQUIT_count, 3)
- self.assertEqual(SIGPIPE_count, 3)
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
- self.assertEqual(SIGINT_count, catcher3.hit_count)
- self.assertEqual(SIGQUIT_count, catcher4.hit_count)
- self.assertEqual(SIGPIPE_count, catcher5.hit_count)
-
- def test_signal_pass_to_process(self):
- SIGUSR1_count = 0
- SIGINT_count = 0
- SIGQUIT_count = 0
- SIGTERM_count = 0
- SIGPIPE_count = 0
-
- def catcher_SIGUSR1(t, sc):
- nonlocal SIGUSR1_count
-
- SIGUSR1_count += 1
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
-
- SIGTERM_count += 1
-
- def catcher_SIGINT(t, sc):
- nonlocal SIGINT_count
-
- SIGINT_count += 1
-
- def catcher_SIGQUIT(t, sc):
- nonlocal SIGQUIT_count
-
- SIGQUIT_count += 1
-
- def catcher_SIGPIPE(t, sc):
- nonlocal SIGPIPE_count
-
- SIGPIPE_count += 1
-
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- catcher1 = d.catch_signal("SIGUSR1", callback=catcher_SIGUSR1)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
- catcher3 = d.catch_signal("SIGINT", callback=catcher_SIGINT)
- catcher4 = d.catch_signal("SIGQUIT", callback=catcher_SIGQUIT)
- catcher5 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
-
- d.cont()
-
- SIGUSR1 = r.recvline()
- SIGTERM = r.recvline()
- SIGINT = r.recvline()
- SIGQUIT = r.recvline()
- SIGPIPE = r.recvline()
-
- SIGUSR1 += r.recvline()
- SIGTERM += r.recvline()
- SIGINT += r.recvline()
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 2)
- self.assertEqual(SIGTERM_count, 2)
- self.assertEqual(SIGINT_count, 2)
- self.assertEqual(SIGQUIT_count, 3)
- self.assertEqual(SIGPIPE_count, 3)
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
- self.assertEqual(SIGINT_count, catcher3.hit_count)
- self.assertEqual(SIGQUIT_count, catcher4.hit_count)
- self.assertEqual(SIGPIPE_count, catcher5.hit_count)
-
- self.assertEqual(SIGUSR1, b"Received signal 10" * 2)
- self.assertEqual(SIGTERM, b"Received signal 15" * 2)
- self.assertEqual(SIGINT, b"Received signal 2" * 2)
- self.assertEqual(SIGQUIT, b"Received signal 3" * 3)
- self.assertEqual(SIGPIPE, b"Received signal 13" * 3)
-
- def test_signal_disable_catch_signal(self):
- SIGUSR1_count = 0
- SIGINT_count = 0
- SIGQUIT_count = 0
- SIGTERM_count = 0
- SIGPIPE_count = 0
-
- def catcher_SIGUSR1(t, sc):
- nonlocal SIGUSR1_count
-
- SIGUSR1_count += 1
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
-
- SIGTERM_count += 1
-
- def catcher_SIGINT(t, sc):
- nonlocal SIGINT_count
-
- SIGINT_count += 1
-
- def catcher_SIGQUIT(t, sc):
- nonlocal SIGQUIT_count
-
- SIGQUIT_count += 1
-
- def catcher_SIGPIPE(t, sc):
- nonlocal SIGPIPE_count
-
- SIGPIPE_count += 1
-
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- catcher1 = d.catch_signal("SIGUSR1", callback=catcher_SIGUSR1)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
- catcher3 = d.catch_signal("SIGINT", callback=catcher_SIGINT)
- catcher4 = d.catch_signal("SIGQUIT", callback=catcher_SIGQUIT)
- catcher5 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
-
- bp = d.breakpoint(0x964)
-
- d.cont()
-
- SIGUSR1 = r.recvline()
- SIGTERM = r.recvline()
- SIGINT = r.recvline()
- SIGQUIT = r.recvline()
- SIGPIPE = r.recvline()
-
- SIGUSR1 += r.recvline()
- SIGTERM += r.recvline()
- SIGINT += r.recvline()
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- # Uncatchering signals
- if bp.hit_on(d):
- catcher4.disable()
- catcher5.disable()
- d.cont()
-
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 2)
- self.assertEqual(SIGTERM_count, 2)
- self.assertEqual(SIGINT_count, 2)
- self.assertEqual(SIGQUIT_count, 2) # 1 times less because of the disable catch
- self.assertEqual(SIGPIPE_count, 2) # 1 times less because of the disable catch
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
- self.assertEqual(SIGINT_count, catcher3.hit_count)
- self.assertEqual(SIGQUIT_count, catcher4.hit_count)
- self.assertEqual(SIGPIPE_count, catcher5.hit_count)
-
- self.assertEqual(SIGUSR1, b"Received signal 10" * 2)
- self.assertEqual(SIGTERM, b"Received signal 15" * 2)
- self.assertEqual(SIGINT, b"Received signal 2" * 2)
- self.assertEqual(SIGQUIT, b"Received signal 3" * 3)
- self.assertEqual(SIGPIPE, b"Received signal 13" * 3)
-
- def test_signal_unblock(self):
- SIGUSR1_count = 0
- SIGINT_count = 0
- SIGQUIT_count = 0
- SIGTERM_count = 0
- SIGPIPE_count = 0
-
- def catcher_SIGUSR1(t, sc):
- nonlocal SIGUSR1_count
-
- SIGUSR1_count += 1
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
-
- SIGTERM_count += 1
-
- def catcher_SIGINT(t, sc):
- nonlocal SIGINT_count
-
- SIGINT_count += 1
-
- def catcher_SIGQUIT(t, sc):
- nonlocal SIGQUIT_count
-
- SIGQUIT_count += 1
-
- def catcher_SIGPIPE(t, sc):
- nonlocal SIGPIPE_count
-
- SIGPIPE_count += 1
-
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- d.signals_to_block = [10, 15, 2, 3, 13]
-
- catcher1 = d.catch_signal("SIGUSR1", callback=catcher_SIGUSR1)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
- catcher3 = d.catch_signal("SIGINT", callback=catcher_SIGINT)
- catcher4 = d.catch_signal("SIGQUIT", callback=catcher_SIGQUIT)
- catcher5 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
-
- bp = d.breakpoint(0x964)
-
- d.cont()
-
- # No block the signals anymore
- if bp.hit_on(d):
- d.signals_to_block = []
-
- d.cont()
-
- signal_received = []
- while True:
- try:
- signal_received.append(r.recvline())
- except RuntimeError:
- break
-
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 2)
- self.assertEqual(SIGTERM_count, 2)
- self.assertEqual(SIGINT_count, 2)
- self.assertEqual(SIGQUIT_count, 3)
- self.assertEqual(SIGPIPE_count, 3)
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
- self.assertEqual(SIGINT_count, catcher3.hit_count)
- self.assertEqual(SIGQUIT_count, catcher4.hit_count)
- self.assertEqual(SIGPIPE_count, catcher5.hit_count)
-
- self.assertEqual(signal_received[0], b"Received signal 3")
- self.assertEqual(signal_received[1], b"Received signal 13")
- self.assertEqual(signal_received[2], b"Exiting normally.")
-
- self.assertEqual(len(signal_received), 3)
-
- def test_signal_disable_catch_signal_unblock(self):
- SIGUSR1_count = 0
- SIGINT_count = 0
- SIGQUIT_count = 0
- SIGTERM_count = 0
- SIGPIPE_count = 0
-
- def catcher_SIGUSR1(t, sc):
- nonlocal SIGUSR1_count
-
- SIGUSR1_count += 1
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
-
- SIGTERM_count += 1
-
- def catcher_SIGINT(t, sc):
- nonlocal SIGINT_count
-
- SIGINT_count += 1
-
- def catcher_SIGQUIT(t, sc):
- nonlocal SIGQUIT_count
-
- SIGQUIT_count += 1
-
- def catcher_SIGPIPE(t, sc):
- nonlocal SIGPIPE_count
-
- SIGPIPE_count += 1
-
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- d.signals_to_block = [10, 15, 2, 3, 13]
-
- catcher1 = d.catch_signal("SIGUSR1", callback=catcher_SIGUSR1)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
- catcher3 = d.catch_signal("SIGINT", callback=catcher_SIGINT)
- catcher4 = d.catch_signal("SIGQUIT", callback=catcher_SIGQUIT)
- catcher5 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
-
- bp = d.breakpoint(0x964)
-
- d.cont()
-
- # No block the signals anymore
- if bp.hit_on(d):
- d.signals_to_block = []
- catcher4.disable()
- catcher5.disable()
-
- d.cont()
-
- signal_received = []
- while True:
- try:
- signal_received.append(r.recvline())
- except RuntimeError:
- break
-
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 2)
- self.assertEqual(SIGTERM_count, 2)
- self.assertEqual(SIGINT_count, 2)
- self.assertEqual(SIGQUIT_count, 2) # 1 times less because of the disable catch
- self.assertEqual(SIGPIPE_count, 2) # 1 times less because of the disable catch
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
- self.assertEqual(SIGINT_count, catcher3.hit_count)
- self.assertEqual(SIGQUIT_count, catcher4.hit_count)
- self.assertEqual(SIGPIPE_count, catcher5.hit_count)
-
- self.assertEqual(signal_received[0], b"Received signal 3")
- self.assertEqual(signal_received[1], b"Received signal 13")
- self.assertEqual(signal_received[2], b"Exiting normally.")
-
- self.assertEqual(len(signal_received), 3)
-
- def test_hijack_signal_with_catch_signal(self):
- def catcher_SIGUSR1(t, sc):
- # Hijack to SIGTERM
- t.signal = 15
-
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- catcher1 = d.catch_signal("SIGUSR1", callback=catcher_SIGUSR1)
-
- d.cont()
-
- SIGUSR1 = r.recvline()
- SIGTERM = r.recvline()
- SIGINT = r.recvline()
- SIGQUIT = r.recvline()
- SIGPIPE = r.recvline()
-
- SIGUSR1 += r.recvline()
- SIGTERM += r.recvline()
- SIGINT += r.recvline()
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- d.kill()
-
- self.assertEqual(catcher1.hit_count, 2)
-
- self.assertEqual(SIGUSR1, b"Received signal 15" * 2) # hijacked signal
- self.assertEqual(SIGTERM, b"Received signal 15" * 2)
- self.assertEqual(SIGINT, b"Received signal 2" * 2)
- self.assertEqual(SIGQUIT, b"Received signal 3" * 3)
- self.assertEqual(SIGPIPE, b"Received signal 13" * 3)
-
- def test_hijack_signal_with_api(self):
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- # Hijack to SIGTERM
- catcher1 = d.hijack_signal("SIGUSR1", 15)
-
- d.cont()
-
- SIGUSR1 = r.recvline()
- SIGTERM = r.recvline()
- SIGINT = r.recvline()
- SIGQUIT = r.recvline()
- SIGPIPE = r.recvline()
-
- SIGUSR1 += r.recvline()
- SIGTERM += r.recvline()
- SIGINT += r.recvline()
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- d.kill()
-
- self.assertEqual(catcher1.hit_count, 2)
-
- self.assertEqual(SIGUSR1, b"Received signal 15" * 2) # hijacked signal
- self.assertEqual(SIGTERM, b"Received signal 15" * 2)
- self.assertEqual(SIGINT, b"Received signal 2" * 2)
- self.assertEqual(SIGQUIT, b"Received signal 3" * 3)
- self.assertEqual(SIGPIPE, b"Received signal 13" * 3)
-
- def test_recursive_true_with_catch_signal(self):
- SIGUSR1_count = 0
- SIGTERM_count = 0
-
- def catcher_SIGUSR1(t, sc):
- nonlocal SIGUSR1_count
- # Hijack to SIGTERM
- t.signal = 15
-
- SIGUSR1_count += 1
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
-
- SIGTERM_count += 1
-
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- catcher1 = d.catch_signal(10, callback=catcher_SIGUSR1, recursive=True)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
-
- d.cont()
-
- SIGUSR1 = r.recvline()
- SIGTERM = r.recvline()
- SIGINT = r.recvline()
- SIGQUIT = r.recvline()
- SIGPIPE = r.recvline()
-
- SIGUSR1 += r.recvline()
- SIGTERM += r.recvline()
- SIGINT += r.recvline()
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 2)
- self.assertEqual(SIGTERM_count, 4) # 2 times more because of the hijack
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
-
- self.assertEqual(SIGUSR1, b"Received signal 15" * 2) # hijacked signal
- self.assertEqual(SIGTERM, b"Received signal 15" * 2)
- self.assertEqual(SIGINT, b"Received signal 2" * 2)
- self.assertEqual(SIGQUIT, b"Received signal 3" * 3)
- self.assertEqual(SIGPIPE, b"Received signal 13" * 3)
-
- def test_recursive_true_with_api(self):
- SIGTERM_count = 0
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
-
- SIGTERM_count += 1
-
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- catcher1 = d.hijack_signal(10, 15, recursive=True)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
-
- d.cont()
-
- SIGUSR1 = r.recvline()
- SIGTERM = r.recvline()
- SIGINT = r.recvline()
- SIGQUIT = r.recvline()
- SIGPIPE = r.recvline()
-
- SIGUSR1 += r.recvline()
- SIGTERM += r.recvline()
- SIGINT += r.recvline()
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- d.kill()
-
- self.assertEqual(SIGTERM_count, 4) # 2 times more because of the hijack
- self.assertEqual(catcher1.hit_count, 2)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
-
- self.assertEqual(SIGUSR1, b"Received signal 15" * 2) # hijacked signal
- self.assertEqual(SIGTERM, b"Received signal 15" * 2)
- self.assertEqual(SIGINT, b"Received signal 2" * 2)
- self.assertEqual(SIGQUIT, b"Received signal 3" * 3)
- self.assertEqual(SIGPIPE, b"Received signal 13" * 3)
-
- def test_recursive_false_with_catch_signal(self):
- SIGUSR1_count = 0
- SIGTERM_count = 0
-
- def catcher_SIGUSR1(t, sc):
- nonlocal SIGUSR1_count
- # Hijack to SIGTERM
- t.signal = 15
-
- SIGUSR1_count += 1
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
-
- SIGTERM_count += 1
-
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- catcher1 = d.catch_signal(10, callback=catcher_SIGUSR1, recursive=False)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
-
- d.cont()
-
- SIGUSR1 = r.recvline()
- SIGTERM = r.recvline()
- SIGINT = r.recvline()
- SIGQUIT = r.recvline()
- SIGPIPE = r.recvline()
-
- SIGUSR1 += r.recvline()
- SIGTERM += r.recvline()
- SIGINT += r.recvline()
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 2)
- self.assertEqual(SIGTERM_count, 2) # 2 times in total because of the recursive=False
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
-
- self.assertEqual(SIGUSR1, b"Received signal 15" * 2) # hijacked signal
- self.assertEqual(SIGTERM, b"Received signal 15" * 2)
- self.assertEqual(SIGINT, b"Received signal 2" * 2)
- self.assertEqual(SIGQUIT, b"Received signal 3" * 3)
- self.assertEqual(SIGPIPE, b"Received signal 13" * 3)
-
- def test_recursive_false_with_api(self):
- SIGTERM_count = 0
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
-
- SIGTERM_count += 1
-
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- catcher1 = d.hijack_signal(10, 15, recursive=False)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
-
- d.cont()
-
- SIGUSR1 = r.recvline()
- SIGTERM = r.recvline()
- SIGINT = r.recvline()
- SIGQUIT = r.recvline()
- SIGPIPE = r.recvline()
-
- SIGUSR1 += r.recvline()
- SIGTERM += r.recvline()
- SIGINT += r.recvline()
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- d.kill()
-
- self.assertEqual(catcher1.hit_count, 2)
- self.assertEqual(SIGTERM_count, 2) # 2 times in total because of the recursive=False
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
-
- self.assertEqual(SIGUSR1, b"Received signal 15" * 2) # hijacked signal
- self.assertEqual(SIGTERM, b"Received signal 15" * 2)
- self.assertEqual(SIGINT, b"Received signal 2" * 2)
- self.assertEqual(SIGQUIT, b"Received signal 3" * 3)
- self.assertEqual(SIGPIPE, b"Received signal 13" * 3)
-
- def test_hijack_signal_with_catch_signal_loop(self):
- # Let create a loop of hijacking signals
-
- def catcher_SIGUSR1(t, sc):
- # Hijack to SIGTERM
- t.signal = 15
-
- def catcher_SIGTERM(t, sc):
- # Hijack to SIGINT
- t.signal = 10
-
- d = debugger("binaries/catch_signal_test")
-
- d.run()
-
- d.catch_signal("SIGUSR1", callback=catcher_SIGUSR1, recursive=True)
- d.catch_signal("SIGTERM", callback=catcher_SIGTERM, recursive=True)
-
- with self.assertRaises(RuntimeError):
- d.cont()
- d.wait()
-
- d.kill()
-
- # Now we set recursive=False to avoid the loop
- d.run()
-
- d.catch_signal("SIGUSR1", callback=catcher_SIGUSR1, recursive=False)
- d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
-
- d.cont()
- d.kill()
-
- d.run()
-
- d.catch_signal("SIGUSR1", callback=catcher_SIGUSR1)
- d.catch_signal("SIGTERM", callback=catcher_SIGTERM, recursive=False)
-
- d.cont()
- d.kill()
-
- d.run()
-
- d.catch_signal("SIGUSR1", callback=catcher_SIGUSR1, recursive=False)
- d.catch_signal("SIGTERM", callback=catcher_SIGTERM, recursive=False)
-
- d.cont()
- d.kill()
-
- def test_hijack_signal_with_api_loop(self):
- # Let create a loop of hijacking signals
-
- d = debugger("binaries/catch_signal_test")
-
- d.run()
-
- d.hijack_signal("SIGUSR1", "SIGTERM", recursive=True)
- d.hijack_signal(15, 10, recursive=True)
-
- with self.assertRaises(RuntimeError):
- d.cont()
- d.wait()
-
- d.kill()
-
- # Now we set recursive=False to avoid the loop
- d.run()
-
- d.hijack_signal("SIGUSR1", "SIGTERM", recursive=False)
- d.hijack_signal(15, 10)
-
- d.cont()
- d.kill()
-
- d.run()
-
- d.hijack_signal("SIGUSR1", "SIGTERM")
- d.hijack_signal(15, 10, recursive=False)
-
- d.cont()
- d.kill()
-
- d.run()
-
- d.hijack_signal("SIGUSR1", "SIGTERM", recursive=False)
- d.hijack_signal(15, 10, recursive=False)
-
- d.cont()
- d.kill()
-
- def test_signal_unhijacking(self):
- SIGUSR1_count = 0
- SIGINT_count = 0
- SIGTERM_count = 0
-
- def catcher_SIGUSR1(t, sc):
- nonlocal SIGUSR1_count
-
- SIGUSR1_count += 1
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
-
- SIGTERM_count += 1
-
- def catcher_SIGINT(t, sc):
- nonlocal SIGINT_count
-
- SIGINT_count += 1
-
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- catcher1 = d.catch_signal("SIGUSR1", callback=catcher_SIGUSR1)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
- catcher3 = d.catch_signal("SIGINT", callback=catcher_SIGINT)
- catcher4 = d.hijack_signal("SIGQUIT", "SIGTERM", recursive=True)
- catcher5 = d.hijack_signal("SIGPIPE", "SIGTERM", recursive=True)
-
- bp = d.breakpoint(0x964)
-
- d.cont()
-
- SIGUSR1 = r.recvline()
- SIGTERM = r.recvline()
- SIGINT = r.recvline()
- SIGQUIT = r.recvline()
- SIGPIPE = r.recvline()
-
- SIGUSR1 += r.recvline()
- SIGTERM += r.recvline()
- SIGINT += r.recvline()
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- # Disable catching of signals
- if bp.hit_on(d):
- catcher4.disable()
- catcher5.disable()
- d.cont()
-
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 2)
- self.assertEqual(SIGTERM_count, 2 + 2 + 2) # 2 times more because of the hijacking * 2 (SIGQUIT and SIGPIPE)
- self.assertEqual(SIGINT_count, 2)
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
- self.assertEqual(SIGINT_count, catcher3.hit_count)
-
- self.assertEqual(SIGUSR1, b"Received signal 10" * 2)
- self.assertEqual(SIGTERM, b"Received signal 15" * 2)
- self.assertEqual(SIGINT, b"Received signal 2" * 2)
- self.assertEqual(SIGQUIT, b"Received signal 15" * 2 + b"Received signal 3")
- self.assertEqual(SIGPIPE, b"Received signal 15" * 2 + b"Received signal 13")
-
- def test_override_catch_signal(self):
- SIGPIPE_count_first = 0
- SIGPIPE_count_second = 0
-
- def catcher_SIGPIPE_first(t, sc):
- nonlocal SIGPIPE_count_first
-
- SIGPIPE_count_first += 1
-
- def catcher_SIGPIPE_second(t, sc):
- nonlocal SIGPIPE_count_second
-
- SIGPIPE_count_second += 1
-
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- catcher1 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE_first)
-
- bp = d.breakpoint(0x964)
-
- d.cont()
-
- SIGUSR1 = r.recvline()
- SIGTERM = r.recvline()
- SIGINT = r.recvline()
- SIGQUIT = r.recvline()
- SIGPIPE = r.recvline()
-
- SIGUSR1 += r.recvline()
- SIGTERM += r.recvline()
- SIGINT += r.recvline()
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- # Overriding the catcher
- if bp.hit_on(d):
- self.assertEqual(catcher1.hit_count, 2)
- catcher2 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE_second)
- d.cont()
-
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- d.kill()
-
- self.assertEqual(SIGPIPE_count_first, 2)
- self.assertEqual(SIGPIPE_count_second, 1)
-
- self.assertEqual(SIGPIPE_count_first, catcher1.hit_count)
- self.assertEqual(SIGPIPE_count_second, catcher2.hit_count)
-
- self.assertEqual(SIGUSR1, b"Received signal 10" * 2)
- self.assertEqual(SIGTERM, b"Received signal 15" * 2)
- self.assertEqual(SIGINT, b"Received signal 2" * 2)
- self.assertEqual(SIGQUIT, b"Received signal 3" * 3)
- self.assertEqual(SIGPIPE, b"Received signal 13" * 3)
-
- self.assertEqual(
- self.log_capture_string.getvalue().count("has already been caught. Overriding it."),
- 1,
- )
-
- def test_override_hijack(self):
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- catcher1 = d.hijack_signal("SIGPIPE", 15)
-
- bp = d.breakpoint(0x964)
-
- d.cont()
-
- SIGUSR1 = r.recvline()
- SIGTERM = r.recvline()
- SIGINT = r.recvline()
- SIGQUIT = r.recvline()
- SIGPIPE = r.recvline()
-
- SIGUSR1 += r.recvline()
- SIGTERM += r.recvline()
- SIGINT += r.recvline()
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- # Overriding the catcher
- if bp.hit_on(d):
- self.assertEqual(catcher1.hit_count, 2)
- catcher2 = d.hijack_signal("SIGPIPE", "SIGINT")
- d.cont()
-
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- d.kill()
-
- self.assertEqual(catcher1.hit_count, 2)
- self.assertEqual(catcher2.hit_count, 1)
-
- self.assertEqual(SIGUSR1, b"Received signal 10" * 2)
- self.assertEqual(SIGTERM, b"Received signal 15" * 2)
- self.assertEqual(SIGINT, b"Received signal 2" * 2)
- self.assertEqual(SIGQUIT, b"Received signal 3" * 3)
- self.assertEqual(SIGPIPE, b"Received signal 15" * 2 + b"Received signal 2")
-
- self.assertEqual(
- self.log_capture_string.getvalue().count("has already been caught. Overriding it."),
- 1,
- )
-
- def test_override_hybrid(self):
- SIGPIPE_count = 0
-
- def catcher_SIGPIPE(t, sc):
- nonlocal SIGPIPE_count
-
- SIGPIPE_count += 1
-
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- catcher1 = d.hijack_signal("SIGPIPE", 15)
-
- bp = d.breakpoint(0x964)
-
- d.cont()
-
- SIGUSR1 = r.recvline()
- SIGTERM = r.recvline()
- SIGINT = r.recvline()
- SIGQUIT = r.recvline()
- SIGPIPE = r.recvline()
-
- SIGUSR1 += r.recvline()
- SIGTERM += r.recvline()
- SIGINT += r.recvline()
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- # Overriding the catcher
- if bp.hit_on(d):
- self.assertEqual(catcher1.hit_count, 2)
- catcher2 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
- d.cont()
-
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- d.kill()
-
- self.assertEqual(catcher1.hit_count, 2)
- self.assertEqual(catcher2.hit_count, 1)
- self.assertEqual(SIGPIPE_count, 1)
-
- self.assertEqual(SIGUSR1, b"Received signal 10" * 2)
- self.assertEqual(SIGTERM, b"Received signal 15" * 2)
- self.assertEqual(SIGINT, b"Received signal 2" * 2)
- self.assertEqual(SIGQUIT, b"Received signal 3" * 3)
- self.assertEqual(SIGPIPE, b"Received signal 15" * 2 + b"Received signal 13")
-
- self.assertEqual(
- self.log_capture_string.getvalue().count("has already been caught. Overriding it."),
- 1,
- )
-
- def test_signal_get_signal(self):
- SIGUSR1_count = 0
- SIGINT_count = 0
- SIGQUIT_count = 0
- SIGTERM_count = 0
- SIGPIPE_count = 0
-
- def catcher_SIGUSR1(t, sc):
- nonlocal SIGUSR1_count
-
- self.assertEqual(t.signal, "SIGUSR1")
-
- SIGUSR1_count += 1
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
-
- self.assertEqual(t.signal, "SIGTERM")
-
- SIGTERM_count += 1
-
- def catcher_SIGINT(t, sc):
- nonlocal SIGINT_count
-
- self.assertEqual(t.signal, "SIGINT")
-
- SIGINT_count += 1
-
- def catcher_SIGQUIT(t, sc):
- nonlocal SIGQUIT_count
-
- self.assertEqual(t.signal, "SIGQUIT")
-
- SIGQUIT_count += 1
-
- def catcher_SIGPIPE(t, sc):
- nonlocal SIGPIPE_count
-
- self.assertEqual(t.signal, "SIGPIPE")
-
- SIGPIPE_count += 1
-
- d = debugger("binaries/catch_signal_test")
-
- d.signals_to_block = ["SIGUSR1", 15, "SIGINT", 3, 13]
-
- d.run()
-
- catcher1 = d.catch_signal(10, callback=catcher_SIGUSR1)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
- catcher3 = d.catch_signal(2, callback=catcher_SIGINT)
- catcher4 = d.catch_signal("SIGQUIT", callback=catcher_SIGQUIT)
- catcher5 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 2)
- self.assertEqual(SIGTERM_count, 2)
- self.assertEqual(SIGINT_count, 2)
- self.assertEqual(SIGQUIT_count, 3)
- self.assertEqual(SIGPIPE_count, 3)
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
- self.assertEqual(SIGINT_count, catcher3.hit_count)
- self.assertEqual(SIGQUIT_count, catcher4.hit_count)
- self.assertEqual(SIGPIPE_count, catcher5.hit_count)
-
- def test_signal_send_signal(self):
- SIGUSR1_count = 0
- SIGINT_count = 0
- SIGTERM_count = 0
-
- def catcher_SIGUSR1(t, sc):
- nonlocal SIGUSR1_count
-
- SIGUSR1_count += 1
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
-
- SIGTERM_count += 1
-
- def catcher_SIGINT(t, sc):
- nonlocal SIGINT_count
-
- SIGINT_count += 1
-
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- catcher1 = d.catch_signal("SIGUSR1", callback=catcher_SIGUSR1)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
- catcher3 = d.catch_signal("SIGINT", callback=catcher_SIGINT)
- catcher4 = d.hijack_signal("SIGQUIT", "SIGTERM", recursive=True)
- catcher5 = d.hijack_signal("SIGPIPE", "SIGTERM", recursive=True)
-
- bp = d.breakpoint(0x964)
-
- d.cont()
-
- SIGUSR1 = r.recvline()
- SIGTERM = r.recvline()
- SIGINT = r.recvline()
- SIGQUIT = r.recvline()
- SIGPIPE = r.recvline()
-
- SIGUSR1 += r.recvline()
- SIGTERM += r.recvline()
- SIGINT += r.recvline()
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- # Uncatchering and send signals
- if bp.hit_on(d):
- catcher4.disable()
- catcher5.disable()
- d.signal = 10
- d.cont()
-
- SIGUSR1 += r.recvline()
- SIGQUIT += r.recvline()
- SIGPIPE += r.recvline()
-
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 2)
- self.assertEqual(SIGTERM_count, 2 + 2 + 2) # 2 times more because of the hijacking * 2 (SIGQUIT and SIGPIPE)
- self.assertEqual(SIGINT_count, 2)
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
- self.assertEqual(SIGINT_count, catcher3.hit_count)
-
- self.assertEqual(SIGUSR1, b"Received signal 10" * 3)
- self.assertEqual(SIGTERM, b"Received signal 15" * 2)
- self.assertEqual(SIGINT, b"Received signal 2" * 2)
- self.assertEqual(SIGQUIT, b"Received signal 15" * 2 + b"Received signal 3")
- self.assertEqual(SIGPIPE, b"Received signal 15" * 2 + b"Received signal 13")
-
- def test_signal_catch_sync_block(self):
- SIGUSR1_count = 0
- SIGINT_count = 0
- SIGQUIT_count = 0
- SIGTERM_count = 0
- SIGPIPE_count = 0
-
- d = debugger("binaries/catch_signal_test")
-
- d.signals_to_block = ["SIGUSR1", 15, "SIGINT", 3, 13]
-
- d.run()
-
- catcher1 = d.catch_signal(10)
- catcher2 = d.catch_signal("SIGTERM")
- catcher3 = d.catch_signal(2)
- catcher4 = d.catch_signal("SIGQUIT")
- catcher5 = d.catch_signal("SIGPIPE")
-
- while not d.dead:
- d.cont()
- d.wait()
- if catcher1.hit_on(d):
- SIGUSR1_count += 1
- elif catcher2.hit_on(d):
- SIGTERM_count += 1
- elif catcher3.hit_on(d):
- SIGINT_count += 1
- elif catcher4.hit_on(d):
- SIGQUIT_count += 1
- elif catcher5.hit_on(d):
- SIGPIPE_count += 1
-
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 2)
- self.assertEqual(SIGTERM_count, 2)
- self.assertEqual(SIGINT_count, 2)
- self.assertEqual(SIGQUIT_count, 3)
- self.assertEqual(SIGPIPE_count, 3)
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
- self.assertEqual(SIGINT_count, catcher3.hit_count)
- self.assertEqual(SIGQUIT_count, catcher4.hit_count)
- self.assertEqual(SIGPIPE_count, catcher5.hit_count)
-
- def test_signal_catch_sync_pass(self):
- SIGUSR1_count = 0
- SIGINT_count = 0
- SIGQUIT_count = 0
- SIGTERM_count = 0
- SIGPIPE_count = 0
-
- signals = b""
-
- d = debugger("binaries/catch_signal_test")
-
- r = d.run()
-
- catcher1 = d.catch_signal(10)
- catcher2 = d.catch_signal("SIGTERM")
- catcher3 = d.catch_signal(2)
- catcher4 = d.catch_signal("SIGQUIT")
- catcher5 = d.catch_signal("SIGPIPE")
-
- signals = b""
- while not d.dead:
- d.cont()
- try:
- signals += r.recvline()
- except:
- pass
- d.wait()
- if catcher1.hit_on(d):
- SIGUSR1_count += 1
- elif catcher2.hit_on(d):
- SIGTERM_count += 1
- elif catcher3.hit_on(d):
- SIGINT_count += 1
- elif catcher4.hit_on(d):
- SIGQUIT_count += 1
- elif catcher5.hit_on(d):
- SIGPIPE_count += 1
-
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 2)
- self.assertEqual(SIGTERM_count, 2)
- self.assertEqual(SIGINT_count, 2)
- self.assertEqual(SIGQUIT_count, 3)
- self.assertEqual(SIGPIPE_count, 3)
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
- self.assertEqual(SIGINT_count, catcher3.hit_count)
- self.assertEqual(SIGQUIT_count, catcher4.hit_count)
- self.assertEqual(SIGPIPE_count, catcher5.hit_count)
-
- self.assertEqual(signals.count(b"Received signal 10"), 2)
- self.assertEqual(signals.count(b"Received signal 15"), 2)
- self.assertEqual(signals.count(b"Received signal 2"), 2)
- self.assertEqual(signals.count(b"Received signal 3"), 3)
- self.assertEqual(signals.count(b"Received signal 13"), 3)
diff --git a/test/aarch64/scripts/control_flow_test.py b/test/aarch64/scripts/control_flow_test.py
deleted file mode 100644
index 7aa3e1fb..00000000
--- a/test/aarch64/scripts/control_flow_test.py
+++ /dev/null
@@ -1,183 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class ControlFlowTest(unittest.TestCase):
- def test_step_until_1(self):
- d = debugger("binaries/breakpoint_test")
- d.run()
-
- bp = d.breakpoint("main")
- d.cont()
-
- self.assertTrue(bp.hit_on(d))
-
- d.step_until(0x0000aaaaaaaa0854)
-
- self.assertTrue(d.regs.pc == 0x0000aaaaaaaa0854)
- self.assertTrue(bp.hit_count == 1)
- self.assertFalse(bp.hit_on(d))
-
- d.kill()
- d.terminate()
-
- def test_step_until_2(self):
- d = debugger("binaries/breakpoint_test")
- d.run()
-
- bp = d.breakpoint(0x7fc, hardware=True)
- d.cont()
-
- self.assertTrue(bp.hit_on(d))
-
- d.step_until(0x0000aaaaaaaa0854, max_steps=7)
-
- self.assertTrue(d.regs.pc == 0x0000aaaaaaaa0818)
- self.assertTrue(bp.hit_count == 1)
- self.assertFalse(bp.hit_on(d))
-
- d.kill()
- d.terminate()
-
- def test_step_until_3(self):
- d = debugger("binaries/breakpoint_test")
- d.run()
-
- bp = d.breakpoint(0x7fc)
-
- # Let's put some breakpoints in-between
- d.breakpoint(0x804)
- d.breakpoint(0x80c)
- d.breakpoint(0x808, hardware=True)
-
- d.cont()
-
- self.assertTrue(bp.hit_on(d))
-
- # trace is [0x7fc, 0x800, 0x804, 0x808, 0x80c, 0x810, 0x814, 0x818]
- d.step_until(0x0000aaaaaaaa0854, max_steps=7)
-
- self.assertTrue(d.regs.pc == 0x0000aaaaaaaa0818)
- self.assertTrue(bp.hit_count == 1)
- self.assertFalse(bp.hit_on(d))
-
- d.kill()
- d.terminate()
-
- def test_step_and_cont(self):
- d = debugger("binaries/breakpoint_test")
- d.run()
-
- bp1 = d.breakpoint("main")
- bp2 = d.breakpoint("random_function")
- d.cont()
-
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.step()
- self.assertTrue(d.regs.pc == 0x0000aaaaaaaa083c)
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.step()
- self.assertTrue(d.regs.pc == 0x0000aaaaaaaa0840)
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.cont()
-
- self.assertTrue(bp2.hit_on(d))
-
- d.cont()
-
- d.kill()
- d.terminate()
-
- def test_step_and_cont_hardware(self):
- d = debugger("binaries/breakpoint_test")
- d.run()
-
- bp1 = d.breakpoint("main", hardware=True)
- bp2 = d.breakpoint("random_function", hardware=True)
- d.cont()
-
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.step()
- self.assertTrue(d.regs.pc == 0x0000aaaaaaaa083c)
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.step()
- self.assertTrue(d.regs.pc == 0x0000aaaaaaaa0840)
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.cont()
-
- self.assertTrue(bp2.hit_on(d))
-
- d.cont()
-
- d.kill()
- d.terminate()
-
- def test_step_until_and_cont(self):
- d = debugger("binaries/breakpoint_test")
- d.run()
-
- bp1 = d.breakpoint("main")
- bp2 = d.breakpoint("random_function")
- d.cont()
-
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.step_until(0x0000aaaaaaaa083c)
-
- self.assertTrue(d.regs.pc == 0x0000aaaaaaaa083c)
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.cont()
-
- self.assertTrue(bp2.hit_on(d))
-
- d.cont()
-
- d.kill()
- d.terminate()
-
- def test_step_until_and_cont_hardware(self):
- d = debugger("binaries/breakpoint_test")
- d.run()
-
- bp1 = d.breakpoint("main", hardware=True)
- bp2 = d.breakpoint("random_function", hardware=True)
- d.cont()
-
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.step_until(0x0000aaaaaaaa083c)
- self.assertTrue(d.regs.pc == 0x0000aaaaaaaa083c)
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.cont()
-
- self.assertTrue(bp2.hit_on(d))
-
- d.cont()
-
- d.kill()
- d.terminate()
\ No newline at end of file
diff --git a/test/aarch64/scripts/death_test.py b/test/aarch64/scripts/death_test.py
deleted file mode 100644
index 0a228b54..00000000
--- a/test/aarch64/scripts/death_test.py
+++ /dev/null
@@ -1,154 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import io
-import logging
-import unittest
-
-from libdebug import debugger
-
-
-class DeathTest(unittest.TestCase):
- def setUp(self):
- # Redirect logging to a string buffer
- self.log_capture_string = io.StringIO()
- self.log_handler = logging.StreamHandler(self.log_capture_string)
- self.log_handler.setLevel(logging.WARNING)
-
- self.logger = logging.getLogger("libdebug")
- self.original_handlers = self.logger.handlers
- self.logger.handlers = []
- self.logger.addHandler(self.log_handler)
- self.logger.setLevel(logging.WARNING)
-
- def tearDown(self):
- self.logger.removeHandler(self.log_handler)
- self.logger.handlers = self.original_handlers
- self.log_handler.close()
-
- def test_io_death(self):
- d = debugger("binaries/segfault_test")
-
- r = d.run()
-
- d.cont()
-
- self.assertEqual(r.recvline(), b"Hello, World!")
- self.assertEqual(r.recvline(), b"Death is coming!")
-
- with self.assertRaises(RuntimeError):
- r.recvline()
-
- d.kill()
-
- def test_cont_death(self):
- d = debugger("binaries/segfault_test")
-
- r = d.run()
-
- d.cont()
-
- self.assertEqual(r.recvline(), b"Hello, World!")
- self.assertEqual(r.recvline(), b"Death is coming!")
-
- d.wait()
-
- with self.assertRaises(RuntimeError):
- d.cont()
-
- self.assertEqual(d.dead, True)
- self.assertEqual(d.threads[0].dead, True)
-
- d.kill()
-
- def test_instr_death(self):
- d = debugger("binaries/segfault_test")
-
- r = d.run()
-
- d.cont()
-
- self.assertEqual(r.recvline(), b"Hello, World!")
- self.assertEqual(r.recvline(), b"Death is coming!")
-
- d.wait()
-
- self.assertEqual(d.regs.pc, 0xaaaaaaaa0784)
-
- d.kill()
-
- def test_exit_signal_death(self):
- d = debugger("binaries/segfault_test")
-
- r = d.run()
-
- d.cont()
-
- self.assertEqual(r.recvline(), b"Hello, World!")
- self.assertEqual(r.recvline(), b"Death is coming!")
-
- d.wait()
-
- self.assertEqual(d.exit_signal, "SIGSEGV")
- self.assertEqual(d.exit_signal, d.threads[0].exit_signal)
-
- d.kill()
-
- def test_exit_code_death(self):
- d = debugger("binaries/segfault_test")
-
- r = d.run()
-
- d.cont()
-
- self.assertEqual(r.recvline(), b"Hello, World!")
- self.assertEqual(r.recvline(), b"Death is coming!")
-
- d.wait()
-
- d.exit_code
-
- self.assertEqual(
- self.log_capture_string.getvalue().count("No exit code available."),
- 1,
- )
-
- d.kill()
-
- def test_exit_code_normal(self):
- d = debugger("binaries/basic_test")
-
- d.run()
-
- d.cont()
-
- d.wait()
-
- self.assertEqual(d.exit_code, 0)
-
- d.exit_signal
-
- self.assertEqual(
- self.log_capture_string.getvalue().count("No exit signal available."),
- 1,
- )
-
- d.kill()
-
- def test_post_mortem_after_kill(self):
- d = debugger("binaries/basic_test")
-
- d.run()
-
- d.cont()
-
- d.interrupt()
- d.kill()
-
- # We should be able to access the registers also after the process has been killed
- d.regs.x0
- d.regs.x1
- d.regs.x2
diff --git a/test/aarch64/scripts/floating_point_test.py b/test/aarch64/scripts/floating_point_test.py
deleted file mode 100644
index 770c67b6..00000000
--- a/test/aarch64/scripts/floating_point_test.py
+++ /dev/null
@@ -1,84 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import sys
-import unittest
-from random import randint
-
-from libdebug import debugger
-
-
-class FloatingPointTest(unittest.TestCase):
- def test_floating_point_reg_access(self):
- d = debugger("binaries/floating_point_test")
-
- d.run()
-
- bp1 = d.bp(0xb10, file="binary")
- bp2 = d.bp(0xb44, file="binary")
-
- d.cont()
-
- assert bp1.hit_on(d)
-
- baseval = int.from_bytes(bytes(list(range(16))), sys.byteorder)
-
- for i in range(16):
- assert hasattr(d.regs, f"q{i}")
- assert getattr(d.regs, f"q{i}") == baseval
- assert getattr(d.regs, f"v{i}") == baseval
- assert getattr(d.regs, f"d{i}") == baseval & 0xFFFFFFFFFFFFFFFF
- assert getattr(d.regs, f"s{i}") == baseval & 0xFFFFFFFF
- assert getattr(d.regs, f"h{i}") == baseval & 0xFFFF
- assert getattr(d.regs, f"b{i}") == baseval & 0xFF
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
-
- baseval = int.from_bytes(bytes(list(range(128, 128 + 16, 1))), sys.byteorder)
-
- for i in range(16, 32, 1):
- assert hasattr(d.regs, f"q{i}")
- assert getattr(d.regs, f"q{i}") == baseval
- assert getattr(d.regs, f"v{i}") == baseval
- assert getattr(d.regs, f"d{i}") == baseval & 0xFFFFFFFFFFFFFFFF
- assert getattr(d.regs, f"s{i}") == baseval & 0xFFFFFFFF
- assert getattr(d.regs, f"h{i}") == baseval & 0xFFFF
- assert getattr(d.regs, f"b{i}") == baseval & 0xFF
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
-
- for i in range(32):
- val = randint(0, (1 << 128) - 1)
- setattr(d.regs, f"q{i}", val)
- assert getattr(d.regs, f"q{i}") == val
- assert getattr(d.regs, f"v{i}") == val
-
- for i in range(32):
- val = randint(0, (1 << 64) - 1)
- setattr(d.regs, f"d{i}", val)
- assert getattr(d.regs, f"d{i}") == val
-
- for i in range(32):
- val = randint(0, (1 << 32) - 1)
- setattr(d.regs, f"s{i}", val)
- assert getattr(d.regs, f"s{i}") == val
-
- for i in range(32):
- val = randint(0, (1 << 16) - 1)
- setattr(d.regs, f"h{i}", val)
- assert getattr(d.regs, f"h{i}") == val
-
- for i in range(32):
- val = randint(0, (1 << 8) - 1)
- setattr(d.regs, f"b{i}", val)
- assert getattr(d.regs, f"b{i}") == val
-
- d.regs.q0 = 0xdeadbeefdeadbeef
-
- d.cont()
-
- assert bp2.hit_on(d)
-
- d.kill()
- d.terminate()
diff --git a/test/aarch64/scripts/hijack_syscall_test.py b/test/aarch64/scripts/hijack_syscall_test.py
deleted file mode 100644
index 4e39195b..00000000
--- a/test/aarch64/scripts/hijack_syscall_test.py
+++ /dev/null
@@ -1,333 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import io
-import sys
-import unittest
-
-from libdebug import debugger
-
-
-class HijackSyscallTest(unittest.TestCase):
- def setUp(self):
- # Redirect stdout
- self.capturedOutput = io.StringIO()
- sys.stdout = self.capturedOutput
-
- def tearDown(self):
- sys.stdout = sys.__stdout__
-
- def test_hijack_syscall(self):
- def on_enter_write(d, sh):
- nonlocal write_count
-
- write_count += 1
-
- d = debugger("binaries/handle_syscall_test")
-
- write_count = 0
- r = d.run()
-
- d.hijack_syscall("getcwd", "write", recursive=True)
-
- # recursive is on, we expect the write handler to be called three times
- handler = d.handle_syscall("write", on_enter=on_enter_write, recursive=True)
-
- r.sendline(b"provola")
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(write_count, handler.hit_count)
- self.assertEqual(handler.hit_count, 3)
-
- write_count = 0
- r = d.run()
-
- d.hijack_syscall("getcwd", "write", recursive=False)
-
- # recursive is off, we expect the write handler to be called only twice
- handler = d.handle_syscall("write", on_enter=on_enter_write)
-
- r.sendline(b"provola")
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(write_count, handler.hit_count)
- self.assertEqual(handler.hit_count, 2)
-
- def test_hijack_syscall_with_pprint(self):
- def on_enter_write(d, sh):
- nonlocal write_count
-
- write_count += 1
-
- d = debugger("binaries/handle_syscall_test")
-
- write_count = 0
- r = d.run()
-
- d.pprint_syscalls = True
- d.hijack_syscall("getcwd", "write", recursive=True)
-
- # recursive is on, we expect the write handler to be called three times
- handler = d.handle_syscall("write", on_enter=on_enter_write, recursive=True)
-
- r.sendline(b"provola")
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(write_count, handler.hit_count)
- self.assertEqual(handler.hit_count, 3)
-
- write_count = 0
- r = d.run()
-
- d.pprint_syscalls = True
- d.hijack_syscall("getcwd", "write", recursive=False)
-
- # recursive is off, we expect the write handler to be called only twice
- handler = d.handle_syscall("write", on_enter=on_enter_write, recursive=False)
-
- r.sendline(b"provola")
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(write_count, handler.hit_count)
- self.assertEqual(handler.hit_count, 2)
-
- def test_hijack_handle_syscall(self):
- def on_enter_write(d, sh):
- nonlocal write_count
-
- write_count += 1
-
- def on_enter_getcwd(d, sh):
- d.syscall_number = 0x40
-
- d = debugger("binaries/handle_syscall_test")
-
- write_count = 0
- r = d.run()
-
- d.handle_syscall("getcwd", on_enter=on_enter_getcwd, recursive=True)
-
- # recursive is on, we expect the write handler to be called three times
- handler = d.handle_syscall("write", on_enter=on_enter_write)
-
- r.sendline(b"provola")
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(write_count, handler.hit_count)
- self.assertEqual(handler.hit_count, 3)
-
- write_count = 0
- r = d.run()
-
- d.handle_syscall("getcwd", on_enter=on_enter_getcwd, recursive=False)
-
- # recursive is off, we expect the write handler to be called only twice
- handler = d.handle_syscall("write", on_enter=on_enter_write)
-
- r.sendline(b"provola")
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(write_count, handler.hit_count)
- self.assertEqual(handler.hit_count, 2)
-
- def test_hijack_handle_syscall_with_pprint(self):
- def on_enter_write(d, sh):
- nonlocal write_count
-
- write_count += 1
-
- def on_enter_getcwd(d, sh):
- d.syscall_number = 0x40
-
- d = debugger("binaries/handle_syscall_test")
-
- write_count = 0
- r = d.run()
-
- d.pprint_syscalls = True
- d.handle_syscall("getcwd", on_enter=on_enter_getcwd, recursive=True)
-
- # recursive hijack is on, we expect the write handler to be called three times
- handler = d.handle_syscall("write", on_enter=on_enter_write, recursive=True)
-
- r.sendline(b"provola")
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(write_count, handler.hit_count)
- self.assertEqual(handler.hit_count, 3)
-
- write_count = 0
- r = d.run()
-
- d.pprint_syscalls = True
- d.handle_syscall("getcwd", on_enter=on_enter_getcwd, recursive=False)
-
- # recursive is off, we expect the write handler to be called only twice
- handler = d.handle_syscall("write", on_enter=on_enter_write)
-
- r.sendline(b"provola")
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(write_count, handler.hit_count)
- self.assertEqual(handler.hit_count, 2)
-
- def test_hijack_syscall_args(self):
- write_buffer = None
-
- def on_enter_write(d, sh):
- nonlocal write_buffer
- nonlocal write_count
-
- write_buffer = d.syscall_arg1
-
- write_count += 1
-
- d = debugger("binaries/handle_syscall_test")
-
- write_count = 0
- r = d.run()
-
- # recursive hijack is on, we expect the write handler to be called three times
- handler = d.handle_syscall("write", on_enter=on_enter_write, recursive=True)
- d.breakpoint(0x9f0, file="binary")
-
- d.cont()
- print(r.recvline())
- # Install the hijack. We expect to receive the "Hello, World!" string
-
- d.wait()
-
- d.hijack_syscall(
- "read",
- "write",
- syscall_arg0=0x1,
- syscall_arg1=write_buffer,
- syscall_arg2=14,
- recursive=True,
- )
-
- d.cont()
-
- print(r.recvline())
-
- d.kill()
-
- self.assertEqual(self.capturedOutput.getvalue().count("Hello, World!"), 2)
- self.assertEqual(write_count, handler.hit_count)
- self.assertEqual(handler.hit_count, 3)
-
- def test_hijack_syscall_args_with_pprint(self):
- write_buffer = None
-
- def on_enter_write(d, sh):
- nonlocal write_buffer
- nonlocal write_count
-
- write_buffer = d.syscall_arg1
-
- write_count += 1
-
- d = debugger("binaries/handle_syscall_test")
-
- write_count = 0
- r = d.run()
-
- d.pprint_syscalls = True
-
- # recursive hijack is on, we expect the write handler to be called three times
- handler = d.handle_syscall("write", on_enter=on_enter_write, recursive=True)
- d.breakpoint(0x9f0, file="binary")
-
- d.cont()
- print(r.recvline())
- # Install the hijack. We expect to receive the "Hello, World!" string
-
- d.wait()
-
- d.hijack_syscall(
- "read",
- "write",
- syscall_arg0=0x1,
- syscall_arg1=write_buffer,
- syscall_arg2=14,
- recursive=True,
- )
-
- d.cont()
-
- print(r.recvline())
-
- d.kill()
-
- self.assertEqual(self.capturedOutput.getvalue().count("Hello, World!"), 2)
- self.assertEqual(self.capturedOutput.getvalue().count("write"), 3)
- self.assertEqual(self.capturedOutput.getvalue().count("0xaaaaaaaa0ab0"), 3)
- self.assertEqual(write_count, handler.hit_count)
- self.assertEqual(handler.hit_count, 3)
-
- def test_hijack_syscall_wrong_args(self):
- d = debugger("binaries/handle_syscall_test")
-
- d.run()
-
- with self.assertRaises(ValueError):
- d.hijack_syscall("read", "write", syscall_arg26=0x1)
-
- d.kill()
-
- def loop_detection_test(self):
- d = debugger("binaries/handle_syscall_test")
-
- r = d.run()
- d.hijack_syscall("getcwd", "write", recursive=True)
- d.hijack_syscall("write", "getcwd", recursive=True)
- r.sendline(b"provola")
-
- # We expect an exception to be raised
- with self.assertRaises(RuntimeError):
- d.cont()
- d.wait()
- d.kill()
-
- r = d.run()
- d.hijack_syscall("getcwd", "write", recursive=False)
- d.hijack_syscall("write", "getcwd", recursive=True)
- r.sendline(b"provola")
-
- # We expect no exception to be raised
- d.cont()
-
- r = d.run()
- d.hijack_syscall("getcwd", "write", recursive=True)
- d.hijack_syscall("write", "getcwd", recursive=False)
- r.sendline(b"provola")
-
- # We expect no exception to be raised
- d.cont()
diff --git a/test/aarch64/scripts/memory_test.py b/test/aarch64/scripts/memory_test.py
deleted file mode 100644
index 91daa85c..00000000
--- a/test/aarch64/scripts/memory_test.py
+++ /dev/null
@@ -1,288 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import io
-import logging
-import sys
-import unittest
-
-from libdebug import debugger, libcontext
-
-
-class MemoryTest(unittest.TestCase):
- def setUp(self) -> None:
- self.d = debugger("binaries/memory_test")
-
- # Redirect logging to a string buffer
- self.log_capture_string = io.StringIO()
- self.log_handler = logging.StreamHandler(self.log_capture_string)
- self.log_handler.setLevel(logging.WARNING)
-
- self.logger = logging.getLogger("libdebug")
- self.original_handlers = self.logger.handlers
- self.logger.handlers = []
- self.logger.addHandler(self.log_handler)
- self.logger.setLevel(logging.WARNING)
-
- def test_memory(self):
- d = self.d
-
- d.run()
-
- bp = d.breakpoint("change_memory")
-
- d.cont()
-
- assert d.regs.pc == bp.address
-
- address = d.regs.x0
- prev = bytes(range(256))
-
- self.assertTrue(d.memory[address, 256] == prev)
-
- d.memory[address + 128 :] = b"abcd123456"
- prev = prev[:128] + b"abcd123456" + prev[138:]
-
- self.assertTrue(d.memory[address : address + 256] == prev)
-
- d.kill()
-
- def test_mem_access_libs(self):
- d = self.d
-
- d.run()
-
- bp = d.breakpoint("leak_address")
-
- d.cont()
-
- assert d.regs.pc == bp.address
-
- address = d.regs.x0
- with libcontext.tmp(sym_lvl=5):
- arena = d.memory["main_arena", 256, "libc"]
-
- def p64(x):
- return x.to_bytes(8, sys.byteorder)
-
- self.assertTrue(p64(address - 0x10) in arena)
-
- d.kill()
-
- def test_memory_exceptions(self):
- d = self.d
-
- d.run()
-
- bp = d.breakpoint("change_memory")
-
- d.cont()
-
- # This should not raise an exception
- file = d.memory[0x0, 256]
-
- # File should start with ELF magic number
- self.assertTrue(file.startswith(b"\x7fELF"))
-
- assert d.regs.pc == bp.address
-
- address = d.regs.x0
- prev = bytes(range(256))
-
- self.assertTrue(d.memory[address, 256] == prev)
-
- d.memory[address + 128 :] = b"abcd123456"
- prev = prev[:128] + b"abcd123456" + prev[138:]
-
- self.assertTrue(d.memory[address : address + 256] == prev)
-
- d.kill()
-
- def test_memory_multiple_runs(self):
- d = self.d
-
- for _ in range(10):
- d.run()
-
- bp = d.breakpoint("change_memory")
-
- d.cont()
-
- assert d.regs.pc == bp.address
-
- address = d.regs.x0
- prev = bytes(range(256))
-
- self.assertTrue(d.memory[address, 256] == prev)
-
- d.memory[address + 128 :] = b"abcd123456"
- prev = prev[:128] + b"abcd123456" + prev[138:]
-
- self.assertTrue(d.memory[address : address + 256] == prev)
-
- d.kill()
-
- def test_memory_access_while_running(self):
- d = debugger("binaries/memory_test_2")
-
- d.run()
-
- bp = d.breakpoint("do_nothing")
-
- d.cont()
-
- # Verify that memory access is only possible when the process is stopped
- value = int.from_bytes(d.memory["state", 8], sys.byteorder)
- self.assertEqual(value, 0xDEADBEEF)
- self.assertEqual(d.regs.pc, bp.address)
-
- d.kill()
-
- def test_memory_access_methods(self):
- d = debugger("binaries/memory_test_2")
-
- d.run()
-
- base = d.regs.pc & 0xFFFFFFFFFFFFF000
-
- # Test different ways to access memory at the start of the file
- file_0 = d.memory[base, 256]
- file_1 = d.memory[0x0, 256]
- file_2 = d.memory[0x0:0x100]
-
- self.assertEqual(file_0, file_1)
- self.assertEqual(file_0, file_2)
-
- # Validate that the length of the read bytes is correct
- file_0 = d.memory[0x0]
- file_1 = d.memory[base]
-
- self.assertEqual(file_0, file_1)
- self.assertEqual(len(file_0), 1)
-
- # Validate that slices work correctly
- file_0 = d.memory[0x0:"do_nothing"]
- file_1 = d.memory[base:"do_nothing"]
-
- self.assertEqual(file_0, file_1)
-
- self.assertRaises(ValueError, lambda: d.memory[0x1000:0x0])
- # _fini is after main
- self.assertRaises(ValueError, lambda: d.memory["_fini":"main"])
-
- # Test different ways to write memory
- d.memory[0x0, 8] = b"abcd1234"
- self.assertEqual(d.memory[0x0, 8], b"abcd1234")
-
- d.memory[0x0, 8] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory[base:] = b"abcd1234"
- self.assertEqual(d.memory[base, 8], b"abcd1234")
-
- d.memory[base:] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory[base] = b"abcd1234"
- self.assertEqual(d.memory[base, 8], b"abcd1234")
-
- d.memory[base] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory[0x0:0x8] = b"abcd1234"
- self.assertEqual(d.memory[0x0, 8], b"abcd1234")
-
- d.memory[0x0, 8] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory["main":] = b"abcd1234"
- self.assertEqual(d.memory["main", 8], b"abcd1234")
-
- d.memory["main":] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory["main"] = b"abcd1234"
- self.assertEqual(d.memory["main", 8], b"abcd1234")
-
- d.memory["main"] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory["main":"main+8"] = b"abcd1234"
- self.assertEqual(d.memory["main", 8], b"abcd1234")
-
- d.kill()
-
- def test_memory_access_methods_backing_file(self):
- d = debugger("binaries/memory_test_2")
-
- d.run()
-
- base = d.regs.pc & 0xFFFFFFFFFFFFF000
-
- # Validate that slices work correctly
- file_0 = d.memory[0x0:"do_nothing", "binary"]
- file_1 = d.memory[0x0:"do_nothing", "memory_test_2"]
- file_2 = d.memory[base:"do_nothing", "binary"]
- file_3 = d.memory[base:"do_nothing", "memory_test_2"]
-
- self.assertEqual(file_0, file_1)
- self.assertEqual(file_1, file_2)
- self.assertEqual(file_2, file_3)
-
- # Test different ways to write memory
- d.memory[0x0, 8, "binary"] = b"abcd1234"
- self.assertEqual(d.memory[0x0, 8, "binary"], b"abcd1234")
-
- d.memory[0x0, 8, "binary"] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory[0x0, 8, "memory_test_2"] = b"abcd1234"
- self.assertEqual(d.memory[0x0, 8, "memory_test_2"], b"abcd1234")
-
- d.memory[0x0, 8, "memory_test_2"] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory[0x0:0x8, "binary"] = b"abcd1234"
- self.assertEqual(d.memory[0x0:8, "binary"], b"abcd1234")
-
- d.memory[0x0, 8, "binary"] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory[0x0:0x8, "memory_test_2"] = b"abcd1234"
- self.assertEqual(d.memory[0x0:8, "memory_test_2"], b"abcd1234")
-
- d.memory[0x0, 8, "memory_test_2"] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory["main":, "binary"] = b"abcd1234"
- self.assertEqual(d.memory["main", 8, "binary"], b"abcd1234")
-
- d.memory["main":, "binary"] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory["main":, "memory_test_2"] = b"abcd1234"
- self.assertEqual(d.memory["main", 8, "binary"], b"abcd1234")
-
- d.memory["main":, "memory_test_2"] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory["main", "binary"] = b"abcd1234"
- self.assertEqual(d.memory["main", 8, "binary"], b"abcd1234")
-
- d.memory[0x0, 8, "binary"] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory["main", "memory_test_2"] = b"abcd1234"
- self.assertEqual(d.memory["main", 8, "memory_test_2"], b"abcd1234")
-
- d.memory[0x0, 8, "memory_test_2"] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory["main":"main+8", "binary"] = b"abcd1234"
- self.assertEqual(d.memory["main":"main+8", "binary"], b"abcd1234")
-
- d.memory[0x0, 8, "binary"] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory["main":"main+8", "memory_test_2"] = b"abcd1234"
- self.assertEqual(d.memory["main":"main+8", "memory_test_2"], b"abcd1234")
-
- d.memory[0x0, 8, "binary"] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- d.memory["main":"main+8", "hybrid"] = b"abcd1234"
- self.assertEqual(d.memory["main":"main+8", "hybrid"], b"abcd1234")
-
- d.memory[0x0, 8, "binary"] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
-
- with self.assertRaises(ValueError):
- d.memory["main":"main+8", "absolute"] = b"abcd1234"
-
- d.kill()
diff --git a/test/aarch64/scripts/next_test.py b/test/aarch64/scripts/next_test.py
deleted file mode 100644
index 8ebadcb4..00000000
--- a/test/aarch64/scripts/next_test.py
+++ /dev/null
@@ -1,111 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Francesco Panebianco, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-import unittest
-
-from libdebug import debugger
-
-TEST_ENTRYPOINT = 0xaaaaaaaa0930
-
-# Addresses of the dummy functions
-CALL_C_ADDRESS = 0xaaaaaaaa0934
-TEST_BREAKPOINT_ADDRESS = 0xaaaaaaaa0920
-
-# Addresses of noteworthy instructions
-RETURN_POINT_FROM_C = 0xaaaaaaaa0938
-
-class NextTest(unittest.TestCase):
- def setUp(self):
- pass
-
- def test_next(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
- d.run()
-
- # Get to test entrypoint
- entrypoint_bp = d.breakpoint(TEST_ENTRYPOINT)
- d.cont()
-
- self.assertEqual(d.regs.pc, TEST_ENTRYPOINT)
-
- # -------- Block 1 ------- #
- # Simple Step #
- # ------------------------ #
-
- # Reach call of function c
- d.next()
- self.assertEqual(d.regs.pc, CALL_C_ADDRESS)
-
- # -------- Block 2 ------- #
- # Skip a call #
- # ------------------------ #
-
- d.next()
- self.assertEqual(d.regs.pc, RETURN_POINT_FROM_C)
-
- d.kill()
- d.terminate()
-
- def test_next_breakpoint(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
- d.run()
-
- # Get to test entrypoint
- entrypoint_bp = d.breakpoint(TEST_ENTRYPOINT)
- d.cont()
-
- self.assertEqual(d.regs.pc, TEST_ENTRYPOINT)
-
- # Reach call of function c
- d.next()
-
- self.assertEqual(d.regs.pc, CALL_C_ADDRESS)
-
- # -------- Block 1 ------- #
- # Call with breakpoint #
- # ------------------------ #
-
- # Set breakpoint
- test_breakpoint = d.breakpoint(TEST_BREAKPOINT_ADDRESS)
-
- d.next()
-
- # Check we hit the breakpoint
- self.assertEqual(d.regs.pc, TEST_BREAKPOINT_ADDRESS)
- self.assertEqual(test_breakpoint.hit_count, 1)
-
- d.kill()
- d.terminate()
-
- def test_next_breakpoint_hw(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
- d.run()
-
- # Get to test entrypoint
- entrypoint_bp = d.breakpoint(TEST_ENTRYPOINT)
- d.cont()
-
- self.assertEqual(d.regs.pc, TEST_ENTRYPOINT)
-
- # Reach call of function c
- d.next()
-
- self.assertEqual(d.regs.pc, CALL_C_ADDRESS)
-
- # -------- Block 1 ------- #
- # Call with breakpoint #
- # ------------------------ #
-
- # Set breakpoint
- test_breakpoint = d.breakpoint(TEST_BREAKPOINT_ADDRESS, hardware=True)
-
- d.next()
-
- # Check we hit the breakpoint
- self.assertEqual(d.regs.pc, TEST_BREAKPOINT_ADDRESS)
- self.assertEqual(test_breakpoint.hit_count, 1)
-
- d.kill()
- d.terminate()
\ No newline at end of file
diff --git a/test/aarch64/scripts/signals_multithread_test.py b/test/aarch64/scripts/signals_multithread_test.py
deleted file mode 100644
index 392e20d4..00000000
--- a/test/aarch64/scripts/signals_multithread_test.py
+++ /dev/null
@@ -1,423 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class SignalMultithreadTest(unittest.TestCase):
- def test_signal_multithread_undet_catch_signal_block(self):
- SIGUSR1_count = 0
- SIGINT_count = 0
- SIGQUIT_count = 0
- SIGTERM_count = 0
- SIGPIPE_count = 0
-
- def catcher_SIGUSR1(t, sc):
- nonlocal SIGUSR1_count
-
- SIGUSR1_count += 1
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
-
- SIGTERM_count += 1
-
- def catcher_SIGINT(t, sc):
- nonlocal SIGINT_count
-
- SIGINT_count += 1
-
- def catcher_SIGQUIT(t, sc):
- nonlocal SIGQUIT_count
-
- SIGQUIT_count += 1
-
- def catcher_SIGPIPE(t, sc):
- nonlocal SIGPIPE_count
-
- SIGPIPE_count += 1
-
- d = debugger("binaries/signals_multithread_undet_test")
-
- r = d.run()
-
- catcher1 = d.catch_signal(10, callback=catcher_SIGUSR1)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
- catcher3 = d.catch_signal(2, callback=catcher_SIGINT)
- catcher4 = d.catch_signal("SIGQUIT", callback=catcher_SIGQUIT)
- catcher5 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
-
- d.signals_to_block = ["SIGUSR1", 15, "SIGINT", 3, 13]
-
- d.cont()
-
- r.sendline(b"sync")
- r.sendline(b"sync")
-
- # Receive the exit message
- r.recvline(2)
-
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 4)
- self.assertEqual(SIGTERM_count, 4)
- self.assertEqual(SIGINT_count, 4)
- self.assertEqual(SIGQUIT_count, 6)
- self.assertEqual(SIGPIPE_count, 6)
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
- self.assertEqual(SIGINT_count, catcher3.hit_count)
- self.assertEqual(SIGQUIT_count, catcher4.hit_count)
- self.assertEqual(SIGPIPE_count, catcher5.hit_count)
-
- def test_signal_multithread_undet_pass(self):
- SIGUSR1_count = 0
- SIGINT_count = 0
- SIGQUIT_count = 0
- SIGTERM_count = 0
- SIGPIPE_count = 0
-
- def catcher_SIGUSR1(t, sc):
- nonlocal SIGUSR1_count
-
- SIGUSR1_count += 1
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
-
- SIGTERM_count += 1
-
- def catcher_SIGINT(t, sc):
- nonlocal SIGINT_count
-
- SIGINT_count += 1
-
- def catcher_SIGQUIT(t, sc):
- nonlocal SIGQUIT_count
-
- SIGQUIT_count += 1
-
- def catcher_SIGPIPE(t, sc):
- nonlocal SIGPIPE_count
-
- SIGPIPE_count += 1
-
- d = debugger("binaries/signals_multithread_undet_test")
-
- r = d.run()
-
- catcher1 = d.catch_signal("SIGUSR1", callback=catcher_SIGUSR1)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
- catcher3 = d.catch_signal("SIGINT", callback=catcher_SIGINT)
- catcher4 = d.catch_signal("SIGQUIT", callback=catcher_SIGQUIT)
- catcher5 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
-
- d.cont()
-
- received = []
- for _ in range(24):
- received.append(r.recvline())
-
- r.sendline(b"sync")
- r.sendline(b"sync")
-
- received.append(r.recvline())
- received.append(r.recvline())
-
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 4)
- self.assertEqual(SIGTERM_count, 4)
- self.assertEqual(SIGINT_count, 4)
- self.assertEqual(SIGQUIT_count, 6)
- self.assertEqual(SIGPIPE_count, 6)
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
- self.assertEqual(SIGINT_count, catcher3.hit_count)
- self.assertEqual(SIGQUIT_count, catcher4.hit_count)
- self.assertEqual(SIGPIPE_count, catcher5.hit_count)
-
- # Count the number of times each signal was received
- self.assertEqual(received.count(b"Received signal 10"), 4)
- self.assertEqual(received.count(b"Received signal 15"), 4)
- self.assertEqual(received.count(b"Received signal 2"), 4)
- self.assertEqual(received.count(b"Received signal 3"), 6)
- self.assertEqual(received.count(b"Received signal 13"), 6)
- # Note: sometimes the signals are passed to ptrace once and received twice
- # Maybe another ptrace/kernel/whatever problem in multithreaded programs (?)
- # Using raise(sig) instead of kill(pid, sig) to send signals in the original
- # program seems to mitigate the problem for whatever reason
- # I will investigate this further in the future, but for now this is fine
-
- def test_signal_multithread_det_catch_signal_block(self):
- SIGUSR1_count = 0
- SIGINT_count = 0
- SIGQUIT_count = 0
- SIGTERM_count = 0
- SIGPIPE_count = 0
- tids = []
-
- def catcher_SIGUSR1(t, sc):
- nonlocal SIGUSR1_count
- nonlocal tids
-
- SIGUSR1_count += 1
- tids.append(t.thread_id)
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
- nonlocal tids
-
- SIGTERM_count += 1
- tids.append(t.thread_id)
-
- def catcher_SIGINT(t, sc):
- nonlocal SIGINT_count
- nonlocal tids
-
- SIGINT_count += 1
- tids.append(t.thread_id)
-
- def catcher_SIGQUIT(t, sc):
- nonlocal SIGQUIT_count
- nonlocal tids
-
- SIGQUIT_count += 1
- tids.append(t.thread_id)
-
- def catcher_SIGPIPE(t, sc):
- nonlocal SIGPIPE_count
- nonlocal tids
-
- SIGPIPE_count += 1
- tids.append(t.thread_id)
-
- d = debugger("binaries/signals_multithread_det_test")
-
- r = d.run()
-
- catcher1 = d.catch_signal(10, callback=catcher_SIGUSR1)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
- catcher3 = d.catch_signal(2, callback=catcher_SIGINT)
- catcher4 = d.catch_signal("SIGQUIT", callback=catcher_SIGQUIT)
- catcher5 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
-
- d.signals_to_block = ["SIGUSR1", 15, "SIGINT", 3, 13]
-
- d.cont()
-
- # Receive the exit message
- r.recvline(timeout=15)
- r.sendline(b"sync")
- r.recvline()
-
- receiver = d.threads[1].thread_id
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 2)
- self.assertEqual(SIGTERM_count, 2)
- self.assertEqual(SIGINT_count, 2)
- self.assertEqual(SIGQUIT_count, 3)
- self.assertEqual(SIGPIPE_count, 3)
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
- self.assertEqual(SIGINT_count, catcher3.hit_count)
- self.assertEqual(SIGQUIT_count, catcher4.hit_count)
- self.assertEqual(SIGPIPE_count, catcher5.hit_count)
-
- set_tids = set(tids)
- self.assertEqual(len(set_tids), 1)
- self.assertEqual(set_tids.pop(), receiver)
-
- def test_signal_multithread_det_pass(self):
- SIGUSR1_count = 0
- SIGINT_count = 0
- SIGQUIT_count = 0
- SIGTERM_count = 0
- SIGPIPE_count = 0
- tids = []
-
- def catcher_SIGUSR1(t, sc):
- nonlocal SIGUSR1_count
- nonlocal tids
-
- SIGUSR1_count += 1
- tids.append(t.thread_id)
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
- nonlocal tids
-
- SIGTERM_count += 1
- tids.append(t.thread_id)
-
- def catcher_SIGINT(t, sc):
- nonlocal SIGINT_count
- nonlocal tids
-
- SIGINT_count += 1
- tids.append(t.thread_id)
-
- def catcher_SIGQUIT(t, sc):
- nonlocal SIGQUIT_count
- nonlocal tids
-
- SIGQUIT_count += 1
- tids.append(t.thread_id)
-
- def catcher_SIGPIPE(t, sc):
- nonlocal SIGPIPE_count
- nonlocal tids
-
- SIGPIPE_count += 1
- tids.append(t.thread_id)
-
- d = debugger("binaries/signals_multithread_det_test")
-
- r = d.run()
-
- catcher1 = d.catch_signal("SIGUSR1", callback=catcher_SIGUSR1)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
- catcher3 = d.catch_signal("SIGINT", callback=catcher_SIGINT)
- catcher4 = d.catch_signal("SIGQUIT", callback=catcher_SIGQUIT)
- catcher5 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
-
- d.cont()
-
- received = []
- for _ in range(13):
- received.append(r.recvline(timeout=5))
-
- r.sendline(b"sync")
- received.append(r.recvline(timeout=5))
-
- receiver = d.threads[1].thread_id
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 2)
- self.assertEqual(SIGTERM_count, 2)
- self.assertEqual(SIGINT_count, 2)
- self.assertEqual(SIGQUIT_count, 3)
- self.assertEqual(SIGPIPE_count, 3)
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
- self.assertEqual(SIGINT_count, catcher3.hit_count)
- self.assertEqual(SIGQUIT_count, catcher4.hit_count)
- self.assertEqual(SIGPIPE_count, catcher5.hit_count)
-
- # Count the number of times each signal was received
- self.assertEqual(received.count(b"Received signal on receiver 10"), 2)
- self.assertEqual(received.count(b"Received signal on receiver 15"), 2)
- self.assertEqual(received.count(b"Received signal on receiver 2"), 2)
- self.assertEqual(received.count(b"Received signal on receiver 3"), 3)
- self.assertEqual(received.count(b"Received signal on receiver 13"), 3)
-
- set_tids = set(tids)
- self.assertEqual(len(set_tids), 1)
- self.assertEqual(set_tids.pop(), receiver)
-
- def test_signal_multithread_send_signal(self):
- SIGUSR1_count = 0
- SIGINT_count = 0
- SIGQUIT_count = 0
- SIGTERM_count = 0
- SIGPIPE_count = 0
- tids = []
-
- def catcher_SIGUSR1(t, sc):
- nonlocal SIGUSR1_count
- nonlocal tids
-
- SIGUSR1_count += 1
- tids.append(t.thread_id)
-
- def catcher_SIGTERM(t, sc):
- nonlocal SIGTERM_count
- nonlocal tids
-
- SIGTERM_count += 1
- tids.append(t.thread_id)
-
- def catcher_SIGINT(t, sc):
- nonlocal SIGINT_count
- nonlocal tids
-
- SIGINT_count += 1
- tids.append(t.thread_id)
-
- def catcher_SIGQUIT(t, sc):
- nonlocal SIGQUIT_count
- nonlocal tids
-
- SIGQUIT_count += 1
- tids.append(t.thread_id)
-
- def catcher_SIGPIPE(t, sc):
- nonlocal SIGPIPE_count
- nonlocal tids
-
- SIGPIPE_count += 1
- tids.append(t.thread_id)
-
- d = debugger("binaries/signals_multithread_det_test")
-
- # Set a breakpoint to stop the program before the end of the receiver thread
- r = d.run()
-
- bp = d.breakpoint(0xf1c, hardware=True, file="binary")
-
- catcher1 = d.catch_signal("SIGUSR1", callback=catcher_SIGUSR1)
- catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
- catcher3 = d.catch_signal("SIGINT", callback=catcher_SIGINT)
- catcher4 = d.catch_signal("SIGQUIT", callback=catcher_SIGQUIT)
- catcher5 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
-
- d.cont()
-
- received = []
- for _ in range(13):
- received.append(r.recvline(timeout=5))
-
- r.sendline(b"sync")
-
- d.wait()
- if bp.hit_on(d.threads[1]):
- d.threads[1].signal = "SIGUSR1"
- d.cont()
- received.append(r.recvline(timeout=5))
- received.append(r.recvline(timeout=5))
-
- receiver = d.threads[1].thread_id
- d.kill()
-
- self.assertEqual(SIGUSR1_count, 2)
- self.assertEqual(SIGTERM_count, 2)
- self.assertEqual(SIGINT_count, 2)
- self.assertEqual(SIGQUIT_count, 3)
- self.assertEqual(SIGPIPE_count, 3)
-
- self.assertEqual(SIGUSR1_count, catcher1.hit_count)
- self.assertEqual(SIGTERM_count, catcher2.hit_count)
- self.assertEqual(SIGINT_count, catcher3.hit_count)
- self.assertEqual(SIGQUIT_count, catcher4.hit_count)
- self.assertEqual(SIGPIPE_count, catcher5.hit_count)
-
- # Count the number of times each signal was received
- self.assertEqual(received.count(b"Received signal on receiver 10"), 3)
- self.assertEqual(received.count(b"Received signal on receiver 15"), 2)
- self.assertEqual(received.count(b"Received signal on receiver 2"), 2)
- self.assertEqual(received.count(b"Received signal on receiver 3"), 3)
- self.assertEqual(received.count(b"Received signal on receiver 13"), 3)
-
- set_tids = set(tids)
- self.assertEqual(len(set_tids), 1)
- self.assertEqual(set_tids.pop(), receiver)
diff --git a/test/aarch64/scripts/speed_test.py b/test/aarch64/scripts/speed_test.py
deleted file mode 100644
index c86bf708..00000000
--- a/test/aarch64/scripts/speed_test.py
+++ /dev/null
@@ -1,57 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-from time import perf_counter_ns
-
-from libdebug import debugger
-
-
-class SpeedTest(unittest.TestCase):
- def setUp(self):
- self.d = debugger("binaries/speed_test")
-
- def test_speed(self):
- d = self.d
-
- start_time = perf_counter_ns()
-
- d.run()
-
- bp = d.breakpoint("do_nothing")
-
- d.cont()
-
- for _ in range(65536):
- self.assertTrue(bp.address == d.regs.pc)
- d.cont()
-
- d.kill()
-
- end_time = perf_counter_ns()
-
- self.assertTrue((end_time - start_time) < 15 * 1e9) # 15 seconds
-
- def test_speed_hardware(self):
- d = self.d
-
- start_time = perf_counter_ns()
-
- d.run()
-
- bp = d.breakpoint("do_nothing", hardware=True)
-
- d.cont()
-
- for _ in range(65536):
- self.assertTrue(bp.address == d.regs.pc)
- d.cont()
-
- d.kill()
-
- end_time = perf_counter_ns()
-
- self.assertTrue((end_time - start_time) < 15 * 1e9) # 15 seconds
diff --git a/test/aarch64/scripts/thread_test.py b/test/aarch64/scripts/thread_test.py
deleted file mode 100644
index 0a31bc68..00000000
--- a/test/aarch64/scripts/thread_test.py
+++ /dev/null
@@ -1,83 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class ThreadTest(unittest.TestCase):
- def setUp(self):
- pass
-
- def test_thread(self):
- d = debugger("binaries/thread_test")
-
- d.run()
-
- bp_t0 = d.breakpoint("do_nothing")
- bp_t1 = d.breakpoint("thread_1_function")
- bp_t2 = d.breakpoint("thread_2_function")
- bp_t3 = d.breakpoint("thread_3_function")
-
- t1_done, t2_done, t3_done = False, False, False
-
- d.cont()
-
- for _ in range(150):
- if bp_t0.address == d.regs.pc:
- self.assertTrue(t1_done)
- self.assertTrue(t2_done)
- self.assertTrue(t3_done)
- break
-
- if len(d.threads) > 1 and bp_t1.address == d.threads[1].regs.pc:
- t1_done = True
- if len(d.threads) > 2 and bp_t2.address == d.threads[2].regs.pc:
- t2_done = True
- if len(d.threads) > 3 and bp_t3.address == d.threads[3].regs.pc:
- t3_done = True
-
- d.cont()
-
- d.kill()
- d.terminate()
-
- def test_thread_hardware(self):
- d = debugger("binaries/thread_test")
-
- d.run()
-
- bp_t0 = d.breakpoint("do_nothing", hardware=True)
- bp_t1 = d.breakpoint("thread_1_function", hardware=True)
- bp_t2 = d.breakpoint("thread_2_function", hardware=True)
- bp_t3 = d.breakpoint("thread_3_function", hardware=True)
-
- t1_done, t2_done, t3_done = False, False, False
-
- d.cont()
-
- for _ in range(15):
- if bp_t0.address == d.regs.pc:
- self.assertTrue(t1_done)
- self.assertTrue(t2_done)
- self.assertTrue(t3_done)
- break
-
- if len(d.threads) > 1 and bp_t1.address == d.threads[1].regs.pc:
- t1_done = True
- if len(d.threads) > 2 and bp_t2.address == d.threads[2].regs.pc:
- t2_done = True
- if len(d.threads) > 3 and bp_t3.address == d.threads[3].regs.pc:
- t3_done = True
-
- d.cont()
-
- d.kill()
- d.terminate()
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/test/aarch64/scripts/thread_test_complex.py b/test/aarch64/scripts/thread_test_complex.py
deleted file mode 100644
index 786f61d7..00000000
--- a/test/aarch64/scripts/thread_test_complex.py
+++ /dev/null
@@ -1,67 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class ThreadTestComplex(unittest.TestCase):
- def setUp(self):
- pass
-
- def test_thread(self):
- def factorial(n):
- if n == 0:
- return 1
- else:
- return (n * factorial(n - 1)) & (2**32 - 1)
-
- d = debugger("binaries/thread_test_complex")
-
- d.run()
-
- bp1_t0 = d.breakpoint("do_nothing")
- bp2_t1 = d.breakpoint("thread_1_function+18")
- bp3_t2 = d.breakpoint("thread_2_function+24")
-
- bp1_hit, bp2_hit, bp3_hit = False, False, False
- t1, t2 = None, None
-
- d.cont()
-
- while True:
- if len(d.threads) == 2:
- t1 = d.threads[1]
-
- if len(d.threads) == 3:
- t2 = d.threads[2]
-
- if t1 and bp2_t1.address == t1.regs.pc:
- bp2_hit = True
- self.assertTrue(bp2_t1.hit_count == (t1.regs.w0 + 1))
-
- if bp1_t0.address == d.regs.pc:
- bp1_hit = True
- self.assertTrue(bp2_hit)
- self.assertEqual(bp2_t1.hit_count, 50)
- self.assertFalse(bp3_hit)
- self.assertEqual(bp1_t0.hit_count, 1)
-
- if t2 and bp3_t2.address == t2.regs.pc:
- bp3_hit = True
- self.assertTrue(factorial(bp3_t2.hit_count) == t2.regs.x0)
- self.assertTrue(bp2_hit)
- self.assertTrue(bp1_hit)
-
- d.cont()
-
- if bp3_t2.hit_count == 49:
- break
-
- d.kill()
- d.terminate()
-
diff --git a/test/aarch64/scripts/watchpoint_test.py b/test/aarch64/scripts/watchpoint_test.py
deleted file mode 100644
index 326543da..00000000
--- a/test/aarch64/scripts/watchpoint_test.py
+++ /dev/null
@@ -1,150 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Francesco Panebianco, Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class WatchpointTest(unittest.TestCase):
- def test_watchpoint(self):
- d = debugger("binaries/watchpoint_test", auto_interrupt_on_command=False)
-
- d.run()
-
- d.breakpoint("global_char", hardware=True, condition="rw", length=1)
- d.breakpoint("global_int", hardware=True, condition="w", length=4)
- d.breakpoint("global_short", hardware=True, condition="r", length=2)
- d.breakpoint("global_long", hardware=True, condition="rw", length=8)
-
- d.cont()
-
- base = d.regs.pc & ~0xfff
-
- # strb w1, [x0] => global_char
- self.assertEqual(d.regs.pc, base + 0x724)
-
- d.cont()
-
- # str w1, [x0] => global_int
- self.assertEqual(d.regs.pc, base + 0x748)
-
- d.cont()
-
- # str x1, [x0] => global_long
- self.assertEqual(d.regs.pc, base + 0x764)
-
- d.cont()
-
- # ldrb w0, [x0] => global_char
- self.assertEqual(d.regs.pc, base + 0x780)
-
- d.cont()
-
- # ldr w0, [x0] => global_short
- self.assertEqual(d.regs.pc, base + 0x790)
-
- d.cont()
-
- # ldr x0, [x0] => global_long
- self.assertEqual(d.regs.pc, base + 0x7b0)
-
- d.cont()
-
- d.kill()
-
- def test_watchpoint_callback(self):
- global_char_ip = []
- global_int_ip = []
- global_short_ip = []
- global_long_ip = []
-
- def watchpoint_global_char(t, b):
- nonlocal global_char_ip
-
- global_char_ip.append(t.regs.pc)
-
- def watchpoint_global_int(t, b):
- nonlocal global_int_ip
-
- global_int_ip.append(t.regs.pc)
-
- def watchpoint_global_short(t, b):
- nonlocal global_short_ip
-
- global_short_ip.append(t.regs.pc)
-
- def watchpoint_global_long(t, b):
- nonlocal global_long_ip
-
- global_long_ip.append(t.regs.pc)
-
- d = debugger("binaries/watchpoint_test", auto_interrupt_on_command=False)
-
- d.run()
-
- base = d.regs.pc & ~0xfff
-
- wp1 = d.breakpoint(
- "global_char",
- hardware=True,
- condition="rw",
- length=1,
- callback=watchpoint_global_char,
- )
- wp2 = d.breakpoint(
- "global_int",
- hardware=True,
- condition="w",
- length=4,
- callback=watchpoint_global_int,
- )
- wp3 = d.breakpoint(
- "global_long",
- hardware=True,
- condition="rw",
- length=8,
- callback=watchpoint_global_long,
- )
- wp4 = d.breakpoint(
- "global_short",
- hardware=True,
- condition="r",
- length=2,
- callback=watchpoint_global_short,
- )
-
- d.cont()
-
- d.kill()
-
- # strb w1, [x0] => global_char
- self.assertEqual(global_char_ip[0], base + 0x724)
-
- # str w1, [x0] => global_int
- self.assertEqual(global_int_ip[0], base + 0x748)
-
- # str x1, [x0] => global_long
- self.assertEqual(global_long_ip[0], base + 0x764)
-
- # ldrb w0, [x0] => global_char
- self.assertEqual(global_char_ip[1], base + 0x780)
-
- # ldr w0, [x0] => global_short
- self.assertEqual(global_short_ip[0], base + 0x790)
-
- # ldr x0, [x0] => global_long
- self.assertEqual(global_long_ip[1], base + 0x7b0)
-
- self.assertEqual(len(global_char_ip), 2)
- self.assertEqual(len(global_int_ip), 1)
- self.assertEqual(len(global_short_ip), 1)
- self.assertEqual(len(global_long_ip), 2)
- self.assertEqual(wp1.hit_count, 2)
- self.assertEqual(wp2.hit_count, 1)
- self.assertEqual(wp3.hit_count, 2)
- self.assertEqual(wp4.hit_count, 1)
-
diff --git a/test/amd64/Makefile b/test/amd64/Makefile
deleted file mode 100644
index 1be7d276..00000000
--- a/test/amd64/Makefile
+++ /dev/null
@@ -1,40 +0,0 @@
-# Makefile for building individual .c files in srcs folder
-
-# Compiler and compiler flags
-CC := gcc
-CFLAGS := -Wall -Wextra -std=gnu11
-LDFLAGS :=
-
-# Directories
-SRC_DIR := srcs
-BIN_DIR := binaries
-
-# Default target to build all executables
-all:
- mkdir -p $(BIN_DIR)
- $(CC) $(CFLAGS) $(SRC_DIR)/basic_test.c -o $(BIN_DIR)/basic_test $(LDFLAGS)
- $(CC) $(CFLAGS) $(SRC_DIR)/breakpoint_test.c -o $(BIN_DIR)/breakpoint_test $(LDFLAGS)
- $(CC) $(CFLAGS) $(SRC_DIR)/memory_test.c -o $(BIN_DIR)/memory_test $(LDFLAGS)
- $(CC) $(CFLAGS) -fPIE -pie $(SRC_DIR)/basic_test_pie.c -o $(BIN_DIR)/basic_test_pie $(LDFLAGS)
- $(CC) $(CFLAGS) $(SRC_DIR)/benchmark.c -o $(BIN_DIR)/benchmark $(LDFLAGS)
- $(CC) $(CFLAGS) $(SRC_DIR)/backtrace.c -o $(BIN_DIR)/backtrace $(LDFLAGS)
- $(CC) $(CFLAGS) $(SRC_DIR)/brute_test.c -o $(BIN_DIR)/brute_test $(LDFLAGS)
- $(CC) $(CFLAGS) $(SRC_DIR)/speed_test.c -o $(BIN_DIR)/speed_test $(LDFLAGS)
- $(CC) $(CFLAGS) $(SRC_DIR)/thread_test.c -o $(BIN_DIR)/thread_test $(LDFLAGS)
- $(CC) $(CFLAGS) $(SRC_DIR)/complex_thread_test.c -o $(BIN_DIR)/complex_thread_test $(LDFLAGS)
- $(CC) $(CFLAGS) $(SRC_DIR)/watchpoint_test.c -o $(BIN_DIR)/watchpoint_test $(LDFLAGS)
- $(CC) $(CFLAGS) $(SRC_DIR)/catch_signal_test.c -o $(BIN_DIR)/catch_signal_test $(LDFLAGS)
- $(CC) $(CFLAGS) $(SRC_DIR)/signals_multithread_undet_test.c -o $(BIN_DIR)/signals_multithread_undet_test $(LDFLAGS)
- $(CC) $(CFLAGS) $(SRC_DIR)/signals_multithread_det_test.c -o $(BIN_DIR)/signals_multithread_det_test $(LDFLAGS)
- $(CC) $(CFLAGS) $(SRC_DIR)/segfault_test.c -o $(BIN_DIR)/segfault_test $(LDFLAGS)
- $(CC) $(CFLAGS) $(SRC_DIR)/executable_section_test.c -o $(BIN_DIR)/executable_section_test $(LDFLAGS)
- $(CC) $(CFLAGS) $(SRC_DIR)/math_loop_test.c -lm -fno-pie -no-pie -o $(BIN_DIR)/math_loop_test $(LDFLAGS)
-
-
-
-# Clean rule to remove compiled files
-clean:
- rm -rf $(BIN_DIR)
-
-# Phony target to avoid conflicts with file names
-.PHONY: all clean
diff --git a/test/amd64/run_suite.py b/test/amd64/run_suite.py
deleted file mode 100644
index 9a70dea6..00000000
--- a/test/amd64/run_suite.py
+++ /dev/null
@@ -1,268 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini, Francesco Panebianco. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import sys
-import unittest
-
-from scripts.alias_test import AliasTest
-from scripts.atexit_handler_test import AtexitHandlerTest
-from scripts.attach_detach_test import AttachDetachTest
-from scripts.auto_waiting_test import AutoWaitingNlinks, AutoWaitingTest
-from scripts.backtrace_test import BacktraceTest
-from scripts.basic_test import BasicPieTest, BasicTest, ControlFlowTest, HwBasicTest
-from scripts.breakpoint_test import BreakpointTest
-from scripts.brute_test import BruteTest
-from scripts.builtin_handler_test import AntidebugEscapingTest
-from scripts.callback_test import CallbackTest
-from scripts.catch_signal_test import SignalCatchTest
-from scripts.death_test import DeathTest
-from scripts.deep_dive_division_test import DeepDiveDivision
-from scripts.finish_test import FinishTest
-from scripts.floating_point_test import FloatingPointTest
-from scripts.handle_syscall_test import HandleSyscallTest
-from scripts.hijack_syscall_test import SyscallHijackTest
-from scripts.jumpout_test import Jumpout
-from scripts.jumpstart_test import JumpstartTest
-from scripts.large_binary_sym_test import LargeBinarySymTest
-from scripts.memory_test import MemoryTest
-from scripts.memory_fast_test import MemoryFastTest
-from scripts.multiple_debuggers_test import MultipleDebuggersTest
-from scripts.next_test import NextTest
-from scripts.nlinks_test import Nlinks
-from scripts.pprint_syscalls_test import PPrintSyscallsTest
-from scripts.signals_multithread_test import SignalMultithreadTest
-from scripts.speed_test import SpeedTest
-from scripts.thread_test import ComplexThreadTest, ThreadTest
-from scripts.vmwhere1_test import Vmwhere1
-from scripts.waiting_test import WaitingNlinks, WaitingTest
-from scripts.watchpoint_alias_test import WatchpointAliasTest
-from scripts.watchpoint_test import WatchpointTest
-
-
-def fast_suite():
- suite = unittest.TestSuite()
- suite.addTest(BasicTest("test_basic"))
- suite.addTest(BasicTest("test_registers"))
- suite.addTest(BasicTest("test_step"))
- suite.addTest(BasicTest("test_step_hardware"))
- suite.addTest(BasicPieTest("test_basic"))
- suite.addTest(BreakpointTest("test_bps"))
- suite.addTest(BreakpointTest("test_bp_disable"))
- suite.addTest(BreakpointTest("test_bp_disable_hw"))
- suite.addTest(BreakpointTest("test_bp_disable_reenable"))
- suite.addTest(BreakpointTest("test_bp_disable_reenable_hw"))
- suite.addTest(BreakpointTest("test_bps_running"))
- suite.addTest(BreakpointTest("test_bp_backing_file"))
- suite.addTest(BreakpointTest("test_bp_disable_on_creation"))
- suite.addTest(BreakpointTest("test_bp_disable_on_creation_2"))
- suite.addTest(BreakpointTest("test_bp_disable_on_creation_hardware"))
- suite.addTest(BreakpointTest("test_bp_disable_on_creation_2_hardware"))
- suite.addTest(MemoryTest("test_memory"))
- suite.addTest(MemoryTest("test_mem_access_libs"))
- suite.addTest(MemoryTest("test_memory_access_methods_backing_file"))
- suite.addTest(MemoryTest("test_memory_exceptions"))
- suite.addTest(MemoryTest("test_memory_multiple_runs"))
- suite.addTest(MemoryTest("test_memory_access_while_running"))
- suite.addTest(MemoryTest("test_memory_access_methods"))
- suite.addTest(MemoryFastTest("test_memory"))
- suite.addTest(MemoryFastTest("test_mem_access_libs"))
- suite.addTest(MemoryFastTest("test_memory_access_methods_backing_file"))
- suite.addTest(MemoryFastTest("test_memory_exceptions"))
- suite.addTest(MemoryFastTest("test_memory_multiple_runs"))
- suite.addTest(MemoryFastTest("test_memory_access_while_running"))
- suite.addTest(MemoryFastTest("test_memory_access_methods"))
- suite.addTest(MemoryFastTest("test_memory_large_read"))
- suite.addTest(MemoryFastTest("test_invalid_memory_location"))
- suite.addTest(MemoryFastTest("test_memory_multiple_threads"))
- suite.addTest(MemoryFastTest("test_memory_mixed_access"))
- suite.addTest(HwBasicTest("test_basic"))
- suite.addTest(HwBasicTest("test_registers"))
- suite.addTest(BacktraceTest("test_backtrace_as_symbols"))
- suite.addTest(BacktraceTest("test_backtrace"))
- suite.addTest(AttachDetachTest("test_attach"))
- suite.addTest(AttachDetachTest("test_attach_and_detach_1"))
- suite.addTest(AttachDetachTest("test_attach_and_detach_2"))
- suite.addTest(AttachDetachTest("test_attach_and_detach_3"))
- suite.addTest(AttachDetachTest("test_attach_and_detach_4"))
- suite.addTest(ThreadTest("test_thread"))
- suite.addTest(ThreadTest("test_thread_hardware"))
- suite.addTest(ComplexThreadTest("test_thread"))
- suite.addTest(CallbackTest("test_callback_simple"))
- suite.addTest(CallbackTest("test_callback_simple_hardware"))
- suite.addTest(CallbackTest("test_callback_memory"))
- suite.addTest(CallbackTest("test_callback_jumpout"))
- suite.addTest(CallbackTest("test_callback_intermixing"))
- suite.addTest(CallbackTest("test_callback_exception"))
- suite.addTest(CallbackTest("test_callback_step"))
- suite.addTest(CallbackTest("test_callback_pid_accessible"))
- suite.addTest(CallbackTest("test_callback_pid_accessible_alias"))
- suite.addTest(CallbackTest("test_callback_tid_accessible_alias"))
- suite.addTest(FinishTest("test_finish_exact_no_auto_interrupt_no_breakpoint"))
- suite.addTest(FinishTest("test_finish_heuristic_no_auto_interrupt_no_breakpoint"))
- suite.addTest(FinishTest("test_finish_exact_auto_interrupt_no_breakpoint"))
- suite.addTest(FinishTest("test_finish_heuristic_auto_interrupt_no_breakpoint"))
- suite.addTest(FinishTest("test_finish_exact_no_auto_interrupt_breakpoint"))
- suite.addTest(FinishTest("test_finish_heuristic_no_auto_interrupt_breakpoint"))
- suite.addTest(FinishTest("test_heuristic_return_address"))
- suite.addTest(FinishTest("test_exact_breakpoint_return"))
- suite.addTest(FinishTest("test_heuristic_breakpoint_return"))
- suite.addTest(FinishTest("test_breakpoint_collision"))
- suite.addTest(FloatingPointTest("test_floating_point_reg_access"))
- suite.addTest(Jumpout("test_jumpout"))
- suite.addTest(Nlinks("test_nlinks"))
- suite.addTest(JumpstartTest("test_cursed_ldpreload"))
- suite.addTest(ControlFlowTest("test_step_until_1"))
- suite.addTest(ControlFlowTest("test_step_until_2"))
- suite.addTest(ControlFlowTest("test_step_until_3"))
- suite.addTest(ControlFlowTest("test_step_and_cont"))
- suite.addTest(ControlFlowTest("test_step_and_cont_hardware"))
- suite.addTest(ControlFlowTest("test_step_until_and_cont"))
- suite.addTest(ControlFlowTest("test_step_until_and_cont_hardware"))
- suite.addTest(MultipleDebuggersTest("test_multiple_debuggers"))
- suite.addTest(LargeBinarySymTest("test_large_binary_symbol_load_times"))
- suite.addTest(LargeBinarySymTest("test_large_binary_demangle"))
- suite.addTest(WaitingTest("test_bps_waiting"))
- suite.addTest(WaitingTest("test_jumpout_waiting"))
- suite.addTest(WaitingNlinks("test_nlinks"))
- suite.addTest(AutoWaitingTest("test_bps_auto_waiting"))
- suite.addTest(AutoWaitingTest("test_jumpout_auto_waiting"))
- suite.addTest(NextTest("test_next"))
- suite.addTest(NextTest("test_next_breakpoint"))
- suite.addTest(NextTest("test_next_breakpoint_hw"))
- suite.addTest(AutoWaitingNlinks("test_nlinks"))
- suite.addTest(WatchpointTest("test_watchpoint"))
- suite.addTest(WatchpointTest("test_watchpoint_callback"))
- suite.addTest(WatchpointTest("test_watchpoint_disable"))
- suite.addTest(WatchpointTest("test_watchpoint_disable_reenable"))
- suite.addTest(WatchpointAliasTest("test_watchpoint_alias"))
- suite.addTest(WatchpointAliasTest("test_watchpoint_callback"))
- suite.addTest(HandleSyscallTest("test_handles"))
- suite.addTest(HandleSyscallTest("test_handles_with_pprint"))
- suite.addTest(HandleSyscallTest("test_handle_disabling"))
- suite.addTest(HandleSyscallTest("test_handle_disabling_with_pprint"))
- suite.addTest(HandleSyscallTest("test_handle_overwrite"))
- suite.addTest(HandleSyscallTest("test_handle_overwrite_with_pprint"))
- suite.addTest(HandleSyscallTest("test_handles_sync"))
- suite.addTest(HandleSyscallTest("test_handles_sync_with_pprint"))
- suite.addTest(AntidebugEscapingTest("test_antidebug_escaping"))
- suite.addTest(SyscallHijackTest("test_hijack_syscall"))
- suite.addTest(SyscallHijackTest("test_hijack_syscall_with_pprint"))
- suite.addTest(SyscallHijackTest("test_hijack_handle_syscall"))
- suite.addTest(SyscallHijackTest("test_hijack_handle_syscall_with_pprint"))
- suite.addTest(SyscallHijackTest("test_hijack_syscall_args"))
- suite.addTest(SyscallHijackTest("test_hijack_syscall_args_with_pprint"))
- suite.addTest(SyscallHijackTest("test_hijack_syscall_wrong_args"))
- suite.addTest(SyscallHijackTest("loop_detection_test"))
- suite.addTest(PPrintSyscallsTest("test_pprint_syscalls_generic"))
- suite.addTest(PPrintSyscallsTest("test_pprint_syscalls_with_statement"))
- suite.addTest(PPrintSyscallsTest("test_pprint_handle_syscalls"))
- suite.addTest(PPrintSyscallsTest("test_pprint_hijack_syscall"))
- suite.addTest(PPrintSyscallsTest("test_pprint_which_syscalls_pprint_after"))
- suite.addTest(PPrintSyscallsTest("test_pprint_which_syscalls_pprint_before"))
- suite.addTest(PPrintSyscallsTest("test_pprint_which_syscalls_pprint_after_and_before"))
- suite.addTest(PPrintSyscallsTest("test_pprint_which_syscalls_not_pprint_after"))
- suite.addTest(PPrintSyscallsTest("test_pprint_which_syscalls_not_pprint_before"))
- suite.addTest(PPrintSyscallsTest("test_pprint_which_syscalls_not_pprint_after_and_before"))
- suite.addTest(SignalCatchTest("test_signal_catch_signal_block"))
- suite.addTest(SignalCatchTest("test_signal_pass_to_process"))
- suite.addTest(SignalCatchTest("test_signal_disable_catch_signal"))
- suite.addTest(SignalCatchTest("test_signal_unblock"))
- suite.addTest(SignalCatchTest("test_signal_disable_catch_signal_unblock"))
- suite.addTest(SignalCatchTest("test_hijack_signal_with_catch_signal"))
- suite.addTest(SignalCatchTest("test_hijack_signal_with_api"))
- suite.addTest(SignalCatchTest("test_recursive_true_with_catch_signal"))
- suite.addTest(SignalCatchTest("test_recursive_true_with_api"))
- suite.addTest(SignalCatchTest("test_recursive_false_with_catch_signal"))
- suite.addTest(SignalCatchTest("test_recursive_false_with_api"))
- suite.addTest(SignalCatchTest("test_hijack_signal_with_catch_signal_loop"))
- suite.addTest(SignalCatchTest("test_hijack_signal_with_api_loop"))
- suite.addTest(SignalCatchTest("test_signal_unhijacking"))
- suite.addTest(SignalCatchTest("test_override_catch_signal"))
- suite.addTest(SignalCatchTest("test_override_hijack"))
- suite.addTest(SignalCatchTest("test_override_hybrid"))
- suite.addTest(SignalCatchTest("test_signal_get_signal"))
- suite.addTest(SignalCatchTest("test_signal_send_signal"))
- suite.addTest(SignalCatchTest("test_signal_catch_sync_block"))
- suite.addTest(SignalCatchTest("test_signal_catch_sync_pass"))
- suite.addTest(SignalMultithreadTest("test_signal_multithread_undet_catch_signal_block"))
- suite.addTest(SignalMultithreadTest("test_signal_multithread_undet_pass"))
- suite.addTest(SignalMultithreadTest("test_signal_multithread_det_catch_signal_block"))
- suite.addTest(SignalMultithreadTest("test_signal_multithread_det_pass"))
- suite.addTest(SignalMultithreadTest("test_signal_multithread_send_signal"))
- suite.addTest(DeathTest("test_io_death"))
- suite.addTest(DeathTest("test_cont_death"))
- suite.addTest(DeathTest("test_instr_death"))
- suite.addTest(DeathTest("test_exit_signal_death"))
- suite.addTest(DeathTest("test_exit_code_death"))
- suite.addTest(DeathTest("test_exit_code_normal"))
- suite.addTest(DeathTest("test_post_mortem_after_kill"))
- suite.addTest(AliasTest("test_basic_alias"))
- suite.addTest(AliasTest("test_step_alias"))
- suite.addTest(AliasTest("test_step_until_alias"))
- suite.addTest(AliasTest("test_memory_alias"))
- suite.addTest(AliasTest("test_finish_alias"))
- suite.addTest(AliasTest("test_waiting_alias"))
- suite.addTest(AliasTest("test_interrupt_alias"))
- suite.addTest(AtexitHandlerTest("test_attach_detach_1"))
- suite.addTest(AtexitHandlerTest("test_attach_detach_2"))
- suite.addTest(AtexitHandlerTest("test_attach_detach_3"))
- suite.addTest(AtexitHandlerTest("test_attach_detach_4"))
- suite.addTest(AtexitHandlerTest("test_attach_1"))
- suite.addTest(AtexitHandlerTest("test_attach_2"))
- suite.addTest(AtexitHandlerTest("test_run_1"))
- suite.addTest(AtexitHandlerTest("test_run_2"))
- suite.addTest(AtexitHandlerTest("test_run_3"))
- suite.addTest(AtexitHandlerTest("test_run_4"))
- return suite
-
-
-def complete_suite():
- suite = fast_suite()
- suite.addTest(Vmwhere1("test_vmwhere1"))
- suite.addTest(Vmwhere1("test_vmwhere1_callback"))
- suite.addTest(BruteTest("test_bruteforce"))
- suite.addTest(CallbackTest("test_callback_bruteforce"))
- suite.addTest(SpeedTest("test_speed"))
- suite.addTest(SpeedTest("test_speed_hardware"))
- suite.addTest(DeepDiveDivision("test_deep_dive_division"))
- return suite
-
-
-def thread_stress_suite():
- suite = unittest.TestSuite()
- for _ in range(1024):
- suite.addTest(ThreadTest("test_thread"))
- suite.addTest(ThreadTest("test_thread_hardware"))
- suite.addTest(ComplexThreadTest("test_thread"))
- return suite
-
-
-if __name__ == "__main__":
- if sys.version_info >= (3, 12):
- runner = unittest.TextTestRunner(verbosity=2, durations=3)
- else:
- runner = unittest.TextTestRunner(verbosity=2)
-
- if len(sys.argv) > 1 and sys.argv[1].lower() == "slow":
- suite = complete_suite()
- elif len(sys.argv) > 1 and sys.argv[1].lower() == "thread_stress":
- suite = thread_stress_suite()
- runner.verbosity = 1
- else:
- suite = fast_suite()
-
- result = runner.run(suite)
-
- if result.wasSuccessful():
- print("All tests passed")
- else:
- print("Some tests failed")
- print("\nFailed Tests:")
- for test, err in result.failures:
- print(f"{test}: {err}")
- print("\nErrors:")
- for test, err in result.errors:
- print(f"{test}: {err}")
diff --git a/test/amd64/scripts/alias_test.py b/test/amd64/scripts/alias_test.py
deleted file mode 100644
index 6b474025..00000000
--- a/test/amd64/scripts/alias_test.py
+++ /dev/null
@@ -1,180 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini, Francesco Panebianco. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class AliasTest(unittest.TestCase):
- def test_basic_alias(self):
- d = debugger("binaries/basic_test")
- d.r()
- bp = d.bp("register_test")
- d.c()
- self.assertTrue(bp.address == d.regs.rip)
- d.c()
- d.kill()
-
- def test_step_alias(self):
- d = debugger("binaries/basic_test")
-
- d.r()
- bp = d.bp("register_test")
- d.c()
-
- self.assertTrue(bp.address == d.regs.rip)
- self.assertTrue(bp.hit_count == 1)
-
- d.si()
-
- self.assertTrue(bp.address + 1 == d.regs.rip)
- self.assertTrue(bp.hit_count == 1)
-
- d.si()
-
- self.assertTrue(bp.address + 4 == d.regs.rip)
- self.assertTrue(bp.hit_count == 1)
-
- d.c()
- d.kill()
-
- def test_step_until_alias(self):
- d = debugger("./binaries/breakpoint_test")
- d.r()
-
- bp1 = d.bp("main")
- bp2 = d.bp("random_function")
- d.c()
-
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.su(0x401180)
- self.assertTrue(d.regs.rip == 0x401180)
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.c()
-
- self.assertTrue(bp2.hit_on(d))
-
- d.c()
-
- d.kill()
-
- def test_memory_alias(self):
- d = debugger("binaries/memory_test")
-
- d.r()
-
- bp = d.bp("change_memory")
-
- d.c()
-
- assert d.regs.rip == bp.address
-
- address = d.regs.rdi
- prev = bytes(range(256))
-
- self.assertTrue(d.mem[address, 256] == prev)
-
- d.mem[address + 128 :] = b"abcd123456"
- prev = prev[:128] + b"abcd123456" + prev[138:]
-
- self.assertTrue(d.mem[address : address + 256] == prev)
-
- d.kill()
-
- def test_finish_alias(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
-
- # ------------------ Block 1 ------------------ #
- # Return from the first function call #
- # --------------------------------------------- #
-
- # Reach function c
- d.r()
- d.bp(0x4011E3)
- d.c()
-
- self.assertEqual(d.regs.rip, 0x4011E3)
-
- # Finish function c
- d.fin(heuristic="step-mode")
-
- self.assertEqual(d.regs.rip, 0x401202)
-
- d.kill()
-
- # ------------------ Block 2 ------------------ #
- # Return from the nested function call #
- # --------------------------------------------- #
-
- # Reach function a
- d.r()
- d.bp(0x401146)
- d.c()
-
- self.assertEqual(d.regs.rip, 0x401146)
-
- # Finish function a
- d.fin(heuristic="step-mode")
-
- self.assertEqual(d.regs.rip, 0x4011E0)
-
- d.kill()
-
- def test_waiting_alias(self):
- d = debugger("binaries/breakpoint_test", auto_interrupt_on_command=True)
-
- d.r()
-
- bp1 = d.breakpoint("random_function")
- bp2 = d.breakpoint(0x40115B)
- bp3 = d.breakpoint(0x40116D)
-
- counter = 1
-
- d.c()
-
- while True:
- d.w()
- if d.regs.rip == bp1.address:
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- elif d.regs.rip == bp2.address:
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- counter += 1
- elif d.regs.rip == bp3.address:
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.rsi == 45)
- self.assertTrue(d.regs.esi == 45)
- self.assertTrue(d.regs.si == 45)
- self.assertTrue(d.regs.sil == 45)
- self.assertTrue(bp3.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- break
-
- d.c()
-
- d.kill()
-
- def test_interrupt_alias(self):
- d = debugger("binaries/basic_test")
-
- d.r()
-
- d.c()
-
- d.int()
- d.kill()
diff --git a/test/amd64/scripts/attach_detach_test.py b/test/amd64/scripts/attach_detach_test.py
deleted file mode 100644
index b7be20c9..00000000
--- a/test/amd64/scripts/attach_detach_test.py
+++ /dev/null
@@ -1,96 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import logging
-import unittest
-
-from pwn import process
-
-from libdebug import debugger
-
-logging.getLogger("pwnlib").setLevel(logging.ERROR)
-
-
-class AttachDetachTest(unittest.TestCase):
- def setUp(self):
- pass
-
- def test_attach(self):
- r = process("binaries/attach_test")
-
- d = debugger()
- d.attach(r.pid)
- bp = d.breakpoint("printName", hardware=True)
- d.cont()
-
- r.recvuntil(b"name:")
- r.sendline(b"Io_no")
-
- self.assertTrue(d.regs.rip == bp.address)
-
- d.cont()
-
- d.kill()
-
- def test_attach_and_detach_1(self):
- r = process("binaries/attach_test")
-
- d = debugger()
-
- # Attach to the process
- d.attach(r.pid)
- d.detach()
-
- # Validate that, after detaching, the process is still running
- r.recvuntil(b"name:", timeout=1)
- r.sendline(b"Io_no")
-
- r.kill()
-
- def test_attach_and_detach_2(self):
- d = debugger("binaries/attach_test")
-
- # Run the process
- r = d.run()
- d.detach()
-
- # Validate that, after detaching, the process is still running
- r.recvuntil(b"name:", timeout=1)
- r.sendline(b"Io_no")
-
- d.kill()
-
- def test_attach_and_detach_3(self):
- d = debugger("binaries/attach_test")
-
- r = d.run()
-
- # We must ensure that any breakpoint is unset before detaching
- d.breakpoint(0x125E)
- d.breakpoint(0x1261, hardware=True)
-
- d.detach()
-
- # Validate that, after detaching, the process is still running
- r.recvuntil(b"name:", timeout=1)
- r.sendline(b"Io_no")
-
- d.kill()
-
- def test_attach_and_detach_4(self):
- r = process("binaries/attach_test")
-
- d = debugger()
- d.attach(r.pid)
- d.detach()
- d.kill()
-
- # Validate that, after detaching and killing, the process is effectively terminated
- self.assertRaises(EOFError, r.sendline, b"provola")
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/test/amd64/scripts/auto_waiting_test.py b/test/amd64/scripts/auto_waiting_test.py
deleted file mode 100644
index 323c9faf..00000000
--- a/test/amd64/scripts/auto_waiting_test.py
+++ /dev/null
@@ -1,205 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import io
-import logging
-import unittest
-
-from libdebug import debugger
-
-
-class AutoWaitingTest(unittest.TestCase):
- def setUp(self):
- # Redirect logging to a string buffer
- self.log_capture_string = io.StringIO()
- self.log_handler = logging.StreamHandler(self.log_capture_string)
- self.log_handler.setLevel(logging.WARNING)
-
- self.logger = logging.getLogger("libdebug")
- self.original_handlers = self.logger.handlers
- self.logger.handlers = []
- self.logger.addHandler(self.log_handler)
- self.logger.setLevel(logging.WARNING)
-
- def test_bps_auto_waiting(self):
- d = debugger("binaries/breakpoint_test", auto_interrupt_on_command=False)
-
- d.run()
-
- bp1 = d.breakpoint("random_function")
- bp2 = d.breakpoint(0x40115B)
- bp3 = d.breakpoint(0x40116D)
-
- counter = 1
-
- d.cont()
-
- while True:
- if d.regs.rip == bp1.address:
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- elif d.regs.rip == bp2.address:
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- counter += 1
- elif d.regs.rip == bp3.address:
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.rsi == 45)
- self.assertTrue(d.regs.esi == 45)
- self.assertTrue(d.regs.si == 45)
- self.assertTrue(d.regs.sil == 45)
- self.assertTrue(bp3.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- break
-
- d.cont()
-
- d.kill()
-
- def test_jumpout_auto_waiting(self):
- flag = ""
- first = 0x55
- second = 0
-
- d = debugger("CTF/jumpout", auto_interrupt_on_command=False)
-
- r = d.run()
-
- bp1 = d.breakpoint(0x140B, hardware=True)
- bp2 = d.breakpoint(0x157C, hardware=True)
-
- d.cont()
-
- r.sendline(b"A" * 0x1D)
-
- while True:
- if d.regs.rip == bp1.address:
- second = d.regs.r9
- elif d.regs.rip == bp2.address:
- address = d.regs.r13 + d.regs.rbx
- third = int.from_bytes(d.memory[address, 1], "little")
- flag += chr((first ^ second ^ third ^ (bp2.hit_count - 1)))
-
- d.cont()
-
- if flag.endswith("}"):
- break
-
- r.recvuntil(b"Wrong...")
-
- d.kill()
-
- self.assertEqual(flag, "SECCON{jump_table_everywhere}")
-
-class AutoWaitingNlinks(unittest.TestCase):
- def setUp(self):
- pass
-
- def get_passsphrase_from_class_1_binaries(self, previous_flag):
- flag = b""
-
- d = debugger("CTF/1", auto_interrupt_on_command=False)
- r = d.run()
-
- d.breakpoint(0x7EF1, hardware=True)
-
- d.cont()
-
- r.recvuntil(b"Passphrase:\n")
- r.send(previous_flag + b"a" * 8)
-
- for _ in range(8):
- offset = ord("a") ^ d.regs.rbp
- d.regs.rbp = d.regs.r13
- flag += (offset ^ d.regs.r13).to_bytes(1, "little")
-
- d.cont()
-
- r.recvline()
-
- d.kill()
-
- self.assertEqual(flag, b"\x00\x006\x00\x00\x00(\x00")
- return flag
-
- def get_passsphrase_from_class_2_binaries(self, previous_flag):
- bitmap = {}
- lastpos = 0
- flag = b""
-
- d = debugger("CTF/2", auto_interrupt_on_command=False)
- r = d.run()
-
- bp1 = d.breakpoint(0xD8C1, hardware=True)
- bp2 = d.breakpoint(0x1858, hardware=True)
- bp3 = d.breakpoint(0xDBA1, hardware=True)
-
- d.cont()
-
- r.recvuntil(b"Passphrase:\n")
- r.send(previous_flag + b"a" * 8)
-
- while True:
- if d.regs.rip == bp1.address:
- lastpos = d.regs.rbp
- d.regs.rbp = d.regs.r13 + 1
- elif d.regs.rip == bp2.address:
- bitmap[d.regs.r12 & 0xFF] = lastpos & 0xFF
- elif d.regs.rip == bp3.address:
- d.regs.rbp = d.regs.r13
- wanted = d.regs.rbp
- needed = 0
- for i in range(8):
- if wanted & (2**i):
- needed |= bitmap[2**i]
- flag += chr(needed).encode()
-
- if bp3.hit_count == 8:
- d.cont()
- break
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(flag, b"\x00\x00\x00\x01\x00\x00a\x00")
-
- def get_passsphrase_from_class_3_binaries(self):
- flag = b""
-
- d = debugger("CTF/0", auto_interrupt_on_command=False)
- r = d.run()
-
- d.breakpoint(0x91A1, hardware=True)
-
- d.cont()
-
- r.send(b"a" * 8)
-
- for _ in range(8):
- offset = ord("a") - d.regs.rbp
- d.regs.rbp = d.regs.r13
-
- flag += chr((d.regs.r13 + offset) % 256).encode("latin-1")
-
- d.cont()
-
- r.recvline()
-
- d.kill()
-
- self.assertEqual(flag, b"BM8\xd3\x02\x00\x00\x00")
- return flag
-
- def test_nlinks(self):
- flag = self.get_passsphrase_from_class_3_binaries()
- flag = self.get_passsphrase_from_class_1_binaries(flag)
- self.get_passsphrase_from_class_2_binaries(flag)
diff --git a/test/amd64/scripts/backtrace_test.py b/test/amd64/scripts/backtrace_test.py
deleted file mode 100644
index b2acdae7..00000000
--- a/test/amd64/scripts/backtrace_test.py
+++ /dev/null
@@ -1,198 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class BacktraceTest(unittest.TestCase):
- def setUp(self):
- self.d = debugger("binaries/backtrace_test")
-
- def test_backtrace_as_symbols(self):
- d = self.d
-
- d.run()
-
- bp0 = d.breakpoint("main+8")
- bp1 = d.breakpoint("function1+8")
- bp2 = d.breakpoint("function2+8")
- bp3 = d.breakpoint("function3+8")
- bp4 = d.breakpoint("function4+8")
- bp5 = d.breakpoint("function5+8")
- bp6 = d.breakpoint("function6+8")
-
- d.cont()
-
- self.assertTrue(d.regs.rip == bp0.address)
- backtrace = d.backtrace(as_symbols=True)
- self.assertIn("_start", backtrace.pop())
- self.assertEqual(backtrace[:1], ["main+8"])
-
- d.cont()
-
- self.assertTrue(d.regs.rip == bp1.address)
- backtrace = d.backtrace(as_symbols=True)
- self.assertIn("_start", backtrace.pop())
- self.assertEqual(backtrace[:2], ["function1+8", "main+16"])
-
- d.cont()
-
- self.assertTrue(d.regs.rip == bp2.address)
- backtrace = d.backtrace(as_symbols=True)
- self.assertIn("_start", backtrace.pop())
- self.assertEqual(backtrace[:3], ["function2+8", "function1+12", "main+16"])
-
- d.cont()
-
- self.assertTrue(d.regs.rip == bp3.address)
- backtrace = d.backtrace(as_symbols=True)
- self.assertIn("_start", backtrace.pop())
- self.assertEqual(
- backtrace[:4], ["function3+8", "function2+1c", "function1+12", "main+16"]
- )
-
- d.cont()
-
- self.assertTrue(d.regs.rip == bp4.address)
- backtrace = d.backtrace(as_symbols=True)
- self.assertIn("_start", backtrace.pop())
- self.assertEqual(
- backtrace[:5],
- ["function4+8", "function3+1c", "function2+1c", "function1+12", "main+16"],
- )
-
- d.cont()
-
- self.assertTrue(d.regs.rip == bp5.address)
- backtrace = d.backtrace(as_symbols=True)
- self.assertIn("_start", backtrace.pop())
- self.assertEqual(
- backtrace[:6],
- [
- "function5+8",
- "function4+1c",
- "function3+1c",
- "function2+1c",
- "function1+12",
- "main+16",
- ],
- )
-
- d.cont()
-
- self.assertTrue(d.regs.rip == bp6.address)
- backtrace = d.backtrace(as_symbols=True)
- self.assertIn("_start", backtrace.pop())
- self.assertEqual(
- backtrace[:7],
- [
- "function6+8",
- "function5+1c",
- "function4+1c",
- "function3+1c",
- "function2+1c",
- "function1+12",
- "main+16",
- ],
- )
-
- d.kill()
-
- def test_backtrace(self):
- d = self.d
-
- d.run()
-
- bp0 = d.breakpoint("main+8")
- bp1 = d.breakpoint("function1+8")
- bp2 = d.breakpoint("function2+8")
- bp3 = d.breakpoint("function3+8")
- bp4 = d.breakpoint("function4+8")
- bp5 = d.breakpoint("function5+8")
- bp6 = d.breakpoint("function6+8")
-
- d.cont()
-
- self.assertTrue(d.regs.rip == bp0.address)
- backtrace = d.backtrace()
- backtrace.pop()
- self.assertEqual(backtrace[:1], [0x555555555151])
-
- d.cont()
-
- self.assertTrue(d.regs.rip == bp1.address)
- backtrace = d.backtrace()
- backtrace.pop()
- self.assertEqual(backtrace[:2], [0x55555555518a, 0x55555555515f])
-
- d.cont()
-
- self.assertTrue(d.regs.rip == bp2.address)
- backtrace = d.backtrace()
- backtrace.pop()
- self.assertEqual(backtrace[:3], [0x55555555519e, 0x555555555194, 0x55555555515f])
-
- d.cont()
-
- self.assertTrue(d.regs.rip == bp3.address)
- backtrace = d.backtrace()
- backtrace.pop()
- self.assertEqual(
- backtrace[:4], [0x5555555551bc, 0x5555555551b2, 0x555555555194, 0x55555555515f]
- )
-
- d.cont()
-
- self.assertTrue(d.regs.rip == bp4.address)
- backtrace = d.backtrace()
- backtrace.pop()
- self.assertEqual(
- backtrace[:5],
- [0x5555555551da, 0x5555555551d0, 0x5555555551b2, 0x555555555194, 0x55555555515f],
- )
-
- d.cont()
-
- self.assertTrue(d.regs.rip == bp5.address)
- backtrace = d.backtrace()
- backtrace.pop()
- self.assertEqual(
- backtrace[:6],
- [
- 0x5555555551f8,
- 0x5555555551ee,
- 0x5555555551d0,
- 0x5555555551b2,
- 0x555555555194,
- 0x55555555515f,
- ],
- )
-
- d.cont()
-
- self.assertTrue(d.regs.rip == bp6.address)
- backtrace = d.backtrace()
- backtrace.pop()
- self.assertEqual(
- backtrace[:7],
- [
- 0x555555555216,
- 0x55555555520c,
- 0x5555555551ee,
- 0x5555555551d0,
- 0x5555555551b2,
- 0x555555555194,
- 0x55555555515f,
- ],
- )
-
- d.kill()
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/test/amd64/scripts/basic_test.py b/test/amd64/scripts/basic_test.py
deleted file mode 100644
index 23bf0af5..00000000
--- a/test/amd64/scripts/basic_test.py
+++ /dev/null
@@ -1,467 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class BasicTest(unittest.TestCase):
- def setUp(self):
- self.d = debugger("binaries/basic_test")
-
- def test_basic(self):
- self.d.run()
- bp = self.d.breakpoint("register_test")
- self.d.cont()
- self.assertTrue(bp.address == self.d.regs.rip)
- self.d.cont()
- self.d.kill()
-
- def test_registers(self):
- d = self.d
-
- d.run()
-
- bp1 = d.breakpoint(0x4011CA)
- bp2 = d.breakpoint(0x40128D)
- bp3 = d.breakpoint(0x401239)
- bp4 = d.breakpoint(0x4011F4)
- bp5 = d.breakpoint(0x401296)
-
- d.cont()
- self.assertTrue(bp1.address == d.regs.rip)
-
- self.assertTrue(d.regs.rax == 0x0011223344556677)
- self.assertTrue(d.regs.rbx == 0x1122334455667700)
- self.assertTrue(d.regs.rcx == 0x2233445566770011)
- self.assertTrue(d.regs.rdx == 0x3344556677001122)
- self.assertTrue(d.regs.rsi == 0x4455667700112233)
- self.assertTrue(d.regs.rdi == 0x5566770011223344)
- self.assertTrue(d.regs.rbp == 0x6677001122334455)
- self.assertTrue(d.regs.r8 == 0xAABBCCDD11223344)
- self.assertTrue(d.regs.r9 == 0xBBCCDD11223344AA)
- self.assertTrue(d.regs.r10 == 0xCCDD11223344AABB)
- self.assertTrue(d.regs.r11 == 0xDD11223344AABBCC)
- self.assertTrue(d.regs.r12 == 0x11223344AABBCCDD)
- self.assertTrue(d.regs.r13 == 0x223344AABBCCDD11)
- self.assertTrue(d.regs.r14 == 0x3344AABBCCDD1122)
- self.assertTrue(d.regs.r15 == 0x44AABBCCDD112233)
-
- d.cont()
- self.assertTrue(bp4.address == d.regs.rip)
-
- self.assertTrue(d.regs.al == 0x11)
- self.assertTrue(d.regs.bl == 0x22)
- self.assertTrue(d.regs.cl == 0x33)
- self.assertTrue(d.regs.dl == 0x44)
- self.assertTrue(d.regs.sil == 0x55)
- self.assertTrue(d.regs.dil == 0x66)
- self.assertTrue(d.regs.bpl == 0x77)
- self.assertTrue(d.regs.r8b == 0x88)
- self.assertTrue(d.regs.r9b == 0x99)
- self.assertTrue(d.regs.r10b == 0xAA)
- self.assertTrue(d.regs.r11b == 0xBB)
- self.assertTrue(d.regs.r12b == 0xCC)
- self.assertTrue(d.regs.r13b == 0xDD)
- self.assertTrue(d.regs.r14b == 0xEE)
- self.assertTrue(d.regs.r15b == 0xFF)
-
- d.cont()
- self.assertTrue(bp3.address == d.regs.rip)
-
- self.assertTrue(d.regs.ax == 0x1122)
- self.assertTrue(d.regs.bx == 0x2233)
- self.assertTrue(d.regs.cx == 0x3344)
- self.assertTrue(d.regs.dx == 0x4455)
- self.assertTrue(d.regs.si == 0x5566)
- self.assertTrue(d.regs.di == 0x6677)
- self.assertTrue(d.regs.bp == 0x7788)
- self.assertTrue(d.regs.r8w == 0x8899)
- self.assertTrue(d.regs.r9w == 0x99AA)
- self.assertTrue(d.regs.r10w == 0xAABB)
- self.assertTrue(d.regs.r11w == 0xBBCC)
- self.assertTrue(d.regs.r12w == 0xCCDD)
- self.assertTrue(d.regs.r13w == 0xDDEE)
- self.assertTrue(d.regs.r14w == 0xEEFF)
- self.assertTrue(d.regs.r15w == 0xFF00)
-
- d.cont()
- self.assertTrue(bp2.address == d.regs.rip)
-
- self.assertTrue(d.regs.eax == 0x11223344)
- self.assertTrue(d.regs.ebx == 0x22334455)
- self.assertTrue(d.regs.ecx == 0x33445566)
- self.assertTrue(d.regs.edx == 0x44556677)
- self.assertTrue(d.regs.esi == 0x55667788)
- self.assertTrue(d.regs.edi == 0x66778899)
- self.assertTrue(d.regs.ebp == 0x778899AA)
- self.assertTrue(d.regs.r8d == 0x8899AABB)
- self.assertTrue(d.regs.r9d == 0x99AABBCC)
- self.assertTrue(d.regs.r10d == 0xAABBCCDD)
- self.assertTrue(d.regs.r11d == 0xBBCCDD11)
- self.assertTrue(d.regs.r12d == 0xCCDD1122)
- self.assertTrue(d.regs.r13d == 0xDD112233)
- self.assertTrue(d.regs.r14d == 0x11223344)
- self.assertTrue(d.regs.r15d == 0x22334455)
-
- d.cont()
- self.assertTrue(bp5.address == d.regs.rip)
-
- self.assertTrue(d.regs.ah == 0x11)
- self.assertTrue(d.regs.bh == 0x22)
- self.assertTrue(d.regs.ch == 0x33)
- self.assertTrue(d.regs.dh == 0x44)
-
- self.d.cont()
- self.d.kill()
-
- def test_step(self):
- d = self.d
-
- d.run()
- bp = d.breakpoint("register_test")
- d.cont()
-
- self.assertTrue(bp.address == d.regs.rip)
- self.assertTrue(bp.hit_count == 1)
-
- d.step()
-
- self.assertTrue(bp.address + 1 == d.regs.rip)
- self.assertTrue(bp.hit_count == 1)
-
- d.step()
-
- self.assertTrue(bp.address + 4 == d.regs.rip)
- self.assertTrue(bp.hit_count == 1)
-
- d.cont()
- d.kill()
-
- def test_step_hardware(self):
- d = self.d
-
- d.run()
- bp = d.breakpoint("register_test", hardware=True)
- d.cont()
-
- self.assertTrue(bp.address == d.regs.rip)
- self.assertTrue(bp.hit_count == 1)
-
- d.step()
-
- self.assertTrue(bp.address + 1 == d.regs.rip)
- self.assertTrue(bp.hit_count == 1)
-
- d.step()
-
- self.assertTrue(bp.address + 4 == d.regs.rip)
- self.assertTrue(bp.hit_count == 1)
-
- d.cont()
- d.kill()
-
-
-class BasicPieTest(unittest.TestCase):
- def setUp(self):
- self.d = debugger("binaries/basic_test_pie")
-
- def test_basic(self):
- d = self.d
-
- d.run()
- bp = d.breakpoint("register_test")
- d.cont()
-
- self.assertTrue(bp.address == d.regs.rip)
- self.assertTrue(d.regs.rdi == 0xAABBCCDD11223344)
-
- self.d.kill()
-
-
-class HwBasicTest(unittest.TestCase):
- def setUp(self):
- self.d = debugger("binaries/basic_test")
-
- def test_basic(self):
- d = self.d
- d.run()
- bp = d.breakpoint(0x4011D1, hardware=True)
- self.d.cont()
- self.assertTrue(bp.address == d.regs.rip)
- self.d.kill()
-
- def test_registers(self):
- d = self.d
-
- d.run()
-
- bp1 = d.breakpoint(0x4011CA, hardware=True)
- bp2 = d.breakpoint(0x40128D, hardware=False)
- bp3 = d.breakpoint(0x401239, hardware=True)
- bp4 = d.breakpoint(0x4011F4, hardware=False)
- bp5 = d.breakpoint(0x401296, hardware=True)
-
- d.cont()
- self.assertTrue(bp1.address == d.regs.rip)
-
- self.assertTrue(d.regs.rax == 0x0011223344556677)
- self.assertTrue(d.regs.rbx == 0x1122334455667700)
- self.assertTrue(d.regs.rcx == 0x2233445566770011)
- self.assertTrue(d.regs.rdx == 0x3344556677001122)
- self.assertTrue(d.regs.rsi == 0x4455667700112233)
- self.assertTrue(d.regs.rdi == 0x5566770011223344)
- self.assertTrue(d.regs.rbp == 0x6677001122334455)
- self.assertTrue(d.regs.r8 == 0xAABBCCDD11223344)
- self.assertTrue(d.regs.r9 == 0xBBCCDD11223344AA)
- self.assertTrue(d.regs.r10 == 0xCCDD11223344AABB)
- self.assertTrue(d.regs.r11 == 0xDD11223344AABBCC)
- self.assertTrue(d.regs.r12 == 0x11223344AABBCCDD)
- self.assertTrue(d.regs.r13 == 0x223344AABBCCDD11)
- self.assertTrue(d.regs.r14 == 0x3344AABBCCDD1122)
- self.assertTrue(d.regs.r15 == 0x44AABBCCDD112233)
-
- d.cont()
- self.assertTrue(bp4.address == d.regs.rip)
-
- self.assertTrue(d.regs.al == 0x11)
- self.assertTrue(d.regs.bl == 0x22)
- self.assertTrue(d.regs.cl == 0x33)
- self.assertTrue(d.regs.dl == 0x44)
- self.assertTrue(d.regs.sil == 0x55)
- self.assertTrue(d.regs.dil == 0x66)
- self.assertTrue(d.regs.bpl == 0x77)
- self.assertTrue(d.regs.r8b == 0x88)
- self.assertTrue(d.regs.r9b == 0x99)
- self.assertTrue(d.regs.r10b == 0xAA)
- self.assertTrue(d.regs.r11b == 0xBB)
- self.assertTrue(d.regs.r12b == 0xCC)
- self.assertTrue(d.regs.r13b == 0xDD)
- self.assertTrue(d.regs.r14b == 0xEE)
- self.assertTrue(d.regs.r15b == 0xFF)
-
- d.cont()
- self.assertTrue(bp3.address == d.regs.rip)
-
- self.assertTrue(d.regs.ax == 0x1122)
- self.assertTrue(d.regs.bx == 0x2233)
- self.assertTrue(d.regs.cx == 0x3344)
- self.assertTrue(d.regs.dx == 0x4455)
- self.assertTrue(d.regs.si == 0x5566)
- self.assertTrue(d.regs.di == 0x6677)
- self.assertTrue(d.regs.bp == 0x7788)
- self.assertTrue(d.regs.r8w == 0x8899)
- self.assertTrue(d.regs.r9w == 0x99AA)
- self.assertTrue(d.regs.r10w == 0xAABB)
- self.assertTrue(d.regs.r11w == 0xBBCC)
- self.assertTrue(d.regs.r12w == 0xCCDD)
- self.assertTrue(d.regs.r13w == 0xDDEE)
- self.assertTrue(d.regs.r14w == 0xEEFF)
- self.assertTrue(d.regs.r15w == 0xFF00)
-
- d.cont()
- self.assertTrue(bp2.address == d.regs.rip)
-
- self.assertTrue(d.regs.eax == 0x11223344)
- self.assertTrue(d.regs.ebx == 0x22334455)
- self.assertTrue(d.regs.ecx == 0x33445566)
- self.assertTrue(d.regs.edx == 0x44556677)
- self.assertTrue(d.regs.esi == 0x55667788)
- self.assertTrue(d.regs.edi == 0x66778899)
- self.assertTrue(d.regs.ebp == 0x778899AA)
- self.assertTrue(d.regs.r8d == 0x8899AABB)
- self.assertTrue(d.regs.r9d == 0x99AABBCC)
- self.assertTrue(d.regs.r10d == 0xAABBCCDD)
- self.assertTrue(d.regs.r11d == 0xBBCCDD11)
- self.assertTrue(d.regs.r12d == 0xCCDD1122)
- self.assertTrue(d.regs.r13d == 0xDD112233)
- self.assertTrue(d.regs.r14d == 0x11223344)
- self.assertTrue(d.regs.r15d == 0x22334455)
-
- d.cont()
- self.assertTrue(bp5.address == d.regs.rip)
-
- self.assertTrue(d.regs.ah == 0x11)
- self.assertTrue(d.regs.bh == 0x22)
- self.assertTrue(d.regs.ch == 0x33)
- self.assertTrue(d.regs.dh == 0x44)
-
- self.d.cont()
- self.d.kill()
-
-
-class ControlFlowTest(unittest.TestCase):
- def setUp(self) -> None:
- pass
-
- def test_step_until_1(self):
- d = debugger("./binaries/breakpoint_test")
- d.run()
-
- bp = d.breakpoint("main")
- d.cont()
-
- self.assertTrue(bp.hit_on(d))
-
- d.step_until(0x40119D)
-
- self.assertTrue(d.regs.rip == 0x40119D)
- self.assertTrue(bp.hit_count == 1)
- self.assertFalse(bp.hit_on(d))
-
- d.kill()
-
- def test_step_until_2(self):
- d = debugger("./binaries/breakpoint_test")
- d.run()
-
- bp = d.breakpoint(0x401148, hardware=True)
- d.cont()
-
- self.assertTrue(bp.hit_on(d))
-
- d.step_until(0x40119D, max_steps=7)
-
- self.assertTrue(d.regs.rip == 0x40115E)
- self.assertTrue(bp.hit_count == 1)
- self.assertFalse(bp.hit_on(d))
-
- d.kill()
-
- def test_step_until_3(self):
- d = debugger("./binaries/breakpoint_test")
- d.run()
-
- bp = d.breakpoint(0x401148)
-
- # Let's put some breakpoints in-between
- d.breakpoint(0x40114F)
- d.breakpoint(0x401156)
- d.breakpoint(0x401162, hardware=True)
-
- d.cont()
-
- self.assertTrue(bp.hit_on(d))
-
- # trace is [0x401148, 0x40114f, 0x401156, 0x401162, 0x401166, 0x401158, 0x40115b, 0x40115e]
- d.step_until(0x40119D, max_steps=7)
-
- self.assertTrue(d.regs.rip == 0x40115E)
- self.assertTrue(bp.hit_count == 1)
- self.assertFalse(bp.hit_on(d))
-
- d.kill()
-
- def test_step_and_cont(self):
- d = debugger("./binaries/breakpoint_test")
- d.run()
-
- bp1 = d.breakpoint("main")
- bp2 = d.breakpoint("random_function")
- d.cont()
-
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.step()
- self.assertTrue(d.regs.rip == 0x401180)
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.step()
- self.assertTrue(d.regs.rip == 0x401183)
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.cont()
-
- self.assertTrue(bp2.hit_on(d))
-
- d.cont()
-
- d.kill()
-
- def test_step_and_cont_hardware(self):
- d = debugger("./binaries/breakpoint_test")
- d.run()
-
- bp1 = d.breakpoint("main", hardware=True)
- bp2 = d.breakpoint("random_function", hardware=True)
- d.cont()
-
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.step()
- self.assertTrue(d.regs.rip == 0x401180)
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.step()
- self.assertTrue(d.regs.rip == 0x401183)
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.cont()
-
- self.assertTrue(bp2.hit_on(d))
-
- d.cont()
-
- d.kill()
-
- def test_step_until_and_cont(self):
- d = debugger("./binaries/breakpoint_test")
- d.run()
-
- bp1 = d.breakpoint("main")
- bp2 = d.breakpoint("random_function")
- d.cont()
-
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.step_until(0x401180)
- self.assertTrue(d.regs.rip == 0x401180)
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.cont()
-
- self.assertTrue(bp2.hit_on(d))
-
- d.cont()
-
- d.kill()
-
- def test_step_until_and_cont_hardware(self):
- d = debugger("./binaries/breakpoint_test")
- d.run()
-
- bp1 = d.breakpoint("main", hardware=True)
- bp2 = d.breakpoint("random_function", hardware=True)
- d.cont()
-
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.step_until(0x401180)
- self.assertTrue(d.regs.rip == 0x401180)
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
-
- d.cont()
-
- self.assertTrue(bp2.hit_on(d))
-
- d.cont()
-
- d.kill()
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/test/amd64/scripts/breakpoint_test.py b/test/amd64/scripts/breakpoint_test.py
deleted file mode 100644
index ea36547b..00000000
--- a/test/amd64/scripts/breakpoint_test.py
+++ /dev/null
@@ -1,452 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import io
-import logging
-import unittest
-
-from libdebug import debugger
-
-
-class BreakpointTest(unittest.TestCase):
- def setUp(self):
- self.d = debugger("binaries/breakpoint_test")
-
- # Redirect logging to a string buffer
- self.log_capture_string = io.StringIO()
- self.log_handler = logging.StreamHandler(self.log_capture_string)
- self.log_handler.setLevel(logging.WARNING)
-
- self.logger = logging.getLogger("libdebug")
- self.original_handlers = self.logger.handlers
- self.logger.handlers = []
- self.logger.addHandler(self.log_handler)
- self.logger.setLevel(logging.WARNING)
-
- def test_bps(self):
- d = self.d
-
- d.run()
-
- bp1 = d.breakpoint("random_function")
- bp2 = d.breakpoint(0x40115B)
- bp3 = d.breakpoint(0x40116D)
-
- counter = 1
-
- d.cont()
-
- while True:
- if d.regs.rip == bp1.address:
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- elif d.regs.rip == bp2.address:
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- counter += 1
- elif d.regs.rip == bp3.address:
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.rsi == 45)
- self.assertTrue(d.regs.esi == 45)
- self.assertTrue(d.regs.si == 45)
- self.assertTrue(d.regs.sil == 45)
- self.assertTrue(bp3.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- break
-
- d.cont()
-
- self.assertEqual(bp2.hit_count, 10)
-
- self.d.kill()
-
- def test_bp_disable(self):
- d = self.d
-
- d.run()
-
- bp1 = d.breakpoint("random_function")
- bp2 = d.breakpoint(0x40115B)
- bp3 = d.breakpoint(0x40116D)
-
- counter = 1
-
- d.cont()
-
- while True:
- if d.regs.rip == bp1.address:
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- elif d.regs.rip == bp2.address:
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- bp2.disable()
- elif d.regs.rip == bp3.address:
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.rsi == 45)
- self.assertTrue(d.regs.esi == 45)
- self.assertTrue(d.regs.si == 45)
- self.assertTrue(d.regs.sil == 45)
- self.assertTrue(bp3.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- break
-
- d.cont()
-
- self.assertEqual(bp2.hit_count, 1)
-
- self.d.kill()
-
- def test_bp_disable_hw(self):
- d = self.d
-
- d.run()
-
- bp1 = d.breakpoint("random_function")
- bp2 = d.breakpoint(0x40115B, hardware=True)
- bp3 = d.breakpoint(0x40116D)
-
- counter = 1
-
- d.cont()
-
- while True:
- if d.regs.rip == bp1.address:
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- elif d.regs.rip == bp2.address:
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- bp2.disable()
- elif d.regs.rip == bp3.address:
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.rsi == 45)
- self.assertTrue(d.regs.esi == 45)
- self.assertTrue(d.regs.si == 45)
- self.assertTrue(d.regs.sil == 45)
- self.assertTrue(bp3.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- break
-
- d.cont()
-
- self.assertEqual(bp2.hit_count, 1)
-
- def test_bp_disable_reenable(self):
- d = self.d
-
- d.run()
-
- bp1 = d.breakpoint("random_function")
- bp2 = d.breakpoint(0x40115B)
- bp4 = d.breakpoint(0x401162)
- bp3 = d.breakpoint(0x40116D)
-
- counter = 1
-
- d.cont()
-
- while True:
- if d.regs.rip == bp1.address:
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- elif d.regs.rip == bp2.address:
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- if bp4.enabled:
- bp4.disable()
- else:
- bp4.enable()
- counter += 1
- elif d.regs.rip == bp3.address:
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.rsi == 45)
- self.assertTrue(d.regs.esi == 45)
- self.assertTrue(d.regs.si == 45)
- self.assertTrue(d.regs.sil == 45)
- self.assertTrue(bp3.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- break
- elif bp4.hit_on(d):
- pass
-
- d.cont()
-
- self.assertEqual(bp4.hit_count, bp2.hit_count // 2 + 1)
-
- self.d.kill()
-
- def test_bp_disable_reenable_hw(self):
- d = self.d
-
- d.run()
-
- bp1 = d.breakpoint("random_function")
- bp2 = d.breakpoint(0x40115B)
- bp4 = d.breakpoint(0x401162, hardware=True)
- bp3 = d.breakpoint(0x40116D)
-
- counter = 1
-
- d.cont()
-
- while True:
- if d.regs.rip == bp1.address:
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- elif d.regs.rip == bp2.address:
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- if bp4.enabled:
- bp4.disable()
- else:
- bp4.enable()
- counter += 1
- elif d.regs.rip == bp3.address:
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.rsi == 45)
- self.assertTrue(d.regs.esi == 45)
- self.assertTrue(d.regs.si == 45)
- self.assertTrue(d.regs.sil == 45)
- self.assertTrue(bp3.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- break
- elif bp4.hit_on(d):
- pass
-
- d.cont()
-
- self.assertEqual(bp4.hit_count, bp2.hit_count // 2 + 1)
-
- self.d.kill()
-
- def test_bps_running(self):
- d = self.d
-
- d.run()
-
- bp1 = d.breakpoint("random_function")
- bp2 = d.breakpoint(0x40115B)
- bp3 = d.breakpoint(0x40116D)
-
- counter = 1
-
- d.cont()
-
- while True:
- if d.running:
- pass
- if d.regs.rip == bp1.address:
- self.assertFalse(d.running)
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- elif d.regs.rip == bp2.address:
- self.assertFalse(d.running)
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- counter += 1
- elif d.regs.rip == bp3.address:
- self.assertFalse(d.running)
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.rsi == 45)
- self.assertTrue(d.regs.esi == 45)
- self.assertTrue(d.regs.si == 45)
- self.assertTrue(d.regs.sil == 45)
- self.assertTrue(bp3.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- break
-
- d.cont()
-
- self.assertEqual(bp2.hit_count, 10)
-
- self.d.kill()
-
- def test_bp_backing_file(self):
- d = debugger("binaries/executable_section_test")
-
- d.run()
-
- bp1 = d.breakpoint(0x1266, file="binary")
-
- d.cont()
-
- d.wait()
-
- if bp1.hit_on(d):
- for vmap in d.maps():
- if "x" in vmap.permissions and "anon" in vmap.backing_file:
- section = vmap.backing_file
- bp2 = d.breakpoint(0xD, file=section)
- d.cont()
-
- d.wait()
-
- if bp2.hit_on(d):
- self.assertEqual(d.memory[d.regs.rip], b"]")
- self.assertEqual(d.regs.rax, 9)
-
- d.kill()
-
- self.assertEqual(bp1.hit_count, 1)
- self.assertEqual(bp2.hit_count, 1)
-
- d.run()
-
- bp1 = d.breakpoint(0x1266, file="executable_section_test")
-
- d.cont()
-
- d.wait()
-
- if bp1.hit_on(d):
- for vmap in d.maps():
- if "x" in vmap.permissions and "anon" in vmap.backing_file:
- section = vmap.backing_file
- bp2 = d.breakpoint(0xD, file=section)
- d.cont()
-
- d.wait()
-
- if bp2.hit_on(d):
- self.assertEqual(d.memory[d.regs.rip], b"]")
- self.assertEqual(d.regs.rax, 9)
-
- d.run()
-
- bp1 = d.breakpoint(0x1266, file="hybrid")
-
- d.cont()
-
- d.wait()
-
- if bp1.hit_on(d):
- for vmap in d.maps():
- if "x" in vmap.permissions and "anon" in vmap.backing_file:
- section = vmap.backing_file
- bp2 = d.breakpoint(0xD, file=section)
- d.cont()
-
- d.wait()
-
- if bp2.hit_on(d):
- self.assertEqual(d.memory[d.regs.rip], b"]")
- self.assertEqual(d.regs.rax, 9)
-
- d.kill()
-
- self.assertEqual(bp1.hit_count, 1)
- self.assertEqual(bp2.hit_count, 1)
-
- d.run()
-
- with self.assertRaises(ValueError):
- d.breakpoint(0x1266, file="absolute")
-
- d.kill()
-
- def test_bp_disable_on_creation(self):
- d = debugger("binaries/breakpoint_test")
-
- d.run()
-
- bp1 = d.bp("random_function")
- bp2 = d.bp(0x40119c)
- bp1.disable()
-
- d.cont()
-
- assert not bp1.hit_on(d)
- assert bp2.hit_on(d)
-
- d.kill()
- d.terminate()
-
- def test_bp_disable_on_creation_2(self):
- d = debugger("binaries/breakpoint_test")
-
- d.run()
-
- bp = d.bp("random_function")
-
- bp.disable()
-
- d.cont()
- d.wait()
-
- # Validate we didn't segfault
- assert d.dead and d.exit_signal is None
-
- d.kill()
- d.terminate()
-
- def test_bp_disable_on_creation_hardware(self):
- d = debugger("binaries/breakpoint_test")
-
- d.run()
-
- bp1 = d.bp("random_function", hardware=True)
- bp2 = d.bp(0x40119c)
- bp1.disable()
-
- d.cont()
-
- assert not bp1.hit_on(d)
- assert bp2.hit_on(d)
-
- d.kill()
- d.terminate()
-
- def test_bp_disable_on_creation_2_hardware(self):
- d = debugger("binaries/breakpoint_test")
-
- d.run()
-
- bp = d.bp("random_function", hardware=True)
-
- bp.disable()
-
- d.cont()
- d.wait()
-
- # Validate we didn't segfault
- assert d.dead and d.exit_signal is None
-
- d.kill()
- d.terminate()
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/test/amd64/scripts/brute_test.py b/test/amd64/scripts/brute_test.py
deleted file mode 100644
index c92d91fc..00000000
--- a/test/amd64/scripts/brute_test.py
+++ /dev/null
@@ -1,54 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Gabriele Digregorio, Francesco Panebianco, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import string
-import unittest
-
-from libdebug import debugger
-
-
-class BruteTest(unittest.TestCase):
- def setUp(self):
- pass
-
- def test_bruteforce(self):
- flag = ""
- counter = 1
-
- d = debugger("binaries/brute_test")
-
- while not flag or flag != "BRUTINOBRUTONE":
- for c in string.printable:
- r = d.run()
- bp = d.breakpoint(0x1222, hardware=True)
- d.cont()
-
- r.sendlineafter(b"chars\n", (flag + c).encode())
-
-
- while bp.address == d.regs.rip:
- d.cont()
-
-
- if bp.hit_count > counter:
- flag += c
- counter = bp.hit_count
- d.kill()
- break
-
- message = r.recvline()
-
- d.kill()
-
- if message == b"Giusto!":
- flag += c
- break
-
- self.assertEqual(flag, "BRUTINOBRUTONE")
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/test/amd64/scripts/builtin_handler_test.py b/test/amd64/scripts/builtin_handler_test.py
deleted file mode 100644
index b00cb152..00000000
--- a/test/amd64/scripts/builtin_handler_test.py
+++ /dev/null
@@ -1,62 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-import string
-
-from libdebug import debugger
-
-
-class AntidebugEscapingTest(unittest.TestCase):
- def test_antidebug_escaping(self):
- d = debugger("binaries/antidebug_brute_test")
-
- # validate that without the handler the binary cannot be debugged
- r = d.run()
- d.cont()
- msg = r.recvline()
- self.assertEqual(msg, b"Debugger detected")
- d.kill()
-
- # validate that with the handler the binary can be debugged
- d = debugger("binaries/antidebug_brute_test", escape_antidebug=True)
- r = d.run()
- d.cont()
- msg = r.recvline()
- self.assertEqual(msg, b"Write up to 64 chars")
- d.interrupt()
- d.kill()
-
- # validate that the binary still works
- flag = ""
- counter = 1
-
- while not flag or flag != "BRUTE":
- for c in string.printable:
- r = d.run()
- bp = d.breakpoint(0x401209, hardware=True)
- d.cont()
-
- r.sendlineafter(b"chars\n", (flag + c).encode())
-
- while bp.address == d.regs.rip:
- d.cont()
-
- if bp.hit_count > counter:
- flag += c
- counter = bp.hit_count
- d.kill()
- break
-
- message = r.recvline()
-
- d.kill()
-
- if message == b"Giusto!":
- flag += c
- break
-
- self.assertEqual(flag, "BRUTE")
diff --git a/test/amd64/scripts/callback_test.py b/test/amd64/scripts/callback_test.py
deleted file mode 100644
index 0e716e2d..00000000
--- a/test/amd64/scripts/callback_test.py
+++ /dev/null
@@ -1,387 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Gabriele Digregorio, Francesco Panebianco, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import string
-import unittest
-
-from libdebug import debugger
-
-
-class CallbackTest(unittest.TestCase):
- def setUp(self):
- self.exceptions = []
-
- def test_callback_simple(self):
- self.exceptions.clear()
-
- global hit
- hit = False
-
- d = debugger("binaries/basic_test")
-
- d.run()
-
- def callback(thread, bp):
- global hit
-
- try:
- self.assertEqual(bp.hit_count, 1)
- self.assertTrue(bp.hit_on(thread))
- except Exception as e:
- self.exceptions.append(e)
-
- hit = True
-
- d.breakpoint("register_test", callback=callback)
-
- d.cont()
-
- d.kill()
-
- self.assertTrue(hit)
-
- if self.exceptions:
- raise self.exceptions[0]
-
- def test_callback_simple_hardware(self):
- self.exceptions.clear()
-
- global hit
- hit = False
-
- d = debugger("binaries/basic_test")
-
- d.run()
-
- def callback(thread, bp):
- global hit
-
- try:
- self.assertEqual(bp.hit_count, 1)
- self.assertTrue(bp.hit_on(thread))
- except Exception as e:
- self.exceptions.append(e)
-
- hit = True
-
- d.breakpoint("register_test", callback=callback, hardware=True)
-
- d.cont()
-
- d.kill()
-
- self.assertTrue(hit)
-
- if self.exceptions:
- raise self.exceptions[0]
-
- def test_callback_memory(self):
- self.exceptions.clear()
-
- global hit
- hit = False
-
- d = debugger("binaries/memory_test")
-
- d.run()
-
- def callback(thread, bp):
- global hit
-
- prev = bytes(range(256))
- try:
- self.assertEqual(bp.address, thread.regs.rip)
- self.assertEqual(bp.hit_count, 1)
- self.assertEqual(thread.memory[thread.regs.rdi, 256], prev)
-
- thread.memory[thread.regs.rdi + 128 :] = b"abcd123456"
- prev = prev[:128] + b"abcd123456" + prev[138:]
-
- self.assertEqual(thread.memory[thread.regs.rdi, 256], prev)
- except Exception as e:
- self.exceptions.append(e)
-
- hit = True
-
- d.breakpoint("change_memory", callback=callback)
-
- d.cont()
-
- d.kill()
-
- self.assertTrue(hit)
-
- if self.exceptions:
- raise self.exceptions[0]
-
- def test_callback_fast_memory(self):
- self.exceptions.clear()
-
- global hit
- hit = False
-
- d = debugger("binaries/memory_test", fast_memory=True)
-
- d.run()
-
- def callback(thread, bp):
- global hit
-
- prev = bytes(range(256))
- try:
- self.assertEqual(bp.address, thread.regs.rip)
- self.assertEqual(bp.hit_count, 1)
- self.assertEqual(thread.memory[thread.regs.rdi, 256], prev)
-
- thread.memory[thread.regs.rdi + 128 :] = b"abcd123456"
- prev = prev[:128] + b"abcd123456" + prev[138:]
-
- self.assertEqual(thread.memory[thread.regs.rdi, 256], prev)
- except Exception as e:
- self.exceptions.append(e)
-
- hit = True
-
- d.breakpoint("change_memory", callback=callback)
-
- d.cont()
-
- d.kill()
-
- self.assertTrue(hit)
-
- if self.exceptions:
- raise self.exceptions[0]
-
- def test_callback_bruteforce(self):
- global flag
- global counter
- global new_counter
-
- flag = ""
- counter = 1
- new_counter = 0
-
- def brute_force(d, b):
- global new_counter
- try:
- new_counter = b.hit_count
- except Exception as e:
- self.exceptions.append(e)
-
- d = debugger("binaries/brute_test")
- while True:
- end = False
- for c in string.printable:
- r = d.run()
-
- d.breakpoint(0x1222, callback=brute_force, hardware=True)
- d.cont()
-
- r.sendlineafter(b"chars\n", (flag + c).encode())
-
- message = r.recvline()
-
- if new_counter > counter:
- flag += c
- counter = new_counter
- d.kill()
- break
- d.kill()
- if message == b"Giusto!":
- flag += c
- end = True
- break
- if end:
- break
-
- self.assertEqual(flag, "BRUTINOBRUTONE")
-
- if self.exceptions:
- raise self.exceptions[0]
-
- def test_callback_jumpout(self):
- global flag
- global first
- global second
-
- flag = ""
- first = 0x55
-
- def second(d, b):
- global second
- try:
- second = d.regs.r9
- except Exception as e:
- self.exceptions.append(e)
-
- def third(d, b):
- global flag
- try:
- address = d.regs.r13 + d.regs.rbx
- third = int.from_bytes(d.memory[address : address + 1], "little")
- flag += chr((first ^ second ^ third ^ (b.hit_count - 1)))
- except Exception as e:
- self.exceptions.append(e)
-
- d = debugger("CTF/jumpout")
- r = d.run()
-
- d.breakpoint(0x140B, callback=second, hardware=True)
- d.breakpoint(0x157C, callback=third, hardware=True)
- d.cont()
-
- r.sendline(b"A" * 0x1D)
- r.recvuntil(b"Wrong...")
-
- d.kill()
-
- self.assertEqual(flag, "SECCON{jump_table_everywhere}")
-
- if self.exceptions:
- raise self.exceptions[0]
-
- def test_callback_intermixing(self):
- global secval
-
- flag = ""
- first = 0x55
-
- d = debugger("CTF/jumpout")
- r = d.run()
-
- def second(d, b):
- global secval
- try:
- secval = d.regs.r9
- except Exception as e:
- self.exceptions.append(e)
-
- d.breakpoint(0x140B, callback=second, hardware=True)
- bp = d.breakpoint(0x157C, hardware=True)
-
- d.cont()
-
- r.sendline(b"A" * 0x1D)
-
- while True:
- if d.regs.rip == bp.address:
- address = d.regs.r13 + d.regs.rbx
- third = int.from_bytes(d.memory[address : address + 1], "little")
- flag += chr((first ^ secval ^ third ^ (bp.hit_count - 1)))
-
- d.cont()
-
- if flag.endswith("}"):
- break
-
- r.recvuntil(b"Wrong...")
-
- d.kill()
-
- self.assertEqual(flag, "SECCON{jump_table_everywhere}")
-
- if self.exceptions:
- raise self.exceptions[0]
-
- def test_callback_exception(self):
- self.exceptions.clear()
-
- d = debugger("binaries/basic_test")
-
- d.run()
-
- def callback(thread, bp):
- # This operation should not raise any exception
- _ = d.regs.rax
-
- d.breakpoint("register_test", callback=callback, hardware=True)
-
- d.cont()
-
- d.kill()
-
- def test_callback_step(self):
- self.exceptions.clear()
-
- d = debugger("binaries/basic_test")
-
- d.run()
-
- def callback(t, bp):
- self.assertEqual(t.regs.rip, bp.address)
- d.step()
- self.assertEqual(t.regs.rip, bp.address + 1)
-
- d.breakpoint("register_test", callback=callback)
-
- d.cont()
-
- d.kill()
-
- def test_callback_pid_accessible(self):
- self.exceptions.clear()
-
- d = debugger("binaries/basic_test")
-
- d.run()
-
- hit = False
-
- def callback(t, bp):
- nonlocal hit
- self.assertEqual(t.process_id, d.process_id)
- hit = True
-
- d.breakpoint("register_test", callback=callback)
-
- d.cont()
- d.kill()
-
- self.assertTrue(hit)
-
- def test_callback_pid_accessible_alias(self):
- self.exceptions.clear()
-
- d = debugger("binaries/basic_test")
-
- d.run()
-
- hit = False
-
- def callback(t, bp):
- nonlocal hit
- self.assertEqual(t.pid, d.pid)
- self.assertEqual(t.pid, t.process_id)
- hit = True
-
- d.breakpoint("register_test", callback=callback)
-
- d.cont()
- d.kill()
-
- self.assertTrue(hit)
-
- def test_callback_tid_accessible_alias(self):
- self.exceptions.clear()
-
- d = debugger("binaries/basic_test")
-
- d.run()
-
- hit = False
-
- def callback(t, bp):
- nonlocal hit
- self.assertEqual(t.tid, t.thread_id)
- hit = True
-
- d.breakpoint("register_test", callback=callback)
-
- d.cont()
- d.kill()
-
- self.assertTrue(hit)
diff --git a/test/amd64/scripts/deep_dive_division_test.py b/test/amd64/scripts/deep_dive_division_test.py
deleted file mode 100644
index 96795c5b..00000000
--- a/test/amd64/scripts/deep_dive_division_test.py
+++ /dev/null
@@ -1,67 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-#
-# deep-dive-division - challenge from KalmarCTF 2024
-#
-
-import string
-import unittest
-
-from libdebug import debugger
-
-
-class DeepDiveDivision(unittest.TestCase):
- def test_deep_dive_division(self):
- def brutone(flag, current):
- def checkino(d, b):
- nonlocal counter
- if int.from_bytes(d.memory[d.regs.rax + d.regs.r9, 1], "little") == 0:
- counter += 1
-
- candidate = []
- for c in string.printable:
- counter = 0
- r = d.run()
- d.breakpoint(0x4012F2, hardware=True, callback=checkino)
- d.cont()
- r.sendlineafter(b"flag?", flag + c.encode())
- r.recvline(2)
-
- d.kill()
- if counter > current:
- candidate.append(c)
- return candidate
-
- d = debugger("CTF/deep-dive-division")
- candidate = {}
-
- flag = b""
- current = 6
-
- candidate = brutone(flag, current)
- while True:
- if len(candidate) == 0:
- break
- elif len(candidate) == 1:
- current += 1
- flag += candidate[0].encode()
- candidate = brutone(flag, current)
- else:
- current += 1
-
- for c in candidate:
- flag_ = flag + c.encode()
- candidate = brutone(flag_, current)
- if candidate != []:
- flag = flag_
- break
-
- self.assertEqual(flag, b"kalmar{vm_in_3d_space!_cb3992b605aafe137}\n")
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/test/amd64/scripts/finish_test.py b/test/amd64/scripts/finish_test.py
deleted file mode 100644
index 1c6337d4..00000000
--- a/test/amd64/scripts/finish_test.py
+++ /dev/null
@@ -1,325 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Francesco Panebianco, Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-import unittest
-
-from libdebug import debugger
-from libdebug.architectures.stack_unwinding_provider import stack_unwinding_provider
-
-# Addresses of the dummy functions
-C_ADDRESS = 0x4011e3
-B_ADDRESS = 0x4011d2
-A_ADDRESS = 0x401146
-
-# Addresses of noteworthy instructions
-RETURN_POINT_FROM_C = 0x401202
-RETURN_POINT_FROM_A = 0x4011e0
-
-class FinishTest(unittest.TestCase):
- def setUp(self):
- pass
-
- def test_finish_exact_no_auto_interrupt_no_breakpoint(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
-
- # ------------------ Block 1 ------------------ #
- # Return from the first function call #
- # --------------------------------------------- #
-
- # Reach function c
- d.run()
- d.breakpoint(C_ADDRESS)
- d.cont()
-
- self.assertEqual(d.regs.rip, C_ADDRESS)
-
- # Finish function c
- d.finish(heuristic="step-mode")
-
- self.assertEqual(d.regs.rip, RETURN_POINT_FROM_C)
-
- d.kill()
-
- # ------------------ Block 2 ------------------ #
- # Return from the nested function call #
- # --------------------------------------------- #
-
- # Reach function a
- d.run()
- d.breakpoint(A_ADDRESS)
- d.cont()
-
- self.assertEqual(d.regs.rip, A_ADDRESS)
-
- # Finish function a
- d.finish(heuristic="step-mode")
-
- self.assertEqual(d.regs.rip, RETURN_POINT_FROM_A)
-
- d.kill()
-
- def test_finish_heuristic_no_auto_interrupt_no_breakpoint(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
-
- # ------------------ Block 1 ------------------ #
- # Return from the first function call #
- # --------------------------------------------- #
-
- # Reach function c
- d.run()
- d.breakpoint(C_ADDRESS)
- d.cont()
-
- self.assertEqual(d.regs.rip, C_ADDRESS)
-
- # Finish function c
- d.finish(heuristic="backtrace")
-
- self.assertEqual(d.regs.rip, RETURN_POINT_FROM_C)
-
- d.kill()
-
- # ------------------ Block 2 ------------------ #
- # Return from the nested function call #
- # --------------------------------------------- #
-
- # Reach function a
- d.run()
- d.breakpoint(A_ADDRESS)
- d.cont()
-
- self.assertEqual(d.regs.rip, A_ADDRESS)
-
- # Finish function a
- d.finish(heuristic="backtrace")
-
- self.assertEqual(d.regs.rip, RETURN_POINT_FROM_A)
-
- d.kill()
-
- def test_finish_exact_auto_interrupt_no_breakpoint(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=True)
-
- # ------------------ Block 1 ------------------ #
- # Return from the first function call #
- # --------------------------------------------- #
-
- # Reach function c
- d.run()
- d.breakpoint(C_ADDRESS)
- d.cont()
- d.wait()
-
- self.assertEqual(d.regs.rip, C_ADDRESS)
-
- # Finish function c
- d.finish(heuristic="step-mode")
-
- self.assertEqual(d.regs.rip, RETURN_POINT_FROM_C)
-
- d.kill()
-
- # ------------------ Block 2 ------------------ #
- # Return from the nested function call #
- # --------------------------------------------- #
-
- # Reach function a
- d.run()
- d.breakpoint(A_ADDRESS)
- d.cont()
- d.wait()
-
- self.assertEqual(d.regs.rip, A_ADDRESS)
-
- # Finish function a
- d.finish(heuristic="step-mode")
-
- self.assertEqual(d.regs.rip, RETURN_POINT_FROM_A)
-
- d.kill()
-
- def test_finish_heuristic_auto_interrupt_no_breakpoint(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=True)
-
- # ------------------ Block 1 ------------------ #
- # Return from the first function call #
- # --------------------------------------------- #
-
- # Reach function c
- d.run()
- d.breakpoint(C_ADDRESS)
- d.cont()
- d.wait()
-
- self.assertEqual(d.regs.rip, C_ADDRESS)
-
- # Finish function c
- d.finish(heuristic="backtrace")
-
- self.assertEqual(d.regs.rip, RETURN_POINT_FROM_C)
-
- d.kill()
-
- # ------------------ Block 2 ------------------ #
- # Return from the nested function call #
- # --------------------------------------------- #
-
- # Reach function a
- d.run()
- d.breakpoint(A_ADDRESS)
- d.cont()
- d.wait()
-
- self.assertEqual(d.regs.rip, A_ADDRESS)
-
- # Finish function a
- d.finish(heuristic="backtrace")
-
- self.assertEqual(d.regs.rip, RETURN_POINT_FROM_A)
-
- d.kill()
-
- def test_finish_exact_no_auto_interrupt_breakpoint(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
-
- # Reach function c
- d.run()
- d.breakpoint(C_ADDRESS)
- d.cont()
-
- self.assertEqual(d.regs.rip, C_ADDRESS)
-
- d.breakpoint(A_ADDRESS)
-
- # Finish function c
- d.finish(heuristic="step-mode")
-
- self.assertEqual(d.regs.rip, A_ADDRESS, f"Expected {hex(A_ADDRESS)} but got {hex(d.regs.rip)}")
-
- d.kill()
-
- def test_finish_heuristic_no_auto_interrupt_breakpoint(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
-
- # Reach function c
- d.run()
- d.breakpoint(C_ADDRESS)
- d.cont()
-
- self.assertEqual(d.regs.rip, C_ADDRESS)
-
- d.breakpoint(A_ADDRESS)
-
- # Finish function c
- d.finish(heuristic="backtrace")
-
- self.assertEqual(d.regs.rip, A_ADDRESS)
-
- d.kill()
-
- def test_heuristic_return_address(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
-
- # Reach function c
- d.run()
- d.breakpoint(C_ADDRESS)
- d.cont()
-
- self.assertEqual(d.regs.rip, C_ADDRESS)
-
- stack_unwinder = stack_unwinding_provider(d._internal_debugger.arch)
-
- # We need to repeat the check for the three stages of the function preamble
-
- # Get current return address
- curr_srip = d.saved_ip
- self.assertEqual(curr_srip, RETURN_POINT_FROM_C)
-
- d.step()
-
- # Get current return address
- curr_srip = d.saved_ip
- self.assertEqual(curr_srip, RETURN_POINT_FROM_C)
-
- d.step()
-
- # Get current return address
- curr_srip = d.saved_ip
- self.assertEqual(curr_srip, RETURN_POINT_FROM_C)
-
- d.kill()
-
- def test_exact_breakpoint_return(self):
- BREAKPOINT_LOCATION = 0x4011f1
-
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
-
- # Reach function c
- d.run()
- d.breakpoint(C_ADDRESS)
- d.cont()
-
- self.assertEqual(d.regs.rip, C_ADDRESS)
-
-
- # Place a breakpoint at a location inbetween
- d.breakpoint(BREAKPOINT_LOCATION)
-
- # Finish function c
- d.finish(heuristic="step-mode")
-
- self.assertEqual(d.regs.rip, BREAKPOINT_LOCATION)
-
- d.kill()
-
- def test_heuristic_breakpoint_return(self):
- BREAKPOINT_LOCATION = 0x4011f1
-
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
-
- # Reach function c
- d.run()
- d.breakpoint(C_ADDRESS)
- d.cont()
-
- self.assertEqual(d.regs.rip, C_ADDRESS)
-
-
- # Place a breakpoint a location in between
- d.breakpoint(BREAKPOINT_LOCATION)
-
- # Finish function c
- d.finish(heuristic="backtrace")
-
- self.assertEqual(d.regs.rip, BREAKPOINT_LOCATION)
-
- d.kill()
-
- def test_breakpoint_collision(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
-
- # Reach function c
- d.run()
- d.breakpoint(C_ADDRESS)
- d.cont()
-
- self.assertEqual(d.regs.rip, C_ADDRESS)
-
- # Place a breakpoint at the same location as the return address
- d.breakpoint(RETURN_POINT_FROM_C)
-
- # Finish function c
- d.finish(heuristic="backtrace")
-
- self.assertEqual(d.regs.rip, RETURN_POINT_FROM_C)
- self.assertFalse(d.running)
-
- d.step()
-
- # Check that the execution is still running and nothing has broken
- self.assertFalse(d.running)
- self.assertFalse(d.dead)
-
- d.kill()
diff --git a/test/amd64/scripts/floating_point_test.py b/test/amd64/scripts/floating_point_test.py
deleted file mode 100644
index 733c725e..00000000
--- a/test/amd64/scripts/floating_point_test.py
+++ /dev/null
@@ -1,285 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-from pathlib import Path
-from random import randint
-
-from libdebug import debugger
-
-
-class FloatingPointTest(unittest.TestCase):
- def test_floating_point_reg_access(self):
- # This test is divided into two parts, depending on the current hardware
-
- # Let's check if we have AVX512
- with Path("/proc/cpuinfo").open() as f:
- cpuinfo = f.read()
-
- if "avx512" in cpuinfo:
- # Run an AVX512 test
- self.avx512()
- self.avx()
- self.mmx()
- elif "avx" in cpuinfo:
- # Run an AVX test
- self.avx()
- self.mmx()
- else:
- # Run a generic test
- self.mmx()
-
- def avx512(self):
- d = debugger("binaries/floating_point_2696_test")
-
- d.run()
-
- bp1 = d.bp(0x40143E)
- bp2 = d.bp(0x401467)
-
- d.cont()
-
- self.assertTrue(bp1.hit_on(d))
-
- self.assertTrue(hasattr(d.regs, "xmm0"))
- self.assertTrue(hasattr(d.regs, "xmm31"))
- self.assertTrue(hasattr(d.regs, "ymm0"))
- self.assertTrue(hasattr(d.regs, "ymm31"))
- self.assertTrue(hasattr(d.regs, "zmm0"))
- self.assertTrue(hasattr(d.regs, "zmm31"))
-
- baseval = int.from_bytes(bytes(list(range(64))), "little")
-
- for i in range(32):
- self.assertEqual(getattr(d.regs, f"xmm{i}"), baseval & ((1 << 128) - 1))
- self.assertEqual(getattr(d.regs, f"ymm{i}"), baseval & ((1 << 256) - 1))
- self.assertEqual(getattr(d.regs, f"zmm{i}"), baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 504)
-
- d.regs.zmm0 = 0xDEADBEEFDEADBEEF
-
- d.cont()
-
- self.assertTrue(bp2.hit_on(d))
-
- for i in range(32):
- val = randint(0, 2**512 - 1)
- setattr(d.regs, f"zmm{i}", val)
- self.assertEqual(getattr(d.regs, f"zmm{i}"), val)
-
- d.kill()
-
- def avx(self):
- d = debugger("binaries/floating_point_896_test")
-
- d.run()
-
- bp1 = d.bp(0x40159E)
- bp2 = d.bp(0x4015C5)
-
- d.cont()
-
- self.assertTrue(bp1.hit_on(d))
-
- self.assertTrue(hasattr(d.regs, "xmm0"))
- self.assertTrue(hasattr(d.regs, "ymm0"))
- self.assertTrue(hasattr(d.regs, "xmm15"))
- self.assertTrue(hasattr(d.regs, "ymm15"))
-
- baseval = int.from_bytes(bytes(list(range(0, 256, 17)) + list(range(16))), "little")
-
- self.assertEqual(d.regs.ymm0, baseval)
- self.assertEqual(d.regs.xmm0, baseval & ((1 << 128) - 1))
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
- self.assertEqual(d.regs.ymm1, baseval)
- self.assertEqual(d.regs.xmm1, baseval & ((1 << 128) - 1))
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
- self.assertEqual(d.regs.ymm2, baseval)
- self.assertEqual(d.regs.xmm2, baseval & ((1 << 128) - 1))
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
- self.assertEqual(d.regs.ymm3, baseval)
- self.assertEqual(d.regs.xmm3, baseval & ((1 << 128) - 1))
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
- self.assertEqual(d.regs.ymm4, baseval)
- self.assertEqual(d.regs.xmm4, baseval & ((1 << 128) - 1))
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
- self.assertEqual(d.regs.ymm5, baseval)
- self.assertEqual(d.regs.xmm5, baseval & ((1 << 128) - 1))
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
- self.assertEqual(d.regs.ymm6, baseval)
- self.assertEqual(d.regs.xmm6, baseval & ((1 << 128) - 1))
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
- self.assertEqual(d.regs.ymm7, baseval)
- self.assertEqual(d.regs.xmm7, baseval & ((1 << 128) - 1))
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
- self.assertEqual(d.regs.ymm8, baseval)
- self.assertEqual(d.regs.xmm8, baseval & ((1 << 128) - 1))
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
- self.assertEqual(d.regs.ymm9, baseval)
- self.assertEqual(d.regs.xmm9, baseval & ((1 << 128) - 1))
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
- self.assertEqual(d.regs.ymm10, baseval)
- self.assertEqual(d.regs.xmm10, baseval & ((1 << 128) - 1))
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
- self.assertEqual(d.regs.ymm11, baseval)
- self.assertEqual(d.regs.xmm11, baseval & ((1 << 128) - 1))
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
- self.assertEqual(d.regs.ymm12, baseval)
- self.assertEqual(d.regs.xmm12, baseval & ((1 << 128) - 1))
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
- self.assertEqual(d.regs.ymm13, baseval)
- self.assertEqual(d.regs.xmm13, baseval & ((1 << 128) - 1))
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
- self.assertEqual(d.regs.ymm14, baseval)
- self.assertEqual(d.regs.xmm14, baseval & ((1 << 128) - 1))
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
- self.assertEqual(d.regs.ymm15, baseval)
- self.assertEqual(d.regs.xmm15, baseval & ((1 << 128) - 1))
-
- d.regs.ymm0 = 0xDEADBEEFDEADBEEF
-
- d.cont()
-
- self.assertTrue(bp2.hit_on(d))
-
- for i in range(16):
- val = randint(0, 2**256 - 1)
- setattr(d.regs, f"ymm{i}", val)
- self.assertEqual(getattr(d.regs, f"xmm{i}"), val & ((1 << 128) - 1))
- self.assertEqual(getattr(d.regs, f"ymm{i}"), val)
-
- # validate that register states are correctly flushed and then restored
- values = []
-
- for i in range(16):
- val = randint(0, 2**256 - 1)
- setattr(d.regs, f"ymm{i}", val)
- values.append(val)
-
- d.step()
-
- for i in range(16):
- self.assertEqual(getattr(d.regs, f"ymm{i}"), values[i])
-
- d.regs.ymm7 = 0xDEADBEEFDEADBEEF
-
- for i in range(16):
- if i == 7:
- continue
-
- self.assertEqual(getattr(d.regs, f"ymm{i}"), values[i])
-
- d.step()
-
- for i in range(16):
- if i == 7:
- continue
-
- self.assertEqual(getattr(d.regs, f"ymm{i}"), values[i])
-
- self.assertEqual(d.regs.ymm7, 0xDEADBEEFDEADBEEF)
-
- d.kill()
-
- def callback(t, _):
- baseval = int.from_bytes(bytes(list(range(0, 256, 17)) + list(range(16))), "little")
- for i in range(16):
- self.assertEqual(getattr(d.regs, f"xmm{i}"), baseval & ((1 << 128) - 1))
- self.assertEqual(getattr(d.regs, f"ymm{i}"), baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 248)
-
- t.regs.ymm0 = 0xDEADBEEFDEADBEEF
-
- d.run()
-
- d.bp(0x40159E, callback=callback)
- bp = d.bp(0x4015C5)
-
- d.cont()
-
- self.assertTrue(bp.hit_on(d))
-
- d.kill()
-
- def mmx(self):
- d = debugger("binaries/floating_point_512_test")
-
- d.run()
-
- bp1 = d.bp(0x401372)
- bp2 = d.bp(0x401399)
-
- d.cont()
-
- self.assertTrue(bp1.hit_on(d))
-
- self.assertTrue(hasattr(d.regs, "xmm0"))
- self.assertTrue(hasattr(d.regs, "xmm15"))
-
- baseval = int.from_bytes(bytes(list(range(0, 256, 17))), "little")
- self.assertEqual(d.regs.xmm0, baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
- self.assertEqual(d.regs.xmm1, baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
- self.assertEqual(d.regs.xmm2, baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
- self.assertEqual(d.regs.xmm3, baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
- self.assertEqual(d.regs.xmm4, baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
- self.assertEqual(d.regs.xmm5, baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
- self.assertEqual(d.regs.xmm6, baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
- self.assertEqual(d.regs.xmm7, baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
- self.assertEqual(d.regs.xmm8, baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
- self.assertEqual(d.regs.xmm9, baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
- self.assertEqual(d.regs.xmm10, baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
- self.assertEqual(d.regs.xmm11, baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
- self.assertEqual(d.regs.xmm12, baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
- self.assertEqual(d.regs.xmm13, baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
- self.assertEqual(d.regs.xmm14, baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
- self.assertEqual(d.regs.xmm15, baseval)
-
- d.regs.xmm0 = 0xDEADBEEFDEADBEEF
-
- d.cont()
-
- self.assertTrue(bp2.hit_on(d))
-
- for i in range(16):
- val = randint(0, 2**128 - 1)
- setattr(d.regs, f"xmm{i}", val)
- self.assertEqual(getattr(d.regs, f"xmm{i}"), val)
-
- d.kill()
-
- def callback(t, _):
- baseval = int.from_bytes(bytes(list(range(0, 256, 17))), "little")
- for i in range(16):
- self.assertEqual(getattr(d.regs, f"xmm{i}"), baseval)
- baseval = (baseval >> 8) + ((baseval & 255) << 120)
-
- t.regs.xmm0 = 0xDEADBEEFDEADBEEF
-
- d.run()
-
- d.bp(0x401372, callback=callback)
- bp = d.bp(0x401399)
-
- d.cont()
-
- self.assertTrue(bp.hit_on(d))
-
- d.kill()
diff --git a/test/amd64/scripts/handle_syscall_test.py b/test/amd64/scripts/handle_syscall_test.py
deleted file mode 100644
index 73a53857..00000000
--- a/test/amd64/scripts/handle_syscall_test.py
+++ /dev/null
@@ -1,543 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import io
-import logging
-import os
-import sys
-import unittest
-
-from libdebug import debugger
-
-
-class HandleSyscallTest(unittest.TestCase):
- def setUp(self):
- # Redirect stdout
- self.capturedOutput = io.StringIO()
- sys.stdout = self.capturedOutput
- sys.stderr = self.capturedOutput
-
- self.log_capture_string = io.StringIO()
- self.log_handler = logging.StreamHandler(self.log_capture_string)
- self.log_handler.setLevel(logging.WARNING)
-
- self.logger = logging.getLogger("libdebug")
- self.original_handlers = self.logger.handlers
- self.logger.handlers = []
- self.logger.addHandler(self.log_handler)
- self.logger.setLevel(logging.WARNING)
-
- def tearDown(self):
- sys.stdout = sys.__stdout__
- sys.stderr = sys.__stderr__
-
- self.logger.removeHandler(self.log_handler)
- self.logger.handlers = self.original_handlers
- self.log_handler.close()
-
- def test_handles(self):
- d = debugger("binaries/handle_syscall_test")
-
- r = d.run()
-
- ptr = 0
- write_count = 0
-
- def on_enter_write(d, sh):
- nonlocal write_count
-
- if write_count == 0:
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
- self.assertEqual(d.syscall_arg0, 1)
- write_count += 1
- else:
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
- self.assertEqual(d.syscall_arg0, 1)
- write_count += 1
-
- def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 9)
-
- nonlocal ptr
-
- ptr = d.regs.rax
-
- def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.syscall_arg0, ptr)
-
- def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.memory[d.syscall_arg0, 8], os.getcwd()[:8].encode())
-
- handler1 = d.handle_syscall("write", on_enter_write, None)
- handler2 = d.handle_syscall("mmap", None, on_exit_mmap)
- handler3 = d.handle_syscall("getcwd", on_enter_getcwd, on_exit_getcwd)
-
- r.sendline(b"provola")
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(write_count, 2)
- self.assertEqual(handler1.hit_count, 2)
- self.assertEqual(handler2.hit_count, 1)
- self.assertEqual(handler3.hit_count, 1)
-
- def test_handles_with_pprint(self):
- d = debugger("binaries/handle_syscall_test")
-
- r = d.run()
-
- d.pprint_syscalls = True
-
- ptr = 0
- write_count = 0
-
- def on_enter_write(d, sh):
- nonlocal write_count
-
- if write_count == 0:
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
- self.assertEqual(d.syscall_arg0, 1)
- write_count += 1
- else:
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
- self.assertEqual(d.syscall_arg0, 1)
- write_count += 1
-
- def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 9)
-
- nonlocal ptr
-
- ptr = d.regs.rax
-
- def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.syscall_arg0, ptr)
-
- def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.memory[d.syscall_arg0, 8], os.getcwd()[:8].encode())
-
- handler1 = d.handle_syscall("write", on_enter_write, None)
- handler2 = d.handle_syscall("mmap", None, on_exit_mmap)
- handler3 = d.handle_syscall("getcwd", on_enter_getcwd, on_exit_getcwd)
-
- r.sendline(b"provola")
-
- d.cont()
- d.wait()
-
- d.kill()
-
- self.assertEqual(write_count, 2)
- self.assertEqual(handler1.hit_count, 2)
- self.assertEqual(handler2.hit_count, 1)
- self.assertEqual(handler3.hit_count, 1)
-
- def test_handle_disabling(self):
- d = debugger("binaries/handle_syscall_test")
-
- r = d.run()
-
- ptr = 0
- write_count = 0
-
- def on_enter_write(d, sh):
- nonlocal write_count
-
- if write_count == 0:
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
- self.assertEqual(d.syscall_arg0, 1)
- write_count += 1
- else:
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
- self.assertEqual(d.syscall_arg0, 1)
- write_count += 1
-
- def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 9)
-
- nonlocal ptr
-
- ptr = d.regs.rax
-
- def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.syscall_arg0, ptr)
-
- def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.memory[d.syscall_arg0, 8], os.getcwd()[:8].encode())
-
- handler1 = d.handle_syscall(1, on_enter_write, None)
- handler2 = d.handle_syscall(9, None, on_exit_mmap)
- handler3 = d.handle_syscall(0x4F, on_enter_getcwd, on_exit_getcwd)
-
- r.sendline(b"provola")
-
- d.breakpoint(0x401196)
-
- d.cont()
-
- d.wait()
-
- self.assertEqual(d.regs.rip, 0x401196)
- handler1.disable()
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(write_count, 1)
- self.assertEqual(handler1.hit_count, 1)
- self.assertEqual(handler2.hit_count, 1)
- self.assertEqual(handler3.hit_count, 1)
-
- def test_handle_disabling_with_pprint(self):
- d = debugger("binaries/handle_syscall_test")
-
- r = d.run()
-
- d.pprint_syscalls = True
-
- ptr = 0
- write_count = 0
-
- def on_enter_write(d, sh):
- nonlocal write_count
-
- if write_count == 0:
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
- self.assertEqual(d.syscall_arg0, 1)
- write_count += 1
- else:
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
- self.assertEqual(d.syscall_arg0, 1)
- write_count += 1
-
- def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 9)
-
- nonlocal ptr
-
- ptr = d.regs.rax
-
- def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.syscall_arg0, ptr)
-
- def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.memory[d.syscall_arg0, 8], os.getcwd()[:8].encode())
-
- handler1 = d.handle_syscall(1, on_enter_write, None)
- handler2 = d.handle_syscall(9, None, on_exit_mmap)
- handler3 = d.handle_syscall(0x4F, on_enter_getcwd, on_exit_getcwd)
-
- r.sendline(b"provola")
-
- d.breakpoint(0x401196)
-
- d.cont()
-
- d.wait()
-
- self.assertEqual(d.regs.rip, 0x401196)
- handler1.disable()
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(write_count, 1)
- self.assertEqual(handler1.hit_count, 1)
- self.assertEqual(handler2.hit_count, 1)
- self.assertEqual(handler3.hit_count, 1)
-
- def test_handle_overwrite(self):
- d = debugger("binaries/handle_syscall_test")
-
- r = d.run()
-
- ptr = 0
- write_count_first = 0
- write_count_second = 0
-
- def on_enter_write_first(d, sh):
- nonlocal write_count_first
-
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
- self.assertEqual(d.syscall_arg0, 1)
- write_count_first += 1
-
- def on_enter_write_second(d, sh):
- nonlocal write_count_second
-
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
- self.assertEqual(d.syscall_arg0, 1)
- write_count_second += 1
-
- def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 9)
-
- nonlocal ptr
-
- ptr = d.regs.rax
-
- def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.syscall_arg0, ptr)
-
- def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.memory[d.syscall_arg0, 8], os.getcwd()[:8].encode())
-
- handler1_1 = d.handle_syscall(1, on_enter_write_first, None)
- handler2 = d.handle_syscall(9, None, on_exit_mmap)
- handler3 = d.handle_syscall(0x4F, on_enter_getcwd, on_exit_getcwd)
-
- r.sendline(b"provola")
-
- d.breakpoint(0x401196)
-
- d.cont()
-
- d.wait()
-
- self.assertEqual(d.regs.rip, 0x401196)
- handler1_2 = d.handle_syscall(1, on_enter_write_second, None)
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(write_count_first, 1)
- self.assertEqual(write_count_second, 1)
- self.assertEqual(handler1_1.hit_count, 2)
- self.assertEqual(handler1_2.hit_count, 2)
- self.assertEqual(handler2.hit_count, 1)
- self.assertEqual(handler3.hit_count, 1)
-
- self.assertIn("WARNING", self.log_capture_string.getvalue())
- self.assertIn(
- "Syscall write is already handled by a user-defined handler. Overriding it.",
- self.log_capture_string.getvalue(),
- )
-
- def test_handle_overwrite_with_pprint(self):
- d = debugger("binaries/handle_syscall_test")
-
- r = d.run()
-
- d.pprint_syscalls = True
-
- ptr = 0
- write_count_first = 0
- write_count_second = 0
-
- def on_enter_write_first(d, sh):
- nonlocal write_count_first
-
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
- self.assertEqual(d.syscall_arg0, 1)
- write_count_first += 1
-
- def on_enter_write_second(d, sh):
- nonlocal write_count_second
-
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
- self.assertEqual(d.syscall_arg0, 1)
- write_count_second += 1
-
- def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 9)
-
- nonlocal ptr
-
- ptr = d.regs.rax
-
- def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.syscall_arg0, ptr)
-
- def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.memory[d.syscall_arg0, 8], os.getcwd()[:8].encode())
-
- handler1_1 = d.handle_syscall(1, on_enter_write_first, None)
- handler2 = d.handle_syscall(9, None, on_exit_mmap)
- handler3 = d.handle_syscall(0x4F, on_enter_getcwd, on_exit_getcwd)
-
- r.sendline(b"provola")
-
- d.breakpoint(0x401196)
-
- d.cont()
-
- d.wait()
-
- self.assertEqual(d.regs.rip, 0x401196)
- handler1_2 = d.handle_syscall(1, on_enter_write_second, None)
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(write_count_first, 1)
- self.assertEqual(write_count_second, 1)
- self.assertEqual(handler1_1.hit_count, 2)
- self.assertEqual(handler1_2.hit_count, 2)
- self.assertEqual(handler2.hit_count, 1)
- self.assertEqual(handler3.hit_count, 1)
-
- self.assertIn("WARNING", self.log_capture_string.getvalue())
- self.assertIn(
- "Syscall write is already handled by a user-defined handler. Overriding it.",
- self.log_capture_string.getvalue(),
- )
-
-
- def test_handles_sync(self):
- d = debugger("binaries/handle_syscall_test")
-
- r = d.run()
-
- ptr = 0
- write_count = 0
-
- def on_enter_write(d, sh):
- nonlocal write_count
-
- if write_count == 0:
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
- self.assertEqual(d.syscall_arg0, 1)
- write_count += 1
- else:
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
- self.assertEqual(d.syscall_arg0, 1)
- write_count += 1
-
- def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 9)
-
- nonlocal ptr
-
- ptr = d.regs.rax
-
- def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.syscall_arg0, ptr)
-
- def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.memory[d.syscall_arg0, 8], os.getcwd()[:8].encode())
-
- handler1 = d.handle_syscall("write")
- handler2 = d.handle_syscall("mmap")
- handler3 = d.handle_syscall("getcwd")
-
- r.sendline(b"provola")
-
- while not d.dead:
- d.cont()
- d.wait()
- if handler1.hit_on_enter(d):
- on_enter_write(d, handler1)
- elif handler2.hit_on_exit(d):
- on_exit_mmap(d, handler2)
- elif handler3.hit_on_enter(d):
- on_enter_getcwd(d, handler3)
- elif handler3.hit_on_exit(d):
- on_exit_getcwd(d, handler3)
-
- d.kill()
-
- self.assertEqual(write_count, 2)
- self.assertEqual(handler1.hit_count, 2)
- self.assertEqual(handler2.hit_count, 1)
- self.assertEqual(handler3.hit_count, 1)
-
- def test_handles_sync_with_pprint(self):
- d = debugger("binaries/handle_syscall_test")
-
- r = d.run()
-
- ptr = 0
- write_count = 0
-
- def on_enter_write(d, sh):
- nonlocal write_count
-
- if write_count == 0:
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
- self.assertEqual(d.syscall_arg0, 1)
- write_count += 1
- else:
- self.assertTrue(sh.syscall_number == 1)
- self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
- self.assertEqual(d.syscall_arg0, 1)
- write_count += 1
-
- def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 9)
-
- nonlocal ptr
-
- ptr = d.regs.rax
-
- def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.syscall_arg0, ptr)
-
- def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 0x4F)
- self.assertEqual(d.memory[d.syscall_arg0, 8], os.getcwd()[:8].encode())
-
- handler1 = d.handle_syscall("write")
- handler2 = d.handle_syscall("mmap")
- handler3 = d.handle_syscall("getcwd")
-
- d.pprint_syscalls = True
-
- r.sendline(b"provola")
-
- while not d.dead:
- d.cont()
- d.wait()
- if handler1.hit_on_enter(d):
- on_enter_write(d, handler1)
- elif handler2.hit_on_exit(d):
- on_exit_mmap(d, handler2)
- elif handler3.hit_on_enter(d):
- on_enter_getcwd(d, handler3)
- elif handler3.hit_on_exit(d):
- on_exit_getcwd(d, handler3)
-
- d.kill()
-
- self.assertEqual(write_count, 2)
- self.assertEqual(handler1.hit_count, 2)
- self.assertEqual(handler2.hit_count, 1)
- self.assertEqual(handler3.hit_count, 1)
diff --git a/test/amd64/scripts/jumpout_test.py b/test/amd64/scripts/jumpout_test.py
deleted file mode 100644
index 9b9bc807..00000000
--- a/test/amd64/scripts/jumpout_test.py
+++ /dev/null
@@ -1,57 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-#
-# jumpout - challenge from SECCON CTF 2023
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class Jumpout(unittest.TestCase):
- def setUp(self):
- pass
-
- def test_jumpout(self):
- flag = ""
- first = 0x55
- second = 0
-
- d = debugger("CTF/jumpout")
-
- r = d.run()
-
- bp1 = d.breakpoint(0x140B, hardware=True, file="binary")
- bp2 = d.breakpoint(0x157C, hardware=True, file="binary")
-
- d.cont()
-
- r.sendline(b"A" * 0x1D)
-
- while True:
- if d.regs.rip == bp1.address:
- second = d.regs.r9
- elif d.regs.rip == bp2.address:
- address = d.regs.r13 + d.regs.rbx
- third = int.from_bytes(d.memory[address, 1], "little")
- flag += chr((first ^ second ^ third ^ (bp2.hit_count - 1)))
-
- d.cont()
-
- if flag.endswith("}"):
- break
-
- r.recvuntil(b"Wrong...")
-
- d.kill()
-
- self.assertEqual(flag, "SECCON{jump_table_everywhere}")
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/test/amd64/scripts/jumpstart_test.py b/test/amd64/scripts/jumpstart_test.py
deleted file mode 100644
index d5e1d7a3..00000000
--- a/test/amd64/scripts/jumpstart_test.py
+++ /dev/null
@@ -1,24 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-class JumpstartTest(unittest.TestCase):
-
- def test_cursed_ldpreload(self):
- d = debugger("binaries/jumpstart_test", env={"LD_PRELOAD": "binaries/jumpstart_test_preload.so"})
-
- r = d.run()
-
- d.cont()
-
- self.assertEqual(r.recvline(), b"Preload library loaded")
- self.assertEqual(r.recvline(), b"Jumpstart test")
- self.assertEqual(r.recvline(), b"execve(/bin/ls, (nil), (nil))")
-
- d.kill()
diff --git a/test/amd64/scripts/multiple_debuggers_test.py b/test/amd64/scripts/multiple_debuggers_test.py
deleted file mode 100644
index a5dfaef8..00000000
--- a/test/amd64/scripts/multiple_debuggers_test.py
+++ /dev/null
@@ -1,173 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class MultipleDebuggersTest(unittest.TestCase):
- def setUp(self):
- pass
-
- def test_multiple_debuggers(self):
- bpd = debugger("binaries/breakpoint_test")
- red = debugger("binaries/basic_test")
-
- bpd.run()
- red.run()
-
- bp1 = bpd.breakpoint("random_function")
- bp2 = bpd.breakpoint(0x40115B)
- bp3 = bpd.breakpoint(0x40116D)
-
- rbp1 = red.breakpoint(0x4011CA, hardware=True)
- rbp2 = red.breakpoint(0x40128D, hardware=False)
- rbp3 = red.breakpoint(0x401239, hardware=True)
- rbp4 = red.breakpoint(0x4011F4, hardware=False)
- rbp5 = red.breakpoint(0x401296, hardware=True)
-
- counter = 1
-
- bpd.cont()
- red.cont()
- disable_red = False
-
- while True:
-
- if bpd.regs.rip == bp1.address:
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(bpd))
- self.assertFalse(bp2.hit_on(bpd))
- self.assertFalse(bp3.hit_on(bpd))
- elif bpd.regs.rip == bp2.address:
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(bpd))
- self.assertFalse(bp1.hit_on(bpd))
- self.assertFalse(bp3.hit_on(bpd))
- counter += 1
- elif bpd.regs.rip == bp3.address:
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(bpd.regs.rsi == 45)
- self.assertTrue(bpd.regs.esi == 45)
- self.assertTrue(bpd.regs.si == 45)
- self.assertTrue(bpd.regs.sil == 45)
- self.assertTrue(bp3.hit_on(bpd))
- self.assertFalse(bp1.hit_on(bpd))
- self.assertFalse(bp2.hit_on(bpd))
- break
-
- if rbp1.hit_on(red):
- self.assertTrue(red.regs.rax == 0x0011223344556677)
- self.assertTrue(red.regs.rbx == 0x1122334455667700)
- self.assertTrue(red.regs.rcx == 0x2233445566770011)
- self.assertTrue(red.regs.rdx == 0x3344556677001122)
- self.assertTrue(red.regs.rsi == 0x4455667700112233)
- self.assertTrue(red.regs.rdi == 0x5566770011223344)
- self.assertTrue(red.regs.rbp == 0x6677001122334455)
- self.assertTrue(red.regs.r8 == 0xAABBCCDD11223344)
- self.assertTrue(red.regs.r9 == 0xBBCCDD11223344AA)
- self.assertTrue(red.regs.r10 == 0xCCDD11223344AABB)
- self.assertTrue(red.regs.r11 == 0xDD11223344AABBCC)
- self.assertTrue(red.regs.r12 == 0x11223344AABBCCDD)
- self.assertTrue(red.regs.r13 == 0x223344AABBCCDD11)
- self.assertTrue(red.regs.r14 == 0x3344AABBCCDD1122)
- self.assertTrue(red.regs.r15 == 0x44AABBCCDD112233)
- self.assertEqual(rbp1.hit_count, 1)
- self.assertEqual(rbp2.hit_count, 0)
- self.assertEqual(rbp3.hit_count, 0)
- self.assertEqual(rbp4.hit_count, 0)
- self.assertEqual(rbp5.hit_count, 0)
- elif rbp4.hit_on(red):
- self.assertTrue(red.regs.al == 0x11)
- self.assertTrue(red.regs.bl == 0x22)
- self.assertTrue(red.regs.cl == 0x33)
- self.assertTrue(red.regs.dl == 0x44)
- self.assertTrue(red.regs.sil == 0x55)
- self.assertTrue(red.regs.dil == 0x66)
- self.assertTrue(red.regs.bpl == 0x77)
- self.assertTrue(red.regs.r8b == 0x88)
- self.assertTrue(red.regs.r9b == 0x99)
- self.assertTrue(red.regs.r10b == 0xAA)
- self.assertTrue(red.regs.r11b == 0xBB)
- self.assertTrue(red.regs.r12b == 0xCC)
- self.assertTrue(red.regs.r13b == 0xDD)
- self.assertTrue(red.regs.r14b == 0xEE)
- self.assertTrue(red.regs.r15b == 0xFF)
- self.assertEqual(rbp1.hit_count, 1)
- self.assertEqual(rbp2.hit_count, 0)
- self.assertEqual(rbp3.hit_count, 0)
- self.assertEqual(rbp4.hit_count, 1)
- self.assertEqual(rbp5.hit_count, 0)
- elif rbp3.hit_on(red):
- self.assertTrue(red.regs.ax == 0x1122)
- self.assertTrue(red.regs.bx == 0x2233)
- self.assertTrue(red.regs.cx == 0x3344)
- self.assertTrue(red.regs.dx == 0x4455)
- self.assertTrue(red.regs.si == 0x5566)
- self.assertTrue(red.regs.di == 0x6677)
- self.assertTrue(red.regs.bp == 0x7788)
- self.assertTrue(red.regs.r8w == 0x8899)
- self.assertTrue(red.regs.r9w == 0x99AA)
- self.assertTrue(red.regs.r10w == 0xAABB)
- self.assertTrue(red.regs.r11w == 0xBBCC)
- self.assertTrue(red.regs.r12w == 0xCCDD)
- self.assertTrue(red.regs.r13w == 0xDDEE)
- self.assertTrue(red.regs.r14w == 0xEEFF)
- self.assertTrue(red.regs.r15w == 0xFF00)
- self.assertEqual(rbp1.hit_count, 1)
- self.assertEqual(rbp2.hit_count, 0)
- self.assertEqual(rbp3.hit_count, 1)
- self.assertEqual(rbp4.hit_count, 1)
- self.assertEqual(rbp5.hit_count, 0)
- elif rbp2.hit_on(red):
- self.assertTrue(red.regs.eax == 0x11223344)
- self.assertTrue(red.regs.ebx == 0x22334455)
- self.assertTrue(red.regs.ecx == 0x33445566)
- self.assertTrue(red.regs.edx == 0x44556677)
- self.assertTrue(red.regs.esi == 0x55667788)
- self.assertTrue(red.regs.edi == 0x66778899)
- self.assertTrue(red.regs.ebp == 0x778899AA)
- self.assertTrue(red.regs.r8d == 0x8899AABB)
- self.assertTrue(red.regs.r9d == 0x99AABBCC)
- self.assertTrue(red.regs.r10d == 0xAABBCCDD)
- self.assertTrue(red.regs.r11d == 0xBBCCDD11)
- self.assertTrue(red.regs.r12d == 0xCCDD1122)
- self.assertTrue(red.regs.r13d == 0xDD112233)
- self.assertTrue(red.regs.r14d == 0x11223344)
- self.assertTrue(red.regs.r15d == 0x22334455)
- self.assertEqual(rbp1.hit_count, 1)
- self.assertEqual(rbp2.hit_count, 1)
- self.assertEqual(rbp3.hit_count, 1)
- self.assertEqual(rbp4.hit_count, 1)
- self.assertEqual(rbp5.hit_count, 0)
- elif rbp5.hit_on(red):
- self.assertTrue(red.regs.ah == 0x11)
- self.assertTrue(red.regs.bh == 0x22)
- self.assertTrue(red.regs.ch == 0x33)
- self.assertTrue(red.regs.dh == 0x44)
- self.assertEqual(rbp1.hit_count, 1)
- self.assertEqual(rbp2.hit_count, 1)
- self.assertEqual(rbp3.hit_count, 1)
- self.assertEqual(rbp4.hit_count, 1)
- self.assertEqual(rbp5.hit_count, 1)
- else:
- self.assertEqual(rbp1.hit_count, 1)
- self.assertEqual(rbp2.hit_count, 1)
- self.assertEqual(rbp3.hit_count, 1)
- self.assertEqual(rbp4.hit_count, 1)
- self.assertEqual(rbp5.hit_count, 1)
- disable_red = True
-
- bpd.cont()
- if not disable_red:
- red.cont()
-
- bpd.kill()
- red.kill()
-
- bpd.terminate()
- red.terminate()
diff --git a/test/amd64/scripts/next_test.py b/test/amd64/scripts/next_test.py
deleted file mode 100644
index af9096d0..00000000
--- a/test/amd64/scripts/next_test.py
+++ /dev/null
@@ -1,111 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Francesco Panebianco. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-import unittest
-
-from libdebug import debugger
-
-TEST_ENTRYPOINT = 0x4011f8
-
-# Addresses of the dummy functions
-CALL_C_ADDRESS = 0x4011fd
-TEST_BREAKPOINT_ADDRESS = 0x4011f1
-
-# Addresses of noteworthy instructions
-RETURN_POINT_FROM_C = 0x401202
-
-class NextTest(unittest.TestCase):
- def setUp(self):
- pass
-
- def test_next(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
- d.run()
-
- # Get to test entrypoint
- entrypoint_bp = d.breakpoint(TEST_ENTRYPOINT)
- d.cont()
-
- self.assertEqual(d.regs.rip, TEST_ENTRYPOINT)
-
- # -------- Block 1 ------- #
- # Simple Step #
- # ------------------------ #
-
- # Reach call of function c
- d.next()
- self.assertEqual(d.regs.rip, CALL_C_ADDRESS)
-
- # -------- Block 2 ------- #
- # Skip a call #
- # ------------------------ #
-
- d.next()
- self.assertEqual(d.regs.rip, RETURN_POINT_FROM_C)
-
- d.kill()
- d.terminate()
-
- def test_next_breakpoint(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
- d.run()
-
- # Get to test entrypoint
- entrypoint_bp = d.breakpoint(TEST_ENTRYPOINT)
- d.cont()
-
- self.assertEqual(d.regs.rip, TEST_ENTRYPOINT)
-
- # Reach call of function c
- d.next()
-
- self.assertEqual(d.regs.rip, CALL_C_ADDRESS)
-
- # -------- Block 1 ------- #
- # Call with breakpoint #
- # ------------------------ #
-
- # Set breakpoint
- test_breakpoint = d.breakpoint(TEST_BREAKPOINT_ADDRESS)
-
- d.next()
-
- # Check we hit the breakpoint
- self.assertEqual(d.regs.rip, TEST_BREAKPOINT_ADDRESS)
- self.assertEqual(test_breakpoint.hit_count, 1)
-
- d.kill()
- d.terminate()
-
- def test_next_breakpoint_hw(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
- d.run()
-
- # Get to test entrypoint
- entrypoint_bp = d.breakpoint(TEST_ENTRYPOINT)
- d.cont()
-
- self.assertEqual(d.regs.rip, TEST_ENTRYPOINT)
-
- # Reach call of function c
- d.next()
-
- self.assertEqual(d.regs.rip, CALL_C_ADDRESS)
-
- # -------- Block 1 ------- #
- # Call with breakpoint #
- # ------------------------ #
-
- # Set breakpoint
- test_breakpoint = d.breakpoint(TEST_BREAKPOINT_ADDRESS, hardware=True)
-
- d.next()
-
- # Check we hit the breakpoint
- self.assertEqual(d.regs.rip, TEST_BREAKPOINT_ADDRESS)
- self.assertEqual(test_breakpoint.hit_count, 1)
-
- d.kill()
- d.terminate()
\ No newline at end of file
diff --git a/test/amd64/scripts/nlinks_test.py b/test/amd64/scripts/nlinks_test.py
deleted file mode 100644
index 7b0f2c2f..00000000
--- a/test/amd64/scripts/nlinks_test.py
+++ /dev/null
@@ -1,129 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-#
-# nlinks - challenge from DEF CON CTF Quals 2023
-# Thanks to the whole mhackeroni CTF team for the exploit
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class Nlinks(unittest.TestCase):
- def setUp(self):
- pass
-
- def get_passsphrase_from_class_1_binaries(self, previous_flag):
- flag = b""
-
- d = debugger("CTF/1")
- r = d.run()
-
- bp = d.breakpoint(0x7EF1, hardware=True)
-
- d.cont()
-
- r.recvuntil(b"Passphrase:\n")
- r.send(previous_flag + b"a" * 8)
-
- for _ in range(8):
- self.assertTrue(d.regs.rip == bp.address)
-
- offset = ord("a") ^ d.regs.rbp
- d.regs.rbp = d.regs.r13
- flag += (offset ^ d.regs.r13).to_bytes(1, "little")
-
- d.cont()
-
- r.recvline()
-
- d.kill()
-
- self.assertEqual(flag, b"\x00\x006\x00\x00\x00(\x00")
- return flag
-
- def get_passsphrase_from_class_2_binaries(self, previous_flag):
- bitmap = {}
- lastpos = 0
- flag = b""
-
- d = debugger("CTF/2")
- r = d.run()
-
- bp1 = d.breakpoint(0xD8C1, hardware=True)
- bp2 = d.breakpoint(0x1858, hardware=True)
- bp3 = d.breakpoint(0xDBA1, hardware=True)
-
- d.cont()
-
- r.recvuntil(b"Passphrase:\n")
- r.send(previous_flag + b"a" * 8)
-
- while True:
- if d.regs.rip == bp1.address:
- lastpos = d.regs.rbp
- d.regs.rbp = d.regs.r13 + 1
- elif d.regs.rip == bp2.address:
- bitmap[d.regs.r12 & 0xFF] = lastpos & 0xFF
- elif d.regs.rip == bp3.address:
- d.regs.rbp = d.regs.r13
- wanted = d.regs.rbp
- needed = 0
- for i in range(8):
- if wanted & (2**i):
- needed |= bitmap[2**i]
- flag += chr(needed).encode()
-
- if bp3.hit_count == 8:
- d.cont()
- break
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(flag, b"\x00\x00\x00\x01\x00\x00a\x00")
-
- def get_passsphrase_from_class_3_binaries(self):
- flag = b""
-
- d = debugger("CTF/0")
- r = d.run()
-
- bp = d.breakpoint(0x91A1, hardware=True)
-
- d.cont()
-
- r.send(b"a" * 8)
-
- for _ in range(8):
-
- self.assertTrue(d.regs.rip == bp.address)
-
- offset = ord("a") - d.regs.rbp
- d.regs.rbp = d.regs.r13
-
- flag += chr((d.regs.r13 + offset) % 256).encode("latin-1")
-
- d.cont()
-
- r.recvline()
-
- d.kill()
-
- self.assertEqual(flag, b"BM8\xd3\x02\x00\x00\x00")
- return flag
-
- def test_nlinks(self):
- flag = self.get_passsphrase_from_class_3_binaries()
- flag = self.get_passsphrase_from_class_1_binaries(flag)
- self.get_passsphrase_from_class_2_binaries(flag)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/test/amd64/scripts/waiting_test.py b/test/amd64/scripts/waiting_test.py
deleted file mode 100644
index 74511ea3..00000000
--- a/test/amd64/scripts/waiting_test.py
+++ /dev/null
@@ -1,197 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class WaitingTest(unittest.TestCase):
-
- def test_bps_waiting(self):
- d = debugger("binaries/breakpoint_test", auto_interrupt_on_command=True)
-
- d.run()
-
- bp1 = d.breakpoint("random_function")
- bp2 = d.breakpoint(0x40115B)
- bp3 = d.breakpoint(0x40116D)
-
- counter = 1
-
- d.cont()
-
- while True:
- d.wait()
- if d.regs.rip == bp1.address:
- self.assertTrue(bp1.hit_count == 1)
- self.assertTrue(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- elif d.regs.rip == bp2.address:
- self.assertTrue(bp2.hit_count == counter)
- self.assertTrue(bp2.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp3.hit_on(d))
- counter += 1
- elif d.regs.rip == bp3.address:
- self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.rsi == 45)
- self.assertTrue(d.regs.esi == 45)
- self.assertTrue(d.regs.si == 45)
- self.assertTrue(d.regs.sil == 45)
- self.assertTrue(bp3.hit_on(d))
- self.assertFalse(bp1.hit_on(d))
- self.assertFalse(bp2.hit_on(d))
- break
-
- d.cont()
-
- d.kill()
-
- def test_jumpout_waiting(self):
- flag = ""
- first = 0x55
- second = 0
-
- d = debugger("CTF/jumpout", auto_interrupt_on_command=True)
-
- r = d.run()
-
- bp1 = d.breakpoint(0x140B, hardware=True, file="binary")
- bp2 = d.breakpoint(0x157C, hardware=True, file="binary")
-
- d.cont()
-
- r.sendline(b"A" * 0x1D)
-
- while True:
- d.wait()
- if d.regs.rip == bp1.address:
- second = d.regs.r9
- elif d.regs.rip == bp2.address:
- address = d.regs.r13 + d.regs.rbx
- third = int.from_bytes(d.memory[address, 1], "little")
- flag += chr((first ^ second ^ third ^ (bp2.hit_count - 1)))
-
- d.cont()
-
- if flag.endswith("}"):
- break
-
- r.recvuntil(b"Wrong...")
-
- d.kill()
-
- self.assertEqual(flag, "SECCON{jump_table_everywhere}")
-
-class WaitingNlinks(unittest.TestCase):
- def setUp(self):
- pass
-
- def get_passsphrase_from_class_1_binaries(self, previous_flag):
- flag = b""
-
- d = debugger("CTF/1", auto_interrupt_on_command=True)
- r = d.run()
-
- d.breakpoint(0x7EF1, hardware=True)
-
- d.cont()
-
- r.recvuntil(b"Passphrase:\n")
- r.send(previous_flag + b"a" * 8)
-
- for _ in range(8):
- d.wait()
- offset = ord("a") ^ d.regs.rbp
- d.regs.rbp = d.regs.r13
- flag += (offset ^ d.regs.r13).to_bytes(1, "little")
-
- d.cont()
-
- r.recvline()
-
- d.kill()
-
- self.assertEqual(flag, b"\x00\x006\x00\x00\x00(\x00")
- return flag
-
- def get_passsphrase_from_class_2_binaries(self, previous_flag):
- bitmap = {}
- lastpos = 0
- flag = b""
-
- d = debugger("CTF/2", auto_interrupt_on_command=True)
- r = d.run()
-
- bp1 = d.breakpoint(0xD8C1, hardware=True)
- bp2 = d.breakpoint(0x1858, hardware=True)
- bp3 = d.breakpoint(0xDBA1, hardware=True)
-
- d.cont()
-
- r.recvuntil(b"Passphrase:\n")
- r.send(previous_flag + b"a" * 8)
-
- while True:
- d.wait()
- if d.regs.rip == bp1.address:
- lastpos = d.regs.rbp
- d.regs.rbp = d.regs.r13 + 1
- elif d.regs.rip == bp2.address:
- bitmap[d.regs.r12 & 0xFF] = lastpos & 0xFF
- elif d.regs.rip == bp3.address:
- d.regs.rbp = d.regs.r13
- wanted = d.regs.rbp
- needed = 0
- for i in range(8):
- if wanted & (2**i):
- needed |= bitmap[2**i]
- flag += chr(needed).encode()
-
- if bp3.hit_count == 8:
- d.cont()
- break
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(flag, b"\x00\x00\x00\x01\x00\x00a\x00")
-
- def get_passsphrase_from_class_3_binaries(self):
- flag = b""
-
- d = debugger("CTF/0", auto_interrupt_on_command=True)
- r = d.run()
-
- d.breakpoint(0x91A1, hardware=True)
-
- d.cont()
-
- r.send(b"a" * 8)
-
- for _ in range(8):
- d.wait()
- offset = ord("a") - d.regs.rbp
- d.regs.rbp = d.regs.r13
-
- flag += chr((d.regs.r13 + offset) % 256).encode("latin-1")
-
- d.cont()
-
- r.recvline()
-
- d.kill()
-
- self.assertEqual(flag, b"BM8\xd3\x02\x00\x00\x00")
- return flag
-
- def test_nlinks(self):
- flag = self.get_passsphrase_from_class_3_binaries()
- flag = self.get_passsphrase_from_class_1_binaries(flag)
- self.get_passsphrase_from_class_2_binaries(flag)
diff --git a/test/amd64/scripts/watchpoint_alias_test.py b/test/amd64/scripts/watchpoint_alias_test.py
deleted file mode 100644
index 7da0391a..00000000
--- a/test/amd64/scripts/watchpoint_alias_test.py
+++ /dev/null
@@ -1,94 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Francesco Panebianco, Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class WatchpointAliasTest(unittest.TestCase):
- def test_watchpoint_alias(self):
- d = debugger("binaries/watchpoint_test", auto_interrupt_on_command=False)
-
- d.run()
-
- d.wp("global_char", condition="rw", length=1)
- d.watchpoint("global_int", condition="w", length=4)
- d.watchpoint("global_long", condition="rw", length=8)
-
- d.cont()
-
- self.assertEqual(d.regs.rip, 0x401111) # mov byte ptr [global_char], 0x1
-
- d.cont()
-
- self.assertEqual(d.regs.rip, 0x401124) # mov dword ptr [global_int], 0x4050607
-
- d.cont()
-
- self.assertEqual(d.regs.rip, 0x401135) # mov qword ptr [global_long], 0x8090a0b0c0d0e0f
-
- d.cont()
-
- self.assertEqual(d.regs.rip, 0x401155) # movzx eax, byte ptr [global_char]
-
- d.cont()
-
- self.assertEqual(d.regs.rip, 0x401173) # mov rax, qword ptr [global_long]
-
- d.cont()
-
- d.kill()
-
- def test_watchpoint_callback(self):
- global_char_ip = []
- global_int_ip = []
- global_long_ip = []
-
- def watchpoint_global_char(t, b):
- nonlocal global_char_ip
-
- global_char_ip.append(t.regs.rip)
-
- def watchpoint_global_int(t, b):
- nonlocal global_int_ip
-
- global_int_ip.append(t.regs.rip)
-
- def watchpoint_global_long(t, b):
- nonlocal global_long_ip
-
- global_long_ip.append(t.regs.rip)
-
- d = debugger("binaries/watchpoint_test", auto_interrupt_on_command=False)
-
- d.run()
-
- wp1 = d.watchpoint("global_char", condition="rw", length=1, callback=watchpoint_global_char)
- wp2 = d.wp("global_int", condition="w", length=4, callback=watchpoint_global_int)
- wp3 = d.watchpoint("global_long", condition="rw", length=8, callback=watchpoint_global_long)
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(global_char_ip[0], 0x401111) # mov byte ptr [global_char], 0x1
- self.assertEqual(global_int_ip[0], 0x401124) # mov dword ptr [global_int], 0x4050607
- self.assertEqual(global_long_ip[0], 0x401135) # mov qword ptr [global_long], 0x8090a0b0c0d0e0f
- self.assertEqual(global_char_ip[1], 0x401155) # movzx eax, byte ptr [global_char]
- self.assertEqual(global_long_ip[1], 0x401173) # mov rax, qword ptr [global_long]
-
- self.assertEqual(len(global_char_ip), 2)
- self.assertEqual(len(global_int_ip), 1)
-
- # There is one extra hit performed by the exit routine of libc
- self.assertEqual(len(global_long_ip), 3)
-
- self.assertEqual(wp1.hit_count, 2)
- self.assertEqual(wp2.hit_count, 1)
-
- # There is one extra hit performed by the exit routine of libc
- self.assertEqual(wp3.hit_count, 3)
diff --git a/test/amd64/scripts/watchpoint_test.py b/test/amd64/scripts/watchpoint_test.py
deleted file mode 100644
index a29b5ae4..00000000
--- a/test/amd64/scripts/watchpoint_test.py
+++ /dev/null
@@ -1,231 +0,0 @@
-#
-# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2023-2024 Francesco Panebianco, Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
-# Licensed under the MIT license. See LICENSE file in the project root for details.
-#
-
-import unittest
-
-from libdebug import debugger
-
-
-class WatchpointTest(unittest.TestCase):
- def test_watchpoint(self):
- d = debugger("binaries/watchpoint_test", auto_interrupt_on_command=False)
-
- d.run()
-
- wp_char = d.breakpoint("global_char", hardware=True, condition="rw", length=1)
- wp_int = d.breakpoint("global_int", hardware=True, condition="w", length=4)
- wp_long = d.breakpoint("global_long", hardware=True, condition="rw", length=8)
-
- d.cont()
-
- self.assertEqual(d.regs.rip, 0x401111) # mov byte ptr [global_char], 0x1
- self.assertEqual(wp_char.hit_count, 1)
- self.assertEqual(wp_int.hit_count, 0)
- self.assertEqual(wp_long.hit_count, 0)
-
- d.cont()
-
- self.assertEqual(d.regs.rip, 0x401124) # mov dword ptr [global_int], 0x4050607
- self.assertEqual(wp_char.hit_count, 1)
- self.assertEqual(wp_int.hit_count, 1)
- self.assertEqual(wp_long.hit_count, 0)
-
- d.cont()
-
- self.assertEqual(
- d.regs.rip, 0x401135
- ) # mov qword ptr [global_long], 0x8090a0b0c0d0e0f
- self.assertEqual(wp_char.hit_count, 1)
- self.assertEqual(wp_int.hit_count, 1)
- self.assertEqual(wp_long.hit_count, 1)
-
- d.cont()
-
- self.assertEqual(d.regs.rip, 0x401155) # movzx eax, byte ptr [global_char]
- self.assertEqual(wp_char.hit_count, 2)
- self.assertEqual(wp_int.hit_count, 1)
- self.assertEqual(wp_long.hit_count, 1)
-
- d.cont()
-
- self.assertEqual(d.regs.rip, 0x401173) # mov rax, qword ptr [global_long]
- self.assertEqual(wp_char.hit_count, 2)
- self.assertEqual(wp_int.hit_count, 1)
- self.assertEqual(wp_long.hit_count, 2)
-
- d.cont()
-
- d.kill()
-
- def test_watchpoint_callback(self):
- global_char_ip = []
- global_int_ip = []
- global_long_ip = []
-
- def watchpoint_global_char(t, b):
- nonlocal global_char_ip
-
- global_char_ip.append(t.regs.rip)
-
- def watchpoint_global_int(t, b):
- nonlocal global_int_ip
-
- global_int_ip.append(t.regs.rip)
-
- def watchpoint_global_long(t, b):
- nonlocal global_long_ip
-
- global_long_ip.append(t.regs.rip)
-
- d = debugger("binaries/watchpoint_test", auto_interrupt_on_command=False)
-
- d.run()
-
- wp1 = d.breakpoint(
- "global_char",
- hardware=True,
- condition="rw",
- length=1,
- callback=watchpoint_global_char,
- )
- wp2 = d.breakpoint(
- "global_int",
- hardware=True,
- condition="w",
- length=4,
- callback=watchpoint_global_int,
- )
- wp3 = d.breakpoint(
- "global_long",
- hardware=True,
- condition="rw",
- length=8,
- callback=watchpoint_global_long,
- )
-
- d.cont()
-
- d.kill()
-
- self.assertEqual(global_char_ip[0], 0x401111) # mov byte ptr [global_char], 0x1
- self.assertEqual(
- global_int_ip[0], 0x401124
- ) # mov dword ptr [global_int], 0x4050607
- self.assertEqual(
- global_long_ip[0], 0x401135
- ) # mov qword ptr [global_long], 0x8090a0b0c0d0e0f
- self.assertEqual(
- global_char_ip[1], 0x401155
- ) # movzx eax, byte ptr [global_char]
- self.assertEqual(
- global_long_ip[1], 0x401173
- ) # mov rax, qword ptr [global_long]
-
- self.assertEqual(len(global_char_ip), 2)
- self.assertEqual(len(global_int_ip), 1)
-
- # There is one extra hit performed by the exit routine of libc
- self.assertEqual(len(global_long_ip), 3)
-
- self.assertEqual(wp1.hit_count, 2)
- self.assertEqual(wp2.hit_count, 1)
-
- # There is one extra hit performed by the exit routine of libc
- self.assertEqual(wp3.hit_count, 3)
-
- def test_watchpoint_disable(self):
- d = debugger("binaries/watchpoint_test", auto_interrupt_on_command=False)
-
- d.run()
-
- wp_char = d.breakpoint("global_char", hardware=True, condition="rw", length=1)
- wp_int = d.breakpoint("global_int", hardware=True, condition="w", length=4)
- wp_long = d.breakpoint("global_long", hardware=True, condition="rw", length=8)
-
- d.cont()
-
- self.assertEqual(d.regs.rip, 0x401111) # mov byte ptr [global_char], 0x1
- self.assertEqual(wp_char.hit_count, 1)
- self.assertEqual(wp_int.hit_count, 0)
- self.assertEqual(wp_long.hit_count, 0)
-
- d.cont()
-
- self.assertEqual(d.regs.rip, 0x401124) # mov dword ptr [global_int], 0x4050607
- self.assertEqual(wp_char.hit_count, 1)
- self.assertEqual(wp_int.hit_count, 1)
- self.assertEqual(wp_long.hit_count, 0)
-
- d.cont()
-
- self.assertEqual(
- d.regs.rip, 0x401135
- ) # mov qword ptr [global_long], 0x8090a0b0c0d0e0f
- self.assertEqual(wp_char.hit_count, 1)
- self.assertEqual(wp_int.hit_count, 1)
- self.assertEqual(wp_long.hit_count, 1)
-
- # disable watchpoint
- wp_char.disable()
-
- d.cont()
-
- self.assertEqual(d.regs.rip, 0x401173) # mov rax, qword ptr [global_long]
- self.assertEqual(wp_char.hit_count, 1)
- self.assertEqual(wp_int.hit_count, 1)
- self.assertEqual(wp_long.hit_count, 2)
-
- d.cont()
-
- d.kill()
-
- def test_watchpoint_disable_reenable(self):
- d = debugger("binaries/watchpoint_test", auto_interrupt_on_command=False)
-
- d.run()
-
- wp_char = d.breakpoint("global_char", hardware=True, condition="rw", length=1)
- wp_int = d.breakpoint("global_int", hardware=True, condition="w", length=4)
- wp_long = d.breakpoint("global_long", hardware=True, condition="rw", length=8)
-
- d.cont()
-
- self.assertEqual(d.regs.rip, 0x401111) # mov byte ptr [global_char], 0x1
- self.assertEqual(wp_char.hit_count, 1)
- self.assertEqual(wp_int.hit_count, 0)
- self.assertEqual(wp_long.hit_count, 0)
-
- d.cont()
-
- self.assertEqual(d.regs.rip, 0x401124) # mov dword ptr [global_int], 0x4050607
- self.assertEqual(wp_char.hit_count, 1)
- self.assertEqual(wp_int.hit_count, 1)
- self.assertEqual(wp_long.hit_count, 0)
-
- # disable watchpoint
- wp_long.disable()
-
- d.cont()
-
-
- self.assertEqual(d.regs.rip, 0x401155) # movzx eax, byte ptr [global_char]
- self.assertEqual(wp_char.hit_count, 2)
- self.assertEqual(wp_int.hit_count, 1)
- self.assertEqual(wp_long.hit_count, 0)
-
- # re-enable watchpoint
- wp_long.enable()
-
- d.cont()
-
- self.assertEqual(d.regs.rip, 0x401173) # mov rax, qword ptr [global_long]
- self.assertEqual(wp_char.hit_count, 2)
- self.assertEqual(wp_int.hit_count, 1)
- self.assertEqual(wp_long.hit_count, 1)
-
- d.cont()
-
- d.kill()
\ No newline at end of file
diff --git a/test/amd64/benchmarks/benchmarks.md b/test/benchmarks/benchmarks.md
similarity index 100%
rename from test/amd64/benchmarks/benchmarks.md
rename to test/benchmarks/benchmarks.md
diff --git a/test/amd64/benchmarks/breakpoint_gdb.py b/test/benchmarks/breakpoint_gdb.py
similarity index 100%
rename from test/amd64/benchmarks/breakpoint_gdb.py
rename to test/benchmarks/breakpoint_gdb.py
diff --git a/test/amd64/benchmarks/breakpoint_libdebug.py b/test/benchmarks/breakpoint_libdebug.py
similarity index 100%
rename from test/amd64/benchmarks/breakpoint_libdebug.py
rename to test/benchmarks/breakpoint_libdebug.py
diff --git a/test/amd64/benchmarks/results/breakpoint_benchmark.svg b/test/benchmarks/results/breakpoint_benchmark.svg
similarity index 100%
rename from test/amd64/benchmarks/results/breakpoint_benchmark.svg
rename to test/benchmarks/results/breakpoint_benchmark.svg
diff --git a/test/amd64/benchmarks/results/breakpoint_gdb.pkl b/test/benchmarks/results/breakpoint_gdb.pkl
similarity index 100%
rename from test/amd64/benchmarks/results/breakpoint_gdb.pkl
rename to test/benchmarks/results/breakpoint_gdb.pkl
diff --git a/test/amd64/benchmarks/results/breakpoint_libdebug.pkl b/test/benchmarks/results/breakpoint_libdebug.pkl
similarity index 100%
rename from test/amd64/benchmarks/results/breakpoint_libdebug.pkl
rename to test/benchmarks/results/breakpoint_libdebug.pkl
diff --git a/test/amd64/benchmarks/results/syscall_benchmark.svg b/test/benchmarks/results/syscall_benchmark.svg
similarity index 100%
rename from test/amd64/benchmarks/results/syscall_benchmark.svg
rename to test/benchmarks/results/syscall_benchmark.svg
diff --git a/test/amd64/benchmarks/results/syscall_gdb.pkl b/test/benchmarks/results/syscall_gdb.pkl
similarity index 100%
rename from test/amd64/benchmarks/results/syscall_gdb.pkl
rename to test/benchmarks/results/syscall_gdb.pkl
diff --git a/test/amd64/benchmarks/results/syscall_libdebug.pkl b/test/benchmarks/results/syscall_libdebug.pkl
similarity index 100%
rename from test/amd64/benchmarks/results/syscall_libdebug.pkl
rename to test/benchmarks/results/syscall_libdebug.pkl
diff --git a/test/amd64/benchmarks/syscall_gdb.py b/test/benchmarks/syscall_gdb.py
similarity index 100%
rename from test/amd64/benchmarks/syscall_gdb.py
rename to test/benchmarks/syscall_gdb.py
diff --git a/test/amd64/benchmarks/syscall_libdebug.py b/test/benchmarks/syscall_libdebug.py
similarity index 100%
rename from test/amd64/benchmarks/syscall_libdebug.py
rename to test/benchmarks/syscall_libdebug.py
diff --git a/test/aarch64/binaries/antidebug_brute_test b/test/binaries/aarch64/antidebug_brute_test
similarity index 100%
rename from test/aarch64/binaries/antidebug_brute_test
rename to test/binaries/aarch64/antidebug_brute_test
diff --git a/test/aarch64/binaries/attach_test b/test/binaries/aarch64/attach_test
similarity index 100%
rename from test/aarch64/binaries/attach_test
rename to test/binaries/aarch64/attach_test
diff --git a/test/aarch64/binaries/backtrace_test b/test/binaries/aarch64/backtrace_test
similarity index 100%
rename from test/aarch64/binaries/backtrace_test
rename to test/binaries/aarch64/backtrace_test
diff --git a/test/aarch64/binaries/basic_test b/test/binaries/aarch64/basic_test
similarity index 100%
rename from test/aarch64/binaries/basic_test
rename to test/binaries/aarch64/basic_test
diff --git a/test/aarch64/binaries/basic_test_pie b/test/binaries/aarch64/basic_test_pie
similarity index 100%
rename from test/aarch64/binaries/basic_test_pie
rename to test/binaries/aarch64/basic_test_pie
diff --git a/test/aarch64/binaries/benchmark b/test/binaries/aarch64/benchmark
similarity index 100%
rename from test/aarch64/binaries/benchmark
rename to test/binaries/aarch64/benchmark
diff --git a/test/aarch64/binaries/breakpoint_test b/test/binaries/aarch64/breakpoint_test
similarity index 100%
rename from test/aarch64/binaries/breakpoint_test
rename to test/binaries/aarch64/breakpoint_test
diff --git a/test/aarch64/binaries/brute_test b/test/binaries/aarch64/brute_test
similarity index 100%
rename from test/aarch64/binaries/brute_test
rename to test/binaries/aarch64/brute_test
diff --git a/test/aarch64/binaries/catch_signal_test b/test/binaries/aarch64/catch_signal_test
similarity index 100%
rename from test/aarch64/binaries/catch_signal_test
rename to test/binaries/aarch64/catch_signal_test
diff --git a/test/aarch64/binaries/executable_section_test b/test/binaries/aarch64/executable_section_test
similarity index 100%
rename from test/aarch64/binaries/executable_section_test
rename to test/binaries/aarch64/executable_section_test
diff --git a/test/aarch64/binaries/finish_test b/test/binaries/aarch64/finish_test
similarity index 100%
rename from test/aarch64/binaries/finish_test
rename to test/binaries/aarch64/finish_test
diff --git a/test/aarch64/binaries/floating_point_test b/test/binaries/aarch64/floating_point_test
similarity index 100%
rename from test/aarch64/binaries/floating_point_test
rename to test/binaries/aarch64/floating_point_test
diff --git a/test/aarch64/binaries/handle_syscall_test b/test/binaries/aarch64/handle_syscall_test
similarity index 100%
rename from test/aarch64/binaries/handle_syscall_test
rename to test/binaries/aarch64/handle_syscall_test
diff --git a/test/binaries/aarch64/infinite_loop_test b/test/binaries/aarch64/infinite_loop_test
new file mode 100755
index 00000000..5097052d
Binary files /dev/null and b/test/binaries/aarch64/infinite_loop_test differ
diff --git a/test/aarch64/binaries/jumpstart_test b/test/binaries/aarch64/jumpstart_test
similarity index 100%
rename from test/aarch64/binaries/jumpstart_test
rename to test/binaries/aarch64/jumpstart_test
diff --git a/test/aarch64/binaries/jumpstart_test_preload.so b/test/binaries/aarch64/jumpstart_test_preload.so
similarity index 100%
rename from test/aarch64/binaries/jumpstart_test_preload.so
rename to test/binaries/aarch64/jumpstart_test_preload.so
diff --git a/test/aarch64/binaries/memory_test b/test/binaries/aarch64/memory_test
similarity index 100%
rename from test/aarch64/binaries/memory_test
rename to test/binaries/aarch64/memory_test
diff --git a/test/aarch64/binaries/memory_test_2 b/test/binaries/aarch64/memory_test_2
similarity index 100%
rename from test/aarch64/binaries/memory_test_2
rename to test/binaries/aarch64/memory_test_2
diff --git a/test/binaries/aarch64/memory_test_3 b/test/binaries/aarch64/memory_test_3
new file mode 100755
index 00000000..8c560995
Binary files /dev/null and b/test/binaries/aarch64/memory_test_3 differ
diff --git a/test/binaries/aarch64/memory_test_4 b/test/binaries/aarch64/memory_test_4
new file mode 100755
index 00000000..1747ce0a
Binary files /dev/null and b/test/binaries/aarch64/memory_test_4 differ
diff --git a/test/binaries/aarch64/multithread_input b/test/binaries/aarch64/multithread_input
new file mode 100755
index 00000000..864e2608
Binary files /dev/null and b/test/binaries/aarch64/multithread_input differ
diff --git a/test/binaries/aarch64/run_pipes_test b/test/binaries/aarch64/run_pipes_test
new file mode 100755
index 00000000..bc3789dd
Binary files /dev/null and b/test/binaries/aarch64/run_pipes_test differ
diff --git a/test/aarch64/binaries/segfault_test b/test/binaries/aarch64/segfault_test
similarity index 100%
rename from test/aarch64/binaries/segfault_test
rename to test/binaries/aarch64/segfault_test
diff --git a/test/aarch64/binaries/signals_multithread_det_test b/test/binaries/aarch64/signals_multithread_det_test
similarity index 100%
rename from test/aarch64/binaries/signals_multithread_det_test
rename to test/binaries/aarch64/signals_multithread_det_test
diff --git a/test/aarch64/binaries/signals_multithread_undet_test b/test/binaries/aarch64/signals_multithread_undet_test
similarity index 100%
rename from test/aarch64/binaries/signals_multithread_undet_test
rename to test/binaries/aarch64/signals_multithread_undet_test
diff --git a/test/aarch64/binaries/speed_test b/test/binaries/aarch64/speed_test
similarity index 100%
rename from test/aarch64/binaries/speed_test
rename to test/binaries/aarch64/speed_test
diff --git a/test/aarch64/binaries/thread_test_complex b/test/binaries/aarch64/thread_complex_test
similarity index 100%
rename from test/aarch64/binaries/thread_test_complex
rename to test/binaries/aarch64/thread_complex_test
diff --git a/test/aarch64/binaries/thread_test b/test/binaries/aarch64/thread_test
similarity index 100%
rename from test/aarch64/binaries/thread_test
rename to test/binaries/aarch64/thread_test
diff --git a/test/aarch64/binaries/watchpoint_test b/test/binaries/aarch64/watchpoint_test
similarity index 100%
rename from test/aarch64/binaries/watchpoint_test
rename to test/binaries/aarch64/watchpoint_test
diff --git a/test/amd64/CTF/0 b/test/binaries/amd64/CTF/0
similarity index 100%
rename from test/amd64/CTF/0
rename to test/binaries/amd64/CTF/0
diff --git a/test/amd64/CTF/1 b/test/binaries/amd64/CTF/1
similarity index 100%
rename from test/amd64/CTF/1
rename to test/binaries/amd64/CTF/1
diff --git a/test/amd64/CTF/2 b/test/binaries/amd64/CTF/2
similarity index 100%
rename from test/amd64/CTF/2
rename to test/binaries/amd64/CTF/2
diff --git a/test/amd64/CTF/deep-dive-division b/test/binaries/amd64/CTF/deep-dive-division
similarity index 100%
rename from test/amd64/CTF/deep-dive-division
rename to test/binaries/amd64/CTF/deep-dive-division
diff --git a/test/amd64/CTF/jumpout b/test/binaries/amd64/CTF/jumpout
similarity index 100%
rename from test/amd64/CTF/jumpout
rename to test/binaries/amd64/CTF/jumpout
diff --git a/test/amd64/CTF/vmwhere1 b/test/binaries/amd64/CTF/vmwhere1
similarity index 100%
rename from test/amd64/CTF/vmwhere1
rename to test/binaries/amd64/CTF/vmwhere1
diff --git a/test/amd64/CTF/vmwhere1_program b/test/binaries/amd64/CTF/vmwhere1_program
similarity index 100%
rename from test/amd64/CTF/vmwhere1_program
rename to test/binaries/amd64/CTF/vmwhere1_program
diff --git a/test/amd64/binaries/antidebug_brute_test b/test/binaries/amd64/antidebug_brute_test
similarity index 100%
rename from test/amd64/binaries/antidebug_brute_test
rename to test/binaries/amd64/antidebug_brute_test
diff --git a/test/amd64/binaries/attach_test b/test/binaries/amd64/attach_test
similarity index 100%
rename from test/amd64/binaries/attach_test
rename to test/binaries/amd64/attach_test
diff --git a/test/amd64/binaries/backtrace_test b/test/binaries/amd64/backtrace_test
similarity index 100%
rename from test/amd64/binaries/backtrace_test
rename to test/binaries/amd64/backtrace_test
diff --git a/test/amd64/binaries/basic_test b/test/binaries/amd64/basic_test
similarity index 100%
rename from test/amd64/binaries/basic_test
rename to test/binaries/amd64/basic_test
diff --git a/test/amd64/binaries/basic_test_pie b/test/binaries/amd64/basic_test_pie
similarity index 100%
rename from test/amd64/binaries/basic_test_pie
rename to test/binaries/amd64/basic_test_pie
diff --git a/test/amd64/binaries/benchmark b/test/binaries/amd64/benchmark
similarity index 100%
rename from test/amd64/binaries/benchmark
rename to test/binaries/amd64/benchmark
diff --git a/test/amd64/binaries/breakpoint_test b/test/binaries/amd64/breakpoint_test
similarity index 100%
rename from test/amd64/binaries/breakpoint_test
rename to test/binaries/amd64/breakpoint_test
diff --git a/test/amd64/binaries/brute_test b/test/binaries/amd64/brute_test
similarity index 100%
rename from test/amd64/binaries/brute_test
rename to test/binaries/amd64/brute_test
diff --git a/test/amd64/binaries/catch_signal_test b/test/binaries/amd64/catch_signal_test
similarity index 100%
rename from test/amd64/binaries/catch_signal_test
rename to test/binaries/amd64/catch_signal_test
diff --git a/test/amd64/binaries/cc_workshop b/test/binaries/amd64/cc_workshop
similarity index 100%
rename from test/amd64/binaries/cc_workshop
rename to test/binaries/amd64/cc_workshop
diff --git a/test/amd64/binaries/executable_section_test b/test/binaries/amd64/executable_section_test
similarity index 100%
rename from test/amd64/binaries/executable_section_test
rename to test/binaries/amd64/executable_section_test
diff --git a/test/amd64/binaries/finish_test b/test/binaries/amd64/finish_test
similarity index 100%
rename from test/amd64/binaries/finish_test
rename to test/binaries/amd64/finish_test
diff --git a/test/amd64/binaries/floating_point_896_test b/test/binaries/amd64/floating_point_avx2_test
similarity index 100%
rename from test/amd64/binaries/floating_point_896_test
rename to test/binaries/amd64/floating_point_avx2_test
diff --git a/test/amd64/binaries/floating_point_2696_test b/test/binaries/amd64/floating_point_avx512_test
similarity index 100%
rename from test/amd64/binaries/floating_point_2696_test
rename to test/binaries/amd64/floating_point_avx512_test
diff --git a/test/amd64/binaries/memory_test_2 b/test/binaries/amd64/floating_point_mmx_test
similarity index 59%
rename from test/amd64/binaries/memory_test_2
rename to test/binaries/amd64/floating_point_mmx_test
index 72397a97..4720addf 100755
Binary files a/test/amd64/binaries/memory_test_2 and b/test/binaries/amd64/floating_point_mmx_test differ
diff --git a/test/amd64/binaries/floating_point_512_test b/test/binaries/amd64/floating_point_sse_test
similarity index 100%
rename from test/amd64/binaries/floating_point_512_test
rename to test/binaries/amd64/floating_point_sse_test
diff --git a/test/amd64/binaries/handle_syscall_test b/test/binaries/amd64/handle_syscall_test
similarity index 100%
rename from test/amd64/binaries/handle_syscall_test
rename to test/binaries/amd64/handle_syscall_test
diff --git a/test/amd64/binaries/infinite_loop_test b/test/binaries/amd64/infinite_loop_test
similarity index 100%
rename from test/amd64/binaries/infinite_loop_test
rename to test/binaries/amd64/infinite_loop_test
diff --git a/test/amd64/binaries/jumpstart_test b/test/binaries/amd64/jumpstart_test
similarity index 100%
rename from test/amd64/binaries/jumpstart_test
rename to test/binaries/amd64/jumpstart_test
diff --git a/test/amd64/binaries/jumpstart_test_preload.so b/test/binaries/amd64/jumpstart_test_preload.so
similarity index 100%
rename from test/amd64/binaries/jumpstart_test_preload.so
rename to test/binaries/amd64/jumpstart_test_preload.so
diff --git a/test/amd64/binaries/math_loop_test b/test/binaries/amd64/math_loop_test
similarity index 100%
rename from test/amd64/binaries/math_loop_test
rename to test/binaries/amd64/math_loop_test
diff --git a/test/amd64/binaries/memory_test b/test/binaries/amd64/memory_test
similarity index 100%
rename from test/amd64/binaries/memory_test
rename to test/binaries/amd64/memory_test
diff --git a/test/binaries/amd64/memory_test_2 b/test/binaries/amd64/memory_test_2
new file mode 100755
index 00000000..fcd6d25d
Binary files /dev/null and b/test/binaries/amd64/memory_test_2 differ
diff --git a/test/amd64/binaries/memory_test_3 b/test/binaries/amd64/memory_test_3
similarity index 100%
rename from test/amd64/binaries/memory_test_3
rename to test/binaries/amd64/memory_test_3
diff --git a/test/amd64/binaries/memory_test_4 b/test/binaries/amd64/memory_test_4
similarity index 100%
rename from test/amd64/binaries/memory_test_4
rename to test/binaries/amd64/memory_test_4
diff --git a/test/binaries/amd64/multi b/test/binaries/amd64/multi
new file mode 100755
index 00000000..bab80954
Binary files /dev/null and b/test/binaries/amd64/multi differ
diff --git a/test/binaries/amd64/multithread_input b/test/binaries/amd64/multithread_input
new file mode 100755
index 00000000..86274b3b
Binary files /dev/null and b/test/binaries/amd64/multithread_input differ
diff --git a/test/amd64/binaries/node b/test/binaries/amd64/node
similarity index 100%
rename from test/amd64/binaries/node
rename to test/binaries/amd64/node
diff --git a/test/binaries/amd64/run_pipes_test b/test/binaries/amd64/run_pipes_test
new file mode 100755
index 00000000..c520f8e4
Binary files /dev/null and b/test/binaries/amd64/run_pipes_test differ
diff --git a/test/amd64/binaries/segfault_test b/test/binaries/amd64/segfault_test
similarity index 100%
rename from test/amd64/binaries/segfault_test
rename to test/binaries/amd64/segfault_test
diff --git a/test/amd64/binaries/signals_multithread_det_test b/test/binaries/amd64/signals_multithread_det_test
similarity index 100%
rename from test/amd64/binaries/signals_multithread_det_test
rename to test/binaries/amd64/signals_multithread_det_test
diff --git a/test/amd64/binaries/signals_multithread_undet_test b/test/binaries/amd64/signals_multithread_undet_test
similarity index 100%
rename from test/amd64/binaries/signals_multithread_undet_test
rename to test/binaries/amd64/signals_multithread_undet_test
diff --git a/test/amd64/binaries/speed_test b/test/binaries/amd64/speed_test
similarity index 100%
rename from test/amd64/binaries/speed_test
rename to test/binaries/amd64/speed_test
diff --git a/test/amd64/binaries/complex_thread_test b/test/binaries/amd64/thread_complex_test
similarity index 100%
rename from test/amd64/binaries/complex_thread_test
rename to test/binaries/amd64/thread_complex_test
diff --git a/test/amd64/binaries/thread_test b/test/binaries/amd64/thread_test
similarity index 100%
rename from test/amd64/binaries/thread_test
rename to test/binaries/amd64/thread_test
diff --git a/test/amd64/binaries/watchpoint_test b/test/binaries/amd64/watchpoint_test
similarity index 100%
rename from test/amd64/binaries/watchpoint_test
rename to test/binaries/amd64/watchpoint_test
diff --git a/test/binaries/i386/antidebug_brute_test b/test/binaries/i386/antidebug_brute_test
new file mode 100755
index 00000000..12391a9d
Binary files /dev/null and b/test/binaries/i386/antidebug_brute_test differ
diff --git a/test/binaries/i386/attach_test b/test/binaries/i386/attach_test
new file mode 100755
index 00000000..f0c5d933
Binary files /dev/null and b/test/binaries/i386/attach_test differ
diff --git a/test/binaries/i386/backtrace_test b/test/binaries/i386/backtrace_test
new file mode 100755
index 00000000..87549268
Binary files /dev/null and b/test/binaries/i386/backtrace_test differ
diff --git a/test/binaries/i386/basic_test b/test/binaries/i386/basic_test
new file mode 100755
index 00000000..1db11553
Binary files /dev/null and b/test/binaries/i386/basic_test differ
diff --git a/test/binaries/i386/basic_test_pie b/test/binaries/i386/basic_test_pie
new file mode 100755
index 00000000..acd8bc4e
Binary files /dev/null and b/test/binaries/i386/basic_test_pie differ
diff --git a/test/binaries/i386/breakpoint_test b/test/binaries/i386/breakpoint_test
new file mode 100755
index 00000000..8387a011
Binary files /dev/null and b/test/binaries/i386/breakpoint_test differ
diff --git a/test/binaries/i386/brute_test b/test/binaries/i386/brute_test
new file mode 100755
index 00000000..7e662647
Binary files /dev/null and b/test/binaries/i386/brute_test differ
diff --git a/test/binaries/i386/catch_signal_test b/test/binaries/i386/catch_signal_test
new file mode 100755
index 00000000..4af77afa
Binary files /dev/null and b/test/binaries/i386/catch_signal_test differ
diff --git a/test/binaries/i386/executable_section_test b/test/binaries/i386/executable_section_test
new file mode 100755
index 00000000..e2761dbb
Binary files /dev/null and b/test/binaries/i386/executable_section_test differ
diff --git a/test/binaries/i386/finish_test b/test/binaries/i386/finish_test
new file mode 100755
index 00000000..aa3ae243
Binary files /dev/null and b/test/binaries/i386/finish_test differ
diff --git a/test/binaries/i386/floating_point_avx2_test b/test/binaries/i386/floating_point_avx2_test
new file mode 100755
index 00000000..f5e2c81e
Binary files /dev/null and b/test/binaries/i386/floating_point_avx2_test differ
diff --git a/test/binaries/i386/floating_point_avx512_test b/test/binaries/i386/floating_point_avx512_test
new file mode 100755
index 00000000..5ef61ee7
Binary files /dev/null and b/test/binaries/i386/floating_point_avx512_test differ
diff --git a/test/binaries/i386/floating_point_mmx_test b/test/binaries/i386/floating_point_mmx_test
new file mode 100755
index 00000000..236b0221
Binary files /dev/null and b/test/binaries/i386/floating_point_mmx_test differ
diff --git a/test/binaries/i386/floating_point_sse_test b/test/binaries/i386/floating_point_sse_test
new file mode 100755
index 00000000..9b486003
Binary files /dev/null and b/test/binaries/i386/floating_point_sse_test differ
diff --git a/test/binaries/i386/handle_syscall_test b/test/binaries/i386/handle_syscall_test
new file mode 100755
index 00000000..da5cb3ce
Binary files /dev/null and b/test/binaries/i386/handle_syscall_test differ
diff --git a/test/binaries/i386/infinite_loop_test b/test/binaries/i386/infinite_loop_test
new file mode 100755
index 00000000..b69af31d
Binary files /dev/null and b/test/binaries/i386/infinite_loop_test differ
diff --git a/test/binaries/i386/jumpstart_test b/test/binaries/i386/jumpstart_test
new file mode 100755
index 00000000..a6718a72
Binary files /dev/null and b/test/binaries/i386/jumpstart_test differ
diff --git a/test/binaries/i386/jumpstart_test_preload.so b/test/binaries/i386/jumpstart_test_preload.so
new file mode 100755
index 00000000..e51080c0
Binary files /dev/null and b/test/binaries/i386/jumpstart_test_preload.so differ
diff --git a/test/binaries/i386/math_loop_test b/test/binaries/i386/math_loop_test
new file mode 100755
index 00000000..b8d605a0
Binary files /dev/null and b/test/binaries/i386/math_loop_test differ
diff --git a/test/binaries/i386/memory_test b/test/binaries/i386/memory_test
new file mode 100755
index 00000000..adfbd893
Binary files /dev/null and b/test/binaries/i386/memory_test differ
diff --git a/test/binaries/i386/memory_test_2 b/test/binaries/i386/memory_test_2
new file mode 100755
index 00000000..93beb716
Binary files /dev/null and b/test/binaries/i386/memory_test_2 differ
diff --git a/test/binaries/i386/memory_test_3 b/test/binaries/i386/memory_test_3
new file mode 100755
index 00000000..7b171d30
Binary files /dev/null and b/test/binaries/i386/memory_test_3 differ
diff --git a/test/binaries/i386/memory_test_4 b/test/binaries/i386/memory_test_4
new file mode 100755
index 00000000..6a9700d8
Binary files /dev/null and b/test/binaries/i386/memory_test_4 differ
diff --git a/test/binaries/i386/multithread_input b/test/binaries/i386/multithread_input
new file mode 100755
index 00000000..f6d9fbc8
Binary files /dev/null and b/test/binaries/i386/multithread_input differ
diff --git a/test/binaries/i386/run_pipes_test b/test/binaries/i386/run_pipes_test
new file mode 100755
index 00000000..7809609d
Binary files /dev/null and b/test/binaries/i386/run_pipes_test differ
diff --git a/test/binaries/i386/segfault_test b/test/binaries/i386/segfault_test
new file mode 100755
index 00000000..1a4fae76
Binary files /dev/null and b/test/binaries/i386/segfault_test differ
diff --git a/test/binaries/i386/signals_multithread_det_test b/test/binaries/i386/signals_multithread_det_test
new file mode 100755
index 00000000..3ee4f4fe
Binary files /dev/null and b/test/binaries/i386/signals_multithread_det_test differ
diff --git a/test/binaries/i386/signals_multithread_undet_test b/test/binaries/i386/signals_multithread_undet_test
new file mode 100755
index 00000000..5d08b96f
Binary files /dev/null and b/test/binaries/i386/signals_multithread_undet_test differ
diff --git a/test/binaries/i386/speed_test b/test/binaries/i386/speed_test
new file mode 100755
index 00000000..073346c5
Binary files /dev/null and b/test/binaries/i386/speed_test differ
diff --git a/test/binaries/i386/thread_complex_test b/test/binaries/i386/thread_complex_test
new file mode 100755
index 00000000..84598285
Binary files /dev/null and b/test/binaries/i386/thread_complex_test differ
diff --git a/test/binaries/i386/thread_test b/test/binaries/i386/thread_test
new file mode 100755
index 00000000..f199c36d
Binary files /dev/null and b/test/binaries/i386/thread_test differ
diff --git a/test/binaries/i386/watchpoint_test b/test/binaries/i386/watchpoint_test
new file mode 100755
index 00000000..00f233fe
Binary files /dev/null and b/test/binaries/i386/watchpoint_test differ
diff --git a/test/amd64/dockerfiles/archlinux.Dockerfile b/test/dockerfiles/archlinux.Dockerfile
similarity index 100%
rename from test/amd64/dockerfiles/archlinux.Dockerfile
rename to test/dockerfiles/archlinux.Dockerfile
diff --git a/test/amd64/dockerfiles/archlinux.Dockerfile.dockerignore b/test/dockerfiles/archlinux.Dockerfile.dockerignore
similarity index 100%
rename from test/amd64/dockerfiles/archlinux.Dockerfile.dockerignore
rename to test/dockerfiles/archlinux.Dockerfile.dockerignore
diff --git a/test/amd64/dockerfiles/debian.Dockerfile b/test/dockerfiles/debian.Dockerfile
similarity index 100%
rename from test/amd64/dockerfiles/debian.Dockerfile
rename to test/dockerfiles/debian.Dockerfile
diff --git a/test/amd64/dockerfiles/debian.Dockerfile.dockerignore b/test/dockerfiles/debian.Dockerfile.dockerignore
similarity index 100%
rename from test/amd64/dockerfiles/debian.Dockerfile.dockerignore
rename to test/dockerfiles/debian.Dockerfile.dockerignore
diff --git a/test/amd64/dockerfiles/fedora.Dockerfile b/test/dockerfiles/fedora.Dockerfile
similarity index 100%
rename from test/amd64/dockerfiles/fedora.Dockerfile
rename to test/dockerfiles/fedora.Dockerfile
diff --git a/test/amd64/dockerfiles/fedora.Dockerfile.dockerignore b/test/dockerfiles/fedora.Dockerfile.dockerignore
similarity index 100%
rename from test/amd64/dockerfiles/fedora.Dockerfile.dockerignore
rename to test/dockerfiles/fedora.Dockerfile.dockerignore
diff --git a/test/amd64/dockerfiles/run_tests.sh b/test/dockerfiles/run_tests.sh
similarity index 100%
rename from test/amd64/dockerfiles/run_tests.sh
rename to test/dockerfiles/run_tests.sh
diff --git a/test/amd64/dockerfiles/ubuntu.Dockerfile b/test/dockerfiles/ubuntu.Dockerfile
similarity index 100%
rename from test/amd64/dockerfiles/ubuntu.Dockerfile
rename to test/dockerfiles/ubuntu.Dockerfile
diff --git a/test/amd64/dockerfiles/ubuntu.Dockerfile.dockerignore b/test/dockerfiles/ubuntu.Dockerfile.dockerignore
similarity index 100%
rename from test/amd64/dockerfiles/ubuntu.Dockerfile.dockerignore
rename to test/dockerfiles/ubuntu.Dockerfile.dockerignore
diff --git a/test/amd64/other_tests/gdb_migration_test.py b/test/other_tests/gdb_migration_test.py
similarity index 100%
rename from test/amd64/other_tests/gdb_migration_test.py
rename to test/other_tests/gdb_migration_test.py
diff --git a/test/other_tests/pipe_interactive_test.py b/test/other_tests/pipe_interactive_test.py
new file mode 100644
index 00000000..382a8a1d
--- /dev/null
+++ b/test/other_tests/pipe_interactive_test.py
@@ -0,0 +1,111 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from libdebug import debugger
+from threading import Thread
+from time import sleep
+import os
+
+def multi_process_spam_sync_test_auto():
+ """Test a multi-process spam binary with syncronous breakpoints and auto_quit=False."""
+ print("Test a multi-process spam binary with syncronous breakpoints and auto_quit=False.")
+ d = debugger('../binaries/multi')
+
+ r = d.run()
+ # Install a brakpoint after the input of the user is read
+ # This is a syncronous breakpoint
+ bp = d.bp(0x129b, hardware=True, file="binary")
+ d.cont()
+
+ # The interactive will NOT quit automatically when the breakpoint is hit
+ print("Press Ctrl+C to pass to the next interactive session.")
+ r.interactive()
+
+ bp.disable()
+
+ d.cont()
+
+ print("Let open again the interactive session. You should see the prompt again and all the missing messages.")
+ print("Press Ctrl+C to pass to the next test.")
+
+ r.interactive()
+
+ print("End of the test.")
+ d.interrupt()
+ d.kill()
+
+def multi_process_spam_sync_test_no_auto():
+ """Test a multi-process spam binary with syncronous breakpoints and auto_quit=True."""
+ print("Test a multi-process spam binary with syncronous breakpoints and auto_quit=True.")
+ d = debugger('../binaries/multi')
+
+ r = d.run()
+ # Install a brakpoint after the input of the user is read
+ # This is a syncronous breakpoint
+ bp = d.bp(0x129b, hardware=True, file="binary")
+ d.cont()
+
+ # The interactive will quit automatically when the breakpoint is hit
+ r.interactive(auto_quit=True)
+
+ bp.disable()
+
+ d.cont()
+
+ print("Let open again the interactive session.")
+ print("Press Ctrl+C to pass to the next test.")
+
+ r.interactive()
+
+ print("End of the test.")
+ d.interrupt()
+ d.kill()
+
+def multi_process_spam_async_test():
+ """Test a multi-process spam binary with asyncronous breakpoints."""
+ print("Test a multi-process spam binary with asyncronous breakpoints.")
+ d = debugger('../binaries/multi')
+
+ r = d.run()
+ # Install a brakpoint after the input of the user is read
+ # This is an asyncronous breakpoint
+ d.bp(0x129b, hardware=True, file="binary", callback=lambda _, __: print("Callback called. Ctrl+C to pass to the next test."))
+ d.cont()
+
+ r.interactive()
+
+ print("End of the test.")
+
+ d.interrupt()
+ d.kill()
+
+def process_death_during_interactive_test():
+ """Test the death of a process during an interactive session."""
+ print("Test the death of a process during an interactive session.")
+ print("Press Ctrl+C to exit the test.")
+ d = debugger('../binaries/multi')
+
+ r = d.run()
+ d.cont()
+
+ # spawn a thread to kill the process after 5 seconds
+ def kill():
+ sleep(5)
+ os.kill(d.pid, 9)
+
+ t = Thread(target=kill)
+ t.start()
+
+ r.interactive()
+
+ print("End of the test.")
+ t.join()
+
+
+multi_process_spam_sync_test_auto()
+multi_process_spam_sync_test_no_auto()
+multi_process_spam_async_test()
+process_death_during_interactive_test()
\ No newline at end of file
diff --git a/test/amd64/run_containerized_tests.sh b/test/run_containerized_tests.sh
similarity index 100%
rename from test/amd64/run_containerized_tests.sh
rename to test/run_containerized_tests.sh
diff --git a/test/run_suite.py b/test/run_suite.py
index 7ebf2c61..b5a235cf 100644
--- a/test/run_suite.py
+++ b/test/run_suite.py
@@ -1,35 +1,98 @@
#
# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+# Copyright (c) 2023-2024 Roberto Alessandro Bertolini, Gabriele Digregorio, Francesco Panebianco. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
-import os
-import platform
import sys
+from argparse import ArgumentParser
+from unittest import TestSuite, TestLoader, TextTestRunner
-architectures = os.listdir(".")
-architectures.remove("common")
-
-if len(sys.argv) > 1 and sys.argv[1] not in architectures:
- print("Usage: python run_test_suite.py ")
- print("Available architectures:")
- for arch in architectures:
- print(f" {arch}")
- sys.exit(1)
-elif len(sys.argv) > 1:
- arch = sys.argv[1]
-else:
- arch = platform.machine()
- match arch:
- case "x86_64":
- arch = "amd64"
- case "i686":
- arch = "i386"
- case "aarch64":
- arch = "aarch64"
- case _:
- raise ValueError(f"Unsupported architecture: {arch}")
-
-os.chdir(arch)
-os.system(" ".join([sys.executable, "run_suite.py"]))
+import scripts
+
+
+def fast_suite():
+ suite = TestSuite()
+
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.AliasTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.AntidebugEscapingTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.AtexitHandlerTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.AttachDetachTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.AutoWaitingTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.BacktraceTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.BreakpointTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.CallbackTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.ControlFlowTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.DeathTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.FinishTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.FloatingPointTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.JumpstartTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.LargeBinarySymTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.MemoryTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.MemoryFastTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.MultipleDebuggersTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.NextTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.NlinksTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.PPrintSyscallsTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.RegisterTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.RunPipesTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.SignalCatchTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.SignalMultithreadTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.SyscallHandleTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.SyscallHijackTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.ThreadTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.WatchpointTest))
+
+ return suite
+
+def full_suite():
+ suite = fast_suite()
+
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.BruteTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.DeepDiveDivisionTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.JumpoutTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.SpeedTest))
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.Vmwhere1Test))
+
+ return suite
+
+def stress_suite():
+ suite = TestSuite()
+
+ for _ in range(1024):
+ suite.addTest(TestLoader().loadTestsFromTestCase(scripts.ThreadTest))
+
+ return suite
+
+def main(suite: str):
+ if sys.version_info >= (3, 12):
+ runner = TextTestRunner(verbosity=2, durations=3)
+ else:
+ runner = TextTestRunner(verbosity=2)
+
+ if suite == "slow":
+ suite = full_suite()
+ elif suite == "stress":
+ suite = stress_suite()
+ runner.verbosity = 1
+ elif suite == "fast":
+ suite = fast_suite()
+ else:
+ raise ValueError(f"Invalid suite: {suite}")
+
+ runner.run(suite)
+
+if __name__ == "__main__":
+ parser = ArgumentParser(prog="libdebug Test Suite", description="Run the test suite")
+ parser.add_argument("suite", type=str, help="The suite to run the tests from", choices=["fast", "slow", "stress"], default="fast", nargs="?")
+
+ dbg = 'dbg' in sys.argv
+ if dbg:
+ sys.argv.remove('dbg')
+
+ args = parser.parse_args()
+
+ if dbg:
+ sys.argv.append('dbg')
+
+ main(args.suite)
diff --git a/test/scripts/__init__.py b/test/scripts/__init__.py
new file mode 100644
index 00000000..702c14d0
--- /dev/null
+++ b/test/scripts/__init__.py
@@ -0,0 +1,36 @@
+from .alias_test import AliasTest
+from .antidebug_escaping_test import AntidebugEscapingTest
+from .atexit_handler_test import AtexitHandlerTest
+from .attach_detach_test import AttachDetachTest
+from .auto_waiting_test import AutoWaitingTest
+from .backtrace_test import BacktraceTest
+from .breakpoint_test import BreakpointTest
+from .brute_test import BruteTest
+from .callback_test import CallbackTest
+from .control_flow_test import ControlFlowTest
+from .death_test import DeathTest
+from .deep_dive_division_test import DeepDiveDivisionTest
+from .finish_test import FinishTest
+from .floating_point_test import FloatingPointTest
+from .jumpout_test import JumpoutTest
+from .jumpstart_test import JumpstartTest
+from .large_binary_sym_test import LargeBinarySymTest
+from .memory_test import MemoryTest
+from .memory_fast_test import MemoryFastTest
+from .multiple_debuggers_test import MultipleDebuggersTest
+from .next_test import NextTest
+from .nlinks_test import NlinksTest
+from .pprint_syscalls_test import PPrintSyscallsTest
+from .register_test import RegisterTest
+from .run_pipes_test import RunPipesTest
+from .signal_catch_test import SignalCatchTest
+from .signal_multithread_test import SignalMultithreadTest
+from .speed_test import SpeedTest
+from .syscall_handle_test import SyscallHandleTest
+from .syscall_hijack_test import SyscallHijackTest
+from .thread_test import ThreadTest
+from .vmwhere1_test import Vmwhere1Test
+from .watchpoint_test import WatchpointTest
+
+
+__all__ = ["AliasTest", "AntidebugEscapingTest", "AttachDetachTest", "AutoWaitingTest", "BacktraceTest", "BreakpointTest", "BruteTest", "CallbackTest", "ControlFlowTest", "DeathTest", "DeepDiveDivisionTest", "FinishTest", "FloatingPointTest", "JumpoutTest", "JumpstartTest", "LargeBinarySymTest", "MemoryTest", "MemoryFastTest", "MultipleDebuggersTest", "NextTest", "NlinksTest", "PPrintSyscallsTest", "RegisterTest", "SignalCatchTest", "SignalMultithreadTest", "SpeedTest", "SyscallHandleTest", "SyscallHijackTest", "ThreadTest", "Vmwhere1Test", "WatchpointTest"]
diff --git a/test/scripts/alias_test.py b/test/scripts/alias_test.py
new file mode 100644
index 00000000..220526ee
--- /dev/null
+++ b/test/scripts/alias_test.py
@@ -0,0 +1,244 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini, Francesco Panebianco. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from unittest import TestCase, skipIf
+from utils.thread_utils import FUN_ARG_0
+from utils.binary_utils import BASE, PLATFORM, RESOLVE_EXE
+
+from libdebug import debugger
+
+
+match PLATFORM:
+ case "amd64":
+ TEST_STEP_ALIAS_OFFSET_1 = 1
+ TEST_STEP_ALIAS_OFFSET_2 = 4
+
+ TEST_STEP_UNTIL_ALIAS_ADDRESS = 0x401180
+
+ TEST_FINISH_ALIAS_ADDRESS_1 = 0x4011E0
+ TEST_FINISH_ALIAS_ADDRESS_2 = 0x401202
+ TEST_FINISH_ALIAS_ADDRESS_3 = 0x4011E3
+ TEST_FINISH_ALIAS_FUNCTION_A_ADDRESS = 0x401146
+
+ TEST_WAITING_ALIAS_BP2_ADDRESS = 0x40115B
+ TEST_WAITING_ALIAS_BP3_ADDRESS = 0x40116D
+
+ def CHECK_REGISTERS(harness, d):
+ harness.assertEqual(d.regs.rsi, 45)
+ harness.assertEqual(d.regs.esi, 45)
+ harness.assertEqual(d.regs.si, 45)
+ harness.assertEqual(d.regs.sil, 45)
+ case "aarch64":
+ TEST_STEP_ALIAS_OFFSET_1 = 4
+ TEST_STEP_ALIAS_OFFSET_2 = 8
+
+ TEST_STEP_UNTIL_ALIAS_ADDRESS = BASE + 0x083c
+
+ TEST_FINISH_ALIAS_ADDRESS_1 = BASE + 0x0908
+ TEST_FINISH_ALIAS_ADDRESS_2 = BASE + 0x0938
+ TEST_FINISH_ALIAS_ADDRESS_3 = BASE + 0x0914
+ TEST_FINISH_ALIAS_FUNCTION_A_ADDRESS = BASE + 0x0814
+
+ TEST_WAITING_ALIAS_BP2_ADDRESS = 0x7fc
+ TEST_WAITING_ALIAS_BP3_ADDRESS = 0x820
+
+ def CHECK_REGISTERS(harness, d):
+ harness.assertEqual(d.regs.x1, 45)
+ harness.assertEqual(d.regs.w1, 45)
+ case "i386":
+ TEST_STEP_ALIAS_OFFSET_1 = 1
+ TEST_STEP_ALIAS_OFFSET_2 = 3
+
+ TEST_STEP_UNTIL_ALIAS_ADDRESS = BASE + 0x11fc
+
+ TEST_FINISH_ALIAS_ADDRESS_1 = BASE + 0x125f
+ TEST_FINISH_ALIAS_ADDRESS_2 = BASE + 0x128f
+ TEST_FINISH_ALIAS_ADDRESS_3 = BASE + 0x1262
+ TEST_FINISH_ALIAS_FUNCTION_A_ADDRESS = BASE + 0x11a9
+
+ TEST_WAITING_ALIAS_BP2_ADDRESS = 0x11d0
+ TEST_WAITING_ALIAS_BP3_ADDRESS = 0x11ea
+
+ def CHECK_REGISTERS(harness, d):
+ value = int.from_bytes(d.memory[d.regs.esp + 4, 4], "little")
+ harness.assertEqual(value, 45)
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+
+class AliasTest(TestCase):
+ def test_basic_alias(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+ d.r()
+ bp = d.bp("register_test")
+ d.c()
+ self.assertTrue(bp.address == d.instruction_pointer)
+ d.c()
+ d.kill()
+ d.terminate()
+
+ def test_step_alias(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+
+ d.r()
+ bp = d.bp("register_test")
+ d.c()
+
+ self.assertTrue(bp.address == d.instruction_pointer)
+ self.assertTrue(bp.hit_count == 1)
+
+ d.si()
+
+ self.assertTrue(bp.address + TEST_STEP_ALIAS_OFFSET_1 == d.instruction_pointer)
+ self.assertTrue(bp.hit_count == 1)
+
+ d.si()
+
+ self.assertTrue(bp.address + TEST_STEP_ALIAS_OFFSET_2 == d.instruction_pointer)
+ self.assertTrue(bp.hit_count == 1)
+
+ d.c()
+ d.kill()
+ d.terminate()
+
+ def test_step_until_alias(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"), aslr=False)
+ d.r()
+
+ bp1 = d.bp("main")
+ bp2 = d.bp("random_function")
+ d.c()
+
+ self.assertTrue(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+
+ d.su(TEST_STEP_UNTIL_ALIAS_ADDRESS)
+ self.assertTrue(d.instruction_pointer == TEST_STEP_UNTIL_ALIAS_ADDRESS)
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+
+ d.c()
+
+ self.assertTrue(bp2.hit_on(d))
+
+ d.c()
+
+ d.kill()
+ d.terminate()
+
+ def test_memory_alias(self):
+ d = debugger(RESOLVE_EXE("memory_test"))
+
+ d.r()
+
+ bp = d.bp("change_memory")
+
+ d.c()
+
+ assert d.instruction_pointer == bp.address
+
+ address = FUN_ARG_0(d)
+ prev = bytes(range(256))
+
+ self.assertTrue(d.mem[address, 256] == prev)
+
+ d.mem[address + 128 :] = b"abcd123456"
+ prev = prev[:128] + b"abcd123456" + prev[138:]
+
+ self.assertTrue(d.mem[address : address + 256] == prev)
+
+ d.kill()
+ d.terminate()
+
+ def test_finish_alias(self):
+ d = debugger(RESOLVE_EXE("finish_test"), auto_interrupt_on_command=False, aslr=False)
+
+ # ------------------ Block 1 ------------------ #
+ # Return from the first function call #
+ # --------------------------------------------- #
+
+ # Reach function c
+ d.r()
+ d.bp(TEST_FINISH_ALIAS_ADDRESS_3)
+ d.c()
+
+ self.assertEqual(d.instruction_pointer, TEST_FINISH_ALIAS_ADDRESS_3)
+
+ # Finish function c
+ d.fin(heuristic="step-mode")
+
+ self.assertEqual(d.instruction_pointer, TEST_FINISH_ALIAS_ADDRESS_2)
+
+ d.kill()
+
+ # ------------------ Block 2 ------------------ #
+ # Return from the nested function call #
+ # --------------------------------------------- #
+
+ # Reach function a
+ d.r()
+ d.bp(TEST_FINISH_ALIAS_FUNCTION_A_ADDRESS)
+ d.c()
+
+ self.assertEqual(d.instruction_pointer, TEST_FINISH_ALIAS_FUNCTION_A_ADDRESS)
+
+ # Finish function a
+ d.fin(heuristic="step-mode")
+
+ self.assertEqual(d.instruction_pointer, TEST_FINISH_ALIAS_ADDRESS_1)
+
+ d.kill()
+ d.terminate()
+
+ def test_waiting_alias(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"), auto_interrupt_on_command=True)
+
+ d.r()
+
+ bp1 = d.breakpoint("random_function")
+ bp2 = d.breakpoint(TEST_WAITING_ALIAS_BP2_ADDRESS, file="binary")
+ bp3 = d.breakpoint(TEST_WAITING_ALIAS_BP3_ADDRESS, file="binary")
+
+ counter = 1
+
+ d.c()
+
+ while True:
+ d.w()
+ if d.instruction_pointer == bp1.address:
+ self.assertTrue(bp1.hit_count == 1)
+ self.assertTrue(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ elif d.instruction_pointer == bp2.address:
+ self.assertTrue(bp2.hit_count == counter)
+ self.assertTrue(bp2.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ counter += 1
+ elif d.instruction_pointer == bp3.address:
+ self.assertTrue(bp3.hit_count == 1)
+ CHECK_REGISTERS(self, d)
+ self.assertTrue(bp3.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ break
+
+ d.c()
+
+ d.kill()
+ d.terminate()
+
+ def test_interrupt_alias(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+
+ d.r()
+
+ d.c()
+
+ d.int()
+ d.kill()
+ d.terminate()
diff --git a/test/aarch64/scripts/builtin_handler_test.py b/test/scripts/antidebug_escaping_test.py
similarity index 69%
rename from test/aarch64/scripts/builtin_handler_test.py
rename to test/scripts/antidebug_escaping_test.py
index 96d0fe88..9c5a0db4 100644
--- a/test/aarch64/scripts/builtin_handler_test.py
+++ b/test/scripts/antidebug_escaping_test.py
@@ -4,15 +4,27 @@
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
-import unittest
import string
+from unittest import TestCase
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
from libdebug import debugger
-class BuiltinHandlerTest(unittest.TestCase):
+match PLATFORM:
+ case "amd64":
+ ADDRESS = 0x401209
+ case "aarch64":
+ ADDRESS = 0xa10
+ case "i386":
+ ADDRESS = 0x128d
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+
+class AntidebugEscapingTest(TestCase):
def test_antidebug_escaping(self):
- d = debugger("binaries/antidebug_brute_test")
+ d = debugger(RESOLVE_EXE("antidebug_brute_test"))
# validate that without the handler the binary cannot be debugged
r = d.run()
@@ -22,7 +34,7 @@ def test_antidebug_escaping(self):
d.kill()
# validate that with the handler the binary can be debugged
- d = debugger("binaries/antidebug_brute_test", escape_antidebug=True)
+ d = debugger(RESOLVE_EXE("antidebug_brute_test"), escape_antidebug=True)
r = d.run()
d.cont()
msg = r.recvline()
@@ -37,12 +49,12 @@ def test_antidebug_escaping(self):
while not flag or flag != "BRUTE":
for c in string.printable:
r = d.run()
- bp = d.breakpoint(0xa10, hardware=True, file="binary")
+ bp = d.breakpoint(ADDRESS, hardware=True, file="binary")
d.cont()
r.sendlineafter(b"chars\n", (flag + c).encode())
- while bp.address == d.regs.pc:
+ while bp.address == d.instruction_pointer:
d.cont()
if bp.hit_count > counter:
@@ -60,3 +72,4 @@ def test_antidebug_escaping(self):
break
self.assertEqual(flag, "BRUTE")
+ d.terminate()
diff --git a/test/amd64/scripts/atexit_handler_test.py b/test/scripts/atexit_handler_test.py
similarity index 68%
rename from test/amd64/scripts/atexit_handler_test.py
rename to test/scripts/atexit_handler_test.py
index 790fcd09..8b4b89a8 100644
--- a/test/amd64/scripts/atexit_handler_test.py
+++ b/test/scripts/atexit_handler_test.py
@@ -7,16 +7,17 @@
import os
import psutil
import signal
-import unittest
+from unittest import TestCase
+from utils.binary_utils import RESOLVE_EXE
from pwn import process
from libdebug import debugger
from libdebug.debugger.internal_debugger_holder import _cleanup_internal_debugger
-class AtexitHandlerTest(unittest.TestCase):
+class AtexitHandlerTest(TestCase):
def test_run_1(self):
- d = debugger("binaries/infinite_loop_test")
+ d = debugger(RESOLVE_EXE("infinite_loop_test"))
r = d.run()
@@ -32,7 +33,7 @@ def test_run_1(self):
self.assertNotIn(pid, psutil.pids())
def test_run_2(self):
- d = debugger("binaries/infinite_loop_test", kill_on_exit=False)
+ d = debugger(RESOLVE_EXE("infinite_loop_test"), kill_on_exit=False)
r = d.run()
@@ -55,7 +56,7 @@ def test_run_2(self):
self.assertNotIn(pid, psutil.pids())
def test_run_3(self):
- d = debugger("binaries/infinite_loop_test", kill_on_exit=False)
+ d = debugger(RESOLVE_EXE("infinite_loop_test"), kill_on_exit=False)
r = d.run()
@@ -73,7 +74,7 @@ def test_run_3(self):
self.assertNotIn(pid, psutil.pids())
def test_run_4(self):
- d = debugger("binaries/infinite_loop_test")
+ d = debugger(RESOLVE_EXE("infinite_loop_test"))
r = d.run()
@@ -98,7 +99,7 @@ def test_run_4(self):
self.assertNotIn(pid, psutil.pids())
def test_attach_detach_1(self):
- p = process("binaries/infinite_loop_test")
+ p = process(RESOLVE_EXE("infinite_loop_test"))
d = debugger()
@@ -116,39 +117,16 @@ def test_attach_detach_1(self):
_cleanup_internal_debugger()
- # The process should now be dead
- self.assertIsNotNone(p.poll(block=False))
-
- def test_attach_detach_2(self):
- p = process("binaries/infinite_loop_test")
-
- d = debugger(kill_on_exit=False)
-
- d.attach(p.pid)
-
- p.sendline(b"3")
-
- d.step()
- d.step()
-
- d.detach()
-
- # If the process is still running, poll() should return None
- self.assertIsNone(p.poll(block=False))
-
- _cleanup_internal_debugger()
-
- # We set kill_on_exit to False, so the process should still be alive
# The process should still be alive
self.assertIsNone(p.poll(block=False))
-
+
p.kill()
-
+
# The process should now be dead
self.assertIsNotNone(p.poll(block=False))
- def test_attach_detach_3(self):
- p = process("binaries/infinite_loop_test")
+ def test_attach_detach_2(self):
+ p = process(RESOLVE_EXE("infinite_loop_test"))
d = debugger(kill_on_exit=False)
@@ -164,35 +142,8 @@ def test_attach_detach_3(self):
# If the process is still running, poll() should return None
self.assertIsNone(p.poll(block=False))
- d.kill_on_exit = True
-
- _cleanup_internal_debugger()
-
- # The process should now be dead
- self.assertIsNotNone(p.poll(block=False))
-
- def test_attach_detach_4(self):
- p = process("binaries/infinite_loop_test")
-
- d = debugger()
-
- d.attach(p.pid)
-
- p.sendline(b"3")
-
- d.step()
- d.step()
-
- d.detach()
-
- # If the process is still running, poll() should return None
- self.assertIsNone(p.poll(block=False))
-
- d.kill_on_exit = False
-
_cleanup_internal_debugger()
- # We set kill_on_exit to False, so the process should still be alive
# The process should still be alive
self.assertIsNone(p.poll(block=False))
@@ -202,7 +153,7 @@ def test_attach_detach_4(self):
self.assertIsNotNone(p.poll(block=False))
def test_attach_1(self):
- p = process("binaries/infinite_loop_test")
+ p = process(RESOLVE_EXE("infinite_loop_test"))
d = debugger()
@@ -222,7 +173,7 @@ def test_attach_1(self):
self.assertIsNotNone(p.poll(block=False))
def test_attach_2(self):
- p = process("binaries/infinite_loop_test")
+ p = process(RESOLVE_EXE("infinite_loop_test"))
d = debugger()
diff --git a/test/scripts/attach_detach_test.py b/test/scripts/attach_detach_test.py
new file mode 100644
index 00000000..daf435a9
--- /dev/null
+++ b/test/scripts/attach_detach_test.py
@@ -0,0 +1,151 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+import logging
+import unittest
+
+import subprocess
+from pwn import process
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
+import os
+import signal
+
+from libdebug import debugger
+
+logging.getLogger("pwnlib").setLevel(logging.ERROR)
+
+
+match PLATFORM:
+ case "amd64":
+ TEST_ATTACH_AND_DETACH_3_BP1_ADDRESS = 0x125E
+ TEST_ATTACH_AND_DETACH_3_BP2_ADDRESS = 0x1261
+
+ TEST_MULTITHREAD_ADDRESS = 0x128a
+ case "aarch64":
+ TEST_ATTACH_AND_DETACH_3_BP1_ADDRESS = 0xa04
+ TEST_ATTACH_AND_DETACH_3_BP2_ADDRESS = 0xa08
+
+ TEST_MULTITHREAD_ADDRESS = 0xaec
+ case "i386":
+ TEST_ATTACH_AND_DETACH_3_BP1_ADDRESS = 0x1251
+ TEST_ATTACH_AND_DETACH_3_BP2_ADDRESS = 0x1255
+
+ TEST_MULTITHREAD_ADDRESS = 0x1243
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+class AttachDetachTest(unittest.TestCase):
+ def test_attach(self):
+ r = process(RESOLVE_EXE("attach_test"))
+
+ d = debugger()
+ d.attach(r.pid)
+ bp = d.breakpoint("printName", hardware=True)
+ d.cont()
+
+ r.recvuntil(b"name:")
+ r.sendline(b"Io_no")
+
+ self.assertTrue(d.instruction_pointer == bp.address)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ def test_attach_multithread(self):
+ r = subprocess.Popen([RESOLVE_EXE("multithread_input")], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+
+ # Synchronize with the process to be sure that all threads have been created
+ while b"All threads have been created." not in r.stdout.readline():
+ pass
+
+ d = debugger()
+ d.attach(r.pid)
+
+ # Breakpoint at the end of the thread function
+ bp = d.breakpoint(TEST_MULTITHREAD_ADDRESS, hardware=True, callback=lambda _, __: _, file="binary")
+
+ self.assertEqual(len(d.threads), 6)
+
+ d.cont()
+
+ for _ in range(5):
+ r.stdin.write(b"1\n")
+ r.stdin.flush()
+
+ d.detach()
+ r.kill()
+
+ self.assertEqual(bp.hit_count, 5)
+
+ d.terminate()
+
+ def test_attach_and_detach_1(self):
+ r = process(RESOLVE_EXE("attach_test"))
+
+ d = debugger()
+
+ # Attach to the process
+ d.attach(r.pid)
+ d.detach()
+
+ # Validate that, after detaching, the process is still running
+ r.recvuntil(b"name:", timeout=1)
+ r.sendline(b"Io_no")
+
+ r.kill()
+ d.terminate()
+
+ def test_attach_and_detach_2(self):
+ d = debugger(RESOLVE_EXE("attach_test"))
+
+ # Run the process
+ r = d.run()
+ pid = d.pid
+ d.detach()
+
+ # Validate that, after detaching, the process is still running
+ r.recvuntil(b"name:", timeout=1)
+ r.sendline(b"Io_no")
+
+ # kill the process
+ os.kill(pid, signal.SIGKILL)
+ d.terminate()
+
+ def test_attach_and_detach_3(self):
+ d = debugger(RESOLVE_EXE("attach_test"))
+
+ r = d.run()
+ pid = d.pid
+
+ # We must ensure that any breakpoint is unset before detaching
+ d.breakpoint(TEST_ATTACH_AND_DETACH_3_BP1_ADDRESS, file="binary")
+ d.breakpoint(TEST_ATTACH_AND_DETACH_3_BP2_ADDRESS, hardware=True, file="binary")
+
+ d.detach()
+
+ # Validate that, after detaching, the process is still running
+ r.recvuntil(b"name:", timeout=1)
+ r.sendline(b"Io_no")
+
+ # kill the process
+ os.kill(pid, signal.SIGKILL)
+ d.terminate()
+
+ def test_attach_and_detach_4(self):
+ r = process(RESOLVE_EXE("attach_test"))
+
+ d = debugger()
+ d.attach(r.pid)
+ d.detach()
+
+ # Validate that, after detaching, the process cannot be killed
+ self.assertRaises(RuntimeError, d.kill)
+
+ # Kill the process
+ r.kill()
+ d.terminate()
diff --git a/test/aarch64/scripts/auto_waiting_test.py b/test/scripts/auto_waiting_test.py
similarity index 51%
rename from test/aarch64/scripts/auto_waiting_test.py
rename to test/scripts/auto_waiting_test.py
index 8de2e2dc..1c236d59 100644
--- a/test/aarch64/scripts/auto_waiting_test.py
+++ b/test/scripts/auto_waiting_test.py
@@ -4,55 +4,45 @@
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
-import io
-import logging
-import unittest
+from unittest import TestCase, skipUnless
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
from libdebug import debugger
-class AutoWaitingTest(unittest.TestCase):
- def setUp(self):
- # Redirect logging to a string buffer
- self.log_capture_string = io.StringIO()
- self.log_handler = logging.StreamHandler(self.log_capture_string)
- self.log_handler.setLevel(logging.WARNING)
-
- self.logger = logging.getLogger("libdebug")
- self.original_handlers = self.logger.handlers
- self.logger.handlers = []
- self.logger.addHandler(self.log_handler)
- self.logger.setLevel(logging.WARNING)
-
- def test_bps_auto_waiting(self):
- d = debugger("binaries/breakpoint_test", auto_interrupt_on_command=False)
+class AutoWaitingTest(TestCase):
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_bps_auto_waiting_amd64(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"), auto_interrupt_on_command=False)
d.run()
bp1 = d.breakpoint("random_function")
- bp2 = d.breakpoint(0x7fc, file="binary")
- bp3 = d.breakpoint(0x820, file="binary")
+ bp2 = d.breakpoint(0x40115B)
+ bp3 = d.breakpoint(0x40116D)
counter = 1
d.cont()
while True:
- if d.regs.pc == bp1.address:
+ if d.regs.rip == bp1.address:
self.assertTrue(bp1.hit_count == 1)
self.assertTrue(bp1.hit_on(d))
self.assertFalse(bp2.hit_on(d))
self.assertFalse(bp3.hit_on(d))
- elif d.regs.pc == bp2.address:
+ elif d.regs.rip == bp2.address:
self.assertTrue(bp2.hit_count == counter)
self.assertTrue(bp2.hit_on(d))
self.assertFalse(bp1.hit_on(d))
self.assertFalse(bp3.hit_on(d))
counter += 1
- elif d.regs.pc == bp3.address:
+ elif d.regs.rip == bp3.address:
self.assertTrue(bp3.hit_count == 1)
- self.assertTrue(d.regs.x1 == 45)
- self.assertTrue(d.regs.w1 == 45)
+ self.assertTrue(d.regs.rsi == 45)
+ self.assertTrue(d.regs.esi == 45)
+ self.assertTrue(d.regs.si == 45)
+ self.assertTrue(d.regs.sil == 45)
self.assertTrue(bp3.hit_on(d))
self.assertFalse(bp1.hit_on(d))
self.assertFalse(bp2.hit_on(d))
@@ -60,4 +50,5 @@ def test_bps_auto_waiting(self):
d.cont()
- d.kill()
\ No newline at end of file
+ d.kill()
+
diff --git a/test/scripts/backtrace_test.py b/test/scripts/backtrace_test.py
new file mode 100644
index 00000000..9d583433
--- /dev/null
+++ b/test/scripts/backtrace_test.py
@@ -0,0 +1,556 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from unittest import TestCase, skipUnless
+from utils.binary_utils import BASE, PLATFORM, RESOLVE_EXE
+
+from libdebug import debugger
+from libdebug.utils.libcontext import libcontext
+
+
+class BacktraceTest(TestCase):
+ def setUp(self):
+ self.d = debugger(RESOLVE_EXE("backtrace_test"), aslr=False)
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_backtrace_as_symbols_amd64(self):
+ d = self.d
+
+ d.run()
+
+ bp0 = d.breakpoint("main+8")
+ bp1 = d.breakpoint("function1+8")
+ bp2 = d.breakpoint("function2+8")
+ bp3 = d.breakpoint("function3+8")
+ bp4 = d.breakpoint("function4+8")
+ bp5 = d.breakpoint("function5+8")
+ bp6 = d.breakpoint("function6+8")
+
+ d.cont()
+
+ self.assertTrue(d.regs.rip == bp0.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(backtrace[:1], ["main+8"])
+
+ d.cont()
+
+ self.assertTrue(d.regs.rip == bp1.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(backtrace[:2], ["function1+8", "main+16"])
+
+ d.cont()
+
+ self.assertTrue(d.regs.rip == bp2.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(backtrace[:3], ["function2+8", "function1+12", "main+16"])
+
+ d.cont()
+
+ self.assertTrue(d.regs.rip == bp3.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(
+ backtrace[:4], ["function3+8", "function2+1c", "function1+12", "main+16"]
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.rip == bp4.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(
+ backtrace[:5],
+ ["function4+8", "function3+1c", "function2+1c", "function1+12", "main+16"],
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.rip == bp5.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(
+ backtrace[:6],
+ [
+ "function5+8",
+ "function4+1c",
+ "function3+1c",
+ "function2+1c",
+ "function1+12",
+ "main+16",
+ ],
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.rip == bp6.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(
+ backtrace[:7],
+ [
+ "function6+8",
+ "function5+1c",
+ "function4+1c",
+ "function3+1c",
+ "function2+1c",
+ "function1+12",
+ "main+16",
+ ],
+ )
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_backtrace_amd64(self):
+ d = self.d
+
+ d.run()
+
+ bp0 = d.breakpoint("main+8")
+ bp1 = d.breakpoint("function1+8")
+ bp2 = d.breakpoint("function2+8")
+ bp3 = d.breakpoint("function3+8")
+ bp4 = d.breakpoint("function4+8")
+ bp5 = d.breakpoint("function5+8")
+ bp6 = d.breakpoint("function6+8")
+
+ d.cont()
+
+ self.assertTrue(d.regs.rip == bp0.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(backtrace[:1], [0x555555555151])
+
+ d.cont()
+
+ self.assertTrue(d.regs.rip == bp1.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(backtrace[:2], [0x55555555518a, 0x55555555515f])
+
+ d.cont()
+
+ self.assertTrue(d.regs.rip == bp2.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(backtrace[:3], [0x55555555519e, 0x555555555194, 0x55555555515f])
+
+ d.cont()
+
+ self.assertTrue(d.regs.rip == bp3.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(
+ backtrace[:4], [0x5555555551bc, 0x5555555551b2, 0x555555555194, 0x55555555515f]
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.rip == bp4.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(
+ backtrace[:5],
+ [0x5555555551da, 0x5555555551d0, 0x5555555551b2, 0x555555555194, 0x55555555515f],
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.rip == bp5.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(
+ backtrace[:6],
+ [
+ 0x5555555551f8,
+ 0x5555555551ee,
+ 0x5555555551d0,
+ 0x5555555551b2,
+ 0x555555555194,
+ 0x55555555515f,
+ ],
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.rip == bp6.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(
+ backtrace[:7],
+ [
+ 0x555555555216,
+ 0x55555555520c,
+ 0x5555555551ee,
+ 0x5555555551d0,
+ 0x5555555551b2,
+ 0x555555555194,
+ 0x55555555515f,
+ ],
+ )
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "aarch64", "Requires aarch64")
+ def test_backtrace_as_symbols_aarch64(self):
+ d = self.d
+
+ d.run()
+
+ bp0 = d.breakpoint("main+8")
+ bp1 = d.breakpoint("function1+8")
+ bp2 = d.breakpoint("function2+8")
+ bp3 = d.breakpoint("function3+8")
+ bp4 = d.breakpoint("function4+8")
+ bp5 = d.breakpoint("function5+8")
+ bp6 = d.breakpoint("function6+8")
+
+ d.cont()
+
+ self.assertTrue(d.regs.pc == bp0.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(backtrace[:1], ["main+8"])
+
+ d.cont()
+
+ self.assertTrue(d.regs.pc == bp1.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(backtrace[:2], ["function1+8", "main+c"])
+
+ d.cont()
+
+ self.assertTrue(d.regs.pc == bp2.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(backtrace[:3], ["function2+8", "function1+10", "main+c"])
+
+ d.cont()
+
+ self.assertTrue(d.regs.pc == bp3.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(
+ backtrace[:4], ["function3+8", "function2+18", "function1+10", "main+c"]
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.pc == bp4.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(
+ backtrace[:5],
+ ["function4+8", "function3+18", "function2+18", "function1+10", "main+c"],
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.pc == bp5.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(
+ backtrace[:6],
+ [
+ "function5+8",
+ "function4+18",
+ "function3+18",
+ "function2+18",
+ "function1+10",
+ "main+c",
+ ],
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.pc == bp6.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(
+ backtrace[:7],
+ [
+ "function6+8",
+ "function5+18",
+ "function4+18",
+ "function3+18",
+ "function2+18",
+ "function1+10",
+ "main+c",
+ ],
+ )
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "aarch64", "Requires aarch64")
+ def test_backtrace_aarch64(self):
+ d = self.d
+
+ d.run()
+
+ bp0 = d.breakpoint("main+8")
+ bp1 = d.breakpoint("function1+8")
+ bp2 = d.breakpoint("function2+8")
+ bp3 = d.breakpoint("function3+8")
+ bp4 = d.breakpoint("function4+8")
+ bp5 = d.breakpoint("function5+8")
+ bp6 = d.breakpoint("function6+8")
+
+ d.cont()
+
+ self.assertTrue(d.regs.pc == bp0.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(backtrace[:1], [BASE + 0x75c])
+
+ d.cont()
+
+ self.assertTrue(d.regs.pc == bp1.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(backtrace[:2], [BASE + 0x788, BASE + 0x760])
+
+ d.cont()
+
+ self.assertTrue(d.regs.pc == bp2.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(backtrace[:3], [BASE + 0x7a0, BASE + 0x790, BASE + 0x760])
+
+ d.cont()
+
+ self.assertTrue(d.regs.pc == bp3.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(
+ backtrace[:4], [BASE + 0x7c0, BASE + 0x7b0, BASE + 0x790, BASE + 0x760]
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.pc == bp4.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(
+ backtrace[:5], [BASE + 0x7e0, BASE + 0x7d0, BASE + 0x7b0, BASE + 0x790, BASE + 0x760]
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.pc == bp5.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(
+ backtrace[:6],
+ [
+ BASE + 0x800,
+ BASE + 0x7f0,
+ BASE + 0x7d0,
+ BASE + 0x7b0,
+ BASE + 0x790,
+ BASE + 0x760,
+ ],
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.pc == bp6.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(
+ backtrace[:7],
+ [
+ BASE + 0x820,
+ BASE + 0x810,
+ BASE + 0x7f0,
+ BASE + 0x7d0,
+ BASE + 0x7b0,
+ BASE + 0x790,
+ BASE + 0x760,
+ ],
+ )
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "i386", "Requires i386")
+ def test_backtrace_as_symbols_i386(self):
+ d = self.d
+
+ d.run()
+
+ bp0 = d.breakpoint("main+e")
+ bp1 = d.breakpoint("function1+9")
+ bp2 = d.breakpoint("function2+9")
+ bp3 = d.breakpoint("function3+9")
+ bp4 = d.breakpoint("function4+9")
+ bp5 = d.breakpoint("function5+9")
+ bp6 = d.breakpoint("function6+9")
+
+ d.cont()
+
+ self.assertTrue(d.regs.eip == bp0.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(backtrace[:1], ["main+e"])
+
+ d.cont()
+
+ self.assertTrue(d.regs.eip == bp1.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(backtrace[:2], ["function1+9", "main+16"])
+
+ d.cont()
+
+ self.assertTrue(d.regs.eip == bp2.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(backtrace[:3], ["function2+9", "function1+10", "main+16"])
+
+ d.cont()
+
+ self.assertTrue(d.regs.eip == bp3.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(
+ backtrace[:4], ["function3+9", "function2+15", "function1+10", "main+16"]
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.eip == bp4.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(
+ backtrace[:5],
+ ["function4+9", "function3+15", "function2+15", "function1+10", "main+16"],
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.eip == bp5.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(
+ backtrace[:6],
+ [
+ "function5+9",
+ "function4+15",
+ "function3+15",
+ "function2+15",
+ "function1+10",
+ "main+16",
+ ],
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.eip == bp6.address)
+ backtrace = d.backtrace(as_symbols=True)
+ self.assertIn("_start", backtrace.pop())
+ self.assertEqual(
+ backtrace[:7],
+ [
+ "function6+9",
+ "function5+15",
+ "function4+15",
+ "function3+15",
+ "function2+15",
+ "function1+10",
+ "main+16",
+ ],
+ )
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "i386", "Requires i386")
+ def test_backtrace_i386(self):
+ d = self.d
+
+ d.run()
+
+ bp0 = d.breakpoint("main+e")
+ bp1 = d.breakpoint("function1+9")
+ bp2 = d.breakpoint("function2+9")
+ bp3 = d.breakpoint("function3+9")
+ bp4 = d.breakpoint("function4+9")
+ bp5 = d.breakpoint("function5+9")
+ bp6 = d.breakpoint("function6+9")
+
+ d.cont()
+
+ self.assertTrue(d.regs.eip == bp0.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(backtrace[:1], [0x8049174])
+
+ d.cont()
+
+ self.assertTrue(d.regs.eip == bp1.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(backtrace[:2], [0x80491a8, 0x804917c])
+
+ d.cont()
+
+ self.assertTrue(d.regs.eip == bp2.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(backtrace[:3], [0x80491bd, 0x80491af, 0x804917c])
+
+ d.cont()
+
+ self.assertTrue(d.regs.eip == bp3.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(backtrace[:4], [0x80491d7, 0x80491c9, 0x80491af, 0x804917c])
+
+ d.cont()
+
+ self.assertTrue(d.regs.eip == bp4.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(
+ backtrace[:5], [0x80491f1, 0x80491e3, 0x80491c9, 0x80491af, 0x804917c]
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.eip == bp5.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(
+ backtrace[:6], [0x804920b, 0x80491fd, 0x80491e3, 0x80491c9, 0x80491af, 0x804917c]
+ )
+
+ d.cont()
+
+ self.assertTrue(d.regs.eip == bp6.address)
+ backtrace = d.backtrace()
+ backtrace.pop()
+ self.assertEqual(
+ backtrace[:7],
+ [
+ 0x8049225,
+ 0x8049217,
+ 0x80491fd,
+ 0x80491e3,
+ 0x80491c9,
+ 0x80491af,
+ 0x804917c,
+ ],
+ )
+
+ d.kill()
+ d.terminate()
diff --git a/test/scripts/breakpoint_test.py b/test/scripts/breakpoint_test.py
new file mode 100644
index 00000000..4fa35666
--- /dev/null
+++ b/test/scripts/breakpoint_test.py
@@ -0,0 +1,688 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+import io
+import logging
+from unittest import TestCase, skipUnless
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
+
+from libdebug import debugger
+
+
+match PLATFORM:
+ case "amd64":
+ TEST_BPS_ADDRESS_1 = 0x40115B
+ TEST_BPS_ADDRESS_2 = 0x40116D
+ TEST_BPS_ADDRESS_3 = 0x401162
+
+ TEST_BP_DISABLE_ON_CREATION_ADDRESS = 0x40119c
+
+ def CHECK_REGISTERS(harness, d):
+ harness.assertEqual(d.regs.rsi, 45)
+ harness.assertEqual(d.regs.esi, 45)
+ harness.assertEqual(d.regs.si, 45)
+ harness.assertEqual(d.regs.sil, 45)
+ case "aarch64":
+ TEST_BPS_ADDRESS_1 = 0x7fc
+ TEST_BPS_ADDRESS_2 = 0x820
+ TEST_BPS_ADDRESS_3 = 0x814
+
+ TEST_BP_DISABLE_ON_CREATION_ADDRESS = 0x854
+
+ def CHECK_REGISTERS(harness, d):
+ harness.assertEqual(d.regs.x1, 45)
+ harness.assertEqual(d.regs.w1, 45)
+ case "i386":
+ TEST_BPS_ADDRESS_1 = 0x11d0
+ TEST_BPS_ADDRESS_2 = 0x11ea
+ TEST_BPS_ADDRESS_3 = 0x11d7
+
+ TEST_BP_DISABLE_ON_CREATION_ADDRESS = 0x1235
+
+ def CHECK_REGISTERS(harness, d):
+ value = int.from_bytes(d.memory[d.regs.esp + 4, 4], "little")
+ harness.assertEqual(value, 45)
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+class BreakpointTest(TestCase):
+ def setUp(self):
+ # Redirect logging to a string buffer
+ self.log_capture_string = io.StringIO()
+ self.log_handler = logging.StreamHandler(self.log_capture_string)
+ self.log_handler.setLevel(logging.WARNING)
+
+ self.logger = logging.getLogger("libdebug")
+ self.original_handlers = self.logger.handlers
+ self.logger.handlers = []
+ self.logger.addHandler(self.log_handler)
+ self.logger.setLevel(logging.WARNING)
+
+
+ def test_bps(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"))
+
+ d.run()
+
+ bp1 = d.breakpoint("random_function")
+ bp2 = d.breakpoint(TEST_BPS_ADDRESS_1)
+ bp3 = d.breakpoint(TEST_BPS_ADDRESS_2)
+
+ counter = 1
+
+ d.cont()
+
+ while True:
+ if d.instruction_pointer == bp1.address:
+ self.assertTrue(bp1.hit_count == 1)
+ self.assertTrue(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ elif d.instruction_pointer == bp2.address:
+ self.assertTrue(bp2.hit_count == counter)
+ self.assertTrue(bp2.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ counter += 1
+ elif d.instruction_pointer == bp3.address:
+ self.assertTrue(bp3.hit_count == 1)
+ CHECK_REGISTERS(self, d)
+ self.assertTrue(bp3.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ break
+
+ d.cont()
+
+ self.assertEqual(bp2.hit_count, 10)
+
+ d.kill()
+ d.terminate()
+
+ def test_bps_waiting(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"), auto_interrupt_on_command=True)
+
+ d.run()
+
+ bp1 = d.breakpoint("random_function")
+ bp2 = d.breakpoint(TEST_BPS_ADDRESS_1)
+ bp3 = d.breakpoint(TEST_BPS_ADDRESS_2)
+
+ counter = 1
+
+ d.cont()
+
+ while True:
+ d.wait()
+ if d.instruction_pointer == bp1.address:
+ self.assertTrue(bp1.hit_count == 1)
+ self.assertTrue(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ elif d.instruction_pointer == bp2.address:
+ self.assertTrue(bp2.hit_count == counter)
+ self.assertTrue(bp2.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ counter += 1
+ elif d.instruction_pointer == bp3.address:
+ self.assertTrue(bp3.hit_count == 1)
+ CHECK_REGISTERS(self, d)
+ self.assertTrue(bp3.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ break
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ def test_bp_disable(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"))
+
+ d.run()
+
+ bp1 = d.breakpoint("random_function")
+ bp2 = d.breakpoint(TEST_BPS_ADDRESS_1)
+ bp3 = d.breakpoint(TEST_BPS_ADDRESS_2)
+
+ counter = 1
+
+ d.cont()
+
+ while True:
+ if d.instruction_pointer == bp1.address:
+ self.assertTrue(bp1.hit_count == 1)
+ self.assertTrue(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ elif d.instruction_pointer == bp2.address:
+ self.assertTrue(bp2.hit_count == counter)
+ self.assertTrue(bp2.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ bp2.disable()
+ elif d.instruction_pointer == bp3.address:
+ self.assertTrue(bp3.hit_count == 1)
+ CHECK_REGISTERS(self, d)
+ self.assertTrue(bp3.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ break
+
+ d.cont()
+
+ self.assertEqual(bp2.hit_count, 1)
+
+ d.kill()
+ d.terminate()
+
+ def test_bp_disable_hw(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"))
+
+ d.run()
+
+ bp1 = d.breakpoint("random_function")
+ bp2 = d.breakpoint(TEST_BPS_ADDRESS_1, hardware=True)
+ bp3 = d.breakpoint(TEST_BPS_ADDRESS_2)
+
+ counter = 1
+
+ d.cont()
+
+ while True:
+ if d.instruction_pointer == bp1.address:
+ self.assertTrue(bp1.hit_count == 1)
+ self.assertTrue(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ elif d.instruction_pointer == bp2.address:
+ self.assertTrue(bp2.hit_count == counter)
+ self.assertTrue(bp2.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ bp2.disable()
+ elif d.instruction_pointer == bp3.address:
+ self.assertTrue(bp3.hit_count == 1)
+ CHECK_REGISTERS(self, d)
+ self.assertTrue(bp3.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ break
+
+ d.cont()
+
+ self.assertEqual(bp2.hit_count, 1)
+
+ d.kill()
+ d.terminate()
+
+ def test_bp_disable_reenable(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"))
+
+ d.run()
+
+ bp1 = d.breakpoint("random_function")
+ bp2 = d.breakpoint(TEST_BPS_ADDRESS_1)
+ bp4 = d.breakpoint(TEST_BPS_ADDRESS_3)
+ bp3 = d.breakpoint(TEST_BPS_ADDRESS_2)
+
+ counter = 1
+
+ d.cont()
+
+ while True:
+ if d.instruction_pointer == bp1.address:
+ self.assertTrue(bp1.hit_count == 1)
+ self.assertTrue(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ elif d.instruction_pointer == bp2.address:
+ self.assertTrue(bp2.hit_count == counter)
+ self.assertTrue(bp2.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ if bp4.enabled:
+ bp4.disable()
+ else:
+ bp4.enable()
+ counter += 1
+ elif d.instruction_pointer == bp3.address:
+ self.assertTrue(bp3.hit_count == 1)
+ CHECK_REGISTERS(self, d)
+ self.assertTrue(bp3.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ break
+ elif bp4.hit_on(d):
+ pass
+
+ d.cont()
+
+ self.assertEqual(bp4.hit_count, bp2.hit_count // 2 + 1)
+
+ d.kill()
+ d.terminate()
+
+ def test_bp_disable_reenable_hw(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"))
+
+ d.run()
+
+ bp1 = d.breakpoint("random_function")
+ bp2 = d.breakpoint(TEST_BPS_ADDRESS_1)
+ bp4 = d.breakpoint(TEST_BPS_ADDRESS_3, hardware=True)
+ bp3 = d.breakpoint(TEST_BPS_ADDRESS_2)
+
+ counter = 1
+
+ d.cont()
+
+ while True:
+ if d.instruction_pointer == bp1.address:
+ self.assertTrue(bp1.hit_count == 1)
+ self.assertTrue(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ elif d.instruction_pointer == bp2.address:
+ self.assertTrue(bp2.hit_count == counter)
+ self.assertTrue(bp2.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ if bp4.enabled:
+ bp4.disable()
+ else:
+ bp4.enable()
+ counter += 1
+ elif d.instruction_pointer == bp3.address:
+ self.assertTrue(bp3.hit_count == 1)
+ CHECK_REGISTERS(self, d)
+ self.assertTrue(bp3.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ break
+ elif bp4.hit_on(d):
+ pass
+
+ d.cont()
+
+ self.assertEqual(bp4.hit_count, bp2.hit_count // 2 + 1)
+
+ d.kill()
+ d.terminate()
+
+ def test_bps_running(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"))
+
+ d.run()
+
+ bp1 = d.breakpoint("random_function")
+ bp2 = d.breakpoint(TEST_BPS_ADDRESS_1)
+ bp3 = d.breakpoint(TEST_BPS_ADDRESS_2)
+
+ counter = 1
+
+ d.cont()
+
+ while True:
+ if d.running:
+ pass
+ if d.instruction_pointer == bp1.address:
+ self.assertFalse(d.running)
+ self.assertTrue(bp1.hit_count == 1)
+ self.assertTrue(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ elif d.instruction_pointer == bp2.address:
+ self.assertFalse(d.running)
+ self.assertTrue(bp2.hit_count == counter)
+ self.assertTrue(bp2.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp3.hit_on(d))
+ counter += 1
+ elif d.instruction_pointer == bp3.address:
+ self.assertFalse(d.running)
+ self.assertTrue(bp3.hit_count == 1)
+ CHECK_REGISTERS(self, d)
+ self.assertTrue(bp3.hit_on(d))
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+ break
+
+ d.cont()
+
+ self.assertEqual(bp2.hit_count, 10)
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_bp_backing_file_amd64(self):
+ d = debugger(RESOLVE_EXE("executable_section_test"))
+
+ d.run()
+
+ bp1 = d.breakpoint(0x1266, file="binary")
+
+ d.cont()
+
+ d.wait()
+
+ if bp1.hit_on(d):
+ for vmap in d.maps:
+ if "x" in vmap.permissions and "anon" in vmap.backing_file:
+ section = vmap.backing_file
+ bp2 = d.breakpoint(0xD, file=section)
+ d.cont()
+
+ d.wait()
+
+ if bp2.hit_on(d):
+ self.assertEqual(d.memory[d.instruction_pointer], b"]")
+ self.assertEqual(d.regs.rax, 9)
+
+ d.kill()
+
+ self.assertEqual(bp1.hit_count, 1)
+ self.assertEqual(bp2.hit_count, 1)
+
+ d.run()
+
+ bp1 = d.breakpoint(0x1266, file="executable_section_test")
+
+ d.cont()
+
+ d.wait()
+
+ if bp1.hit_on(d):
+ for vmap in d.maps:
+ if "x" in vmap.permissions and "anon" in vmap.backing_file:
+ section = vmap.backing_file
+ bp2 = d.breakpoint(0xD, file=section)
+ d.cont()
+
+ d.wait()
+
+ if bp2.hit_on(d):
+ self.assertEqual(d.memory[d.instruction_pointer], b"]")
+ self.assertEqual(d.regs.rax, 9)
+
+ d.run()
+
+ bp1 = d.breakpoint(0x1266, file="hybrid")
+
+ d.cont()
+
+ d.wait()
+
+ if bp1.hit_on(d):
+ for vmap in d.maps:
+ if "x" in vmap.permissions and "anon" in vmap.backing_file:
+ section = vmap.backing_file
+ bp2 = d.breakpoint(0xD, file=section)
+ d.cont()
+
+ d.wait()
+
+ if bp2.hit_on(d):
+ self.assertEqual(d.memory[d.instruction_pointer], b"]")
+ self.assertEqual(d.regs.rax, 9)
+
+ d.kill()
+
+ self.assertEqual(bp1.hit_count, 1)
+ self.assertEqual(bp2.hit_count, 1)
+
+ d.run()
+
+ with self.assertRaises(ValueError):
+ d.breakpoint(0x1266, file="absolute")
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "aarch64", "Requires aarch64")
+ def test_bp_backing_file_aarch64(self):
+ d = debugger(RESOLVE_EXE("executable_section_test"))
+
+ d.run()
+
+ bp1 = d.breakpoint(0x968, file="binary")
+
+ d.cont()
+
+ d.wait()
+
+ if bp1.hit_on(d):
+ for vmap in d.maps:
+ if "x" in vmap.permissions and "anon" in vmap.backing_file:
+ section = vmap.backing_file
+ bp2 = d.breakpoint(0x10, file=section)
+ d.cont()
+
+ d.wait()
+
+ if bp2.hit_on(d):
+ self.assertEqual(d.memory[d.regs.pc, 4], bytes.fromhex("ff430091"))
+ self.assertEqual(d.regs.w0, 9)
+
+ d.kill()
+
+ self.assertEqual(bp1.hit_count, 1)
+ self.assertEqual(bp2.hit_count, 1)
+
+ d.run()
+
+ bp1 = d.breakpoint(0x968, file="executable_section_test")
+
+ d.cont()
+
+ d.wait()
+
+ if bp1.hit_on(d):
+ for vmap in d.maps:
+ if "x" in vmap.permissions and "anon" in vmap.backing_file:
+ section = vmap.backing_file
+ bp2 = d.breakpoint(0x10, file=section)
+ d.cont()
+
+ d.wait()
+
+ if bp2.hit_on(d):
+ self.assertEqual(d.memory[d.regs.pc, 4], bytes.fromhex("ff430091"))
+ self.assertEqual(d.regs.w0, 9)
+
+ d.run()
+
+ bp1 = d.breakpoint(0x968, file="hybrid")
+
+ d.cont()
+
+ d.wait()
+
+ if bp1.hit_on(d):
+ for vmap in d.maps:
+ if "x" in vmap.permissions and "anon" in vmap.backing_file:
+ section = vmap.backing_file
+ bp2 = d.breakpoint(0x10, file=section)
+ d.cont()
+
+ d.wait()
+
+ if bp2.hit_on(d):
+ self.assertEqual(d.memory[d.regs.pc, 4], bytes.fromhex("ff430091"))
+ self.assertEqual(d.regs.w0, 9)
+
+ d.kill()
+
+ self.assertEqual(bp1.hit_count, 1)
+ self.assertEqual(bp2.hit_count, 1)
+
+ d.run()
+
+ with self.assertRaises(ValueError):
+ d.breakpoint(0x968, file="absolute")
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "i386", "Requires i386")
+ def test_bp_backing_file_i386(self):
+ d = debugger(RESOLVE_EXE("executable_section_test"))
+
+ d.run()
+
+ bp1 = d.breakpoint(0x804926b, file="binary")
+
+ d.cont()
+
+ d.wait()
+
+ if bp1.hit_on(d):
+ for vmap in d.maps:
+ if "x" in vmap.permissions and "anon" in vmap.backing_file:
+ section = vmap.backing_file
+ bp2 = d.breakpoint(0x9, file=section)
+ d.cont()
+
+ d.wait()
+
+ if bp2.hit_on(d):
+ self.assertEqual(d.memory[d.instruction_pointer], b"]")
+ self.assertEqual(d.regs.eax, 9)
+
+ d.kill()
+
+ self.assertEqual(bp1.hit_count, 1)
+ self.assertEqual(bp2.hit_count, 1)
+
+ d.run()
+
+ bp1 = d.breakpoint(0x804926b, file="executable_section_test")
+
+ d.cont()
+
+ d.wait()
+
+ if bp1.hit_on(d):
+ for vmap in d.maps:
+ if "x" in vmap.permissions and "anon" in vmap.backing_file:
+ section = vmap.backing_file
+ bp2 = d.breakpoint(0x9, file=section)
+ d.cont()
+
+ d.wait()
+
+ if bp2.hit_on(d):
+ self.assertEqual(d.memory[d.instruction_pointer], b"]")
+ self.assertEqual(d.regs.eax, 9)
+
+ d.run()
+
+ bp1 = d.breakpoint(0x804926b, file="hybrid")
+
+ d.cont()
+
+ d.wait()
+
+ if bp1.hit_on(d):
+ for vmap in d.maps:
+ if "x" in vmap.permissions and "anon" in vmap.backing_file:
+ section = vmap.backing_file
+ bp2 = d.breakpoint(0x9, file=section)
+ d.cont()
+
+ d.wait()
+
+ if bp2.hit_on(d):
+ self.assertEqual(d.memory[d.instruction_pointer], b"]")
+ self.assertEqual(d.regs.eax, 9)
+
+ d.kill()
+
+ self.assertEqual(bp1.hit_count, 1)
+ self.assertEqual(bp2.hit_count, 1)
+
+ d.run()
+
+ with self.assertRaises(ValueError):
+ d.breakpoint(0x9D0, file="absolute")
+
+ d.kill()
+ d.terminate()
+
+ def test_bp_disable_on_creation(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"))
+
+ d.run()
+
+ bp1 = d.bp("random_function")
+ bp2 = d.bp(TEST_BP_DISABLE_ON_CREATION_ADDRESS)
+ bp1.disable()
+
+ d.cont()
+
+ self.assertFalse(bp1.hit_on(d))
+ self.assertTrue(bp2.hit_on(d))
+
+ d.kill()
+ d.terminate()
+
+ def test_bp_disable_on_creation_2(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"))
+
+ d.run()
+
+ bp = d.bp("random_function")
+
+ bp.disable()
+
+ d.cont()
+ d.wait()
+
+ # Validate we didn't segfault
+ self.assertTrue(d.dead)
+ self.assertIsNone(d.exit_signal)
+
+ d.kill()
+ d.terminate()
+
+ def test_bp_disable_on_creation_hardware(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"))
+
+ d.run()
+
+ bp1 = d.bp("random_function", hardware=True)
+ bp2 = d.bp(TEST_BP_DISABLE_ON_CREATION_ADDRESS)
+ bp1.disable()
+
+ d.cont()
+
+ self.assertFalse(bp1.hit_on(d))
+ self.assertTrue(bp2.hit_on(d))
+
+ d.kill()
+ d.terminate()
+
+ def test_bp_disable_on_creation_2_hardware(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"))
+
+ d.run()
+
+ bp = d.bp("random_function", hardware=True)
+
+ bp.disable()
+
+ d.cont()
+ d.wait()
+
+ # Validate we didn't segfault
+ self.assertTrue(d.dead)
+ self.assertIsNone(d.exit_signal)
+
+ d.kill()
+ d.terminate()
diff --git a/test/scripts/brute_test.py b/test/scripts/brute_test.py
new file mode 100644
index 00000000..5675d757
--- /dev/null
+++ b/test/scripts/brute_test.py
@@ -0,0 +1,110 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2023-2024 Gabriele Digregorio, Francesco Panebianco, Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+import string
+from unittest import TestCase
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
+
+from libdebug import debugger
+
+
+match PLATFORM:
+ case "amd64":
+ ADDRESS = 0x1222
+ case "aarch64":
+ ADDRESS = 0x974
+ case "i386":
+ ADDRESS = 0x1235
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+class BruteTest(TestCase):
+ def setUp(self):
+ self.exceptions = []
+
+ def test_bruteforce(self):
+ flag = ""
+ counter = 1
+
+ d = debugger(RESOLVE_EXE("brute_test"))
+
+ while not flag or flag != "BRUTINOBRUTONE":
+ for c in string.printable:
+ r = d.run()
+ bp = d.breakpoint(ADDRESS, hardware=True)
+ d.cont()
+
+ r.sendlineafter(b"chars\n", (flag + c).encode())
+
+
+ while bp.address == d.instruction_pointer:
+ d.cont()
+
+
+ if bp.hit_count > counter:
+ flag += c
+ counter = bp.hit_count
+ d.kill()
+ break
+
+ message = r.recvline()
+
+ d.kill()
+
+ if message == b"Giusto!":
+ flag += c
+ break
+
+ self.assertEqual(flag, "BRUTINOBRUTONE")
+ d.terminate()
+
+ def test_callback_bruteforce(self):
+ global flag
+ global counter
+ global new_counter
+
+ flag = ""
+ counter = 1
+ new_counter = 0
+
+ def brute_force(d, b):
+ global new_counter
+ try:
+ new_counter = b.hit_count
+ except Exception as e:
+ self.exceptions.append(e)
+
+ d = debugger(RESOLVE_EXE("brute_test"))
+ while True:
+ end = False
+ for c in string.printable:
+ r = d.run()
+
+ d.breakpoint(ADDRESS, callback=brute_force, hardware=True)
+ d.cont()
+
+ r.sendlineafter(b"chars\n", (flag + c).encode())
+
+ message = r.recvline()
+
+ if new_counter > counter:
+ flag += c
+ counter = new_counter
+ d.kill()
+ break
+ d.kill()
+ if message == b"Giusto!":
+ flag += c
+ end = True
+ break
+ if end:
+ break
+
+ self.assertEqual(flag, "BRUTINOBRUTONE")
+ d.terminate()
+
+ if self.exceptions:
+ raise self.exceptions[0]
diff --git a/test/aarch64/scripts/callback_test.py b/test/scripts/callback_test.py
similarity index 64%
rename from test/aarch64/scripts/callback_test.py
rename to test/scripts/callback_test.py
index e8ba0ef0..2b9c2efc 100644
--- a/test/aarch64/scripts/callback_test.py
+++ b/test/scripts/callback_test.py
@@ -1,16 +1,27 @@
#
# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
-# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio, Francesco Panebianco. All rights reserved.
+# Copyright (c) 2023-2024 Gabriele Digregorio, Francesco Panebianco, Roberto Alessandro Bertolini. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
-import string
-import unittest
+from unittest import TestCase, skipUnless
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
+from utils.thread_utils import FUN_ARG_0
from libdebug import debugger
-class CallbackTest(unittest.TestCase):
+match PLATFORM:
+ case "amd64":
+ TEST_CALLBACK_STEP_OFFSET = 1
+ case "aarch64":
+ TEST_CALLBACK_STEP_OFFSET = 4
+ case "i386":
+ TEST_CALLBACK_STEP_OFFSET = 1
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+class CallbackTest(TestCase):
def setUp(self):
self.exceptions = []
@@ -20,7 +31,7 @@ def test_callback_simple(self):
global hit
hit = False
- d = debugger("binaries/basic_test")
+ d = debugger(RESOLVE_EXE("basic_test"))
d.run()
@@ -40,6 +51,7 @@ def callback(thread, bp):
d.cont()
d.kill()
+ d.terminate()
self.assertTrue(hit)
@@ -52,7 +64,7 @@ def test_callback_simple_hardware(self):
global hit
hit = False
- d = debugger("binaries/basic_test")
+ d = debugger(RESOLVE_EXE("basic_test"))
d.run()
@@ -72,6 +84,7 @@ def callback(thread, bp):
d.cont()
d.kill()
+ d.terminate()
self.assertTrue(hit)
@@ -84,7 +97,7 @@ def test_callback_memory(self):
global hit
hit = False
- d = debugger("binaries/memory_test")
+ d = debugger(RESOLVE_EXE("memory_test"))
d.run()
@@ -93,14 +106,14 @@ def callback(thread, bp):
prev = bytes(range(256))
try:
- self.assertEqual(bp.address, thread.regs.pc)
+ self.assertEqual(bp.address, thread.instruction_pointer)
self.assertEqual(bp.hit_count, 1)
- self.assertEqual(thread.memory[thread.regs.x0, 256], prev)
+ self.assertEqual(thread.memory[FUN_ARG_0(thread), 256], prev)
- thread.memory[thread.regs.x0 + 128 :] = b"abcd123456"
+ thread.memory[FUN_ARG_0(thread) + 128 :] = b"abcd123456"
prev = prev[:128] + b"abcd123456" + prev[138:]
- self.assertEqual(thread.memory[thread.regs.x0, 256], prev)
+ self.assertEqual(thread.memory[FUN_ARG_0(thread), 256], prev)
except Exception as e:
self.exceptions.append(e)
@@ -111,98 +124,54 @@ def callback(thread, bp):
d.cont()
d.kill()
+ d.terminate()
self.assertTrue(hit)
if self.exceptions:
raise self.exceptions[0]
- def test_callback_bruteforce(self):
- global flag
- global counter
- global new_counter
-
- flag = ""
- counter = 1
- new_counter = 0
-
- def brute_force(d, b):
- global new_counter
- try:
- new_counter = b.hit_count
- except Exception as e:
- self.exceptions.append(e)
-
- d = debugger("binaries/brute_test")
- while True:
- end = False
- for c in string.printable:
- r = d.run()
-
- d.breakpoint(0x974, callback=brute_force, hardware=True)
- d.cont()
-
- r.sendlineafter(b"chars\n", (flag + c).encode())
-
- message = r.recvline()
-
- if new_counter > counter:
- flag += c
- counter = new_counter
- d.kill()
- break
- d.kill()
- if message == b"Giusto!":
- flag += c
- end = True
- break
- if end:
- break
-
- self.assertEqual(flag, "BRUTINOBRUTONE")
-
- if self.exceptions:
- raise self.exceptions[0]
-
def test_callback_exception(self):
self.exceptions.clear()
- d = debugger("binaries/basic_test")
+ d = debugger(RESOLVE_EXE("basic_test"))
d.run()
def callback(thread, bp):
# This operation should not raise any exception
- _ = d.regs.x0
+ _ = FUN_ARG_0(thread)
d.breakpoint("register_test", callback=callback, hardware=True)
d.cont()
d.kill()
+ d.terminate()
def test_callback_step(self):
self.exceptions.clear()
- d = debugger("binaries/basic_test")
+ d = debugger(RESOLVE_EXE("basic_test"))
d.run()
def callback(t, bp):
- self.assertEqual(t.regs.pc, bp.address)
+ self.assertEqual(t.instruction_pointer, bp.address)
d.step()
- self.assertEqual(t.regs.pc, bp.address + 4)
+ self.assertEqual(t.instruction_pointer, bp.address + TEST_CALLBACK_STEP_OFFSET)
d.breakpoint("register_test", callback=callback)
d.cont()
d.kill()
+ d = debugger(RESOLVE_EXE("basic_test"))
def test_callback_pid_accessible(self):
self.exceptions.clear()
- d = debugger("binaries/basic_test")
+ d = debugger(RESOLVE_EXE("basic_test"))
d.run()
@@ -217,13 +186,14 @@ def callback(t, bp):
d.cont()
d.kill()
+ d.terminate()
self.assertTrue(hit)
def test_callback_pid_accessible_alias(self):
self.exceptions.clear()
- d = debugger("binaries/basic_test")
+ d = debugger(RESOLVE_EXE("basic_test"))
d.run()
@@ -239,13 +209,14 @@ def callback(t, bp):
d.cont()
d.kill()
+ d.terminate()
self.assertTrue(hit)
def test_callback_tid_accessible_alias(self):
self.exceptions.clear()
- d = debugger("binaries/basic_test")
+ d = debugger(RESOLVE_EXE("basic_test"))
d.run()
@@ -260,5 +231,25 @@ def callback(t, bp):
d.cont()
d.kill()
+ d.terminate()
self.assertTrue(hit)
+
+ def test_callback_empty(self):
+ self.exceptions.clear()
+
+ d = debugger(RESOLVE_EXE("basic_test"))
+
+ d.run()
+
+ bp = d.breakpoint("register_test", callback=True)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(bp.hit_count, 1)
+
+ if self.exceptions:
+ raise self.exceptions[0]
diff --git a/test/scripts/control_flow_test.py b/test/scripts/control_flow_test.py
new file mode 100644
index 00000000..b8719f96
--- /dev/null
+++ b/test/scripts/control_flow_test.py
@@ -0,0 +1,327 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini, Francesco Panebianco. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from unittest import TestCase, skipUnless
+from utils.binary_utils import PLATFORM, BASE, RESOLVE_EXE
+
+from libdebug import debugger
+
+
+match PLATFORM:
+ case "amd64":
+ TEST_STEP_ALIAS_OFFSET_1 = 1
+ TEST_STEP_ALIAS_OFFSET_2 = 4
+
+ TEST_STEP_UNTIL_1_ADDRESS = 0x40119D
+
+ TEST_STEP_UNTIL_2_ADDRESS_1 = 0x401148
+ TEST_STEP_UNTIL_2_ADDRESS_2 = 0x40119D
+ TEST_STEP_UNTIL_2_ADDRESS_3 = 0x40115E
+
+ TEST_STEP_UNTIL_3_ADDRESS_1 = 0x401148
+ TEST_STEP_UNTIL_3_BP_1 = 0x40114F
+ TEST_STEP_UNTIL_3_BP_2 = 0x401156
+ TEST_STEP_UNTIL_3_BP_3 = 0x401162
+ TEST_STEP_UNTIL_3_ADDRESS_2 = 0x40119D
+ TEST_STEP_UNTIL_3_ADDRESS_3 = 0x40115E
+
+ TEST_STEP_AND_CONT_ADDRESS_1 = 0x401180
+ TEST_STEP_AND_CONT_ADDRESS_2 = 0x401183
+
+ TEST_STEP_UNTIL_AND_CONT_ADDRESS = 0x401180
+ case "aarch64":
+ TEST_STEP_ALIAS_OFFSET_1 = 4
+ TEST_STEP_ALIAS_OFFSET_2 = 8
+
+ TEST_STEP_UNTIL_1_ADDRESS = BASE + 0x854
+
+ TEST_STEP_UNTIL_2_ADDRESS_1 = 0x7fc
+ TEST_STEP_UNTIL_2_ADDRESS_2 = BASE + 0x854
+ TEST_STEP_UNTIL_2_ADDRESS_3 = BASE + 0x818
+
+ TEST_STEP_UNTIL_3_ADDRESS_1 = 0x7fc
+ TEST_STEP_UNTIL_3_BP_1 = 0x804
+ TEST_STEP_UNTIL_3_BP_2 = 0x80c
+ TEST_STEP_UNTIL_3_BP_3 = 0x808
+ TEST_STEP_UNTIL_3_ADDRESS_2 = BASE + 0x854
+ TEST_STEP_UNTIL_3_ADDRESS_3 = BASE + 0x818
+
+ TEST_STEP_AND_CONT_ADDRESS_1 = BASE + 0x83c
+ TEST_STEP_AND_CONT_ADDRESS_2 = BASE + 0x840
+
+ TEST_STEP_UNTIL_AND_CONT_ADDRESS = BASE + 0x83c
+ case "i386":
+ TEST_STEP_ALIAS_OFFSET_1 = 1
+ TEST_STEP_ALIAS_OFFSET_2 = 3
+
+ TEST_STEP_UNTIL_1_ADDRESS = BASE + 0x1238
+
+ TEST_STEP_UNTIL_2_ADDRESS_1 = BASE + 0x11bd
+ TEST_STEP_UNTIL_2_ADDRESS_2 = BASE + 0x1238
+ TEST_STEP_UNTIL_2_ADDRESS_3 = BASE + 0x11d3
+
+ TEST_STEP_UNTIL_3_ADDRESS_1 = BASE + 0x11bd
+ TEST_STEP_UNTIL_3_BP_1 = BASE + 0x11c4
+ TEST_STEP_UNTIL_3_BP_2 = BASE + 0x11cb
+ TEST_STEP_UNTIL_3_BP_3 = BASE + 0x11d7
+ TEST_STEP_UNTIL_3_ADDRESS_2 = BASE + 0x1238
+ TEST_STEP_UNTIL_3_ADDRESS_3 = BASE + 0x11d3
+
+ TEST_STEP_AND_CONT_ADDRESS_1 = BASE + 0x11fc
+ TEST_STEP_AND_CONT_ADDRESS_2 = BASE + 0x11ff
+
+ TEST_STEP_UNTIL_AND_CONT_ADDRESS = BASE + 0x11fc
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+
+class ControlFlowTest(TestCase):
+ def test_basic(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+ d.run()
+ bp = d.breakpoint("register_test")
+ d.cont()
+ self.assertTrue(bp.address == d.instruction_pointer)
+ d.cont()
+ d.kill()
+ d.terminate()
+
+ def test_basic_hardware(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+ d.run()
+ bp = d.breakpoint("register_test", hardware=True)
+ d.cont()
+ self.assertTrue(bp.address == d.instruction_pointer)
+ d.kill()
+ d.terminate()
+
+ def test_basic_pie(self):
+ d = debugger(RESOLVE_EXE("basic_test_pie"))
+ d.run()
+ bp = d.breakpoint("register_test")
+ d.cont()
+ self.assertTrue(bp.address == d.instruction_pointer)
+ d.kill()
+ d.terminate()
+
+ def test_step(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+
+ d.run()
+ bp = d.breakpoint("register_test")
+ d.cont()
+
+ self.assertTrue(bp.address == d.instruction_pointer)
+ self.assertTrue(bp.hit_count == 1)
+
+ d.step()
+
+ self.assertTrue(bp.address + TEST_STEP_ALIAS_OFFSET_1 == d.instruction_pointer)
+ self.assertTrue(bp.hit_count == 1)
+
+ d.step()
+
+ self.assertTrue(bp.address + TEST_STEP_ALIAS_OFFSET_2 == d.instruction_pointer)
+ self.assertTrue(bp.hit_count == 1)
+
+ d.cont()
+ d.kill()
+ d.terminate()
+
+ def test_step_hardware(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+
+ d.run()
+ bp = d.breakpoint("register_test", hardware=True)
+ d.cont()
+
+ self.assertTrue(bp.address == d.instruction_pointer)
+ self.assertTrue(bp.hit_count == 1)
+
+ d.step()
+
+ self.assertTrue(bp.address + TEST_STEP_ALIAS_OFFSET_1 == d.instruction_pointer)
+ self.assertTrue(bp.hit_count == 1)
+
+ d.step()
+
+ self.assertTrue(bp.address + TEST_STEP_ALIAS_OFFSET_2 == d.instruction_pointer)
+ self.assertTrue(bp.hit_count == 1)
+
+ d.cont()
+ d.kill()
+ d.terminate()
+
+ def test_step_until_1(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"), aslr=False)
+ d.run()
+
+ bp = d.breakpoint("main")
+ d.cont()
+
+ self.assertTrue(bp.hit_on(d))
+
+ d.step_until(TEST_STEP_UNTIL_1_ADDRESS)
+
+ self.assertTrue(d.instruction_pointer == TEST_STEP_UNTIL_1_ADDRESS)
+ self.assertTrue(bp.hit_count == 1)
+ self.assertFalse(bp.hit_on(d))
+
+ d.kill()
+ d.terminate()
+
+ def test_step_until_2(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"), aslr=False)
+ d.run()
+
+ bp = d.breakpoint(TEST_STEP_UNTIL_2_ADDRESS_1, hardware=True)
+ d.cont()
+
+ self.assertTrue(bp.hit_on(d))
+
+ d.step_until(TEST_STEP_UNTIL_2_ADDRESS_2, max_steps=7)
+
+ self.assertTrue(d.instruction_pointer == TEST_STEP_UNTIL_2_ADDRESS_3)
+ self.assertTrue(bp.hit_count == 1)
+ self.assertFalse(bp.hit_on(d))
+
+ d.kill()
+ d.terminate()
+
+ def test_step_until_3(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"), aslr=False)
+ d.run()
+
+ bp = d.breakpoint(TEST_STEP_UNTIL_3_ADDRESS_1)
+
+ # Let's put some breakpoints in-between
+ d.breakpoint(TEST_STEP_UNTIL_3_BP_1)
+ d.breakpoint(TEST_STEP_UNTIL_3_BP_2)
+ d.breakpoint(TEST_STEP_UNTIL_3_BP_3, hardware=True)
+
+ d.cont()
+
+ self.assertTrue(bp.hit_on(d))
+
+ # trace is [0x401148, 0x40114f, 0x401156, 0x401162, 0x401166, 0x401158, 0x40115b, 0x40115e]
+ d.step_until(TEST_STEP_UNTIL_3_ADDRESS_2, max_steps=7)
+
+ self.assertTrue(d.instruction_pointer == TEST_STEP_UNTIL_3_ADDRESS_3)
+ self.assertTrue(bp.hit_count == 1)
+ self.assertFalse(bp.hit_on(d))
+
+ d.kill()
+ d.terminate()
+
+ def test_step_and_cont(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"), aslr=False)
+ d.run()
+
+ bp1 = d.breakpoint("main")
+ bp2 = d.breakpoint("random_function")
+ d.cont()
+
+ self.assertTrue(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+
+ d.step()
+ self.assertTrue(d.instruction_pointer == TEST_STEP_AND_CONT_ADDRESS_1)
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+
+ d.step()
+ self.assertTrue(d.instruction_pointer == TEST_STEP_AND_CONT_ADDRESS_2)
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+
+ d.cont()
+
+ self.assertTrue(bp2.hit_on(d))
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ def test_step_and_cont_hardware(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"), aslr=False)
+ d.run()
+
+ bp1 = d.breakpoint("main", hardware=True)
+ bp2 = d.breakpoint("random_function", hardware=True)
+ d.cont()
+
+ self.assertTrue(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+
+ d.step()
+ self.assertTrue(d.instruction_pointer == TEST_STEP_AND_CONT_ADDRESS_1)
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+
+ d.step()
+ self.assertTrue(d.instruction_pointer == TEST_STEP_AND_CONT_ADDRESS_2)
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+
+ d.cont()
+
+ self.assertTrue(bp2.hit_on(d))
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ def test_step_until_and_cont(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"), aslr=False)
+ d.run()
+
+ bp1 = d.breakpoint("main")
+ bp2 = d.breakpoint("random_function")
+ d.cont()
+
+ self.assertTrue(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+
+ d.step_until(TEST_STEP_UNTIL_AND_CONT_ADDRESS)
+ self.assertTrue(d.instruction_pointer == TEST_STEP_UNTIL_AND_CONT_ADDRESS)
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+
+ d.cont()
+
+ self.assertTrue(bp2.hit_on(d))
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ def test_step_until_and_cont_hardware(self):
+ d = debugger(RESOLVE_EXE("breakpoint_test"), aslr=False)
+ d.run()
+
+ bp1 = d.breakpoint("main", hardware=True)
+ bp2 = d.breakpoint("random_function", hardware=True)
+ d.cont()
+
+ self.assertTrue(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+
+ d.step_until(TEST_STEP_UNTIL_AND_CONT_ADDRESS)
+ self.assertTrue(d.instruction_pointer == TEST_STEP_UNTIL_AND_CONT_ADDRESS)
+ self.assertFalse(bp1.hit_on(d))
+ self.assertFalse(bp2.hit_on(d))
+
+ d.cont()
+
+ self.assertTrue(bp2.hit_on(d))
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
diff --git a/test/amd64/scripts/death_test.py b/test/scripts/death_test.py
similarity index 73%
rename from test/amd64/scripts/death_test.py
rename to test/scripts/death_test.py
index db51acf5..1ea5b2c4 100644
--- a/test/amd64/scripts/death_test.py
+++ b/test/scripts/death_test.py
@@ -6,12 +6,25 @@
import io
import logging
-import unittest
+from unittest import TestCase
+from utils.binary_utils import PLATFORM, BASE, RESOLVE_EXE
from libdebug import debugger
+from libdebug.utils.libcontext import libcontext
-class DeathTest(unittest.TestCase):
+match PLATFORM:
+ case "amd64":
+ DEATH_LOCATION = 0x55555555517F
+ case "aarch64":
+ DEATH_LOCATION = BASE + 0x784
+ case "i386":
+ DEATH_LOCATION = BASE + 0x11d4
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+
+class DeathTest(TestCase):
def setUp(self):
# Redirect logging to a string buffer
self.log_capture_string = io.StringIO()
@@ -30,7 +43,7 @@ def tearDown(self):
self.log_handler.close()
def test_io_death(self):
- d = debugger("binaries/segfault_test")
+ d = debugger(RESOLVE_EXE("segfault_test"))
r = d.run()
@@ -43,9 +56,10 @@ def test_io_death(self):
r.recvline()
d.kill()
+ d.terminate()
def test_cont_death(self):
- d = debugger("binaries/segfault_test")
+ d = debugger(RESOLVE_EXE("segfault_test"))
r = d.run()
@@ -65,7 +79,7 @@ def test_cont_death(self):
d.kill()
def test_instr_death(self):
- d = debugger("binaries/segfault_test")
+ d = debugger(RESOLVE_EXE("segfault_test"), aslr=False)
r = d.run()
@@ -76,12 +90,13 @@ def test_instr_death(self):
d.wait()
- self.assertEqual(d.regs.rip, 0x55555555517F)
+ self.assertEqual(d.instruction_pointer, DEATH_LOCATION)
d.kill()
+ d.terminate()
def test_exit_signal_death(self):
- d = debugger("binaries/segfault_test")
+ d = debugger(RESOLVE_EXE("segfault_test"))
r = d.run()
@@ -96,9 +111,10 @@ def test_exit_signal_death(self):
self.assertEqual(d.exit_signal, d.threads[0].exit_signal)
d.kill()
+ d.terminate()
def test_exit_code_death(self):
- d = debugger("binaries/segfault_test")
+ d = debugger(RESOLVE_EXE("segfault_test"))
r = d.run()
@@ -117,9 +133,10 @@ def test_exit_code_death(self):
)
d.kill()
+ d.terminate()
def test_exit_code_normal(self):
- d = debugger("binaries/basic_test")
+ d = debugger(RESOLVE_EXE("basic_test"))
d.run()
@@ -137,9 +154,10 @@ def test_exit_code_normal(self):
)
d.kill()
+ d.terminate()
def test_post_mortem_after_kill(self):
- d = debugger("binaries/basic_test")
+ d = debugger(RESOLVE_EXE("basic_test"))
d.run()
@@ -149,6 +167,8 @@ def test_post_mortem_after_kill(self):
d.kill()
# We should be able to access the registers also after the process has been killed
- d.regs.rax
- d.regs.rbx
- d.regs.rcx
+ d.instruction_pointer
+ d.syscall_arg0
+ d.syscall_arg1
+
+ d.terminate()
diff --git a/test/scripts/deep_dive_division_test.py b/test/scripts/deep_dive_division_test.py
new file mode 100644
index 00000000..a0283fdc
--- /dev/null
+++ b/test/scripts/deep_dive_division_test.py
@@ -0,0 +1,113 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2023-2024 Gabriele Digregorio. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+#
+# deep-dive-division - challenge from KalmarCTF 2024
+#
+
+import string
+from unittest import TestCase, skipUnless
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
+
+from libdebug import debugger
+
+
+class DeepDiveDivisionTest(TestCase):
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_deep_dive_division_recvline(self):
+ def brutone(flag, current):
+ def checkino(d, b):
+ nonlocal counter
+ if int.from_bytes(d.memory[d.regs.rax + d.regs.r9, 1, "absolute"], "little") == 0:
+ counter += 1
+
+ candidate = []
+ for c in string.printable:
+ counter = 0
+ r = d.run()
+ d.breakpoint(0x4012F2, hardware=True, callback=checkino)
+ d.cont()
+ r.sendlineafter(b"flag?", flag + c.encode())
+ r.recvline(2)
+
+ d.kill()
+ if counter > current:
+ candidate.append(c)
+ return candidate
+
+ d = debugger(RESOLVE_EXE("CTF/deep-dive-division"))
+ candidate = {}
+
+ flag = b""
+ current = 6
+
+ candidate = brutone(flag, current)
+ while True:
+ if len(candidate) == 0:
+ break
+ elif len(candidate) == 1:
+ current += 1
+ flag += candidate[0].encode()
+ candidate = brutone(flag, current)
+ else:
+ current += 1
+
+ for c in candidate:
+ flag_ = flag + c.encode()
+ candidate = brutone(flag_, current)
+ if candidate != []:
+ flag = flag_
+ break
+
+ self.assertEqual(flag, b"kalmar{vm_in_3d_space!_cb3992b605aafe137}\n")
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_deep_dive_division_recv(self):
+ def brutone(flag, current):
+ def checkino(d, b):
+ nonlocal counter
+ if int.from_bytes(d.memory[d.regs.rax + d.regs.r9, 1, "absolute"], "little") == 0:
+ counter += 1
+
+ candidate = []
+ for c in string.printable:
+ counter = 0
+ r = d.run()
+ d.breakpoint(0x4012F2, hardware=True, callback=checkino)
+ d.cont()
+ r.sendlineafter(b"flag?", flag + c.encode())
+ r.recv(11)
+
+ d.kill()
+ if counter > current:
+ candidate.append(c)
+ return candidate
+
+ d = debugger(RESOLVE_EXE("CTF/deep-dive-division"))
+ candidate = {}
+
+ flag = b""
+ current = 6
+
+ candidate = brutone(flag, current)
+ while True:
+ if len(candidate) == 0:
+ break
+ elif len(candidate) == 1:
+ current += 1
+ flag += candidate[0].encode()
+ candidate = brutone(flag, current)
+ else:
+ current += 1
+
+ for c in candidate:
+ flag_ = flag + c.encode()
+ candidate = brutone(flag_, current)
+ if candidate != []:
+ flag = flag_
+ break
+
+ self.assertEqual(flag, b"kalmar{vm_in_3d_space!_cb3992b605aafe137}\n")
diff --git a/test/aarch64/scripts/finish_test.py b/test/scripts/finish_test.py
similarity index 59%
rename from test/aarch64/scripts/finish_test.py
rename to test/scripts/finish_test.py
index e2d7e025..112e8897 100644
--- a/test/aarch64/scripts/finish_test.py
+++ b/test/scripts/finish_test.py
@@ -3,26 +3,55 @@
# Copyright (c) 2024 Francesco Panebianco, Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
-import unittest
+
+from unittest import TestCase, skipIf
+from utils.binary_utils import PLATFORM, BASE, RESOLVE_EXE
from libdebug import debugger
from libdebug.architectures.stack_unwinding_provider import stack_unwinding_provider
-# Addresses of the dummy functions
-C_ADDRESS = 0xaaaaaaaa0914
-B_ADDRESS = 0xaaaaaaaa08fc
-A_ADDRESS = 0xaaaaaaaa0814
-# Addresses of noteworthy instructions
-RETURN_POINT_FROM_C = 0xaaaaaaaa0938
-RETURN_POINT_FROM_A = 0xaaaaaaaa0908
+match PLATFORM:
+ case "amd64":
+ # Addresses of the dummy functions
+ C_ADDRESS = 0x4011e3
+ B_ADDRESS = 0x4011d2
+ A_ADDRESS = 0x401146
+
+ # Addresses of noteworthy instructions
+ RETURN_POINT_FROM_C = 0x401202
+ RETURN_POINT_FROM_A = 0x4011e0
+
+ BREAKPOINT_LOCATION = 0x4011f1
+ case "aarch64":
+ # Addresses of the dummy functions
+ C_ADDRESS = BASE + 0x914
+ B_ADDRESS = BASE + 0x8fc
+ A_ADDRESS = BASE + 0x814
+
+ # Addresses of noteworthy instructions
+ RETURN_POINT_FROM_C = BASE + 0x938
+ RETURN_POINT_FROM_A = BASE + 0x908
+
+ BREAKPOINT_LOCATION = BASE + 0x920
+ case "i386":
+ # Addresses of the dummy functions
+ C_ADDRESS = BASE + 0x1262
+ B_ADDRESS = BASE+ 0x124a
+ A_ADDRESS = BASE + 0x11a9
-class FinishTest(unittest.TestCase):
- def setUp(self):
- pass
+ # Addresses of noteworthy instructions
+ RETURN_POINT_FROM_C = BASE + 0x128f
+ RETURN_POINT_FROM_A = BASE + 0x125f
+ BREAKPOINT_LOCATION = BASE + 0x1277
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+
+class FinishTest(TestCase):
def test_finish_exact_no_auto_interrupt_no_breakpoint(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
+ d = debugger(RESOLVE_EXE("finish_test"), auto_interrupt_on_command=False, aslr=False)
# ------------------ Block 1 ------------------ #
# Return from the first function call #
@@ -33,12 +62,12 @@ def test_finish_exact_no_auto_interrupt_no_breakpoint(self):
d.breakpoint(C_ADDRESS)
d.cont()
- self.assertEqual(d.regs.pc, C_ADDRESS)
+ self.assertEqual(d.instruction_pointer, C_ADDRESS)
# Finish function c
d.finish(heuristic="step-mode")
- self.assertEqual(d.regs.pc, RETURN_POINT_FROM_C)
+ self.assertEqual(d.instruction_pointer, RETURN_POINT_FROM_C)
d.kill()
@@ -51,17 +80,18 @@ def test_finish_exact_no_auto_interrupt_no_breakpoint(self):
d.breakpoint(A_ADDRESS)
d.cont()
- self.assertEqual(d.regs.pc, A_ADDRESS)
+ self.assertEqual(d.instruction_pointer, A_ADDRESS)
# Finish function a
d.finish(heuristic="step-mode")
- self.assertEqual(d.regs.pc, RETURN_POINT_FROM_A)
+ self.assertEqual(d.instruction_pointer, RETURN_POINT_FROM_A)
d.kill()
+ d.terminate()
def test_finish_heuristic_no_auto_interrupt_no_breakpoint(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
+ d = debugger(RESOLVE_EXE("finish_test"), auto_interrupt_on_command=False, aslr=False)
# ------------------ Block 1 ------------------ #
# Return from the first function call #
@@ -72,12 +102,12 @@ def test_finish_heuristic_no_auto_interrupt_no_breakpoint(self):
d.breakpoint(C_ADDRESS)
d.cont()
- self.assertEqual(d.regs.pc, C_ADDRESS)
+ self.assertEqual(d.instruction_pointer, C_ADDRESS)
# Finish function c
d.finish(heuristic="backtrace")
- self.assertEqual(d.regs.pc, RETURN_POINT_FROM_C)
+ self.assertEqual(d.instruction_pointer, RETURN_POINT_FROM_C)
d.kill()
@@ -90,17 +120,18 @@ def test_finish_heuristic_no_auto_interrupt_no_breakpoint(self):
d.breakpoint(A_ADDRESS)
d.cont()
- self.assertEqual(d.regs.pc, A_ADDRESS)
+ self.assertEqual(d.instruction_pointer, A_ADDRESS)
# Finish function a
d.finish(heuristic="backtrace")
- self.assertEqual(d.regs.pc, RETURN_POINT_FROM_A)
+ self.assertEqual(d.instruction_pointer, RETURN_POINT_FROM_A)
d.kill()
+ d.terminate()
def test_finish_exact_auto_interrupt_no_breakpoint(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=True)
+ d = debugger(RESOLVE_EXE("finish_test"), auto_interrupt_on_command=True, aslr=False)
# ------------------ Block 1 ------------------ #
# Return from the first function call #
@@ -112,12 +143,12 @@ def test_finish_exact_auto_interrupt_no_breakpoint(self):
d.cont()
d.wait()
- self.assertEqual(d.regs.pc, C_ADDRESS)
+ self.assertEqual(d.instruction_pointer, C_ADDRESS)
# Finish function c
d.finish(heuristic="step-mode")
- self.assertEqual(d.regs.pc, RETURN_POINT_FROM_C)
+ self.assertEqual(d.instruction_pointer, RETURN_POINT_FROM_C)
d.kill()
@@ -131,17 +162,18 @@ def test_finish_exact_auto_interrupt_no_breakpoint(self):
d.cont()
d.wait()
- self.assertEqual(d.regs.pc, A_ADDRESS)
+ self.assertEqual(d.instruction_pointer, A_ADDRESS)
# Finish function a
d.finish(heuristic="step-mode")
- self.assertEqual(d.regs.pc, RETURN_POINT_FROM_A)
+ self.assertEqual(d.instruction_pointer, RETURN_POINT_FROM_A)
d.kill()
+ d.terminate()
def test_finish_heuristic_auto_interrupt_no_breakpoint(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=True)
+ d = debugger(RESOLVE_EXE("finish_test"), auto_interrupt_on_command=True, aslr=False)
# ------------------ Block 1 ------------------ #
# Return from the first function call #
@@ -153,12 +185,12 @@ def test_finish_heuristic_auto_interrupt_no_breakpoint(self):
d.cont()
d.wait()
- self.assertEqual(d.regs.pc, C_ADDRESS)
+ self.assertEqual(d.instruction_pointer, C_ADDRESS)
# Finish function c
d.finish(heuristic="backtrace")
- self.assertEqual(d.regs.pc, RETURN_POINT_FROM_C)
+ self.assertEqual(d.instruction_pointer, RETURN_POINT_FROM_C)
d.kill()
@@ -172,62 +204,65 @@ def test_finish_heuristic_auto_interrupt_no_breakpoint(self):
d.cont()
d.wait()
- self.assertEqual(d.regs.pc, A_ADDRESS)
+ self.assertEqual(d.instruction_pointer, A_ADDRESS)
# Finish function a
d.finish(heuristic="backtrace")
- self.assertEqual(d.regs.pc, RETURN_POINT_FROM_A)
+ self.assertEqual(d.instruction_pointer, RETURN_POINT_FROM_A)
d.kill()
+ d.terminate()
def test_finish_exact_no_auto_interrupt_breakpoint(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
+ d = debugger(RESOLVE_EXE("finish_test"), auto_interrupt_on_command=False, aslr=False)
# Reach function c
d.run()
d.breakpoint(C_ADDRESS)
d.cont()
- self.assertEqual(d.regs.pc, C_ADDRESS)
+ self.assertEqual(d.instruction_pointer, C_ADDRESS)
d.breakpoint(A_ADDRESS)
# Finish function c
d.finish(heuristic="step-mode")
- self.assertEqual(d.regs.pc, A_ADDRESS, f"Expected {hex(A_ADDRESS)} but got {hex(d.regs.pc)}")
+ self.assertEqual(d.instruction_pointer, A_ADDRESS, f"Expected {hex(A_ADDRESS)} but got {hex(d.instruction_pointer)}")
d.kill()
+ d.terminate()
def test_finish_heuristic_no_auto_interrupt_breakpoint(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
+ d = debugger(RESOLVE_EXE("finish_test"), auto_interrupt_on_command=False, aslr=False)
# Reach function c
d.run()
d.breakpoint(C_ADDRESS)
d.cont()
- self.assertEqual(d.regs.pc, C_ADDRESS)
+ self.assertEqual(d.instruction_pointer, C_ADDRESS)
d.breakpoint(A_ADDRESS)
# Finish function c
d.finish(heuristic="backtrace")
- self.assertEqual(d.regs.pc, A_ADDRESS)
+ self.assertEqual(d.instruction_pointer, A_ADDRESS)
d.kill()
+ d.terminate()
def test_heuristic_return_address(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
+ d = debugger(RESOLVE_EXE("finish_test"), auto_interrupt_on_command=False, aslr=False)
# Reach function c
d.run()
d.breakpoint(C_ADDRESS)
d.cont()
- self.assertEqual(d.regs.pc, C_ADDRESS)
+ self.assertEqual(d.instruction_pointer, C_ADDRESS)
stack_unwinder = stack_unwinding_provider(d._internal_debugger.arch)
@@ -250,18 +285,17 @@ def test_heuristic_return_address(self):
self.assertEqual(curr_srip, RETURN_POINT_FROM_C)
d.kill()
+ d.terminate()
def test_exact_breakpoint_return(self):
- BREAKPOINT_LOCATION = 0xaaaaaaaa0920
-
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
+ d = debugger(RESOLVE_EXE("finish_test"), auto_interrupt_on_command=False, aslr=False)
# Reach function c
d.run()
d.breakpoint(C_ADDRESS)
d.cont()
- self.assertEqual(d.regs.pc, C_ADDRESS)
+ self.assertEqual(d.instruction_pointer, C_ADDRESS)
# Place a breakpoint at a location inbetween
@@ -270,21 +304,20 @@ def test_exact_breakpoint_return(self):
# Finish function c
d.finish(heuristic="step-mode")
- self.assertEqual(d.regs.pc, BREAKPOINT_LOCATION)
+ self.assertEqual(d.instruction_pointer, BREAKPOINT_LOCATION)
d.kill()
+ d.terminate()
def test_heuristic_breakpoint_return(self):
- BREAKPOINT_LOCATION = 0xaaaaaaaa0920
-
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
+ d = debugger(RESOLVE_EXE("finish_test"), auto_interrupt_on_command=False, aslr=False)
# Reach function c
d.run()
d.breakpoint(C_ADDRESS)
d.cont()
- self.assertEqual(d.regs.pc, C_ADDRESS)
+ self.assertEqual(d.instruction_pointer, C_ADDRESS)
# Place a breakpoint a location in between
@@ -293,19 +326,20 @@ def test_heuristic_breakpoint_return(self):
# Finish function c
d.finish(heuristic="backtrace")
- self.assertEqual(d.regs.pc, BREAKPOINT_LOCATION)
+ self.assertEqual(d.instruction_pointer, BREAKPOINT_LOCATION)
d.kill()
+ d.terminate()
def test_breakpoint_collision(self):
- d = debugger("binaries/finish_test", auto_interrupt_on_command=False)
+ d = debugger(RESOLVE_EXE("finish_test"), auto_interrupt_on_command=False, aslr=False)
# Reach function c
d.run()
d.breakpoint(C_ADDRESS)
d.cont()
- self.assertEqual(d.regs.pc, C_ADDRESS)
+ self.assertEqual(d.instruction_pointer, C_ADDRESS)
# Place a breakpoint at the same location as the return address
d.breakpoint(RETURN_POINT_FROM_C)
@@ -313,7 +347,7 @@ def test_breakpoint_collision(self):
# Finish function c
d.finish(heuristic="backtrace")
- self.assertEqual(d.regs.pc, RETURN_POINT_FROM_C)
+ self.assertEqual(d.instruction_pointer, RETURN_POINT_FROM_C)
self.assertFalse(d.running)
d.step()
@@ -323,3 +357,4 @@ def test_breakpoint_collision(self):
self.assertFalse(d.dead)
d.kill()
+ d.terminate()
diff --git a/test/scripts/floating_point_test.py b/test/scripts/floating_point_test.py
new file mode 100644
index 00000000..9b5cd4b5
--- /dev/null
+++ b/test/scripts/floating_point_test.py
@@ -0,0 +1,750 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+import sys
+from pathlib import Path
+from random import randint
+from unittest import TestCase, skipUnless
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
+
+from libdebug import debugger
+
+
+class FloatingPointTest(TestCase):
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_floating_point_reg_access_amd64(self):
+ # This test is divided into two parts, depending on the current hardware
+
+ # Let's check if we have AVX512
+ with Path("/proc/cpuinfo").open() as f:
+ cpuinfo = f.read()
+
+ if "avx512" in cpuinfo:
+ # Run an AVX512 test
+ self.amd64_avx512()
+ self.amd64_avx()
+ self.amd64_xmm()
+ self.amd64_mmx()
+ self.amd64_st()
+ elif "avx" in cpuinfo:
+ # Run an AVX test
+ self.amd64_avx()
+ self.amd64_xmm()
+ self.amd64_mmx()
+ self.amd64_st()
+ else:
+ # Run a generic test
+ self.amd64_xmm()
+ self.amd64_mmx()
+ self.amd64_st()
+
+ def amd64_avx512(self):
+ d = debugger(RESOLVE_EXE("floating_point_avx512_test"))
+
+ d.run()
+
+ bp1 = d.bp(0x40143E)
+ bp2 = d.bp(0x401467)
+
+ d.cont()
+
+ self.assertTrue(bp1.hit_on(d))
+
+ self.assertTrue(hasattr(d.regs, "xmm0"))
+ self.assertTrue(hasattr(d.regs, "xmm31"))
+ self.assertTrue(hasattr(d.regs, "ymm0"))
+ self.assertTrue(hasattr(d.regs, "ymm31"))
+ self.assertTrue(hasattr(d.regs, "zmm0"))
+ self.assertTrue(hasattr(d.regs, "zmm31"))
+
+ baseval = int.from_bytes(bytes(list(range(64))), "little")
+
+ for i in range(32):
+ self.assertEqual(getattr(d.regs, f"xmm{i}"), baseval & ((1 << 128) - 1))
+ self.assertEqual(getattr(d.regs, f"ymm{i}"), baseval & ((1 << 256) - 1))
+ self.assertEqual(getattr(d.regs, f"zmm{i}"), baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 504)
+
+ d.regs.zmm0 = 0xDEADBEEFDEADBEEF
+
+ d.cont()
+
+ self.assertTrue(bp2.hit_on(d))
+
+ for i in range(32):
+ val = randint(0, 2**512 - 1)
+ setattr(d.regs, f"zmm{i}", val)
+ self.assertEqual(getattr(d.regs, f"zmm{i}"), val)
+
+ d.kill()
+ d.terminate()
+
+ def amd64_avx(self):
+ d = debugger(RESOLVE_EXE("floating_point_avx2_test"))
+
+ d.run()
+
+ bp1 = d.bp(0x40159E)
+ bp2 = d.bp(0x4015C5)
+
+ d.cont()
+
+ self.assertTrue(bp1.hit_on(d))
+
+ self.assertTrue(hasattr(d.regs, "xmm0"))
+ self.assertTrue(hasattr(d.regs, "ymm0"))
+ self.assertTrue(hasattr(d.regs, "xmm15"))
+ self.assertTrue(hasattr(d.regs, "ymm15"))
+
+ baseval = int.from_bytes(bytes(list(range(0, 256, 17)) + list(range(16))), "little")
+
+ self.assertEqual(d.regs.ymm0, baseval)
+ self.assertEqual(d.regs.xmm0, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm1, baseval)
+ self.assertEqual(d.regs.xmm1, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm2, baseval)
+ self.assertEqual(d.regs.xmm2, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm3, baseval)
+ self.assertEqual(d.regs.xmm3, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm4, baseval)
+ self.assertEqual(d.regs.xmm4, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm5, baseval)
+ self.assertEqual(d.regs.xmm5, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm6, baseval)
+ self.assertEqual(d.regs.xmm6, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm7, baseval)
+ self.assertEqual(d.regs.xmm7, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm8, baseval)
+ self.assertEqual(d.regs.xmm8, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm9, baseval)
+ self.assertEqual(d.regs.xmm9, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm10, baseval)
+ self.assertEqual(d.regs.xmm10, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm11, baseval)
+ self.assertEqual(d.regs.xmm11, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm12, baseval)
+ self.assertEqual(d.regs.xmm12, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm13, baseval)
+ self.assertEqual(d.regs.xmm13, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm14, baseval)
+ self.assertEqual(d.regs.xmm14, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm15, baseval)
+ self.assertEqual(d.regs.xmm15, baseval & ((1 << 128) - 1))
+
+ d.regs.ymm0 = 0xDEADBEEFDEADBEEF
+
+ d.cont()
+
+ self.assertTrue(bp2.hit_on(d))
+
+ for i in range(16):
+ val = randint(0, 2**256 - 1)
+ setattr(d.regs, f"ymm{i}", val)
+ self.assertEqual(getattr(d.regs, f"xmm{i}"), val & ((1 << 128) - 1))
+ self.assertEqual(getattr(d.regs, f"ymm{i}"), val)
+
+ # validate that register states are correctly flushed and then restored
+ values = []
+
+ for i in range(16):
+ val = randint(0, 2**256 - 1)
+ setattr(d.regs, f"ymm{i}", val)
+ values.append(val)
+
+ d.step()
+
+ for i in range(16):
+ self.assertEqual(getattr(d.regs, f"ymm{i}"), values[i])
+
+ d.regs.ymm7 = 0xDEADBEEFDEADBEEF
+
+ for i in range(16):
+ if i == 7:
+ continue
+
+ self.assertEqual(getattr(d.regs, f"ymm{i}"), values[i])
+
+ d.step()
+
+ for i in range(16):
+ if i == 7:
+ continue
+
+ self.assertEqual(getattr(d.regs, f"ymm{i}"), values[i])
+
+ self.assertEqual(d.regs.ymm7, 0xDEADBEEFDEADBEEF)
+
+ d.kill()
+
+ def callback(t, _):
+ baseval = int.from_bytes(bytes(list(range(0, 256, 17)) + list(range(16))), "little")
+ for i in range(16):
+ self.assertEqual(getattr(d.regs, f"xmm{i}"), baseval & ((1 << 128) - 1))
+ self.assertEqual(getattr(d.regs, f"ymm{i}"), baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+
+ t.regs.ymm0 = 0xDEADBEEFDEADBEEF
+
+ d.run()
+
+ d.bp(0x40159E, callback=callback)
+ bp = d.bp(0x4015C5)
+
+ d.cont()
+
+ self.assertTrue(bp.hit_on(d))
+
+ d.kill()
+ d.terminate()
+
+ def amd64_xmm(self):
+ d = debugger(RESOLVE_EXE("floating_point_sse_test"))
+
+ d.run()
+
+ bp1 = d.bp(0x401372)
+ bp2 = d.bp(0x401399)
+
+ d.cont()
+
+ self.assertTrue(bp1.hit_on(d))
+
+ self.assertTrue(hasattr(d.regs, "xmm0"))
+ self.assertTrue(hasattr(d.regs, "xmm15"))
+
+ baseval = int.from_bytes(bytes(list(range(0, 256, 17))), "little")
+ self.assertEqual(d.regs.xmm0, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm1, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm2, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm3, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm4, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm5, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm6, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm7, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm8, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm9, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm10, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm11, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm12, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm13, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm14, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm15, baseval)
+
+ d.regs.xmm0 = 0xDEADBEEFDEADBEEF
+
+ d.cont()
+
+ self.assertTrue(bp2.hit_on(d))
+
+ for i in range(16):
+ val = randint(0, 2**128 - 1)
+ setattr(d.regs, f"xmm{i}", val)
+ self.assertEqual(getattr(d.regs, f"xmm{i}"), val)
+
+ d.kill()
+
+ def callback(t, _):
+ baseval = int.from_bytes(bytes(list(range(0, 256, 17))), "little")
+ for i in range(16):
+ self.assertEqual(getattr(d.regs, f"xmm{i}"), baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+
+ t.regs.xmm0 = 0xDEADBEEFDEADBEEF
+
+ d.run()
+
+ d.bp(0x401372, callback=callback)
+ bp = d.bp(0x401399)
+
+ d.cont()
+
+ self.assertTrue(bp.hit_on(d))
+
+ d.kill()
+ d.terminate()
+
+ def amd64_mmx(self):
+ d = debugger(RESOLVE_EXE("floating_point_mmx_test"))
+
+ d.run()
+
+ bp1 = d.bp(0x40119e)
+ bp2 = d.bp(0x4011be)
+
+ d.cont()
+
+ self.assertTrue(bp1.hit_on(d))
+
+ self.assertTrue(hasattr(d.regs, "mm0"))
+ self.assertTrue(hasattr(d.regs, "mm7"))
+
+ for i in range(8):
+ baseval = int.from_bytes(bytes(list(range(17 * i, 128 + 17 * i, 17))), "little")
+ self.assertEqual(getattr(d.regs, f"mm{i}"), baseval)
+
+ d.regs.mm0 = 0xDEADBEEFDEADBEEF
+
+ d.cont()
+
+ self.assertTrue(bp2.hit_on(d))
+
+ for i in range(8):
+ val = randint(0, 2**64 - 1)
+ setattr(d.regs, f"mm{i}", val)
+ self.assertEqual(getattr(d.regs, f"mm{i}"), val)
+
+ d.kill()
+ d.terminate()
+
+ def amd64_st(self):
+ d = debugger(RESOLVE_EXE("floating_point_mmx_test"))
+
+ d.run()
+
+ bp1 = d.bp(0x40124e)
+ bp2 = d.bp(0x401271)
+
+ d.cont()
+
+ self.assertTrue(bp1.hit_on(d))
+
+ self.assertTrue(hasattr(d.regs, "st0"))
+ self.assertTrue(hasattr(d.regs, "st7"))
+
+ self.assertAlmostEqual(d.regs.st0, 890.123)
+ self.assertAlmostEqual(d.regs.st1, 789.012)
+ self.assertAlmostEqual(d.regs.st2, 678.901)
+ self.assertAlmostEqual(d.regs.st3, 567.890)
+ self.assertAlmostEqual(d.regs.st4, 456.789)
+ self.assertAlmostEqual(d.regs.st5, 345.678)
+ self.assertAlmostEqual(d.regs.st6, 234.567)
+ self.assertAlmostEqual(d.regs.st7, 123.456)
+
+ d.regs.st0 = 1337.1337
+
+ d.cont()
+
+ self.assertTrue(bp2.hit_on(d))
+
+ for i in range(8):
+ val = randint(0, 2**64 - 1)
+ setattr(d.regs, f"st{i}", val)
+ self.assertAlmostEqual(getattr(d.regs, f"st{i}"), val)
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "aarch64", "Requires aarch64")
+ def test_floating_point_reg_access_aarch64(self):
+ d = debugger(RESOLVE_EXE("floating_point_test"))
+
+ d.run()
+
+ bp1 = d.bp(0xb10, file="binary")
+ bp2 = d.bp(0xb44, file="binary")
+
+ d.cont()
+
+ assert bp1.hit_on(d)
+
+ baseval = int.from_bytes(bytes(list(range(16))), sys.byteorder)
+
+ for i in range(16):
+ assert hasattr(d.regs, f"q{i}")
+ assert getattr(d.regs, f"q{i}") == baseval
+ assert getattr(d.regs, f"v{i}") == baseval
+ assert getattr(d.regs, f"d{i}") == baseval & 0xFFFFFFFFFFFFFFFF
+ assert getattr(d.regs, f"s{i}") == baseval & 0xFFFFFFFF
+ assert getattr(d.regs, f"h{i}") == baseval & 0xFFFF
+ assert getattr(d.regs, f"b{i}") == baseval & 0xFF
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+
+ baseval = int.from_bytes(bytes(list(range(128, 128 + 16, 1))), sys.byteorder)
+
+ for i in range(16, 32, 1):
+ assert hasattr(d.regs, f"q{i}")
+ assert getattr(d.regs, f"q{i}") == baseval
+ assert getattr(d.regs, f"v{i}") == baseval
+ assert getattr(d.regs, f"d{i}") == baseval & 0xFFFFFFFFFFFFFFFF
+ assert getattr(d.regs, f"s{i}") == baseval & 0xFFFFFFFF
+ assert getattr(d.regs, f"h{i}") == baseval & 0xFFFF
+ assert getattr(d.regs, f"b{i}") == baseval & 0xFF
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+
+ for i in range(32):
+ val = randint(0, (1 << 128) - 1)
+ setattr(d.regs, f"q{i}", val)
+ assert getattr(d.regs, f"q{i}") == val
+ assert getattr(d.regs, f"v{i}") == val
+
+ for i in range(32):
+ val = randint(0, (1 << 64) - 1)
+ setattr(d.regs, f"d{i}", val)
+ assert getattr(d.regs, f"d{i}") == val
+
+ for i in range(32):
+ val = randint(0, (1 << 32) - 1)
+ setattr(d.regs, f"s{i}", val)
+ assert getattr(d.regs, f"s{i}") == val
+
+ for i in range(32):
+ val = randint(0, (1 << 16) - 1)
+ setattr(d.regs, f"h{i}", val)
+ assert getattr(d.regs, f"h{i}") == val
+
+ for i in range(32):
+ val = randint(0, (1 << 8) - 1)
+ setattr(d.regs, f"b{i}", val)
+ assert getattr(d.regs, f"b{i}") == val
+
+ d.regs.q0 = 0xdeadbeefdeadbeef
+
+ d.cont()
+
+ assert bp2.hit_on(d)
+
+ @skipUnless(PLATFORM == "i386", "Requires i386")
+ def test_floating_point_reg_access_i386(self):
+ # This test is divided into two parts, depending on the current hardware
+
+ # Let's check if we have AVX512
+ with Path("/proc/cpuinfo").open() as f:
+ cpuinfo = f.read()
+
+ if "avx512" in cpuinfo:
+ # Run an AVX512 test
+ self.i386_avx512()
+ self.i386_avx()
+ self.i386_xmm()
+ self.i386_mmx()
+ self.i386_st()
+ elif "avx" in cpuinfo:
+ # Run an AVX test
+ self.i386_avx()
+ self.i386_xmm()
+ self.i386_mmx()
+ self.i386_st()
+ else:
+ # Run a generic test
+ self.i386_xmm()
+ self.i386_mmx()
+ self.i386_st()
+
+ def i386_avx512(self):
+ d = debugger(RESOLVE_EXE("floating_point_avx512_test"))
+
+ d.run()
+
+ bp1 = d.bp(0x804926e)
+ bp2 = d.bp(0x804928d)
+
+ d.cont()
+
+ self.assertTrue(bp1.hit_on(d))
+
+ self.assertTrue(hasattr(d.regs, "xmm0"))
+ self.assertTrue(hasattr(d.regs, "xmm7"))
+ self.assertTrue(hasattr(d.regs, "ymm0"))
+ self.assertTrue(hasattr(d.regs, "ymm7"))
+ self.assertTrue(hasattr(d.regs, "zmm0"))
+ self.assertTrue(hasattr(d.regs, "zmm7"))
+
+ baseval = int.from_bytes(bytes(list(range(64))), "little")
+
+ for i in range(8):
+ self.assertEqual(getattr(d.regs, f"xmm{i}"), baseval & ((1 << 128) - 1))
+ self.assertEqual(getattr(d.regs, f"ymm{i}"), baseval & ((1 << 256) - 1))
+ self.assertEqual(getattr(d.regs, f"zmm{i}"), baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 504)
+
+ d.regs.zmm0 = 0xdeadbeef
+
+ d.cont()
+
+ self.assertTrue(bp2.hit_on(d))
+
+ for i in range(8):
+ val = randint(0, 2**512 - 1)
+ setattr(d.regs, f"zmm{i}", val)
+ self.assertEqual(getattr(d.regs, f"zmm{i}"), val)
+
+ d.kill()
+ d.terminate()
+
+ def i386_avx(self):
+ d = debugger(RESOLVE_EXE("floating_point_avx2_test"))
+
+ d.run()
+
+ bp1 = d.bp(0x80493b9)
+ bp2 = d.bp(0x80493d6)
+
+ d.cont()
+
+ self.assertTrue(bp1.hit_on(d))
+
+ self.assertTrue(hasattr(d.regs, "xmm0"))
+ self.assertTrue(hasattr(d.regs, "ymm0"))
+ self.assertTrue(hasattr(d.regs, "xmm7"))
+ self.assertTrue(hasattr(d.regs, "ymm7"))
+
+ baseval = int.from_bytes(bytes(list(range(0, 256, 17)) + list(range(16))), "little")
+
+ self.assertEqual(d.regs.ymm0, baseval)
+ self.assertEqual(d.regs.xmm0, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm1, baseval)
+ self.assertEqual(d.regs.xmm1, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm2, baseval)
+ self.assertEqual(d.regs.xmm2, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm3, baseval)
+ self.assertEqual(d.regs.xmm3, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm4, baseval)
+ self.assertEqual(d.regs.xmm4, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm5, baseval)
+ self.assertEqual(d.regs.xmm5, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm6, baseval)
+ self.assertEqual(d.regs.xmm6, baseval & ((1 << 128) - 1))
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+ self.assertEqual(d.regs.ymm7, baseval)
+ self.assertEqual(d.regs.xmm7, baseval & ((1 << 128) - 1))
+
+ d.regs.ymm0 = 0xDEADBEEF
+
+ d.cont()
+
+ self.assertTrue(bp2.hit_on(d))
+
+ for i in range(8):
+ val = randint(0, 2**256 - 1)
+ setattr(d.regs, f"ymm{i}", val)
+ self.assertEqual(getattr(d.regs, f"xmm{i}"), val & ((1 << 128) - 1))
+ self.assertEqual(getattr(d.regs, f"ymm{i}"), val)
+
+ # validate that register states are correctly flushed and then restored
+ values = []
+
+ for i in range(8):
+ val = randint(0, 2**256 - 1)
+ setattr(d.regs, f"ymm{i}", val)
+ values.append(val)
+
+ d.step()
+
+ for i in range(8):
+ self.assertEqual(getattr(d.regs, f"ymm{i}"), values[i])
+
+ d.regs.ymm7 = 0xDEADBEEF
+
+ for i in range(8):
+ if i == 7:
+ continue
+
+ self.assertEqual(getattr(d.regs, f"ymm{i}"), values[i])
+
+ d.step()
+
+ for i in range(8):
+ if i == 7:
+ continue
+
+ self.assertEqual(getattr(d.regs, f"ymm{i}"), values[i])
+
+ self.assertEqual(d.regs.ymm7, 0xDEADBEEF)
+
+ d.kill()
+
+ def callback(t, _):
+ baseval = int.from_bytes(bytes(list(range(0, 256, 17)) + list(range(16))), "little")
+ for i in range(8):
+ self.assertEqual(getattr(d.regs, f"xmm{i}"), baseval & ((1 << 128) - 1))
+ self.assertEqual(getattr(d.regs, f"ymm{i}"), baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 248)
+
+ t.regs.ymm0 = 0xDEADBEEF
+
+ d.run()
+
+ d.bp(0x80493b9, callback=callback)
+ bp = d.bp(0x80493d6)
+
+ d.cont()
+
+ self.assertTrue(bp.hit_on(d))
+
+ d.kill()
+ d.terminate()
+
+ def i386_xmm(self):
+ d = debugger(RESOLVE_EXE("floating_point_sse_test"))
+
+ d.run()
+
+ bp1 = d.bp(0x804926d)
+ bp2 = d.bp(0x804928a)
+
+ d.cont()
+
+ self.assertTrue(bp1.hit_on(d))
+
+ self.assertTrue(hasattr(d.regs, "xmm0"))
+ self.assertTrue(hasattr(d.regs, "xmm7"))
+
+ baseval = int.from_bytes(bytes(list(range(0, 256, 17))), "little")
+ self.assertEqual(d.regs.xmm0, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm1, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm2, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm3, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm4, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm5, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm6, baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+ self.assertEqual(d.regs.xmm7, baseval)
+
+ d.regs.xmm0 = 0xDEADBEEF
+
+ d.cont()
+
+ self.assertTrue(bp2.hit_on(d))
+
+ for i in range(8):
+ val = randint(0, 2**128 - 1)
+ setattr(d.regs, f"xmm{i}", val)
+ self.assertEqual(getattr(d.regs, f"xmm{i}"), val)
+
+ d.kill()
+
+ def callback(t, _):
+ baseval = int.from_bytes(bytes(list(range(0, 256, 17))), "little")
+ for i in range(8):
+ self.assertEqual(getattr(d.regs, f"xmm{i}"), baseval)
+ baseval = (baseval >> 8) + ((baseval & 255) << 120)
+
+ t.regs.xmm0 = 0xdeadbeef
+
+ d.run()
+
+ d.bp(0x804926d, callback=callback)
+ bp = d.bp(0x804928a)
+
+ d.cont()
+
+ self.assertTrue(bp.hit_on(d))
+
+ d.kill()
+ d.terminate()
+
+ def i386_mmx(self):
+ d = debugger(RESOLVE_EXE("floating_point_mmx_test"))
+
+ d.run()
+
+ bp1 = d.bp(0x8049231)
+ bp2 = d.bp(0x8049251)
+
+ d.cont()
+
+ self.assertTrue(bp1.hit_on(d))
+
+ self.assertTrue(hasattr(d.regs, "mm0"))
+ self.assertTrue(hasattr(d.regs, "mm7"))
+
+ for i in range(8):
+ baseval = int.from_bytes(bytes(list(range(17 * i, 128 + 17 * i, 17))), "little")
+ self.assertEqual(getattr(d.regs, f"mm{i}"), baseval)
+
+ d.regs.mm0 = 0xdeadbeef
+
+ d.cont()
+
+ self.assertTrue(bp2.hit_on(d))
+
+ for i in range(8):
+ val = randint(0, 2**64 - 1)
+ setattr(d.regs, f"mm{i}", val)
+ self.assertEqual(getattr(d.regs, f"mm{i}"), val)
+
+ d.kill()
+ d.terminate()
+
+ def i386_st(self):
+ d = debugger(RESOLVE_EXE("floating_point_mmx_test"))
+
+ d.run()
+
+ bp1 = d.bp(0x80492c5)
+ bp2 = d.bp(0x80492f4)
+
+ d.cont()
+
+ self.assertTrue(bp1.hit_on(d))
+
+ self.assertTrue(hasattr(d.regs, "st0"))
+ self.assertTrue(hasattr(d.regs, "st7"))
+
+ self.assertAlmostEqual(d.regs.st0, 890.123)
+ self.assertAlmostEqual(d.regs.st1, 789.012)
+ self.assertAlmostEqual(d.regs.st2, 678.901)
+ self.assertAlmostEqual(d.regs.st3, 567.890)
+ self.assertAlmostEqual(d.regs.st4, 456.789)
+ self.assertAlmostEqual(d.regs.st5, 345.678)
+ self.assertAlmostEqual(d.regs.st6, 234.567)
+ self.assertAlmostEqual(d.regs.st7, 123.456)
+
+ d.regs.st0 = 1337.1337
+
+ d.cont()
+
+ self.assertTrue(bp2.hit_on(d))
+
+ for i in range(8):
+ val = randint(0, 2**64 - 1)
+ setattr(d.regs, f"st{i}", val)
+ self.assertAlmostEqual(getattr(d.regs, f"st{i}"), val)
+
+ d.kill()
+ d.terminate()
\ No newline at end of file
diff --git a/test/scripts/jumpout_test.py b/test/scripts/jumpout_test.py
new file mode 100644
index 00000000..092a292e
--- /dev/null
+++ b/test/scripts/jumpout_test.py
@@ -0,0 +1,219 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+#
+# jumpout - challenge from SECCON CTF 2023
+#
+
+from unittest import TestCase, skipUnless
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
+
+from libdebug import debugger
+
+
+class JumpoutTest(TestCase):
+ def setUp(self):
+ self.exceptions = []
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_jumpout(self):
+ flag = ""
+ first = 0x55
+ second = 0
+
+ d = debugger(RESOLVE_EXE("CTF/jumpout"))
+
+ r = d.run()
+
+ bp1 = d.breakpoint(0x140B, hardware=True, file="binary")
+ bp2 = d.breakpoint(0x157C, hardware=True, file="binary")
+
+ d.cont()
+
+ r.sendline(b"A" * 0x1D)
+
+ while True:
+ if d.regs.rip == bp1.address:
+ second = d.regs.r9
+ elif d.regs.rip == bp2.address:
+ address = d.regs.r13 + d.regs.rbx
+ third = int.from_bytes(d.memory[address, 1], "little")
+ flag += chr((first ^ second ^ third ^ (bp2.hit_count - 1)))
+
+ d.cont()
+
+ if flag.endswith("}"):
+ break
+
+ r.recvuntil(b"Wrong...")
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(flag, "SECCON{jump_table_everywhere}")
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_callback_jumpout(self):
+ global flag
+ global first
+ global second
+
+ flag = ""
+ first = 0x55
+
+ def second(d, b):
+ global second
+ try:
+ second = d.regs.r9
+ except Exception as e:
+ self.exceptions.append(e)
+
+ def third(d, b):
+ global flag
+ try:
+ address = d.regs.r13 + d.regs.rbx
+ third = int.from_bytes(d.memory[address : address + 1], "little")
+ flag += chr((first ^ second ^ third ^ (b.hit_count - 1)))
+ except Exception as e:
+ self.exceptions.append(e)
+
+ d = debugger(RESOLVE_EXE("CTF/jumpout"))
+ r = d.run()
+
+ d.breakpoint(0x140B, callback=second, hardware=True)
+ d.breakpoint(0x157C, callback=third, hardware=True)
+ d.cont()
+
+ r.sendline(b"A" * 0x1D)
+ r.recvuntil(b"Wrong...")
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(flag, "SECCON{jump_table_everywhere}")
+
+ if self.exceptions:
+ raise self.exceptions[0]
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_callback_intermixing(self):
+ global secval
+
+ flag = ""
+ first = 0x55
+
+ d = debugger(RESOLVE_EXE("CTF/jumpout"))
+ r = d.run()
+
+ def second(d, b):
+ global secval
+ try:
+ secval = d.regs.r9
+ except Exception as e:
+ self.exceptions.append(e)
+
+ d.breakpoint(0x140B, callback=second, hardware=True)
+ bp = d.breakpoint(0x157C, hardware=True)
+
+ d.cont()
+
+ r.sendline(b"A" * 0x1D)
+
+ while True:
+ if d.instruction_pointer == bp.address:
+ address = d.regs.r13 + d.regs.rbx
+ third = int.from_bytes(d.memory[address : address + 1], "little")
+ flag += chr((first ^ secval ^ third ^ (bp.hit_count - 1)))
+
+ d.cont()
+
+ if flag.endswith("}"):
+ break
+
+ r.recvuntil(b"Wrong...")
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(flag, "SECCON{jump_table_everywhere}")
+
+ if self.exceptions:
+ raise self.exceptions[0]
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_jumpout_auto_waiting(self):
+ flag = ""
+ first = 0x55
+ second = 0
+
+ d = debugger(RESOLVE_EXE("CTF/jumpout"), auto_interrupt_on_command=False)
+
+ r = d.run()
+
+ bp1 = d.breakpoint(0x140B, hardware=True, file="binary")
+ bp2 = d.breakpoint(0x157C, hardware=True, file="binary")
+
+ d.cont()
+
+ r.sendline(b"A" * 0x1D)
+
+ while True:
+ if d.regs.rip == bp1.address:
+ second = d.regs.r9
+ elif d.regs.rip == bp2.address:
+ address = d.regs.r13 + d.regs.rbx
+ third = int.from_bytes(d.memory[address, 1], "little")
+ flag += chr((first ^ second ^ third ^ (bp2.hit_count - 1)))
+
+ d.cont()
+
+ if flag.endswith("}"):
+ break
+
+ r.recvuntil(b"Wrong...")
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(flag, "SECCON{jump_table_everywhere}")
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_jumpout_waiting(self):
+ flag = ""
+ first = 0x55
+ second = 0
+
+ d = debugger(RESOLVE_EXE("CTF/jumpout"), auto_interrupt_on_command=True)
+
+ r = d.run()
+
+ bp1 = d.breakpoint(0x140B, hardware=True, file="binary")
+ bp2 = d.breakpoint(0x157C, hardware=True, file="binary")
+
+ d.cont()
+
+ r.sendline(b"A" * 0x1D)
+
+ while True:
+ d.wait()
+ if d.instruction_pointer == bp1.address:
+ second = d.regs.r9
+ elif d.instruction_pointer == bp2.address:
+ address = d.regs.r13 + d.regs.rbx
+ third = int.from_bytes(d.memory[address, 1], "little")
+ flag += chr((first ^ second ^ third ^ (bp2.hit_count - 1)))
+
+ d.cont()
+
+ if flag.endswith("}"):
+ break
+
+ r.recvuntil(b"Wrong...")
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(flag, "SECCON{jump_table_everywhere}")
diff --git a/test/aarch64/scripts/jumpstart_test.py b/test/scripts/jumpstart_test.py
similarity index 70%
rename from test/aarch64/scripts/jumpstart_test.py
rename to test/scripts/jumpstart_test.py
index d5e1d7a3..9c0adc16 100644
--- a/test/aarch64/scripts/jumpstart_test.py
+++ b/test/scripts/jumpstart_test.py
@@ -4,14 +4,15 @@
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
-import unittest
+from unittest import TestCase
+from utils.binary_utils import RESOLVE_EXE
from libdebug import debugger
-class JumpstartTest(unittest.TestCase):
+class JumpstartTest(TestCase):
def test_cursed_ldpreload(self):
- d = debugger("binaries/jumpstart_test", env={"LD_PRELOAD": "binaries/jumpstart_test_preload.so"})
+ d = debugger(RESOLVE_EXE("jumpstart_test"), env={"LD_PRELOAD": RESOLVE_EXE("jumpstart_test_preload.so")})
r = d.run()
@@ -22,3 +23,4 @@ def test_cursed_ldpreload(self):
self.assertEqual(r.recvline(), b"execve(/bin/ls, (nil), (nil))")
d.kill()
+ d.terminate()
diff --git a/test/amd64/scripts/large_binary_sym_test.py b/test/scripts/large_binary_sym_test.py
similarity index 86%
rename from test/amd64/scripts/large_binary_sym_test.py
rename to test/scripts/large_binary_sym_test.py
index b713de59..7a294e7c 100644
--- a/test/amd64/scripts/large_binary_sym_test.py
+++ b/test/scripts/large_binary_sym_test.py
@@ -4,15 +4,18 @@
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
-import unittest
+from unittest import TestCase, skipUnless
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
from time import perf_counter_ns
-from libdebug import debugger, libcontext
+from libdebug import debugger
+from libdebug.utils.libcontext import libcontext
-class LargeBinarySymTest(unittest.TestCase):
+class LargeBinarySymTest(TestCase):
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
def test_large_binary_symbol_load_times(self):
- d = debugger("binaries/node")
+ d = debugger(RESOLVE_EXE("node"))
d.run()
@@ -33,9 +36,11 @@ def test_large_binary_symbol_load_times(self):
self.assertTrue((t1_stop - t1_start) < 2e9)
d.kill()
+ d.terminate()
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
def test_large_binary_demangle(self):
- d = debugger("binaries/node")
+ d = debugger(RESOLVE_EXE("node"))
d.run()
@@ -63,3 +68,4 @@ def test_large_binary_demangle(self):
pass
d.kill()
+ d.terminate()
diff --git a/test/amd64/scripts/memory_fast_test.py b/test/scripts/memory_fast_test.py
similarity index 72%
rename from test/amd64/scripts/memory_fast_test.py
rename to test/scripts/memory_fast_test.py
index 80bcd7a9..a4943ab4 100644
--- a/test/amd64/scripts/memory_fast_test.py
+++ b/test/scripts/memory_fast_test.py
@@ -4,14 +4,18 @@
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
-import unittest
+from pwn import process
+from unittest import TestCase
+from utils.binary_utils import RESOLVE_EXE, base_of
+from utils.thread_utils import FUN_ARG_0
from libdebug import debugger, libcontext
+from libdebug.utils.platform_utils import get_platform_register_size
-class MemoryFastTest(unittest.TestCase):
+class MemoryFastTest(TestCase):
def test_memory(self):
- d = debugger("binaries/memory_test", fast_memory=True)
+ d = debugger(RESOLVE_EXE("memory_test"), fast_memory=True)
d.run()
@@ -19,9 +23,9 @@ def test_memory(self):
d.cont()
- assert d.regs.rip == bp.address
+ assert d.instruction_pointer == bp.address
- address = d.regs.rdi
+ address = FUN_ARG_0(d)
prev = bytes(range(256))
self.assertTrue(d.memory[address, 256] == prev)
@@ -35,7 +39,7 @@ def test_memory(self):
d.terminate()
def test_mem_access_libs(self):
- d = debugger("binaries/memory_test", fast_memory=True)
+ d = debugger(RESOLVE_EXE("memory_test"), fast_memory=True)
d.run()
@@ -43,22 +47,22 @@ def test_mem_access_libs(self):
d.cont()
- assert d.regs.rip == bp.address
+ assert d.instruction_pointer == bp.address
- address = d.regs.rdi
+ address = FUN_ARG_0(d)
with libcontext.tmp(sym_lvl=5):
arena = d.memory["main_arena", 256, "libc"]
- def p64(x):
- return x.to_bytes(8, "little")
+ def pack(x):
+ return x.to_bytes(get_platform_register_size(d.arch), "little")
- self.assertTrue(p64(address - 0x10) in arena)
+ self.assertTrue(pack(address - get_platform_register_size(d.arch) * 2) in arena)
d.kill()
d.terminate()
def test_memory_exceptions(self):
- d = debugger("binaries/memory_test", fast_memory=True)
+ d = debugger(RESOLVE_EXE("memory_test"), fast_memory=True)
d.run()
@@ -72,9 +76,9 @@ def test_memory_exceptions(self):
# File should start with ELF magic number
self.assertTrue(file.startswith(b"\x7fELF"))
- assert d.regs.rip == bp.address
+ assert d.instruction_pointer == bp.address
- address = d.regs.rdi
+ address = FUN_ARG_0(d)
prev = bytes(range(256))
self.assertTrue(d.memory[address, 256] == prev)
@@ -88,7 +92,7 @@ def test_memory_exceptions(self):
d.terminate()
def test_memory_multiple_runs(self):
- d = debugger("binaries/memory_test", fast_memory=True)
+ d = debugger(RESOLVE_EXE("memory_test"), fast_memory=True)
for _ in range(10):
d.run()
@@ -97,9 +101,9 @@ def test_memory_multiple_runs(self):
d.cont()
- assert d.regs.rip == bp.address
+ assert d.instruction_pointer == bp.address
- address = d.regs.rdi
+ address = FUN_ARG_0(d)
prev = bytes(range(256))
self.assertTrue(d.memory[address, 256] == prev)
@@ -114,7 +118,7 @@ def test_memory_multiple_runs(self):
d.terminate()
def test_memory_access_while_running(self):
- d = debugger("binaries/memory_test_2", fast_memory=True)
+ d = debugger(RESOLVE_EXE("memory_test_2"), fast_memory=True)
d.run()
@@ -125,17 +129,17 @@ def test_memory_access_while_running(self):
# Verify that memory access is only possible when the process is stopped
value = int.from_bytes(d.memory["state", 8], "little")
self.assertEqual(value, 0xDEADBEEF)
- self.assertEqual(d.regs.rip, bp.address)
+ self.assertEqual(d.instruction_pointer, bp.address)
d.kill()
d.terminate()
def test_memory_access_methods(self):
- d = debugger("binaries/memory_test_2", fast_memory=True)
+ d = debugger(RESOLVE_EXE("memory_test_2"), fast_memory=True)
d.run()
- base = d.regs.rip & 0xFFFFFFFFFFFFF000 - 0x1000
+ base = base_of(d)
# Test different ways to access memory at the start of the file
file_0 = d.memory[base, 256]
@@ -200,11 +204,11 @@ def test_memory_access_methods(self):
d.terminate()
def test_memory_access_methods_backing_file(self):
- d = debugger("binaries/memory_test_2", fast_memory=True)
+ d = debugger(RESOLVE_EXE("memory_test_2"), fast_memory=True)
d.run()
- base = d.regs.rip & 0xFFFFFFFFFFFFF000 - 0x1000
+ base = base_of(d)
# Validate that slices work correctly
file_0 = d.memory[0x0:"do_nothing", "binary"]
@@ -279,7 +283,7 @@ def test_memory_access_methods_backing_file(self):
d.terminate()
def test_memory_large_read(self):
- d = debugger("binaries/memory_test_3", fast_memory=True)
+ d = debugger(RESOLVE_EXE("memory_test_3"), fast_memory=True)
d.run()
@@ -289,7 +293,7 @@ def test_memory_large_read(self):
assert bp.hit_on(d)
- leak = d.regs.rdi
+ leak = FUN_ARG_0(d)
# Read 4MB of memory
data = d.memory[leak, 4 * 1024 * 1024]
@@ -300,7 +304,7 @@ def test_memory_large_read(self):
d.terminate()
def test_invalid_memory_location(self):
- d = debugger("binaries/memory_test", fast_memory=True)
+ d = debugger(RESOLVE_EXE("memory_test"), fast_memory=True)
d.run()
@@ -308,7 +312,7 @@ def test_invalid_memory_location(self):
d.cont()
- assert d.regs.rip == bp.address
+ assert d.instruction_pointer == bp.address
address = 0xDEADBEEFD00D
@@ -319,7 +323,7 @@ def test_invalid_memory_location(self):
d.terminate()
def test_memory_multiple_threads(self):
- d = debugger("binaries/memory_test_4", fast_memory=True)
+ d = debugger(RESOLVE_EXE("memory_test_4"), fast_memory=True)
d.run()
@@ -327,8 +331,8 @@ def test_memory_multiple_threads(self):
leak_addresses = []
def leak(t, _):
- leaks.append(t.memory[t.regs.rdi, 16])
- leak_addresses.append(t.regs.rdi)
+ leaks.append(t.memory[FUN_ARG_0(t), 16])
+ leak_addresses.append(FUN_ARG_0(t))
d.bp("leak", callback=leak, hardware=True)
exit = d.bp("before_exit", hardware=True)
@@ -351,11 +355,11 @@ def leak(t, _):
d.terminate()
def test_memory_mixed_access(self):
- d = debugger("binaries/memory_test_2", fast_memory=True)
+ d = debugger(RESOLVE_EXE("memory_test_2"), fast_memory=True)
d.run()
- base = d.regs.rip & 0xFFFFFFFFFFFFF000 - 0x1000
+ base = base_of(d)
# Test different ways to access memory at the start of the file
file_0 = d.memory[base, 256]
@@ -389,3 +393,58 @@ def test_memory_mixed_access(self):
d.kill()
d.terminate()
+
+ def test_memory_attach(self):
+ # Ensure that fast-memory works when attaching to a process
+ r = process(RESOLVE_EXE("attach_test"))
+
+ d = debugger(fast_memory=True)
+
+ d.attach(r.pid)
+
+ self.assertEqual(d.memory[0x0, 4, "binary"], b"\x7fELF")
+
+ d.kill()
+ d.terminate()
+
+ def test_search_memory(self):
+ d = debugger(RESOLVE_EXE("memory_test"), fast_memory=True)
+
+ d.run()
+
+ bp = d.breakpoint("change_memory")
+
+ d.cont()
+
+ assert d.instruction_pointer == bp.address
+
+ address = FUN_ARG_0(d)
+ prev = bytes(range(256))
+
+ self.assertTrue(d.memory[address, 256] == prev)
+
+ d.memory[address + 128 :] = b"abcd123456"
+ prev = prev[:128] + b"abcd123456" + prev[138:]
+
+ self.assertTrue(d.memory[address : address + 256] == prev)
+
+ start = d.maps.filter("heap")[0].start
+ end = d.maps.filter("heap")[-1].end - 1
+
+ # Search for the string "abcd123456" in the whole memory
+ self.assertTrue(d.memory.find(b"abcd123456") == [address + 128])
+
+ # Search for the string "abcd123456" in the memory starting from start
+ self.assertTrue(d.memory.find(b"abcd123456", start=start) == [address + 128])
+
+ # Search for the string "abcd123456" in the memory ending at end
+ self.assertTrue(d.memory.find(b"abcd123456", end=end) == [address + 128])
+
+ # Search for the string "abcd123456" in the heap using backing file
+ self.assertTrue(d.memory.find(b"abcd123456", file="heap") == [address + 128])
+
+ # Search for the string "abcd123456" in the heap using start and end
+ self.assertTrue(d.memory.find(b"abcd123456", start=start, end=end) == [address + 128])
+
+ d.kill()
+ d.terminate()
\ No newline at end of file
diff --git a/test/amd64/scripts/memory_test.py b/test/scripts/memory_test.py
similarity index 77%
rename from test/amd64/scripts/memory_test.py
rename to test/scripts/memory_test.py
index febbf77e..ab277fab 100644
--- a/test/amd64/scripts/memory_test.py
+++ b/test/scripts/memory_test.py
@@ -6,14 +6,18 @@
import io
import logging
-import unittest
+from unittest import TestCase
+from utils.binary_utils import RESOLVE_EXE, base_of
+from utils.thread_utils import FUN_ARG_0, STACK_POINTER
-from libdebug import debugger, libcontext
+from libdebug import debugger
+from libdebug.utils.libcontext import libcontext
+from libdebug.utils.platform_utils import get_platform_register_size
-class MemoryTest(unittest.TestCase):
+class MemoryTest(TestCase):
def setUp(self) -> None:
- self.d = debugger("binaries/memory_test")
+ self.d = debugger(RESOLVE_EXE("memory_test"))
# Redirect logging to a string buffer
self.log_capture_string = io.StringIO()
@@ -35,9 +39,9 @@ def test_memory(self):
d.cont()
- assert d.regs.rip == bp.address
+ assert d.instruction_pointer == bp.address
- address = d.regs.rdi
+ address = FUN_ARG_0(d)
prev = bytes(range(256))
self.assertTrue(d.memory[address, 256] == prev)
@@ -48,6 +52,7 @@ def test_memory(self):
self.assertTrue(d.memory[address : address + 256] == prev)
d.kill()
+ d.terminate()
def test_mem_access_libs(self):
d = self.d
@@ -58,18 +63,19 @@ def test_mem_access_libs(self):
d.cont()
- assert d.regs.rip == bp.address
+ assert d.instruction_pointer == bp.address
- address = d.regs.rdi
+ address = FUN_ARG_0(d)
with libcontext.tmp(sym_lvl=5):
arena = d.memory["main_arena", 256, "libc"]
- def p64(x):
- return x.to_bytes(8, "little")
+ def pack(x):
+ return x.to_bytes(get_platform_register_size(d.arch), "little")
- self.assertTrue(p64(address - 0x10) in arena)
+ self.assertTrue(pack(address - get_platform_register_size(d.arch) * 2) in arena)
d.kill()
+ d.terminate()
def test_memory_exceptions(self):
d = self.d
@@ -86,9 +92,9 @@ def test_memory_exceptions(self):
# File should start with ELF magic number
self.assertTrue(file.startswith(b"\x7fELF"))
- assert d.regs.rip == bp.address
+ assert d.instruction_pointer == bp.address
- address = d.regs.rdi
+ address = FUN_ARG_0(d)
prev = bytes(range(256))
self.assertTrue(d.memory[address, 256] == prev)
@@ -99,6 +105,7 @@ def test_memory_exceptions(self):
self.assertTrue(d.memory[address : address + 256] == prev)
d.kill()
+ d.terminate()
def test_memory_multiple_runs(self):
d = self.d
@@ -110,9 +117,9 @@ def test_memory_multiple_runs(self):
d.cont()
- assert d.regs.rip == bp.address
+ assert d.instruction_pointer == bp.address
- address = d.regs.rdi
+ address = FUN_ARG_0(d)
prev = bytes(range(256))
self.assertTrue(d.memory[address, 256] == prev)
@@ -124,8 +131,10 @@ def test_memory_multiple_runs(self):
d.kill()
+ d.terminate()
+
def test_memory_access_while_running(self):
- d = debugger("binaries/memory_test_2")
+ d = debugger(RESOLVE_EXE("memory_test_2"))
d.run()
@@ -136,16 +145,17 @@ def test_memory_access_while_running(self):
# Verify that memory access is only possible when the process is stopped
value = int.from_bytes(d.memory["state", 8], "little")
self.assertEqual(value, 0xDEADBEEF)
- self.assertEqual(d.regs.rip, bp.address)
+ self.assertEqual(d.instruction_pointer, bp.address)
d.kill()
+ d.terminate()
def test_memory_access_methods(self):
- d = debugger("binaries/memory_test_2")
+ d = debugger(RESOLVE_EXE("memory_test_2"))
d.run()
- base = d.regs.rip & 0xFFFFFFFFFFFFF000 - 0x1000
+ base = base_of(d)
# Test different ways to access memory at the start of the file
file_0 = d.memory[base, 256]
@@ -207,13 +217,14 @@ def test_memory_access_methods(self):
self.assertEqual(d.memory["main", 8], b"abcd1234")
d.kill()
+ d.terminate()
def test_memory_access_methods_backing_file(self):
- d = debugger("binaries/memory_test_2")
+ d = debugger(RESOLVE_EXE("memory_test_2"))
d.run()
- base = d.regs.rip & 0xFFFFFFFFFFFFF000 - 0x1000
+ base = base_of(d)
# Validate that slices work correctly
file_0 = d.memory[0x0:"do_nothing", "binary"]
@@ -285,9 +296,46 @@ def test_memory_access_methods_backing_file(self):
d.memory["main":"main+8", "absolute"] = b"abcd1234"
d.kill()
+ d.terminate()
+
+ def test_search_maps(self):
+ d = self.d
+
+ d.run()
+
+ bp = d.breakpoint("leak_address")
+
+ d.cont()
+
+ assert d.instruction_pointer == bp.address
+
+ maps = d.maps.filter("memory_test")
+
+ for vmap in maps:
+ self.assertIn(RESOLVE_EXE("memory_test"), vmap.backing_file)
+
+ maps_bin = d.maps.filter("binary")
+
+ for vmap in maps_bin:
+ self.assertIn(RESOLVE_EXE("memory_test"), vmap.backing_file)
+
+ self.assertEqual(maps, maps_bin)
+
+ maps = d.maps.filter("libc")
+
+ for vmap in maps:
+ self.assertIn("libc", vmap.backing_file)
+
+ maps = d.maps.filter(STACK_POINTER(d))
+
+ for vmap in maps:
+ self.assertIn("stack", vmap.backing_file)
+
+ d.kill()
+ d.terminate()
def test_memory_large_read(self):
- d = debugger("binaries/memory_test_3")
+ d = debugger(RESOLVE_EXE("memory_test_3"))
d.run()
@@ -297,7 +345,7 @@ def test_memory_large_read(self):
assert bp.hit_on(d)
- leak = d.regs.rdi
+ leak = FUN_ARG_0(d)
# Read 256K of memory
data = d.memory[leak, 256 * 1024]
@@ -308,7 +356,7 @@ def test_memory_large_read(self):
d.terminate()
def test_invalid_memory_location(self):
- d = debugger("binaries/memory_test")
+ d = debugger(RESOLVE_EXE("memory_test"))
d.run()
@@ -316,9 +364,9 @@ def test_invalid_memory_location(self):
d.cont()
- assert d.regs.rip == bp.address
+ assert d.instruction_pointer == bp.address
- address = 0xDEADBEEFD00D
+ address = 0xDEADBEEF
with self.assertRaises(ValueError):
d.memory[address, 256, "absolute"]
@@ -327,7 +375,7 @@ def test_invalid_memory_location(self):
d.terminate()
def test_memory_multiple_threads(self):
- d = debugger("binaries/memory_test_4")
+ d = debugger(RESOLVE_EXE("memory_test_4"))
d.run()
@@ -335,8 +383,8 @@ def test_memory_multiple_threads(self):
leak_addresses = []
def leak(t, _):
- leaks.append(t.memory[t.regs.rdi, 16])
- leak_addresses.append(t.regs.rdi)
+ leaks.append(t.memory[FUN_ARG_0(t), 16])
+ leak_addresses.append(FUN_ARG_0(t))
d.bp("leak", callback=leak, hardware=True)
exit = d.bp("before_exit", hardware=True)
@@ -357,7 +405,3 @@ def leak(t, _):
d.kill()
d.terminate()
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/test/scripts/multiple_debuggers_test.py b/test/scripts/multiple_debuggers_test.py
new file mode 100644
index 00000000..d2c19e97
--- /dev/null
+++ b/test/scripts/multiple_debuggers_test.py
@@ -0,0 +1,207 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Roberto Alessandro Bertolini, Gabriele Digregorio. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from unittest import TestCase, skipUnless
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
+
+from libdebug import debugger
+
+
+match PLATFORM:
+ case "amd64":
+ BP2_ADDRESS = 0x40115B
+ BP3_ADDRESS = 0x40116D
+ RBP1_ADDRESS = 0x4011CA
+ RBP2_ADDRESS = 0x40128D
+ RBP3_ADDRESS = 0x401239
+ RBP4_ADDRESS = 0x4011F4
+ RBP5_ADDRESS = 0x401296
+
+ def BP3_VALIDATE(harness, bpd):
+ harness.assertTrue(bpd.regs.rsi == 45)
+ harness.assertTrue(bpd.regs.esi == 45)
+ harness.assertTrue(bpd.regs.si == 45)
+ harness.assertTrue(bpd.regs.sil == 45)
+
+ def RBP1_VALIDATE(harness, red):
+ harness.assertTrue(red.regs.rax == 0x0011223344556677)
+ harness.assertTrue(red.regs.rbx == 0x1122334455667700)
+ harness.assertTrue(red.regs.rcx == 0x2233445566770011)
+ harness.assertTrue(red.regs.rdx == 0x3344556677001122)
+ harness.assertTrue(red.regs.rsi == 0x4455667700112233)
+ harness.assertTrue(red.regs.rdi == 0x5566770011223344)
+ harness.assertTrue(red.regs.rbp == 0x6677001122334455)
+ harness.assertTrue(red.regs.r8 == 0xAABBCCDD11223344)
+ harness.assertTrue(red.regs.r9 == 0xBBCCDD11223344AA)
+ harness.assertTrue(red.regs.r10 == 0xCCDD11223344AABB)
+ harness.assertTrue(red.regs.r11 == 0xDD11223344AABBCC)
+ harness.assertTrue(red.regs.r12 == 0x11223344AABBCCDD)
+ harness.assertTrue(red.regs.r13 == 0x223344AABBCCDD11)
+ harness.assertTrue(red.regs.r14 == 0x3344AABBCCDD1122)
+ harness.assertTrue(red.regs.r15 == 0x44AABBCCDD112233)
+
+ def RBP4_VALIDATE(harness, red):
+ harness.assertTrue(red.regs.al == 0x11)
+ harness.assertTrue(red.regs.bl == 0x22)
+ harness.assertTrue(red.regs.cl == 0x33)
+ harness.assertTrue(red.regs.dl == 0x44)
+ harness.assertTrue(red.regs.sil == 0x55)
+ harness.assertTrue(red.regs.dil == 0x66)
+ harness.assertTrue(red.regs.bpl == 0x77)
+ harness.assertTrue(red.regs.r8b == 0x88)
+ harness.assertTrue(red.regs.r9b == 0x99)
+ harness.assertTrue(red.regs.r10b == 0xAA)
+ harness.assertTrue(red.regs.r11b == 0xBB)
+ harness.assertTrue(red.regs.r12b == 0xCC)
+ harness.assertTrue(red.regs.r13b == 0xDD)
+ harness.assertTrue(red.regs.r14b == 0xEE)
+ harness.assertTrue(red.regs.r15b == 0xFF)
+
+ def RBP3_VALIDATE(harness, red):
+ harness.assertTrue(red.regs.ax == 0x1122)
+ harness.assertTrue(red.regs.bx == 0x2233)
+ harness.assertTrue(red.regs.cx == 0x3344)
+ harness.assertTrue(red.regs.dx == 0x4455)
+ harness.assertTrue(red.regs.si == 0x5566)
+ harness.assertTrue(red.regs.di == 0x6677)
+ harness.assertTrue(red.regs.bp == 0x7788)
+ harness.assertTrue(red.regs.r8w == 0x8899)
+ harness.assertTrue(red.regs.r9w == 0x99AA)
+ harness.assertTrue(red.regs.r10w == 0xAABB)
+ harness.assertTrue(red.regs.r11w == 0xBBCC)
+ harness.assertTrue(red.regs.r12w == 0xCCDD)
+ harness.assertTrue(red.regs.r13w == 0xDDEE)
+ harness.assertTrue(red.regs.r14w == 0xEEFF)
+ harness.assertTrue(red.regs.r15w == 0xFF00)
+
+ def RBP2_VALIDATE(harness, red):
+ harness.assertTrue(red.regs.eax == 0x11223344)
+ harness.assertTrue(red.regs.ebx == 0x22334455)
+ harness.assertTrue(red.regs.ecx == 0x33445566)
+ harness.assertTrue(red.regs.edx == 0x44556677)
+ harness.assertTrue(red.regs.esi == 0x55667788)
+ harness.assertTrue(red.regs.edi == 0x66778899)
+ harness.assertTrue(red.regs.ebp == 0x778899AA)
+ harness.assertTrue(red.regs.r8d == 0x8899AABB)
+ harness.assertTrue(red.regs.r9d == 0x99AABBCC)
+ harness.assertTrue(red.regs.r10d == 0xAABBCCDD)
+ harness.assertTrue(red.regs.r11d == 0xBBCCDD11)
+ harness.assertTrue(red.regs.r12d == 0xCCDD1122)
+ harness.assertTrue(red.regs.r13d == 0xDD112233)
+ harness.assertTrue(red.regs.r14d == 0x11223344)
+ harness.assertTrue(red.regs.r15d == 0x22334455)
+
+ def RBP5_VALIDATE(harness, red):
+ harness.assertTrue(red.regs.ah == 0x11)
+ harness.assertTrue(red.regs.bh == 0x22)
+ harness.assertTrue(red.regs.ch == 0x33)
+ harness.assertTrue(red.regs.dh == 0x44)
+ case "aarch64":
+ pass
+ case "i386":
+ pass
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+
+class MultipleDebuggersTest(TestCase):
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_multiple_debuggers_amd64(self):
+ bpd = debugger(RESOLVE_EXE("breakpoint_test"))
+ red = debugger(RESOLVE_EXE("basic_test"))
+
+ bpd.run()
+ red.run()
+
+ bp1 = bpd.breakpoint("random_function")
+ bp2 = bpd.breakpoint(BP2_ADDRESS)
+ bp3 = bpd.breakpoint(BP3_ADDRESS)
+
+ rbp1 = red.breakpoint(RBP1_ADDRESS, hardware=True)
+ rbp2 = red.breakpoint(RBP2_ADDRESS, hardware=False)
+ rbp3 = red.breakpoint(RBP3_ADDRESS, hardware=True)
+ rbp4 = red.breakpoint(RBP4_ADDRESS, hardware=False)
+ rbp5 = red.breakpoint(RBP5_ADDRESS, hardware=True)
+
+ counter = 1
+
+ bpd.cont()
+ red.cont()
+ disable_red = False
+
+ while True:
+
+ if bpd.instruction_pointer == bp1.address:
+ self.assertTrue(bp1.hit_count == 1)
+ self.assertTrue(bp1.hit_on(bpd))
+ self.assertFalse(bp2.hit_on(bpd))
+ self.assertFalse(bp3.hit_on(bpd))
+ elif bpd.instruction_pointer == bp2.address:
+ self.assertTrue(bp2.hit_count == counter)
+ self.assertTrue(bp2.hit_on(bpd))
+ self.assertFalse(bp1.hit_on(bpd))
+ self.assertFalse(bp3.hit_on(bpd))
+ counter += 1
+ elif bpd.instruction_pointer == bp3.address:
+ self.assertTrue(bp3.hit_count == 1)
+ BP3_VALIDATE(self, bpd)
+ self.assertTrue(bp3.hit_on(bpd))
+ self.assertFalse(bp1.hit_on(bpd))
+ self.assertFalse(bp2.hit_on(bpd))
+ break
+
+ if rbp1.hit_on(red):
+ RBP1_VALIDATE(self, red)
+ self.assertEqual(rbp1.hit_count, 1)
+ self.assertEqual(rbp2.hit_count, 0)
+ self.assertEqual(rbp3.hit_count, 0)
+ self.assertEqual(rbp4.hit_count, 0)
+ self.assertEqual(rbp5.hit_count, 0)
+ elif rbp4.hit_on(red):
+ RBP4_VALIDATE(self, red)
+ self.assertEqual(rbp1.hit_count, 1)
+ self.assertEqual(rbp2.hit_count, 0)
+ self.assertEqual(rbp3.hit_count, 0)
+ self.assertEqual(rbp4.hit_count, 1)
+ self.assertEqual(rbp5.hit_count, 0)
+ elif rbp3.hit_on(red):
+ RBP3_VALIDATE(self, red)
+ self.assertEqual(rbp1.hit_count, 1)
+ self.assertEqual(rbp2.hit_count, 0)
+ self.assertEqual(rbp3.hit_count, 1)
+ self.assertEqual(rbp4.hit_count, 1)
+ self.assertEqual(rbp5.hit_count, 0)
+ elif rbp2.hit_on(red):
+ RBP2_VALIDATE(self, red)
+ self.assertEqual(rbp1.hit_count, 1)
+ self.assertEqual(rbp2.hit_count, 1)
+ self.assertEqual(rbp3.hit_count, 1)
+ self.assertEqual(rbp4.hit_count, 1)
+ self.assertEqual(rbp5.hit_count, 0)
+ elif rbp5.hit_on(red):
+ RBP5_VALIDATE(self, red)
+ self.assertEqual(rbp1.hit_count, 1)
+ self.assertEqual(rbp2.hit_count, 1)
+ self.assertEqual(rbp3.hit_count, 1)
+ self.assertEqual(rbp4.hit_count, 1)
+ self.assertEqual(rbp5.hit_count, 1)
+ else:
+ self.assertEqual(rbp1.hit_count, 1)
+ self.assertEqual(rbp2.hit_count, 1)
+ self.assertEqual(rbp3.hit_count, 1)
+ self.assertEqual(rbp4.hit_count, 1)
+ self.assertEqual(rbp5.hit_count, 1)
+ disable_red = True
+
+ bpd.cont()
+ if not disable_red:
+ red.cont()
+
+ bpd.kill()
+ red.kill()
+
+ bpd.terminate()
+ red.terminate()
diff --git a/test/scripts/next_test.py b/test/scripts/next_test.py
new file mode 100644
index 00000000..af93d4d1
--- /dev/null
+++ b/test/scripts/next_test.py
@@ -0,0 +1,134 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Francesco Panebianco, Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from unittest import TestCase
+from utils.binary_utils import PLATFORM, BASE, RESOLVE_EXE
+
+from libdebug import debugger
+
+
+match PLATFORM:
+ case "amd64":
+ TEST_ENTRYPOINT = 0x4011f8
+
+ # Addresses of the dummy functions
+ CALL_C_ADDRESS = 0x4011fd
+ TEST_BREAKPOINT_ADDRESS = 0x4011f1
+
+ # Addresses of noteworthy instructions
+ RETURN_POINT_FROM_C = 0x401202
+ case "aarch64":
+ TEST_ENTRYPOINT = BASE + 0x930
+
+ # Addresses of the dummy functions
+ CALL_C_ADDRESS = BASE + 0x934
+ TEST_BREAKPOINT_ADDRESS = BASE + 0x920
+
+ # Addresses of noteworthy instructions
+ RETURN_POINT_FROM_C = BASE + 0x938
+ case "i386":
+ TEST_ENTRYPOINT = BASE + 0x1285
+
+ # Addresses of the dummy functions
+ CALL_C_ADDRESS = BASE + 0x128a
+ TEST_BREAKPOINT_ADDRESS = BASE + 0x1277
+
+ # Addresses of noteworthy instructions
+ RETURN_POINT_FROM_C = BASE + 0x128f
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+
+class NextTest(TestCase):
+ def test_next(self):
+ d = debugger(RESOLVE_EXE("finish_test"), auto_interrupt_on_command=False, aslr=False)
+ d.run()
+
+ # Get to test entrypoint
+ entrypoint_bp = d.breakpoint(TEST_ENTRYPOINT)
+ d.cont()
+
+ self.assertEqual(d.instruction_pointer, TEST_ENTRYPOINT)
+
+ # -------- Block 1 ------- #
+ # Simple Step #
+ # ------------------------ #
+
+ # Reach call of function c
+ d.next()
+ self.assertEqual(d.instruction_pointer, CALL_C_ADDRESS)
+
+ # -------- Block 2 ------- #
+ # Skip a call #
+ # ------------------------ #
+
+ d.next()
+ self.assertEqual(d.instruction_pointer, RETURN_POINT_FROM_C)
+
+ d.kill()
+ d.terminate()
+
+ def test_next_breakpoint(self):
+ d = debugger(RESOLVE_EXE("finish_test"), auto_interrupt_on_command=False, aslr=False)
+ d.run()
+
+ # Get to test entrypoint
+ entrypoint_bp = d.breakpoint(TEST_ENTRYPOINT)
+ d.cont()
+
+ self.assertEqual(d.instruction_pointer, TEST_ENTRYPOINT)
+
+ # Reach call of function c
+ d.next()
+
+ self.assertEqual(d.instruction_pointer, CALL_C_ADDRESS)
+
+ # -------- Block 1 ------- #
+ # Call with breakpoint #
+ # ------------------------ #
+
+ # Set breakpoint
+ test_breakpoint = d.breakpoint(TEST_BREAKPOINT_ADDRESS)
+
+ d.next()
+
+ # Check we hit the breakpoint
+ self.assertEqual(d.instruction_pointer, TEST_BREAKPOINT_ADDRESS)
+ self.assertEqual(test_breakpoint.hit_count, 1)
+
+ d.kill()
+ d.terminate()
+
+ def test_next_breakpoint_hw(self):
+ d = debugger(RESOLVE_EXE("finish_test"), auto_interrupt_on_command=False, aslr=False)
+ d.run()
+
+ # Get to test entrypoint
+ entrypoint_bp = d.breakpoint(TEST_ENTRYPOINT)
+ d.cont()
+
+ self.assertEqual(d.instruction_pointer, TEST_ENTRYPOINT)
+
+ # Reach call of function c
+ d.next()
+
+ self.assertEqual(d.instruction_pointer, CALL_C_ADDRESS)
+
+ # -------- Block 1 ------- #
+ # Call with breakpoint #
+ # ------------------------ #
+
+ # Set breakpoint
+ test_breakpoint = d.breakpoint(TEST_BREAKPOINT_ADDRESS, hardware=True)
+
+ d.next()
+
+ # Check we hit the breakpoint
+ self.assertEqual(d.instruction_pointer, TEST_BREAKPOINT_ADDRESS)
+ self.assertEqual(test_breakpoint.hit_count, 1)
+
+ d.kill()
+ d.terminate()
diff --git a/test/scripts/nlinks_test.py b/test/scripts/nlinks_test.py
new file mode 100644
index 00000000..ec479a78
--- /dev/null
+++ b/test/scripts/nlinks_test.py
@@ -0,0 +1,340 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+#
+# nlinks - challenge from DEF CON CTF Quals 2023
+# Thanks to the whole mhackeroni CTF team for the exploit
+#
+
+from unittest import TestCase, skipUnless
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
+
+from libdebug import debugger
+
+
+class NlinksTest(TestCase):
+ def get_passsphrase_from_class_1_binaries(self, previous_flag):
+ flag = b""
+
+ d = debugger(RESOLVE_EXE("CTF/1"))
+ r = d.run()
+
+ bp = d.breakpoint(0x7EF1, hardware=True)
+
+ d.cont()
+
+ r.recvuntil(b"Passphrase:\n")
+ r.send(previous_flag + b"a" * 8)
+
+ for _ in range(8):
+ self.assertTrue(d.regs.rip == bp.address)
+
+ offset = ord("a") ^ d.regs.rbp
+ d.regs.rbp = d.regs.r13
+ flag += (offset ^ d.regs.r13).to_bytes(1, "little")
+
+ d.cont()
+
+ r.recvline()
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(flag, b"\x00\x006\x00\x00\x00(\x00")
+ return flag
+
+ def get_passsphrase_from_class_2_binaries(self, previous_flag):
+ bitmap = {}
+ lastpos = 0
+ flag = b""
+
+ d = debugger(RESOLVE_EXE("CTF/2"))
+ r = d.run()
+
+ bp1 = d.breakpoint(0xD8C1, hardware=True)
+ bp2 = d.breakpoint(0x1858, hardware=True)
+ bp3 = d.breakpoint(0xDBA1, hardware=True)
+
+ d.cont()
+
+ r.recvuntil(b"Passphrase:\n")
+ r.send(previous_flag + b"a" * 8)
+
+ while True:
+ if d.regs.rip == bp1.address:
+ lastpos = d.regs.rbp
+ d.regs.rbp = d.regs.r13 + 1
+ elif d.regs.rip == bp2.address:
+ bitmap[d.regs.r12 & 0xFF] = lastpos & 0xFF
+ elif d.regs.rip == bp3.address:
+ d.regs.rbp = d.regs.r13
+ wanted = d.regs.rbp
+ needed = 0
+ for i in range(8):
+ if wanted & (2**i):
+ needed |= bitmap[2**i]
+ flag += chr(needed).encode()
+
+ if bp3.hit_count == 8:
+ d.cont()
+ break
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(flag, b"\x00\x00\x00\x01\x00\x00a\x00")
+
+ def get_passsphrase_from_class_3_binaries(self):
+ flag = b""
+
+ d = debugger(RESOLVE_EXE("CTF/0"))
+ r = d.run()
+
+ bp = d.breakpoint(0x91A1, hardware=True)
+
+ d.cont()
+
+ r.send(b"a" * 8)
+
+ for _ in range(8):
+
+ self.assertTrue(d.regs.rip == bp.address)
+
+ offset = ord("a") - d.regs.rbp
+ d.regs.rbp = d.regs.r13
+
+ flag += chr((d.regs.r13 + offset) % 256).encode("latin-1")
+
+ d.cont()
+
+ r.recvline()
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(flag, b"BM8\xd3\x02\x00\x00\x00")
+ return flag
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_nlinks(self):
+ flag = self.get_passsphrase_from_class_3_binaries()
+ flag = self.get_passsphrase_from_class_1_binaries(flag)
+ self.get_passsphrase_from_class_2_binaries(flag)
+
+ def aw_get_passsphrase_from_class_1_binaries(self, previous_flag):
+ flag = b""
+
+ d = debugger(RESOLVE_EXE("CTF/1"), auto_interrupt_on_command=False)
+ r = d.run()
+
+ d.breakpoint(0x7EF1, hardware=True, file="binary")
+
+ d.cont()
+
+ r.recvuntil(b"Passphrase:\n")
+ r.send(previous_flag + b"a" * 8)
+
+ for _ in range(8):
+ offset = ord("a") ^ d.regs.rbp
+ d.regs.rbp = d.regs.r13
+ flag += (offset ^ d.regs.r13).to_bytes(1, "little")
+
+ d.cont()
+
+ r.recvline()
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(flag, b"\x00\x006\x00\x00\x00(\x00")
+ return flag
+
+ def aw_get_passsphrase_from_class_2_binaries(self, previous_flag):
+ bitmap = {}
+ lastpos = 0
+ flag = b""
+
+ d = debugger(RESOLVE_EXE("CTF/2"), auto_interrupt_on_command=False)
+ r = d.run()
+
+ bp1 = d.breakpoint(0xD8C1, hardware=True, file="binary")
+ bp2 = d.breakpoint(0x1858, hardware=True, file="binary")
+ bp3 = d.breakpoint(0xDBA1, hardware=True, file="binary")
+
+ d.cont()
+
+ r.recvuntil(b"Passphrase:\n")
+ r.send(previous_flag + b"a" * 8)
+
+ while True:
+ if d.regs.rip == bp1.address:
+ lastpos = d.regs.rbp
+ d.regs.rbp = d.regs.r13 + 1
+ elif d.regs.rip == bp2.address:
+ bitmap[d.regs.r12 & 0xFF] = lastpos & 0xFF
+ elif d.regs.rip == bp3.address:
+ d.regs.rbp = d.regs.r13
+ wanted = d.regs.rbp
+ needed = 0
+ for i in range(8):
+ if wanted & (2**i):
+ needed |= bitmap[2**i]
+ flag += chr(needed).encode()
+
+ if bp3.hit_count == 8:
+ d.cont()
+ break
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(flag, b"\x00\x00\x00\x01\x00\x00a\x00")
+
+ def aw_get_passsphrase_from_class_3_binaries(self):
+ flag = b""
+
+ d = debugger(RESOLVE_EXE("CTF/0"), auto_interrupt_on_command=False)
+ r = d.run()
+
+ d.breakpoint(0x91A1, hardware=True, file="binary")
+
+ d.cont()
+
+ r.send(b"a" * 8)
+
+ for _ in range(8):
+ offset = ord("a") - d.regs.rbp
+ d.regs.rbp = d.regs.r13
+
+ flag += chr((d.regs.r13 + offset) % 256).encode("latin-1")
+
+ d.cont()
+
+ r.recvline()
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(flag, b"BM8\xd3\x02\x00\x00\x00")
+ return flag
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_nlinks_auto_waiting(self):
+ flag = self.aw_get_passsphrase_from_class_3_binaries()
+ flag = self.aw_get_passsphrase_from_class_1_binaries(flag)
+ self.aw_get_passsphrase_from_class_2_binaries(flag)
+
+ def w_get_passsphrase_from_class_1_binaries(self, previous_flag):
+ flag = b""
+
+ d = debugger(RESOLVE_EXE("CTF/1"), auto_interrupt_on_command=True)
+ r = d.run()
+
+ d.breakpoint(0x7EF1, hardware=True)
+
+ d.cont()
+
+ r.recvuntil(b"Passphrase:\n")
+ r.send(previous_flag + b"a" * 8)
+
+ for _ in range(8):
+ d.wait()
+ offset = ord("a") ^ d.regs.rbp
+ d.regs.rbp = d.regs.r13
+ flag += (offset ^ d.regs.r13).to_bytes(1, "little")
+
+ d.cont()
+
+ r.recvline()
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(flag, b"\x00\x006\x00\x00\x00(\x00")
+ return flag
+
+ def w_get_passsphrase_from_class_2_binaries(self, previous_flag):
+ bitmap = {}
+ lastpos = 0
+ flag = b""
+
+ d = debugger(RESOLVE_EXE("CTF/2"), auto_interrupt_on_command=True)
+ r = d.run()
+
+ bp1 = d.breakpoint(0xD8C1, hardware=True)
+ bp2 = d.breakpoint(0x1858, hardware=True)
+ bp3 = d.breakpoint(0xDBA1, hardware=True)
+
+ d.cont()
+
+ r.recvuntil(b"Passphrase:\n")
+ r.send(previous_flag + b"a" * 8)
+
+ while True:
+ d.wait()
+ if d.instruction_pointer == bp1.address:
+ lastpos = d.regs.rbp
+ d.regs.rbp = d.regs.r13 + 1
+ elif d.instruction_pointer == bp2.address:
+ bitmap[d.regs.r12 & 0xFF] = lastpos & 0xFF
+ elif d.instruction_pointer == bp3.address:
+ d.regs.rbp = d.regs.r13
+ wanted = d.regs.rbp
+ needed = 0
+ for i in range(8):
+ if wanted & (2**i):
+ needed |= bitmap[2**i]
+ flag += chr(needed).encode()
+
+ if bp3.hit_count == 8:
+ d.cont()
+ break
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(flag, b"\x00\x00\x00\x01\x00\x00a\x00")
+
+ def w_get_passsphrase_from_class_3_binaries(self):
+ flag = b""
+
+ d = debugger(RESOLVE_EXE("CTF/0"), auto_interrupt_on_command=True)
+ r = d.run()
+
+ d.breakpoint(0x91A1, hardware=True)
+
+ d.cont()
+
+ r.send(b"a" * 8)
+
+ for _ in range(8):
+ d.wait()
+ offset = ord("a") - d.regs.rbp
+ d.regs.rbp = d.regs.r13
+
+ flag += chr((d.regs.r13 + offset) % 256).encode("latin-1")
+
+ d.cont()
+
+ r.recvline()
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(flag, b"BM8\xd3\x02\x00\x00\x00")
+ return flag
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_nlinks_waiting(self):
+ flag = self.w_get_passsphrase_from_class_3_binaries()
+ flag = self.w_get_passsphrase_from_class_1_binaries(flag)
+ self.w_get_passsphrase_from_class_2_binaries(flag)
diff --git a/test/amd64/scripts/pprint_syscalls_test.py b/test/scripts/pprint_syscalls_test.py
similarity index 72%
rename from test/amd64/scripts/pprint_syscalls_test.py
rename to test/scripts/pprint_syscalls_test.py
index 82070123..41d84142 100644
--- a/test/amd64/scripts/pprint_syscalls_test.py
+++ b/test/scripts/pprint_syscalls_test.py
@@ -6,12 +6,29 @@
import io
import sys
-import unittest
+from unittest import TestCase
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
from libdebug import debugger
-class PPrintSyscallsTest(unittest.TestCase):
+match PLATFORM:
+ case "amd64":
+ READ_NUM = 0
+ MMAP_NUM = 9
+ MMAP_NAME = "mmap"
+ case "aarch64":
+ READ_NUM = 63
+ MMAP_NUM = 222
+ MMAP_NAME = "mmap"
+ case "i386":
+ READ_NUM = 3
+ MMAP_NUM = 192
+ MMAP_NAME = "mmap_pgoff"
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+class PPrintSyscallsTest(TestCase):
def setUp(self):
# Redirect stdout
self.capturedOutput = io.StringIO()
@@ -21,7 +38,7 @@ def tearDown(self):
sys.stdout = sys.__stdout__
def test_pprint_syscalls_generic(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
d.pprint_syscalls = True
@@ -34,7 +51,7 @@ def test_pprint_syscalls_generic(self):
self.assertIn("write", self.capturedOutput.getvalue())
self.assertIn("read", self.capturedOutput.getvalue())
- self.assertIn("mmap", self.capturedOutput.getvalue())
+ self.assertIn(MMAP_NAME, self.capturedOutput.getvalue())
self.assertIn("getcwd", self.capturedOutput.getvalue())
self.assertIn("exit_group", self.capturedOutput.getvalue())
@@ -44,12 +61,12 @@ def test_pprint_syscalls_generic(self):
self.assertEqual(self.capturedOutput.getvalue().count("write"), 2)
self.assertEqual(self.capturedOutput.getvalue().count("read"), 1)
- self.assertEqual(self.capturedOutput.getvalue().count("mmap"), 1)
+ self.assertEqual(self.capturedOutput.getvalue().count(MMAP_NAME), 1)
self.assertEqual(self.capturedOutput.getvalue().count("getcwd"), 1)
self.assertEqual(self.capturedOutput.getvalue().count("exit_group"), 1)
def test_pprint_syscalls_with_statement(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
with d.pprint_syscalls_context(True):
@@ -58,10 +75,11 @@ def test_pprint_syscalls_with_statement(self):
d.cont()
d.kill()
+ d.terminate()
self.assertIn("write", self.capturedOutput.getvalue())
self.assertIn("read", self.capturedOutput.getvalue())
- self.assertIn("mmap", self.capturedOutput.getvalue())
+ self.assertIn(MMAP_NAME, self.capturedOutput.getvalue())
self.assertIn("getcwd", self.capturedOutput.getvalue())
self.assertIn("exit_group", self.capturedOutput.getvalue())
@@ -71,7 +89,7 @@ def test_pprint_syscalls_with_statement(self):
self.assertEqual(self.capturedOutput.getvalue().count("write"), 2)
self.assertEqual(self.capturedOutput.getvalue().count("read"), 1)
- self.assertEqual(self.capturedOutput.getvalue().count("mmap"), 1)
+ self.assertEqual(self.capturedOutput.getvalue().count(MMAP_NAME), 1)
self.assertEqual(self.capturedOutput.getvalue().count("getcwd"), 1)
self.assertEqual(self.capturedOutput.getvalue().count("exit_group"), 1)
@@ -82,7 +100,7 @@ def on_enter_read(d, sh):
def on_exit_read(d, sh):
d.syscall_return = 0xDEADBEEF
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
d.pprint_syscalls = True
@@ -94,10 +112,11 @@ def on_exit_read(d, sh):
d.cont()
d.kill()
+ d.terminate()
self.assertIn("write", self.capturedOutput.getvalue())
self.assertIn("read", self.capturedOutput.getvalue())
- self.assertIn("mmap", self.capturedOutput.getvalue())
+ self.assertIn(MMAP_NAME, self.capturedOutput.getvalue())
self.assertIn("getcwd", self.capturedOutput.getvalue())
self.assertIn("exit_group", self.capturedOutput.getvalue())
self.assertIn(
@@ -111,13 +130,13 @@ def on_exit_read(d, sh):
self.assertEqual(self.capturedOutput.getvalue().count("write"), 2)
self.assertEqual(self.capturedOutput.getvalue().count("read"), 1)
- self.assertEqual(self.capturedOutput.getvalue().count("mmap"), 1)
+ self.assertEqual(self.capturedOutput.getvalue().count(MMAP_NAME), 1)
self.assertEqual(self.capturedOutput.getvalue().count("getcwd"), 1)
self.assertEqual(self.capturedOutput.getvalue().count("exit_group"), 1)
self.assertEqual(self.capturedOutput.getvalue().count("callback"), 1)
def test_pprint_hijack_syscall(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
@@ -130,14 +149,15 @@ def test_pprint_hijack_syscall(self):
d.cont()
d.kill()
+ d.terminate()
self.assertIn("write", self.capturedOutput.getvalue())
self.assertIn("read", self.capturedOutput.getvalue())
- self.assertIn("mmap", self.capturedOutput.getvalue())
+ self.assertIn(MMAP_NAME, self.capturedOutput.getvalue())
self.assertIn("getcwd", self.capturedOutput.getvalue())
self.assertIn("exit_group", self.capturedOutput.getvalue())
self.assertIn(
- "(user hijacked) \x1b[9m\x1b[94mgetcwd\x1b[39m",
+ "(hijacked) \x1b[9m\x1b[94mgetcwd\x1b[39m",
self.capturedOutput.getvalue(),
)
@@ -145,40 +165,41 @@ def test_pprint_hijack_syscall(self):
self.capturedOutput.getvalue().count("write"), 3
) # 2 from the test, 1 from the hijack
self.assertEqual(self.capturedOutput.getvalue().count("read"), 1)
- self.assertEqual(self.capturedOutput.getvalue().count("mmap"), 1)
+ self.assertEqual(self.capturedOutput.getvalue().count(MMAP_NAME), 1)
self.assertEqual(self.capturedOutput.getvalue().count("getcwd"), 1)
self.assertEqual(self.capturedOutput.getvalue().count("exit_group"), 1)
self.assertEqual(self.capturedOutput.getvalue().count("hijacked"), 1)
def test_pprint_which_syscalls_pprint_after(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
d.pprint_syscalls = True
- d.syscalls_to_pprint = [0, "write", 9] # after d.pprint_syscalls = True
+ d.syscalls_to_pprint = [READ_NUM, "write", MMAP_NUM] # after d.pprint_syscalls = True
r.sendline(b"provola")
d.cont()
d.kill()
+ d.terminate()
self.assertIn("write", self.capturedOutput.getvalue())
self.assertIn("read", self.capturedOutput.getvalue())
- self.assertIn("mmap", self.capturedOutput.getvalue())
+ self.assertIn(MMAP_NAME, self.capturedOutput.getvalue())
self.assertNotIn("getcwd", self.capturedOutput.getvalue())
self.assertNotIn("exit_group", self.capturedOutput.getvalue())
self.assertEqual(self.capturedOutput.getvalue().count("write"), 2)
self.assertEqual(self.capturedOutput.getvalue().count("read"), 1)
- self.assertEqual(self.capturedOutput.getvalue().count("mmap"), 1)
+ self.assertEqual(self.capturedOutput.getvalue().count(MMAP_NAME), 1)
def test_pprint_which_syscalls_pprint_before(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
- d.syscalls_to_pprint = [0, "write", 9] # before d.pprint_syscalls = True
+ d.syscalls_to_pprint = [READ_NUM, "write", MMAP_NUM] # before d.pprint_syscalls = True
d.pprint_syscalls = True
r.sendline(b"provola")
@@ -186,56 +207,59 @@ def test_pprint_which_syscalls_pprint_before(self):
d.cont()
d.kill()
+ d.terminate()
self.assertIn("write", self.capturedOutput.getvalue())
self.assertIn("read", self.capturedOutput.getvalue())
- self.assertIn("mmap", self.capturedOutput.getvalue())
+ self.assertIn(MMAP_NAME, self.capturedOutput.getvalue())
self.assertNotIn("getcwd", self.capturedOutput.getvalue())
self.assertNotIn("exit_group", self.capturedOutput.getvalue())
self.assertEqual(self.capturedOutput.getvalue().count("write"), 2)
self.assertEqual(self.capturedOutput.getvalue().count("read"), 1)
- self.assertEqual(self.capturedOutput.getvalue().count("mmap"), 1)
+ self.assertEqual(self.capturedOutput.getvalue().count(MMAP_NAME), 1)
def test_pprint_which_syscalls_pprint_after_and_before(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
- d.syscalls_to_pprint = [0, "write", 9]
+ d.syscalls_to_pprint = [READ_NUM, "write", MMAP_NUM]
d.pprint_syscalls = True
- d.syscalls_to_pprint = ["write", 9]
+ d.syscalls_to_pprint = ["write", MMAP_NUM]
r.sendline(b"provola")
d.cont()
d.kill()
+ d.terminate()
self.assertIn("write", self.capturedOutput.getvalue())
self.assertNotIn("read", self.capturedOutput.getvalue())
- self.assertIn("mmap", self.capturedOutput.getvalue())
+ self.assertIn(MMAP_NAME, self.capturedOutput.getvalue())
self.assertNotIn("getcwd", self.capturedOutput.getvalue())
self.assertNotIn("exit_group", self.capturedOutput.getvalue())
self.assertEqual(self.capturedOutput.getvalue().count("write"), 2)
- self.assertEqual(self.capturedOutput.getvalue().count("mmap"), 1)
+ self.assertEqual(self.capturedOutput.getvalue().count(MMAP_NAME), 1)
def test_pprint_which_syscalls_not_pprint_after(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
d.pprint_syscalls = True
- d.syscalls_to_not_pprint = [0, "write", 9]
+ d.syscalls_to_not_pprint = [READ_NUM, "write", MMAP_NUM]
r.sendline(b"provola")
d.cont()
d.kill()
+ d.terminate()
self.assertNotIn("write", self.capturedOutput.getvalue())
self.assertNotIn("read", self.capturedOutput.getvalue())
- self.assertNotIn("mmap", self.capturedOutput.getvalue())
+ self.assertNotIn(MMAP_NAME, self.capturedOutput.getvalue())
self.assertIn("getcwd", self.capturedOutput.getvalue())
self.assertIn("exit_group", self.capturedOutput.getvalue())
@@ -243,10 +267,10 @@ def test_pprint_which_syscalls_not_pprint_after(self):
self.assertEqual(self.capturedOutput.getvalue().count("exit_group"), 1)
def test_pprint_which_syscalls_not_pprint_before(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
- d.syscalls_to_not_pprint = [0, "write", 9]
+ d.syscalls_to_not_pprint = [READ_NUM, "write", MMAP_NUM]
d.pprint_syscalls = True
r.sendline(b"provola")
@@ -254,10 +278,11 @@ def test_pprint_which_syscalls_not_pprint_before(self):
d.cont()
d.kill()
+ d.terminate()
self.assertNotIn("write", self.capturedOutput.getvalue())
self.assertNotIn("read", self.capturedOutput.getvalue())
- self.assertNotIn("mmap", self.capturedOutput.getvalue())
+ self.assertNotIn(MMAP_NAME, self.capturedOutput.getvalue())
self.assertIn("getcwd", self.capturedOutput.getvalue())
self.assertIn("exit_group", self.capturedOutput.getvalue())
@@ -265,29 +290,26 @@ def test_pprint_which_syscalls_not_pprint_before(self):
self.assertEqual(self.capturedOutput.getvalue().count("exit_group"), 1)
def test_pprint_which_syscalls_not_pprint_after_and_before(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
- d.syscalls_to_not_pprint = [0, "write", 9]
+ d.syscalls_to_not_pprint = [READ_NUM, "write", MMAP_NUM]
d.pprint_syscalls = True
- d.syscalls_to_not_pprint = ["write", 9]
+ d.syscalls_to_not_pprint = ["write", MMAP_NUM]
r.sendline(b"provola")
d.cont()
d.kill()
+ d.terminate()
self.assertNotIn("write", self.capturedOutput.getvalue())
self.assertIn("read", self.capturedOutput.getvalue())
- self.assertNotIn("mmap", self.capturedOutput.getvalue())
+ self.assertNotIn(MMAP_NAME, self.capturedOutput.getvalue())
self.assertIn("getcwd", self.capturedOutput.getvalue())
self.assertIn("exit_group", self.capturedOutput.getvalue())
self.assertEqual(self.capturedOutput.getvalue().count("read"), 1)
self.assertEqual(self.capturedOutput.getvalue().count("getcwd"), 1)
self.assertEqual(self.capturedOutput.getvalue().count("exit_group"), 1)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/test/scripts/register_test.py b/test/scripts/register_test.py
new file mode 100644
index 00000000..fd0855b1
--- /dev/null
+++ b/test/scripts/register_test.py
@@ -0,0 +1,783 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini, Francesco Panebianco. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+import sys
+from io import StringIO
+from unittest import TestCase, skipUnless
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
+
+from libdebug import debugger
+
+class RegisterTest(TestCase):
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_registers_amd64(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+
+ d.run()
+
+ bp1 = d.breakpoint(0x4011CA)
+ bp2 = d.breakpoint(0x40128D)
+ bp3 = d.breakpoint(0x401239)
+ bp4 = d.breakpoint(0x4011F4)
+ bp5 = d.breakpoint(0x401296)
+
+ d.cont()
+ self.assertTrue(bp1.address, d.regs.rip)
+
+ self.assertTrue(d.regs.rax, 0x0011223344556677)
+ self.assertTrue(d.regs.rbx, 0x1122334455667700)
+ self.assertTrue(d.regs.rcx, 0x2233445566770011)
+ self.assertTrue(d.regs.rdx, 0x3344556677001122)
+ self.assertTrue(d.regs.rsi, 0x4455667700112233)
+ self.assertTrue(d.regs.rdi, 0x5566770011223344)
+ self.assertTrue(d.regs.rbp, 0x6677001122334455)
+ self.assertTrue(d.regs.r8, 0xAABBCCDD11223344)
+ self.assertTrue(d.regs.r9, 0xBBCCDD11223344AA)
+ self.assertTrue(d.regs.r10, 0xCCDD11223344AABB)
+ self.assertTrue(d.regs.r11, 0xDD11223344AABBCC)
+ self.assertTrue(d.regs.r12, 0x11223344AABBCCDD)
+ self.assertTrue(d.regs.r13, 0x223344AABBCCDD11)
+ self.assertTrue(d.regs.r14, 0x3344AABBCCDD1122)
+ self.assertTrue(d.regs.r15, 0x44AABBCCDD112233)
+
+ d.cont()
+ self.assertTrue(bp4.address, d.regs.rip)
+
+ self.assertTrue(d.regs.al, 0x11)
+ self.assertTrue(d.regs.bl, 0x22)
+ self.assertTrue(d.regs.cl, 0x33)
+ self.assertTrue(d.regs.dl, 0x44)
+ self.assertTrue(d.regs.sil, 0x55)
+ self.assertTrue(d.regs.dil, 0x66)
+ self.assertTrue(d.regs.bpl, 0x77)
+ self.assertTrue(d.regs.r8b, 0x88)
+ self.assertTrue(d.regs.r9b, 0x99)
+ self.assertTrue(d.regs.r10b, 0xAA)
+ self.assertTrue(d.regs.r11b, 0xBB)
+ self.assertTrue(d.regs.r12b, 0xCC)
+ self.assertTrue(d.regs.r13b, 0xDD)
+ self.assertTrue(d.regs.r14b, 0xEE)
+ self.assertTrue(d.regs.r15b, 0xFF)
+
+ d.cont()
+ self.assertTrue(bp3.address, d.regs.rip)
+
+ self.assertTrue(d.regs.ax, 0x1122)
+ self.assertTrue(d.regs.bx, 0x2233)
+ self.assertTrue(d.regs.cx, 0x3344)
+ self.assertTrue(d.regs.dx, 0x4455)
+ self.assertTrue(d.regs.si, 0x5566)
+ self.assertTrue(d.regs.di, 0x6677)
+ self.assertTrue(d.regs.bp, 0x7788)
+ self.assertTrue(d.regs.r8w, 0x8899)
+ self.assertTrue(d.regs.r9w, 0x99AA)
+ self.assertTrue(d.regs.r10w, 0xAABB)
+ self.assertTrue(d.regs.r11w, 0xBBCC)
+ self.assertTrue(d.regs.r12w, 0xCCDD)
+ self.assertTrue(d.regs.r13w, 0xDDEE)
+ self.assertTrue(d.regs.r14w, 0xEEFF)
+ self.assertTrue(d.regs.r15w, 0xFF00)
+
+ d.cont()
+ self.assertTrue(bp2.address, d.regs.rip)
+
+ self.assertTrue(d.regs.eax, 0x11223344)
+ self.assertTrue(d.regs.ebx, 0x22334455)
+ self.assertTrue(d.regs.ecx, 0x33445566)
+ self.assertTrue(d.regs.edx, 0x44556677)
+ self.assertTrue(d.regs.esi, 0x55667788)
+ self.assertTrue(d.regs.edi, 0x66778899)
+ self.assertTrue(d.regs.ebp, 0x778899AA)
+ self.assertTrue(d.regs.r8d, 0x8899AABB)
+ self.assertTrue(d.regs.r9d, 0x99AABBCC)
+ self.assertTrue(d.regs.r10d, 0xAABBCCDD)
+ self.assertTrue(d.regs.r11d, 0xBBCCDD11)
+ self.assertTrue(d.regs.r12d, 0xCCDD1122)
+ self.assertTrue(d.regs.r13d, 0xDD112233)
+ self.assertTrue(d.regs.r14d, 0x11223344)
+ self.assertTrue(d.regs.r15d, 0x22334455)
+
+ d.cont()
+ self.assertTrue(bp5.address, d.regs.rip)
+
+ self.assertTrue(d.regs.ah, 0x11)
+ self.assertTrue(d.regs.bh, 0x22)
+ self.assertTrue(d.regs.ch, 0x33)
+ self.assertTrue(d.regs.dh, 0x44)
+
+ d.cont()
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_registers_hardware_amd64(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+
+ d.run()
+
+ bp1 = d.breakpoint(0x4011CA, hardware=True)
+ bp2 = d.breakpoint(0x40128D, hardware=False)
+ bp3 = d.breakpoint(0x401239, hardware=True)
+ bp4 = d.breakpoint(0x4011F4, hardware=False)
+ bp5 = d.breakpoint(0x401296, hardware=True)
+
+ d.cont()
+ self.assertTrue(bp1.address, d.regs.rip)
+
+ self.assertTrue(d.regs.rax, 0x0011223344556677)
+ self.assertTrue(d.regs.rbx, 0x1122334455667700)
+ self.assertTrue(d.regs.rcx, 0x2233445566770011)
+ self.assertTrue(d.regs.rdx, 0x3344556677001122)
+ self.assertTrue(d.regs.rsi, 0x4455667700112233)
+ self.assertTrue(d.regs.rdi, 0x5566770011223344)
+ self.assertTrue(d.regs.rbp, 0x6677001122334455)
+ self.assertTrue(d.regs.r8, 0xAABBCCDD11223344)
+ self.assertTrue(d.regs.r9, 0xBBCCDD11223344AA)
+ self.assertTrue(d.regs.r10, 0xCCDD11223344AABB)
+ self.assertTrue(d.regs.r11, 0xDD11223344AABBCC)
+ self.assertTrue(d.regs.r12, 0x11223344AABBCCDD)
+ self.assertTrue(d.regs.r13, 0x223344AABBCCDD11)
+ self.assertTrue(d.regs.r14, 0x3344AABBCCDD1122)
+ self.assertTrue(d.regs.r15, 0x44AABBCCDD112233)
+
+ d.cont()
+ self.assertTrue(bp4.address, d.regs.rip)
+
+ self.assertTrue(d.regs.al, 0x11)
+ self.assertTrue(d.regs.bl, 0x22)
+ self.assertTrue(d.regs.cl, 0x33)
+ self.assertTrue(d.regs.dl, 0x44)
+ self.assertTrue(d.regs.sil, 0x55)
+ self.assertTrue(d.regs.dil, 0x66)
+ self.assertTrue(d.regs.bpl, 0x77)
+ self.assertTrue(d.regs.r8b, 0x88)
+ self.assertTrue(d.regs.r9b, 0x99)
+ self.assertTrue(d.regs.r10b, 0xAA)
+ self.assertTrue(d.regs.r11b, 0xBB)
+ self.assertTrue(d.regs.r12b, 0xCC)
+ self.assertTrue(d.regs.r13b, 0xDD)
+ self.assertTrue(d.regs.r14b, 0xEE)
+ self.assertTrue(d.regs.r15b, 0xFF)
+
+ d.cont()
+ self.assertTrue(bp3.address, d.regs.rip)
+
+ self.assertTrue(d.regs.ax, 0x1122)
+ self.assertTrue(d.regs.bx, 0x2233)
+ self.assertTrue(d.regs.cx, 0x3344)
+ self.assertTrue(d.regs.dx, 0x4455)
+ self.assertTrue(d.regs.si, 0x5566)
+ self.assertTrue(d.regs.di, 0x6677)
+ self.assertTrue(d.regs.bp, 0x7788)
+ self.assertTrue(d.regs.r8w, 0x8899)
+ self.assertTrue(d.regs.r9w, 0x99AA)
+ self.assertTrue(d.regs.r10w, 0xAABB)
+ self.assertTrue(d.regs.r11w, 0xBBCC)
+ self.assertTrue(d.regs.r12w, 0xCCDD)
+ self.assertTrue(d.regs.r13w, 0xDDEE)
+ self.assertTrue(d.regs.r14w, 0xEEFF)
+ self.assertTrue(d.regs.r15w, 0xFF00)
+
+ d.cont()
+ self.assertTrue(bp2.address, d.regs.rip)
+
+ self.assertTrue(d.regs.eax, 0x11223344)
+ self.assertTrue(d.regs.ebx, 0x22334455)
+ self.assertTrue(d.regs.ecx, 0x33445566)
+ self.assertTrue(d.regs.edx, 0x44556677)
+ self.assertTrue(d.regs.esi, 0x55667788)
+ self.assertTrue(d.regs.edi, 0x66778899)
+ self.assertTrue(d.regs.ebp, 0x778899AA)
+ self.assertTrue(d.regs.r8d, 0x8899AABB)
+ self.assertTrue(d.regs.r9d, 0x99AABBCC)
+ self.assertTrue(d.regs.r10d, 0xAABBCCDD)
+ self.assertTrue(d.regs.r11d, 0xBBCCDD11)
+ self.assertTrue(d.regs.r12d, 0xCCDD1122)
+ self.assertTrue(d.regs.r13d, 0xDD112233)
+ self.assertTrue(d.regs.r14d, 0x11223344)
+ self.assertTrue(d.regs.r15d, 0x22334455)
+
+ d.cont()
+ self.assertTrue(bp5.address, d.regs.rip)
+
+ self.assertTrue(d.regs.ah, 0x11)
+ self.assertTrue(d.regs.bh, 0x22)
+ self.assertTrue(d.regs.ch, 0x33)
+ self.assertTrue(d.regs.dh, 0x44)
+
+ d.cont()
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "aarch64", "Requires aarch64")
+ def test_registers_aarch64(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+ d.run()
+
+ bp = d.breakpoint(0x4008a4, hardware=False)
+
+ d.cont()
+
+ self.assertEqual(d.regs.pc, bp.address)
+
+ self.assertEqual(d.regs.x0, 0x4444333322221111)
+ self.assertEqual(d.regs.x1, 0x8888777766665555)
+ self.assertEqual(d.regs.x2, 0xccccbbbbaaaa9999)
+ self.assertEqual(d.regs.x3, 0x1111ffffeeeedddd)
+ self.assertEqual(d.regs.x4, 0x5555444433332222)
+ self.assertEqual(d.regs.x5, 0x9999888877776666)
+ self.assertEqual(d.regs.x6, 0xddddccccbbbbaaaa)
+ self.assertEqual(d.regs.x7, 0x22221111ffffeeee)
+ self.assertEqual(d.regs.x8, 0x6666555544443333)
+ self.assertEqual(d.regs.x9, 0xaaaa999988887777)
+ self.assertEqual(d.regs.x10, 0xeeeeddddccccbbbb)
+ self.assertEqual(d.regs.x11, 0x333322221111ffff)
+ self.assertEqual(d.regs.x12, 0x7777666655554444)
+ self.assertEqual(d.regs.x13, 0xbbbbaaaa99998888)
+ self.assertEqual(d.regs.x14, 0xffffeeeeddddcccc)
+ self.assertEqual(d.regs.x15, 0x4444333322221111)
+ self.assertEqual(d.regs.x16, 0x8888777766665555)
+ self.assertEqual(d.regs.x17, 0xccccbbbbaaaa9999)
+ self.assertEqual(d.regs.x18, 0x1111ffffeeeedddd)
+ self.assertEqual(d.regs.x19, 0x5555444433332222)
+ self.assertEqual(d.regs.x20, 0x9999888877776666)
+ self.assertEqual(d.regs.x21, 0xddddccccbbbbaaaa)
+ self.assertEqual(d.regs.x22, 0x22221111ffffeeee)
+ self.assertEqual(d.regs.x23, 0x6666555544443333)
+ self.assertEqual(d.regs.x24, 0xaaaa999988887777)
+ self.assertEqual(d.regs.x25, 0xeeeeddddccccbbbb)
+ self.assertEqual(d.regs.x26, 0x333322221111ffff)
+ self.assertEqual(d.regs.x27, 0x7777666655554444)
+ self.assertEqual(d.regs.x28, 0xbbbbaaaa99998888)
+ self.assertEqual(d.regs.x29, 0xffffeeeeddddcccc)
+ self.assertEqual(d.regs.x30, 0x4444333322221111)
+
+ self.assertEqual(d.regs.lr, 0x4444333322221111)
+ self.assertEqual(d.regs.fp, 0xffffeeeeddddcccc)
+ self.assertEqual(d.regs.xzr, 0)
+ self.assertEqual(d.regs.wzr, 0)
+
+ d.regs.xzr = 0x123456789abcdef0
+ d.regs.wzr = 0x12345678
+
+ self.assertEqual(d.regs.xzr, 0)
+ self.assertEqual(d.regs.wzr, 0)
+
+ self.assertEqual(d.regs.w0, 0x22221111)
+ self.assertEqual(d.regs.w1, 0x66665555)
+ self.assertEqual(d.regs.w2, 0xaaaa9999)
+ self.assertEqual(d.regs.w3, 0xeeeedddd)
+ self.assertEqual(d.regs.w4, 0x33332222)
+ self.assertEqual(d.regs.w5, 0x77776666)
+ self.assertEqual(d.regs.w6, 0xbbbbaaaa)
+ self.assertEqual(d.regs.w7, 0xffffeeee)
+ self.assertEqual(d.regs.w8, 0x44443333)
+ self.assertEqual(d.regs.w9, 0x88887777)
+ self.assertEqual(d.regs.w10, 0xccccbbbb)
+ self.assertEqual(d.regs.w11, 0x1111ffff)
+ self.assertEqual(d.regs.w12, 0x55554444)
+ self.assertEqual(d.regs.w13, 0x99998888)
+ self.assertEqual(d.regs.w14, 0xddddcccc)
+ self.assertEqual(d.regs.w15, 0x22221111)
+ self.assertEqual(d.regs.w16, 0x66665555)
+ self.assertEqual(d.regs.w17, 0xaaaa9999)
+ self.assertEqual(d.regs.w18, 0xeeeedddd)
+ self.assertEqual(d.regs.w19, 0x33332222)
+ self.assertEqual(d.regs.w20, 0x77776666)
+ self.assertEqual(d.regs.w21, 0xbbbbaaaa)
+ self.assertEqual(d.regs.w22, 0xffffeeee)
+ self.assertEqual(d.regs.w23, 0x44443333)
+ self.assertEqual(d.regs.w24, 0x88887777)
+ self.assertEqual(d.regs.w25, 0xccccbbbb)
+ self.assertEqual(d.regs.w26, 0x1111ffff)
+ self.assertEqual(d.regs.w27, 0x55554444)
+ self.assertEqual(d.regs.w28, 0x99998888)
+ self.assertEqual(d.regs.w29, 0xddddcccc)
+ self.assertEqual(d.regs.w30, 0x22221111)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "aarch64", "Requires aarch64")
+ def test_registers_hardware_aarch64(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+ d.run()
+
+ bp = d.breakpoint(0x4008a4, hardware=True)
+
+ d.cont()
+
+ self.assertEqual(d.regs.pc, bp.address)
+
+ self.assertEqual(d.regs.x0, 0x4444333322221111)
+ self.assertEqual(d.regs.x1, 0x8888777766665555)
+ self.assertEqual(d.regs.x2, 0xccccbbbbaaaa9999)
+ self.assertEqual(d.regs.x3, 0x1111ffffeeeedddd)
+ self.assertEqual(d.regs.x4, 0x5555444433332222)
+ self.assertEqual(d.regs.x5, 0x9999888877776666)
+ self.assertEqual(d.regs.x6, 0xddddccccbbbbaaaa)
+ self.assertEqual(d.regs.x7, 0x22221111ffffeeee)
+ self.assertEqual(d.regs.x8, 0x6666555544443333)
+ self.assertEqual(d.regs.x9, 0xaaaa999988887777)
+ self.assertEqual(d.regs.x10, 0xeeeeddddccccbbbb)
+ self.assertEqual(d.regs.x11, 0x333322221111ffff)
+ self.assertEqual(d.regs.x12, 0x7777666655554444)
+ self.assertEqual(d.regs.x13, 0xbbbbaaaa99998888)
+ self.assertEqual(d.regs.x14, 0xffffeeeeddddcccc)
+ self.assertEqual(d.regs.x15, 0x4444333322221111)
+ self.assertEqual(d.regs.x16, 0x8888777766665555)
+ self.assertEqual(d.regs.x17, 0xccccbbbbaaaa9999)
+ self.assertEqual(d.regs.x18, 0x1111ffffeeeedddd)
+ self.assertEqual(d.regs.x19, 0x5555444433332222)
+ self.assertEqual(d.regs.x20, 0x9999888877776666)
+ self.assertEqual(d.regs.x21, 0xddddccccbbbbaaaa)
+ self.assertEqual(d.regs.x22, 0x22221111ffffeeee)
+ self.assertEqual(d.regs.x23, 0x6666555544443333)
+ self.assertEqual(d.regs.x24, 0xaaaa999988887777)
+ self.assertEqual(d.regs.x25, 0xeeeeddddccccbbbb)
+ self.assertEqual(d.regs.x26, 0x333322221111ffff)
+ self.assertEqual(d.regs.x27, 0x7777666655554444)
+ self.assertEqual(d.regs.x28, 0xbbbbaaaa99998888)
+ self.assertEqual(d.regs.x29, 0xffffeeeeddddcccc)
+ self.assertEqual(d.regs.x30, 0x4444333322221111)
+
+ self.assertEqual(d.regs.lr, 0x4444333322221111)
+ self.assertEqual(d.regs.fp, 0xffffeeeeddddcccc)
+ self.assertEqual(d.regs.xzr, 0)
+ self.assertEqual(d.regs.wzr, 0)
+
+ d.regs.xzr = 0x123456789abcdef0
+ d.regs.wzr = 0x12345678
+
+ self.assertEqual(d.regs.xzr, 0)
+ self.assertEqual(d.regs.wzr, 0)
+
+ self.assertEqual(d.regs.w0, 0x22221111)
+ self.assertEqual(d.regs.w1, 0x66665555)
+ self.assertEqual(d.regs.w2, 0xaaaa9999)
+ self.assertEqual(d.regs.w3, 0xeeeedddd)
+ self.assertEqual(d.regs.w4, 0x33332222)
+ self.assertEqual(d.regs.w5, 0x77776666)
+ self.assertEqual(d.regs.w6, 0xbbbbaaaa)
+ self.assertEqual(d.regs.w7, 0xffffeeee)
+ self.assertEqual(d.regs.w8, 0x44443333)
+ self.assertEqual(d.regs.w9, 0x88887777)
+ self.assertEqual(d.regs.w10, 0xccccbbbb)
+ self.assertEqual(d.regs.w11, 0x1111ffff)
+ self.assertEqual(d.regs.w12, 0x55554444)
+ self.assertEqual(d.regs.w13, 0x99998888)
+ self.assertEqual(d.regs.w14, 0xddddcccc)
+ self.assertEqual(d.regs.w15, 0x22221111)
+ self.assertEqual(d.regs.w16, 0x66665555)
+ self.assertEqual(d.regs.w17, 0xaaaa9999)
+ self.assertEqual(d.regs.w18, 0xeeeedddd)
+ self.assertEqual(d.regs.w19, 0x33332222)
+ self.assertEqual(d.regs.w20, 0x77776666)
+ self.assertEqual(d.regs.w21, 0xbbbbaaaa)
+ self.assertEqual(d.regs.w22, 0xffffeeee)
+ self.assertEqual(d.regs.w23, 0x44443333)
+ self.assertEqual(d.regs.w24, 0x88887777)
+ self.assertEqual(d.regs.w25, 0xccccbbbb)
+ self.assertEqual(d.regs.w26, 0x1111ffff)
+ self.assertEqual(d.regs.w27, 0x55554444)
+ self.assertEqual(d.regs.w28, 0x99998888)
+ self.assertEqual(d.regs.w29, 0xddddcccc)
+ self.assertEqual(d.regs.w30, 0x22221111)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "i386", "Requires i386")
+ def test_registers_i386(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+ d.run()
+
+ bp1 = d.breakpoint(0x8049186, hardware=False)
+ bp2 = d.breakpoint(0x80491a3, hardware=False)
+ bp3 = d.breakpoint(0x80491ac, hardware=False)
+ bp4 = d.breakpoint(0x80491b5, hardware=False)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, bp1.address)
+
+ self.assertEqual(d.regs.eax, 0x00112233)
+ self.assertEqual(d.regs.ebx, 0x11223344)
+ self.assertEqual(d.regs.ecx, 0x22334455)
+ self.assertEqual(d.regs.edx, 0x33445566)
+ self.assertEqual(d.regs.esi, 0x44556677)
+ self.assertEqual(d.regs.edi, 0x55667788)
+ self.assertEqual(d.regs.ebp, 0x66778899)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, bp2.address)
+
+ self.assertEqual(d.regs.ax, 0x1122)
+ self.assertEqual(d.regs.bx, 0x2233)
+ self.assertEqual(d.regs.cx, 0x3344)
+ self.assertEqual(d.regs.dx, 0x4455)
+ self.assertEqual(d.regs.si, 0x5566)
+ self.assertEqual(d.regs.di, 0x6677)
+ self.assertEqual(d.regs.bp, 0x7788)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, bp3.address)
+
+ self.assertEqual(d.regs.al, 0x11)
+ self.assertEqual(d.regs.bl, 0x22)
+ self.assertEqual(d.regs.cl, 0x33)
+ self.assertEqual(d.regs.dl, 0x44)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, bp4.address)
+
+ self.assertEqual(d.regs.ah, 0x12)
+ self.assertEqual(d.regs.bh, 0x23)
+ self.assertEqual(d.regs.ch, 0x34)
+ self.assertEqual(d.regs.dh, 0x45)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "i386", "Requires i386")
+ def test_registers_hardware_i386(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+ d.run()
+
+ bp1 = d.breakpoint(0x8049186, hardware=True)
+ bp2 = d.breakpoint(0x80491a3, hardware=True)
+ bp3 = d.breakpoint(0x80491ac, hardware=True)
+ bp4 = d.breakpoint(0x80491b5, hardware=True)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, bp1.address)
+
+ self.assertEqual(d.regs.eax, 0x00112233)
+ self.assertEqual(d.regs.ebx, 0x11223344)
+ self.assertEqual(d.regs.ecx, 0x22334455)
+ self.assertEqual(d.regs.edx, 0x33445566)
+ self.assertEqual(d.regs.esi, 0x44556677)
+ self.assertEqual(d.regs.edi, 0x55667788)
+ self.assertEqual(d.regs.ebp, 0x66778899)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, bp2.address)
+
+ self.assertEqual(d.regs.ax, 0x1122)
+ self.assertEqual(d.regs.bx, 0x2233)
+ self.assertEqual(d.regs.cx, 0x3344)
+ self.assertEqual(d.regs.dx, 0x4455)
+ self.assertEqual(d.regs.si, 0x5566)
+ self.assertEqual(d.regs.di, 0x6677)
+ self.assertEqual(d.regs.bp, 0x7788)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, bp3.address)
+
+ self.assertEqual(d.regs.al, 0x11)
+ self.assertEqual(d.regs.bl, 0x22)
+ self.assertEqual(d.regs.cl, 0x33)
+ self.assertEqual(d.regs.dl, 0x44)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, bp4.address)
+
+ self.assertEqual(d.regs.ah, 0x12)
+ self.assertEqual(d.regs.bh, 0x23)
+ self.assertEqual(d.regs.ch, 0x34)
+ self.assertEqual(d.regs.dh, 0x45)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_register_find_amd64(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+
+ d.run()
+
+ bp1 = d.breakpoint(0x4011CA)
+ bp2 = d.breakpoint(0x40128D)
+ bp3 = d.breakpoint(0x401239)
+ bp4 = d.breakpoint(0x4011F4)
+ bp5 = d.breakpoint(0x401296)
+
+ d.cont()
+ self.assertTrue(bp1.address == d.regs.rip)
+
+ self.assertIn("rax", d.regs.filter(0x0011223344556677))
+ self.assertIn("rbx", d.regs.filter(0x1122334455667700))
+ self.assertIn("rcx", d.regs.filter(0x2233445566770011))
+ self.assertIn("rdx", d.regs.filter(0x3344556677001122))
+ self.assertIn("rsi", d.regs.filter(0x4455667700112233))
+ self.assertIn("rdi", d.regs.filter(0x5566770011223344))
+ self.assertIn("rbp", d.regs.filter(0x6677001122334455))
+ self.assertIn("r8", d.regs.filter(0xAABBCCDD11223344))
+ self.assertIn("r9", d.regs.filter(0xBBCCDD11223344AA))
+ self.assertIn("r10", d.regs.filter(0xCCDD11223344AABB))
+ self.assertIn("r11", d.regs.filter(0xDD11223344AABBCC))
+ self.assertIn("r12", d.regs.filter(0x11223344AABBCCDD))
+ self.assertIn("r13", d.regs.filter(0x223344AABBCCDD11))
+ self.assertIn("r14", d.regs.filter(0x3344AABBCCDD1122))
+ self.assertIn("r15", d.regs.filter(0x44AABBCCDD112233))
+
+ d.cont()
+ self.assertTrue(bp4.address == d.regs.rip)
+
+ self.assertIn("al", d.regs.filter(0x11))
+ self.assertIn("bl", d.regs.filter(0x22))
+ self.assertIn("cl", d.regs.filter(0x33))
+ self.assertIn("dl", d.regs.filter(0x44))
+ self.assertIn("sil", d.regs.filter(0x55))
+ self.assertIn("dil", d.regs.filter(0x66))
+ self.assertIn("bpl", d.regs.filter(0x77))
+ self.assertIn("r8b", d.regs.filter(0x88))
+ self.assertIn("r9b", d.regs.filter(0x99))
+ self.assertIn("r10b", d.regs.filter(0xAA))
+ self.assertIn("r11b", d.regs.filter(0xBB))
+ self.assertIn("r12b", d.regs.filter(0xCC))
+ self.assertIn("r13b", d.regs.filter(0xDD))
+ self.assertIn("r14b", d.regs.filter(0xEE))
+ self.assertIn("r15b", d.regs.filter(0xFF))
+
+ d.cont()
+ self.assertTrue(bp3.address == d.regs.rip)
+
+ self.assertIn("ax", d.regs.filter(0x1122))
+ self.assertIn("bx", d.regs.filter(0x2233))
+ self.assertIn("cx", d.regs.filter(0x3344))
+ self.assertIn("dx", d.regs.filter(0x4455))
+ self.assertIn("si", d.regs.filter(0x5566))
+ self.assertIn("di", d.regs.filter(0x6677))
+ self.assertIn("bp", d.regs.filter(0x7788))
+ self.assertIn("r8w", d.regs.filter(0x8899))
+ self.assertIn("r9w", d.regs.filter(0x99AA))
+ self.assertIn("r10w", d.regs.filter(0xAABB))
+ self.assertIn("r11w", d.regs.filter(0xBBCC))
+ self.assertIn("r12w", d.regs.filter(0xCCDD))
+ self.assertIn("r13w", d.regs.filter(0xDDEE))
+ self.assertIn("r14w", d.regs.filter(0xEEFF))
+ self.assertIn("r15w", d.regs.filter(0xFF00))
+
+ d.cont()
+ self.assertTrue(bp2.address == d.regs.rip)
+
+ self.assertIn("eax", d.regs.filter(0x11223344))
+ self.assertIn("ebx", d.regs.filter(0x22334455))
+ self.assertIn("ecx", d.regs.filter(0x33445566))
+ self.assertIn("edx", d.regs.filter(0x44556677))
+ self.assertIn("esi", d.regs.filter(0x55667788))
+ self.assertIn("edi", d.regs.filter(0x66778899))
+ self.assertIn("ebp", d.regs.filter(0x778899AA))
+ self.assertIn("r8d", d.regs.filter(0x8899AABB))
+ self.assertIn("r9d", d.regs.filter(0x99AABBCC))
+ self.assertIn("r10d", d.regs.filter(0xAABBCCDD))
+ self.assertIn("r11d", d.regs.filter(0xBBCCDD11))
+ self.assertIn("r12d", d.regs.filter(0xCCDD1122))
+ self.assertIn("r13d", d.regs.filter(0xDD112233))
+ self.assertIn("r14d", d.regs.filter(0x11223344))
+ self.assertIn("r15d", d.regs.filter(0x22334455))
+
+
+ d.cont()
+ self.assertTrue(bp5.address == d.regs.rip)
+
+ self.assertIn("ah", d.regs.filter(0x11))
+ self.assertIn("bh", d.regs.filter(0x22))
+ self.assertIn("ch", d.regs.filter(0x33))
+ self.assertIn("dh", d.regs.filter(0x44))
+
+
+ d.cont()
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "aarch64", "Requires aarch64")
+ def test_register_find_aarch64(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+ d.run()
+
+ bp = d.breakpoint(0x4008a4)
+
+ d.cont()
+
+ assert d.regs.pc == bp.address
+
+ self.assertIn("x0", d.regs.filter(0x4444333322221111))
+ self.assertIn("x1", d.regs.filter(0x8888777766665555))
+ self.assertIn("x2", d.regs.filter(0xccccbbbbaaaa9999))
+ self.assertIn("x3", d.regs.filter(0x1111ffffeeeedddd))
+ self.assertIn("x4", d.regs.filter(0x5555444433332222))
+ self.assertIn("x5", d.regs.filter(0x9999888877776666))
+ self.assertIn("x6", d.regs.filter(0xddddccccbbbbaaaa))
+ self.assertIn("x7", d.regs.filter(0x22221111ffffeeee))
+ self.assertIn("x8", d.regs.filter(0x6666555544443333))
+ self.assertIn("x9", d.regs.filter(0xaaaa999988887777))
+ self.assertIn("x10", d.regs.filter(0xeeeeddddccccbbbb))
+ self.assertIn("x11", d.regs.filter(0x333322221111ffff))
+ self.assertIn("x12", d.regs.filter(0x7777666655554444))
+ self.assertIn("x13", d.regs.filter(0xbbbbaaaa99998888))
+ self.assertIn("x14", d.regs.filter(0xffffeeeeddddcccc))
+ self.assertIn("x15", d.regs.filter(0x4444333322221111))
+ self.assertIn("x16", d.regs.filter(0x8888777766665555))
+ self.assertIn("x17", d.regs.filter(0xccccbbbbaaaa9999))
+ self.assertIn("x18", d.regs.filter(0x1111ffffeeeedddd))
+ self.assertIn("x19", d.regs.filter(0x5555444433332222))
+ self.assertIn("x20", d.regs.filter(0x9999888877776666))
+ self.assertIn("x21", d.regs.filter(0xddddccccbbbbaaaa))
+ self.assertIn("x22", d.regs.filter(0x22221111ffffeeee))
+ self.assertIn("x23", d.regs.filter(0x6666555544443333))
+ self.assertIn("x24", d.regs.filter(0xaaaa999988887777))
+ self.assertIn("x25", d.regs.filter(0xeeeeddddccccbbbb))
+ self.assertIn("x26", d.regs.filter(0x333322221111ffff))
+ self.assertIn("x27", d.regs.filter(0x7777666655554444))
+ self.assertIn("x28", d.regs.filter(0xbbbbaaaa99998888))
+ self.assertIn("x29", d.regs.filter(0xffffeeeeddddcccc))
+ self.assertIn("x30", d.regs.filter(0x4444333322221111))
+
+ self.assertIn("lr", d.regs.filter(0x4444333322221111))
+ self.assertIn("fp", d.regs.filter(0xffffeeeeddddcccc))
+ self.assertIn("xzr", d.regs.filter(0))
+ self.assertIn("wzr", d.regs.filter(0))
+
+ d.regs.xzr = 0x123456789abcdef0
+ d.regs.wzr = 0x12345678
+
+ assert d.regs.xzr == 0
+ assert d.regs.wzr == 0
+
+ self.assertIn("wzr", d.regs.filter(0))
+ self.assertIn("xzr", d.regs.filter(0))
+
+ self.assertIn("w0", d.regs.filter(0x22221111))
+ self.assertIn("w1", d.regs.filter(0x66665555))
+ self.assertIn("w2", d.regs.filter(0xaaaa9999))
+ self.assertIn("w3", d.regs.filter(0xeeeedddd))
+ self.assertIn("w4", d.regs.filter(0x33332222))
+ self.assertIn("w5", d.regs.filter(0x77776666))
+ self.assertIn("w6", d.regs.filter(0xbbbbaaaa))
+ self.assertIn("w7", d.regs.filter(0xffffeeee))
+ self.assertIn("w8", d.regs.filter(0x44443333))
+ self.assertIn("w9", d.regs.filter(0x88887777))
+ self.assertIn("w10", d.regs.filter(0xccccbbbb))
+ self.assertIn("w11", d.regs.filter(0x1111ffff))
+ self.assertIn("w12", d.regs.filter(0x55554444))
+ self.assertIn("w13", d.regs.filter(0x99998888))
+ self.assertIn("w14", d.regs.filter(0xddddcccc))
+ self.assertIn("w15", d.regs.filter(0x22221111))
+ self.assertIn("w16", d.regs.filter(0x66665555))
+ self.assertIn("w17", d.regs.filter(0xaaaa9999))
+ self.assertIn("w18", d.regs.filter(0xeeeedddd))
+ self.assertIn("w19", d.regs.filter(0x33332222))
+ self.assertIn("w20", d.regs.filter(0x77776666))
+ self.assertIn("w21", d.regs.filter(0xbbbbaaaa))
+ self.assertIn("w22", d.regs.filter(0xffffeeee))
+ self.assertIn("w23", d.regs.filter(0x44443333))
+ self.assertIn("w24", d.regs.filter(0x88887777))
+ self.assertIn("w25", d.regs.filter(0xccccbbbb))
+ self.assertIn("w26", d.regs.filter(0x1111ffff))
+ self.assertIn("w27", d.regs.filter(0x55554444))
+ self.assertIn("w28", d.regs.filter(0x99998888))
+ self.assertIn("w29", d.regs.filter(0xddddcccc))
+ self.assertIn("w30", d.regs.filter(0x22221111))
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "i386", "Requires i386")
+ def test_register_find_i386(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+ d.run()
+
+ bp1 = d.breakpoint(0x8049186)
+ bp2 = d.breakpoint(0x80491a3)
+ bp3 = d.breakpoint(0x80491ac)
+ bp4 = d.breakpoint(0x80491b5)
+
+ d.cont()
+
+ self.assertTrue(bp1.address == d.regs.eip)
+
+ self.assertIn("eax", d.regs.filter(0x00112233))
+ self.assertIn("ebx", d.regs.filter(0x11223344))
+ self.assertIn("ecx", d.regs.filter(0x22334455))
+ self.assertIn("edx", d.regs.filter(0x33445566))
+ self.assertIn("esi", d.regs.filter(0x44556677))
+ self.assertIn("edi", d.regs.filter(0x55667788))
+ self.assertIn("ebp", d.regs.filter(0x66778899))
+
+ d.cont()
+
+ self.assertTrue(bp2.address == d.regs.eip)
+
+ self.assertIn("ax", d.regs.filter(0x1122))
+ self.assertIn("bx", d.regs.filter(0x2233))
+ self.assertIn("cx", d.regs.filter(0x3344))
+ self.assertIn("dx", d.regs.filter(0x4455))
+ self.assertIn("si", d.regs.filter(0x5566))
+ self.assertIn("di", d.regs.filter(0x6677))
+ self.assertIn("bp", d.regs.filter(0x7788))
+
+ d.cont()
+
+ self.assertTrue(bp3.address == d.regs.eip)
+
+ self.assertIn("al", d.regs.filter(0x11))
+ self.assertIn("bl", d.regs.filter(0x22))
+ self.assertIn("cl", d.regs.filter(0x33))
+ self.assertIn("dl", d.regs.filter(0x44))
+
+ d.cont()
+
+ self.assertTrue(bp4.address == d.regs.eip)
+
+ self.assertIn("ah", d.regs.filter(0x12))
+ self.assertIn("bh", d.regs.filter(0x23))
+ self.assertIn("ch", d.regs.filter(0x34))
+ self.assertIn("dh", d.regs.filter(0x45))
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ def test_registers_pprint(self):
+ d = debugger(RESOLVE_EXE("basic_test"))
+
+ d.run()
+
+ # Temporarily redirect stdout to suppress output
+ stdout = sys.stdout
+ sys.stdout = StringIO()
+
+ # The following calls should not terminate
+ d.pprint_registers()
+ d.pprint_registers_all()
+ d.pprint_regs()
+ d.pprint_regs_all()
+
+ # Reset stdout
+ sys.stdout = stdout
+
+ d.kill()
+ d.terminate()
diff --git a/test/scripts/run_pipes_test.py b/test/scripts/run_pipes_test.py
new file mode 100644
index 00000000..de6bcf22
--- /dev/null
+++ b/test/scripts/run_pipes_test.py
@@ -0,0 +1,94 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from subprocess import Popen, PIPE
+from unittest import TestCase
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
+
+
+class RunPipesTest(TestCase):
+ def test_binary_proxy(self):
+ unpatched = Popen([RESOLVE_EXE("run_pipes_test")], stdin=PIPE, stdout=PIPE, stderr=PIPE)
+
+ buffer = b""
+ while b"1." not in buffer:
+ buffer += unpatched.stdout.read(1)
+
+ # Option 1 should print the flag
+ unpatched.stdin.write(b"1\n")
+ unpatched.stdin.flush()
+
+ buffer = b""
+ while b"1." not in buffer:
+ buffer += unpatched.stdout.read(1)
+ self.assertIn(b"flag{provola}", buffer)
+
+ # Option 2 should print the flag after the admin mode check
+ unpatched.stdin.write(b"2\n")
+ unpatched.stdin.flush()
+ unpatched.stdin.write(b"admin\n")
+ unpatched.stdin.flush()
+
+ buffer = b""
+ while b"1." not in buffer:
+ buffer += unpatched.stdout.read(1)
+ self.assertIn(b"flag{provola}", buffer)
+
+ # Option 3 should print the flag after the signal handler
+ unpatched.stdin.write(b"3\n")
+ unpatched.stdin.flush()
+
+ buffer = b""
+ while b"1." not in buffer:
+ buffer += unpatched.stdout.read(1)
+ self.assertIn(b"flag{provola}", buffer)
+
+ # Exit
+ unpatched.stdin.write(b"4\n")
+ unpatched.stdin.flush()
+
+ unpatched.kill()
+
+ patched = Popen(["python3", f"scripts/run_pipes_test_script_{PLATFORM}.py", RESOLVE_EXE("run_pipes_test")], stdin=PIPE, stdout=PIPE, stderr=PIPE)
+
+ buffer = b""
+ while b"1." not in buffer:
+ buffer += patched.stdout.read(1)
+
+ # Option 1 should print not the flag
+ patched.stdin.write(b"1\n")
+ patched.stdin.flush()
+
+ buffer = b""
+ while b"1." not in buffer:
+ buffer += patched.stdout.read(1)
+ self.assertIn(b"flag{nahmate}", buffer)
+
+ # Option 2 should not print the flag after the admin mode check
+ patched.stdin.write(b"2\n")
+ patched.stdin.flush()
+ patched.stdin.write(b"admin\n")
+ patched.stdin.flush()
+
+ buffer = b""
+ while b"1." not in buffer:
+ buffer += patched.stdout.read(1)
+ self.assertIn(b"Wrong password", buffer)
+
+ # Option 3 should not print the flag after the signal handler
+ patched.stdin.write(b"3\n")
+ patched.stdin.flush()
+
+ buffer = b""
+ while b"1." not in buffer:
+ buffer += patched.stdout.read(1)
+ self.assertIn(b"SIGPROVOLA", buffer)
+
+ # Exit
+ patched.stdin.write(b"4\n")
+ patched.stdin.flush()
+
+ patched.kill()
diff --git a/test/scripts/run_pipes_test_script_aarch64.py b/test/scripts/run_pipes_test_script_aarch64.py
new file mode 100644
index 00000000..e978b187
--- /dev/null
+++ b/test/scripts/run_pipes_test_script_aarch64.py
@@ -0,0 +1,36 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+import sys
+
+from libdebug import debugger
+
+d = debugger(sys.argv[1])
+
+def on_enter_write(t, sh):
+ if b"flag" in t.memory[t.syscall_arg1, 50]:
+ t.memory[t.syscall_arg1, 50] = t.memory[t.syscall_arg1, 50].replace(b"flag{provola}", b"flag{nahmate}")
+
+def strcmp_admin_mode(t, bp):
+ t.regs.x0 = 3
+
+def on_signal_sigprovola(t, sc):
+ print("SIGPROVOLA", flush=True)
+
+d.run(redirect_pipes=False)
+
+bp = d.breakpoint(0x400aec, callback=strcmp_admin_mode)
+
+sh = d.handle_syscall("write", on_enter=on_enter_write)
+
+sc = d.catch_signal(50, callback=on_signal_sigprovola)
+d.signals_to_block = [50]
+
+d.cont()
+
+d.wait()
+
+d.terminate()
diff --git a/test/scripts/run_pipes_test_script_amd64.py b/test/scripts/run_pipes_test_script_amd64.py
new file mode 100644
index 00000000..8da075c8
--- /dev/null
+++ b/test/scripts/run_pipes_test_script_amd64.py
@@ -0,0 +1,36 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+import sys
+
+from libdebug import debugger
+
+d = debugger(sys.argv[1])
+
+def on_enter_write(t, sh):
+ if b"flag" in t.memory[t.syscall_arg1, 50]:
+ t.memory[t.syscall_arg1, 50] = t.memory[t.syscall_arg1, 50].replace(b"flag{provola}", b"flag{nahmate}")
+
+def strcmp_admin_mode(t, bp):
+ t.regs.rax = 3
+
+def on_signal_sigprovola(t, sc):
+ print("SIGPROVOLA", flush=True)
+
+d.run(redirect_pipes=False)
+
+bp = d.breakpoint(0x401218, callback=strcmp_admin_mode)
+
+sh = d.handle_syscall("write", on_enter=on_enter_write)
+
+sc = d.catch_signal(50, callback=on_signal_sigprovola)
+d.signals_to_block = [50]
+
+d.cont()
+
+d.wait()
+
+d.terminate()
diff --git a/test/scripts/run_pipes_test_script_i386.py b/test/scripts/run_pipes_test_script_i386.py
new file mode 100644
index 00000000..d69e6b52
--- /dev/null
+++ b/test/scripts/run_pipes_test_script_i386.py
@@ -0,0 +1,36 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+import sys
+
+from libdebug import debugger
+
+d = debugger(sys.argv[1])
+
+def on_enter_write(t, sh):
+ if b"flag" in t.memory[t.syscall_arg1, 50]:
+ t.memory[t.syscall_arg1, 50] = t.memory[t.syscall_arg1, 50].replace(b"flag{provola}", b"flag{nahmate}")
+
+def strcmp_admin_mode(t, bp):
+ t.regs.eax = 3
+
+def on_signal_sigprovola(t, sc):
+ print("SIGPROVOLA", flush=True)
+
+d.run(redirect_pipes=False)
+
+bp = d.breakpoint(0x12ba, callback=strcmp_admin_mode, file="binary")
+
+sh = d.handle_syscall("write", on_enter=on_enter_write)
+
+sc = d.catch_signal(50, callback=on_signal_sigprovola)
+d.signals_to_block = [50]
+
+d.cont()
+
+d.wait()
+
+d.terminate()
diff --git a/test/amd64/scripts/catch_signal_test.py b/test/scripts/signal_catch_test.py
similarity index 90%
rename from test/amd64/scripts/catch_signal_test.py
rename to test/scripts/signal_catch_test.py
index 8afc7979..4e7d8d73 100644
--- a/test/amd64/scripts/catch_signal_test.py
+++ b/test/scripts/signal_catch_test.py
@@ -6,12 +6,23 @@
import io
import logging
-import unittest
+from unittest import TestCase
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
from libdebug import debugger
-class SignalCatchTest(unittest.TestCase):
+match PLATFORM:
+ case "amd64":
+ ADDRESS = 0x12c4
+ case "aarch64":
+ ADDRESS = 0x964
+ case "i386":
+ ADDRESS = 0x12fa
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+class SignalCatchTest(TestCase):
def setUp(self):
# Redirect logging to a string buffer
self.log_capture_string = io.StringIO()
@@ -61,7 +72,7 @@ def catcher_SIGPIPE(t, sc):
SIGPIPE_count += 1
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
d.signals_to_block = ["SIGUSR1", 15, "SIGINT", 3, 13]
@@ -76,6 +87,7 @@ def catcher_SIGPIPE(t, sc):
d.cont()
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 2)
self.assertEqual(SIGTERM_count, 2)
@@ -121,7 +133,7 @@ def catcher_SIGPIPE(t, sc):
SIGPIPE_count += 1
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
@@ -149,6 +161,7 @@ def catcher_SIGPIPE(t, sc):
SIGPIPE += r.recvline()
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 2)
self.assertEqual(SIGTERM_count, 2)
@@ -200,7 +213,7 @@ def catcher_SIGPIPE(t, sc):
SIGPIPE_count += 1
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
@@ -210,7 +223,7 @@ def catcher_SIGPIPE(t, sc):
catcher4 = d.catch_signal("SIGQUIT", callback=catcher_SIGQUIT)
catcher5 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
- bp = d.breakpoint(0x12C4)
+ bp = d.breakpoint(ADDRESS)
d.cont()
@@ -236,6 +249,7 @@ def catcher_SIGPIPE(t, sc):
SIGPIPE += r.recvline()
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 2)
self.assertEqual(SIGTERM_count, 2)
@@ -287,7 +301,7 @@ def catcher_SIGPIPE(t, sc):
SIGPIPE_count += 1
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
@@ -299,7 +313,7 @@ def catcher_SIGPIPE(t, sc):
catcher4 = d.catch_signal("SIGQUIT", callback=catcher_SIGQUIT)
catcher5 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
- bp = d.breakpoint(0x12C4)
+ bp = d.breakpoint(ADDRESS)
d.cont()
@@ -317,6 +331,7 @@ def catcher_SIGPIPE(t, sc):
break
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 2)
self.assertEqual(SIGTERM_count, 2)
@@ -368,7 +383,7 @@ def catcher_SIGPIPE(t, sc):
SIGPIPE_count += 1
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
@@ -380,7 +395,7 @@ def catcher_SIGPIPE(t, sc):
catcher4 = d.catch_signal("SIGQUIT", callback=catcher_SIGQUIT)
catcher5 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE)
- bp = d.breakpoint(0x12C4)
+ bp = d.breakpoint(ADDRESS)
d.cont()
@@ -400,6 +415,7 @@ def catcher_SIGPIPE(t, sc):
break
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 2)
self.assertEqual(SIGTERM_count, 2)
@@ -424,7 +440,7 @@ def catcher_SIGUSR1(t, sc):
# Hijack to SIGTERM
t.signal = 15
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
@@ -448,6 +464,7 @@ def catcher_SIGUSR1(t, sc):
SIGPIPE += r.recvline()
d.kill()
+ d.terminate()
self.assertEqual(catcher1.hit_count, 2)
@@ -458,7 +475,7 @@ def catcher_SIGUSR1(t, sc):
self.assertEqual(SIGPIPE, b"Received signal 13" * 3)
def test_hijack_signal_with_api(self):
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
@@ -483,6 +500,7 @@ def test_hijack_signal_with_api(self):
SIGPIPE += r.recvline()
d.kill()
+ d.terminate()
self.assertEqual(catcher1.hit_count, 2)
@@ -508,7 +526,7 @@ def catcher_SIGTERM(t, sc):
SIGTERM_count += 1
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
@@ -533,6 +551,7 @@ def catcher_SIGTERM(t, sc):
SIGPIPE += r.recvline()
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 2)
self.assertEqual(SIGTERM_count, 4) # 2 times more because of the hijack
@@ -554,7 +573,7 @@ def catcher_SIGTERM(t, sc):
SIGTERM_count += 1
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
@@ -579,6 +598,7 @@ def catcher_SIGTERM(t, sc):
SIGPIPE += r.recvline()
d.kill()
+ d.terminate()
self.assertEqual(SIGTERM_count, 4) # 2 times more because of the hijack
self.assertEqual(catcher1.hit_count, 2)
@@ -606,7 +626,7 @@ def catcher_SIGTERM(t, sc):
SIGTERM_count += 1
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
@@ -631,6 +651,7 @@ def catcher_SIGTERM(t, sc):
SIGPIPE += r.recvline()
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 2)
self.assertEqual(SIGTERM_count, 2) # 2 times in total because of the recursive=False
@@ -652,7 +673,7 @@ def catcher_SIGTERM(t, sc):
SIGTERM_count += 1
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
@@ -677,6 +698,7 @@ def catcher_SIGTERM(t, sc):
SIGPIPE += r.recvline()
d.kill()
+ d.terminate()
self.assertEqual(catcher1.hit_count, 2)
self.assertEqual(SIGTERM_count, 2) # 2 times in total because of the recursive=False
@@ -699,7 +721,7 @@ def catcher_SIGTERM(t, sc):
# Hijack to SIGINT
t.signal = 10
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
d.run()
@@ -736,11 +758,12 @@ def catcher_SIGTERM(t, sc):
d.cont()
d.kill()
+ d.terminate()
def test_hijack_signal_with_api_loop(self):
# Let create a loop of hijacking signals
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
d.run()
@@ -777,6 +800,7 @@ def test_hijack_signal_with_api_loop(self):
d.cont()
d.kill()
+ d.terminate()
def test_signal_unhijacking(self):
SIGUSR1_count = 0
@@ -798,7 +822,7 @@ def catcher_SIGINT(t, sc):
SIGINT_count += 1
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
@@ -808,7 +832,7 @@ def catcher_SIGINT(t, sc):
catcher4 = d.hijack_signal("SIGQUIT", "SIGTERM", recursive=True)
catcher5 = d.hijack_signal("SIGPIPE", "SIGTERM", recursive=True)
- bp = d.breakpoint(0x12C4)
+ bp = d.breakpoint(ADDRESS)
d.cont()
@@ -834,6 +858,7 @@ def catcher_SIGINT(t, sc):
SIGPIPE += r.recvline()
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 2)
self.assertEqual(SIGTERM_count, 2 + 2 + 2) # 2 times more because of the hijacking * 2 (SIGQUIT and SIGPIPE)
@@ -863,13 +888,13 @@ def catcher_SIGPIPE_second(t, sc):
SIGPIPE_count_second += 1
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
catcher1 = d.catch_signal("SIGPIPE", callback=catcher_SIGPIPE_first)
- bp = d.breakpoint(0x12C4)
+ bp = d.breakpoint(ADDRESS)
d.cont()
@@ -895,6 +920,7 @@ def catcher_SIGPIPE_second(t, sc):
SIGPIPE += r.recvline()
d.kill()
+ d.terminate()
self.assertEqual(SIGPIPE_count_first, 2)
self.assertEqual(SIGPIPE_count_second, 1)
@@ -914,13 +940,13 @@ def catcher_SIGPIPE_second(t, sc):
)
def test_override_hijack(self):
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
catcher1 = d.hijack_signal("SIGPIPE", 15)
- bp = d.breakpoint(0x12C4)
+ bp = d.breakpoint(ADDRESS)
d.cont()
@@ -946,6 +972,7 @@ def test_override_hijack(self):
SIGPIPE += r.recvline()
d.kill()
+ d.terminate()
self.assertEqual(catcher1.hit_count, 2)
self.assertEqual(catcher2.hit_count, 1)
@@ -969,13 +996,13 @@ def catcher_SIGPIPE(t, sc):
SIGPIPE_count += 1
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
catcher1 = d.hijack_signal("SIGPIPE", 15)
- bp = d.breakpoint(0x12C4)
+ bp = d.breakpoint(ADDRESS)
d.cont()
@@ -1001,6 +1028,7 @@ def catcher_SIGPIPE(t, sc):
SIGPIPE += r.recvline()
d.kill()
+ d.terminate()
self.assertEqual(catcher1.hit_count, 2)
self.assertEqual(catcher2.hit_count, 1)
@@ -1059,7 +1087,7 @@ def catcher_SIGPIPE(t, sc):
SIGPIPE_count += 1
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
d.signals_to_block = ["SIGUSR1", 15, "SIGINT", 3, 13]
@@ -1074,6 +1102,7 @@ def catcher_SIGPIPE(t, sc):
d.cont()
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 2)
self.assertEqual(SIGTERM_count, 2)
@@ -1107,7 +1136,7 @@ def catcher_SIGINT(t, sc):
SIGINT_count += 1
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
@@ -1117,7 +1146,7 @@ def catcher_SIGINT(t, sc):
catcher4 = d.hijack_signal("SIGQUIT", "SIGTERM", recursive=True)
catcher5 = d.hijack_signal("SIGPIPE", "SIGTERM", recursive=True)
- bp = d.breakpoint(0x12C4)
+ bp = d.breakpoint(ADDRESS)
d.cont()
@@ -1145,6 +1174,7 @@ def catcher_SIGINT(t, sc):
SIGPIPE += r.recvline()
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 2)
self.assertEqual(SIGTERM_count, 2 + 2 + 2) # 2 times more because of the hijacking * 2 (SIGQUIT and SIGPIPE)
@@ -1167,7 +1197,7 @@ def test_signal_catch_sync_block(self):
SIGTERM_count = 0
SIGPIPE_count = 0
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
d.signals_to_block = ["SIGUSR1", 15, "SIGINT", 3, 13]
@@ -1194,6 +1224,7 @@ def test_signal_catch_sync_block(self):
SIGPIPE_count += 1
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 2)
self.assertEqual(SIGTERM_count, 2)
@@ -1216,7 +1247,7 @@ def test_signal_catch_sync_pass(self):
signals = b""
- d = debugger("binaries/catch_signal_test")
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
r = d.run()
@@ -1246,6 +1277,7 @@ def test_signal_catch_sync_pass(self):
SIGPIPE_count += 1
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 2)
self.assertEqual(SIGTERM_count, 2)
@@ -1264,3 +1296,52 @@ def test_signal_catch_sync_pass(self):
self.assertEqual(signals.count(b"Received signal 2"), 2)
self.assertEqual(signals.count(b"Received signal 3"), 3)
self.assertEqual(signals.count(b"Received signal 13"), 3)
+
+ def test_signal_empty_callback(self):
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
+
+ d.signals_to_block = ["SIGUSR1", 15, "SIGINT", 3, 13]
+
+ d.run()
+
+ catcher1 = d.catch_signal(10, callback=True)
+ catcher2 = d.catch_signal("SIGTERM", callback=True)
+ catcher3 = d.catch_signal(2, callback=True)
+ catcher4 = d.catch_signal("SIGQUIT", callback=True)
+ catcher5 = d.catch_signal("SIGPIPE", callback=True)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(2, catcher1.hit_count)
+ self.assertEqual(2, catcher2.hit_count)
+ self.assertEqual(2, catcher3.hit_count)
+ self.assertEqual(3, catcher4.hit_count)
+ self.assertEqual(3, catcher5.hit_count)
+
+ def test_catch_all_signals(self):
+ d = debugger(RESOLVE_EXE("catch_signal_test"))
+
+ d.signals_to_block = ["SIGUSR1", 15, "SIGINT", 3, 13]
+
+ for value in ["ALL", "all", "*", "pkm", -1]:
+ counter = 0
+
+ d.run()
+
+ def catcher(t, cs):
+ nonlocal counter
+ counter += 1
+
+ catcher = d.catch_signal(value, callback=catcher)
+
+ d.cont()
+
+ d.kill()
+
+ self.assertEqual(12, catcher.hit_count)
+ self.assertEqual(12, counter)
+
+ d.terminate()
diff --git a/test/amd64/scripts/signals_multithread_test.py b/test/scripts/signal_multithread_test.py
similarity index 92%
rename from test/amd64/scripts/signals_multithread_test.py
rename to test/scripts/signal_multithread_test.py
index e42a481c..28461e98 100644
--- a/test/amd64/scripts/signals_multithread_test.py
+++ b/test/scripts/signal_multithread_test.py
@@ -4,12 +4,23 @@
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
-import unittest
+from unittest import TestCase
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
from libdebug import debugger
-class SignalMultithreadTest(unittest.TestCase):
+match PLATFORM:
+ case "amd64":
+ TEST_SIGNAL_MULTITHREAD_SEND_SIGNAL_BP_ADDRESS = 0x14d8
+ case "aarch64":
+ TEST_SIGNAL_MULTITHREAD_SEND_SIGNAL_BP_ADDRESS = 0xf1c
+ case "i386":
+ TEST_SIGNAL_MULTITHREAD_SEND_SIGNAL_BP_ADDRESS = 0x156a
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+class SignalMultithreadTest(TestCase):
def test_signal_multithread_undet_catch_signal_block(self):
SIGUSR1_count = 0
SIGINT_count = 0
@@ -42,7 +53,7 @@ def catcher_SIGPIPE(t, sc):
SIGPIPE_count += 1
- d = debugger("binaries/signals_multithread_undet_test")
+ d = debugger(RESOLVE_EXE("signals_multithread_undet_test"))
r = d.run()
@@ -63,6 +74,7 @@ def catcher_SIGPIPE(t, sc):
r.recvline(2)
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 4)
self.assertEqual(SIGTERM_count, 4)
@@ -108,7 +120,7 @@ def catcher_SIGPIPE(t, sc):
SIGPIPE_count += 1
- d = debugger("binaries/signals_multithread_undet_test")
+ d = debugger(RESOLVE_EXE("signals_multithread_undet_test"))
r = d.run()
@@ -131,6 +143,7 @@ def catcher_SIGPIPE(t, sc):
received.append(r.recvline())
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 4)
self.assertEqual(SIGTERM_count, 4)
@@ -199,7 +212,7 @@ def catcher_SIGPIPE(t, sc):
SIGPIPE_count += 1
tids.append(t.thread_id)
- d = debugger("binaries/signals_multithread_det_test")
+ d = debugger(RESOLVE_EXE("signals_multithread_det_test"))
r = d.run()
@@ -220,6 +233,7 @@ def catcher_SIGPIPE(t, sc):
receiver = d.threads[1].thread_id
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 2)
self.assertEqual(SIGTERM_count, 2)
@@ -280,7 +294,7 @@ def catcher_SIGPIPE(t, sc):
SIGPIPE_count += 1
tids.append(t.thread_id)
- d = debugger("binaries/signals_multithread_det_test")
+ d = debugger(RESOLVE_EXE("signals_multithread_det_test"))
r = d.run()
@@ -301,6 +315,7 @@ def catcher_SIGPIPE(t, sc):
receiver = d.threads[1].thread_id
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 2)
self.assertEqual(SIGTERM_count, 2)
@@ -368,12 +383,12 @@ def catcher_SIGPIPE(t, sc):
SIGPIPE_count += 1
tids.append(t.thread_id)
- d = debugger("binaries/signals_multithread_det_test")
+ d = debugger(RESOLVE_EXE("signals_multithread_det_test"))
# Set a breakpoint to stop the program before the end of the receiver thread
r = d.run()
- bp = d.breakpoint(0x14d8, hardware=True, file="binary")
+ bp = d.breakpoint(TEST_SIGNAL_MULTITHREAD_SEND_SIGNAL_BP_ADDRESS, hardware=True, file="binary")
catcher1 = d.catch_signal("SIGUSR1", callback=catcher_SIGUSR1)
catcher2 = d.catch_signal("SIGTERM", callback=catcher_SIGTERM)
@@ -398,6 +413,7 @@ def catcher_SIGPIPE(t, sc):
receiver = d.threads[1].thread_id
d.kill()
+ d.terminate()
self.assertEqual(SIGUSR1_count, 2)
self.assertEqual(SIGTERM_count, 2)
diff --git a/test/amd64/scripts/speed_test.py b/test/scripts/speed_test.py
similarity index 76%
rename from test/amd64/scripts/speed_test.py
rename to test/scripts/speed_test.py
index 623091f0..4e14eb75 100644
--- a/test/amd64/scripts/speed_test.py
+++ b/test/scripts/speed_test.py
@@ -4,15 +4,16 @@
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
-import unittest
+from unittest import TestCase
+from utils.binary_utils import RESOLVE_EXE
from time import perf_counter_ns
from libdebug import debugger
-class SpeedTest(unittest.TestCase):
+class SpeedTest(TestCase):
def setUp(self):
- self.d = debugger("binaries/speed_test")
+ self.d = debugger(RESOLVE_EXE("speed_test"))
def test_speed(self):
d = self.d
@@ -26,10 +27,11 @@ def test_speed(self):
d.cont()
for _ in range(65536):
- self.assertTrue(bp.address == d.regs.rip)
+ self.assertTrue(bp.address == d.instruction_pointer)
d.cont()
d.kill()
+ d.terminate()
end_time = perf_counter_ns()
@@ -47,15 +49,12 @@ def test_speed_hardware(self):
d.cont()
for _ in range(65536):
- self.assertTrue(bp.address == d.regs.rip)
+ self.assertTrue(bp.address == d.instruction_pointer)
d.cont()
d.kill()
+ d.terminate()
end_time = perf_counter_ns()
self.assertTrue((end_time - start_time) < 15 * 1e9) # 15 seconds
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/test/aarch64/scripts/handle_syscall_test.py b/test/scripts/syscall_handle_test.py
similarity index 56%
rename from test/aarch64/scripts/handle_syscall_test.py
rename to test/scripts/syscall_handle_test.py
index 9ca32a1d..2d185a17 100644
--- a/test/aarch64/scripts/handle_syscall_test.py
+++ b/test/scripts/syscall_handle_test.py
@@ -8,12 +8,37 @@
import logging
import os
import sys
-import unittest
+from unittest import TestCase
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
+from utils.thread_utils import FUN_RET_VAL
from libdebug import debugger
-class HandleSyscallTest(unittest.TestCase):
+match PLATFORM:
+ case "amd64":
+ WRITE_NUM = 1
+ MMAP_NUM = 9
+ GETCWD_NUM = 0x4F
+ BP_ADDRESS = 0x401196
+ MMAP_NAME = "mmap"
+ case "aarch64":
+ WRITE_NUM = 64
+ MMAP_NUM = 222
+ GETCWD_NUM = 17
+ BP_ADDRESS = 0x9d4
+ MMAP_NAME = "mmap"
+ case "i386":
+ WRITE_NUM = 4
+ MMAP_NUM = 192
+ GETCWD_NUM = 183
+ BP_ADDRESS = 0x121a
+ MMAP_NAME = "mmap_pgoff"
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+
+class SyscallHandleTest(TestCase):
def setUp(self):
# Redirect stdout
self.capturedOutput = io.StringIO()
@@ -39,7 +64,7 @@ def tearDown(self):
self.log_handler.close()
def test_handles(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
@@ -50,39 +75,38 @@ def on_enter_write(d, sh):
nonlocal write_count
if write_count == 0:
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
self.assertEqual(d.syscall_arg0, 1)
write_count += 1
else:
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
self.assertEqual(d.syscall_arg0, 1)
write_count += 1
def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 222)
+ self.assertTrue(sh.syscall_number == MMAP_NUM)
nonlocal ptr
- ptr = d.regs.x0
+ ptr = FUN_RET_VAL(d)
def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.syscall_arg0, ptr)
def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.memory[ptr, 8], os.getcwd()[:8].encode())
handler1 = d.handle_syscall("write", on_enter_write, None)
- handler2 = d.handle_syscall("mmap", None, on_exit_mmap)
+ handler2 = d.handle_syscall(MMAP_NAME, None, on_exit_mmap)
handler3 = d.handle_syscall("getcwd", on_enter_getcwd, on_exit_getcwd)
r.sendline(b"provola")
d.cont()
- d.wait()
d.kill()
d.terminate()
@@ -93,7 +117,7 @@ def on_exit_getcwd(d, sh):
self.assertEqual(handler3.hit_count, 1)
def test_handles_with_pprint(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
@@ -106,33 +130,33 @@ def on_enter_write(d, sh):
nonlocal write_count
if write_count == 0:
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
self.assertEqual(d.syscall_arg0, 1)
write_count += 1
else:
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
self.assertEqual(d.syscall_arg0, 1)
write_count += 1
def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 222)
+ self.assertTrue(sh.syscall_number == MMAP_NUM)
nonlocal ptr
- ptr = d.regs.x0
+ ptr = FUN_RET_VAL(d)
def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.syscall_arg0, ptr)
def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.memory[ptr, 8], os.getcwd()[:8].encode())
handler1 = d.handle_syscall("write", on_enter_write, None)
- handler2 = d.handle_syscall("mmap", None, on_exit_mmap)
+ handler2 = d.handle_syscall(MMAP_NAME, None, on_exit_mmap)
handler3 = d.handle_syscall("getcwd", on_enter_getcwd, on_exit_getcwd)
r.sendline(b"provola")
@@ -149,7 +173,7 @@ def on_exit_getcwd(d, sh):
self.assertEqual(handler3.hit_count, 1)
def test_handle_disabling(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
@@ -160,44 +184,44 @@ def on_enter_write(d, sh):
nonlocal write_count
if write_count == 0:
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
self.assertEqual(d.syscall_arg0, 1)
write_count += 1
else:
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
self.assertEqual(d.syscall_arg0, 1)
write_count += 1
def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 222)
+ self.assertTrue(sh.syscall_number == MMAP_NUM)
nonlocal ptr
- ptr = d.regs.x0
+ ptr = FUN_RET_VAL(d)
def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.syscall_arg0, ptr)
def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.memory[ptr, 8], os.getcwd()[:8].encode())
- handler1 = d.handle_syscall(0x40, on_enter_write, None)
- handler2 = d.handle_syscall(222, None, on_exit_mmap)
- handler3 = d.handle_syscall(17, on_enter_getcwd, on_exit_getcwd)
+ handler1 = d.handle_syscall(WRITE_NUM, on_enter_write, None)
+ handler2 = d.handle_syscall(MMAP_NUM, None, on_exit_mmap)
+ handler3 = d.handle_syscall(GETCWD_NUM, on_enter_getcwd, on_exit_getcwd)
r.sendline(b"provola")
- bp = d.breakpoint(0x9d4, file="binary")
+ bp = d.breakpoint(BP_ADDRESS)
d.cont()
d.wait()
- self.assertEqual(d.regs.pc, bp.address)
+ self.assertEqual(d.instruction_pointer, bp.address)
handler1.disable()
d.cont()
@@ -211,7 +235,7 @@ def on_exit_getcwd(d, sh):
self.assertEqual(handler3.hit_count, 1)
def test_handle_disabling_with_pprint(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
@@ -224,44 +248,44 @@ def on_enter_write(d, sh):
nonlocal write_count
if write_count == 0:
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
self.assertEqual(d.syscall_arg0, 1)
write_count += 1
else:
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
self.assertEqual(d.syscall_arg0, 1)
write_count += 1
def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 222)
+ self.assertTrue(sh.syscall_number == MMAP_NUM)
nonlocal ptr
- ptr = d.regs.x0
+ ptr = FUN_RET_VAL(d)
def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.syscall_arg0, ptr)
def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.memory[ptr, 8], os.getcwd()[:8].encode())
- handler1 = d.handle_syscall(0x40, on_enter_write, None)
- handler2 = d.handle_syscall(222, None, on_exit_mmap)
- handler3 = d.handle_syscall(17, on_enter_getcwd, on_exit_getcwd)
+ handler1 = d.handle_syscall(WRITE_NUM, on_enter_write, None)
+ handler2 = d.handle_syscall(MMAP_NUM, None, on_exit_mmap)
+ handler3 = d.handle_syscall(GETCWD_NUM, on_enter_getcwd, on_exit_getcwd)
r.sendline(b"provola")
- bp = d.breakpoint(0x9d4, file="binary")
+ bp = d.breakpoint(BP_ADDRESS)
d.cont()
d.wait()
- self.assertEqual(d.regs.pc, bp.address)
+ self.assertEqual(d.instruction_pointer, bp.address)
handler1.disable()
d.cont()
@@ -275,7 +299,7 @@ def on_exit_getcwd(d, sh):
self.assertEqual(handler3.hit_count, 1)
def test_handle_overwrite(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
@@ -286,7 +310,7 @@ def test_handle_overwrite(self):
def on_enter_write_first(d, sh):
nonlocal write_count_first
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
self.assertEqual(d.syscall_arg0, 1)
write_count_first += 1
@@ -294,40 +318,40 @@ def on_enter_write_first(d, sh):
def on_enter_write_second(d, sh):
nonlocal write_count_second
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
self.assertEqual(d.syscall_arg0, 1)
write_count_second += 1
def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 222)
+ self.assertTrue(sh.syscall_number == MMAP_NUM)
nonlocal ptr
- ptr = d.regs.x0
+ ptr = FUN_RET_VAL(d)
def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.syscall_arg0, ptr)
def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.memory[ptr, 8], os.getcwd()[:8].encode())
- handler1_1 = d.handle_syscall(0x40, on_enter_write_first, None)
- handler2 = d.handle_syscall(222, None, on_exit_mmap)
- handler3 = d.handle_syscall(17, on_enter_getcwd, on_exit_getcwd)
+ handler1_1 = d.handle_syscall(WRITE_NUM, on_enter_write_first, None)
+ handler2 = d.handle_syscall(MMAP_NUM, None, on_exit_mmap)
+ handler3 = d.handle_syscall(GETCWD_NUM, on_enter_getcwd, on_exit_getcwd)
r.sendline(b"provola")
- bp = d.breakpoint(0x9d4, file="binary")
+ bp = d.breakpoint(BP_ADDRESS)
d.cont()
d.wait()
- self.assertEqual(d.regs.pc, bp.address)
- handler1_2 = d.handle_syscall(0x40, on_enter_write_second, None)
+ self.assertEqual(d.instruction_pointer, bp.address)
+ handler1_2 = d.handle_syscall(WRITE_NUM, on_enter_write_second, None)
d.cont()
@@ -348,7 +372,7 @@ def on_exit_getcwd(d, sh):
)
def test_handle_overwrite_with_pprint(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
@@ -361,7 +385,7 @@ def test_handle_overwrite_with_pprint(self):
def on_enter_write_first(d, sh):
nonlocal write_count_first
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
self.assertEqual(d.syscall_arg0, 1)
write_count_first += 1
@@ -369,40 +393,40 @@ def on_enter_write_first(d, sh):
def on_enter_write_second(d, sh):
nonlocal write_count_second
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
self.assertEqual(d.syscall_arg0, 1)
write_count_second += 1
def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 222)
+ self.assertTrue(sh.syscall_number == MMAP_NUM)
nonlocal ptr
- ptr = d.regs.x0
+ ptr = FUN_RET_VAL(d)
def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.syscall_arg0, ptr)
def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.memory[ptr, 8], os.getcwd()[:8].encode())
- handler1_1 = d.handle_syscall(0x40, on_enter_write_first, None)
- handler2 = d.handle_syscall(222, None, on_exit_mmap)
- handler3 = d.handle_syscall(17, on_enter_getcwd, on_exit_getcwd)
+ handler1_1 = d.handle_syscall(WRITE_NUM, on_enter_write_first, None)
+ handler2 = d.handle_syscall(MMAP_NUM, None, on_exit_mmap)
+ handler3 = d.handle_syscall(GETCWD_NUM, on_enter_getcwd, on_exit_getcwd)
r.sendline(b"provola")
- bp = d.breakpoint(0x9d4, file="binary")
+ bp = d.breakpoint(BP_ADDRESS)
d.cont()
d.wait()
- self.assertEqual(d.regs.pc, bp.address)
- handler1_2 = d.handle_syscall(0x40, on_enter_write_second, None)
+ self.assertEqual(d.instruction_pointer, bp.address)
+ handler1_2 = d.handle_syscall(WRITE_NUM, on_enter_write_second, None)
d.cont()
@@ -424,7 +448,7 @@ def on_exit_getcwd(d, sh):
def test_handles_sync(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
@@ -435,33 +459,33 @@ def on_enter_write(d, sh):
nonlocal write_count
if write_count == 0:
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
self.assertEqual(d.syscall_arg0, 1)
write_count += 1
else:
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
self.assertEqual(d.syscall_arg0, 1)
write_count += 1
def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 222)
+ self.assertTrue(sh.syscall_number == MMAP_NUM)
nonlocal ptr
- ptr = d.regs.x0
+ ptr = FUN_RET_VAL(d)
def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.syscall_arg0, ptr)
def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.memory[ptr, 8], os.getcwd()[:8].encode())
handler1 = d.handle_syscall("write")
- handler2 = d.handle_syscall("mmap")
+ handler2 = d.handle_syscall(MMAP_NAME)
handler3 = d.handle_syscall("getcwd")
r.sendline(b"provola")
@@ -487,7 +511,7 @@ def on_exit_getcwd(d, sh):
self.assertEqual(handler3.hit_count, 1)
def test_handles_sync_with_pprint(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
@@ -498,33 +522,33 @@ def on_enter_write(d, sh):
nonlocal write_count
if write_count == 0:
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
self.assertEqual(d.syscall_arg0, 1)
write_count += 1
else:
- self.assertTrue(sh.syscall_number == 0x40)
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
self.assertEqual(d.syscall_arg0, 1)
write_count += 1
def on_exit_mmap(d, sh):
- self.assertTrue(sh.syscall_number == 222)
+ self.assertTrue(sh.syscall_number == MMAP_NUM)
nonlocal ptr
- ptr = d.regs.x0
+ ptr = FUN_RET_VAL(d)
def on_enter_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.syscall_arg0, ptr)
def on_exit_getcwd(d, sh):
- self.assertTrue(sh.syscall_number == 17)
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
self.assertEqual(d.memory[ptr, 8], os.getcwd()[:8].encode())
handler1 = d.handle_syscall("write")
- handler2 = d.handle_syscall("mmap")
+ handler2 = d.handle_syscall(MMAP_NAME)
handler3 = d.handle_syscall("getcwd")
d.pprint_syscalls = True
@@ -550,3 +574,139 @@ def on_exit_getcwd(d, sh):
self.assertEqual(handler1.hit_count, 2)
self.assertEqual(handler2.hit_count, 1)
self.assertEqual(handler3.hit_count, 1)
+
+ def test_handles_sync_hit_on(self):
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
+
+ r = d.run()
+
+ ptr = 0
+ write_count = 0
+
+ def on_enter_write(d, sh):
+ nonlocal write_count
+
+ if write_count == 0:
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
+ self.assertEqual(d.memory[d.syscall_arg1, 13], b"Hello, World!")
+ self.assertEqual(d.syscall_arg0, 1)
+ write_count += 1
+ else:
+ self.assertTrue(sh.syscall_number == WRITE_NUM)
+ self.assertEqual(d.memory[d.syscall_arg1, 7], b"provola")
+ self.assertEqual(d.syscall_arg0, 1)
+ write_count += 1
+
+ def on_exit_mmap(d, sh):
+ self.assertTrue(sh.syscall_number == MMAP_NUM)
+
+ nonlocal ptr
+
+ ptr = FUN_RET_VAL(d)
+
+ def on_enter_getcwd(d, sh):
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
+ self.assertEqual(d.syscall_arg0, ptr)
+
+ def on_exit_getcwd(d, sh):
+ self.assertTrue(sh.syscall_number == GETCWD_NUM)
+ self.assertEqual(d.memory[ptr, 8], os.getcwd()[:8].encode())
+
+ handler1 = d.handle_syscall("write")
+ handler2 = d.handle_syscall(MMAP_NAME)
+ handler3 = d.handle_syscall("getcwd")
+
+ r.sendline(b"provola")
+
+ on_enter_1 = True
+ on_exit_2 = True
+ on_enter_3 = True
+
+ while not d.dead:
+ d.cont()
+ d.wait()
+ if handler1.hit_on(d):
+ if on_enter_1:
+ on_enter_write(d, handler1)
+ on_enter_1 = False
+ else:
+ on_enter_1 = True
+ elif handler2.hit_on(d):
+ if on_exit_2:
+ on_exit_2 = False
+ else:
+ on_exit_mmap(d, handler2)
+ on_exit_2 = True
+ elif handler3.hit_on(d):
+ if on_enter_3:
+ on_enter_getcwd(d, handler3)
+ on_enter_3 = False
+ else:
+ on_exit_getcwd(d, handler3)
+ on_enter_3 = True
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(write_count, 2)
+ self.assertEqual(handler1.hit_count, 2)
+ self.assertEqual(handler2.hit_count, 1)
+ self.assertEqual(handler3.hit_count, 1)
+
+ def test_handles_empty_callback(self):
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
+
+ r = d.run()
+
+ handler1 = d.handle_syscall("write", True, None)
+ handler2 = d.handle_syscall(MMAP_NAME, None, True)
+ handler3 = d.handle_syscall("getcwd", True, True)
+
+ r.sendline(b"provola")
+
+ d.cont()
+ d.wait()
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(handler1.hit_count, 2)
+ self.assertEqual(handler2.hit_count, 1)
+ self.assertEqual(handler3.hit_count, 1)
+
+ def test_handle_all_syscalls(self):
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
+
+ for value in ["all", "*", "ALL", -1, "pkm"]:
+ r = d.run()
+
+ enter_count = 0
+ exit_count = 0
+
+ def on_enter_handler(t, hs):
+ nonlocal enter_count
+ enter_count += 1
+
+ def on_exit_handler(t, hs):
+ nonlocal exit_count
+ exit_count += 1
+
+ handler = d.handle_syscall(
+ value, on_enter=on_enter_handler, on_exit=on_exit_handler
+ )
+
+ r.sendline(b"provola")
+
+ d.cont()
+
+ d.kill()
+
+ # The exit_group syscall is handled only during entering for obvious reasons.
+ # Hence, we have 6 enter events and 5 exit events. The hit_count is incremented
+ # at the end of the syscall execution, so it is incremented only during the exit
+ # event.
+ self.assertEqual(handler.hit_count, 5)
+ self.assertEqual(enter_count, 6)
+ self.assertEqual(exit_count, 5)
+
+ d.terminate()
diff --git a/test/amd64/scripts/hijack_syscall_test.py b/test/scripts/syscall_hijack_test.py
similarity index 84%
rename from test/amd64/scripts/hijack_syscall_test.py
rename to test/scripts/syscall_hijack_test.py
index e2067a7d..627b2a77 100644
--- a/test/amd64/scripts/hijack_syscall_test.py
+++ b/test/scripts/syscall_hijack_test.py
@@ -6,12 +6,30 @@
import io
import sys
-import unittest
+from unittest import TestCase
+from utils.binary_utils import PLATFORM, BASE, RESOLVE_EXE
from libdebug import debugger
-class SyscallHijackTest(unittest.TestCase):
+match PLATFORM:
+ case "amd64":
+ WRITE_NUM = 1
+ BP_ADDRESS = 0x4011B0
+ OUTPUT_STR = "0x402010"
+ case "aarch64":
+ WRITE_NUM = 64
+ BP_ADDRESS = 0x9f0
+ OUTPUT_STR = hex(BASE + 0xab0)
+ case "i386":
+ WRITE_NUM = 4
+ BP_ADDRESS = 0x122f
+ OUTPUT_STR = hex(BASE + 0x2008)
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+
+class SyscallHijackTest(TestCase):
def setUp(self):
# Redirect stdout
self.capturedOutput = io.StringIO()
@@ -26,7 +44,7 @@ def on_enter_write(d, sh):
write_count += 1
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
write_count = 0
r = d.run()
@@ -58,6 +76,7 @@ def on_enter_write(d, sh):
d.cont()
d.kill()
+ d.terminate()
self.assertEqual(write_count, handler.hit_count)
self.assertEqual(handler.hit_count, 2)
@@ -68,7 +87,7 @@ def on_enter_write(d, sh):
write_count += 1
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
write_count = 0
r = d.run()
@@ -102,6 +121,7 @@ def on_enter_write(d, sh):
d.cont()
d.kill()
+ d.terminate()
self.assertEqual(write_count, handler.hit_count)
self.assertEqual(handler.hit_count, 2)
@@ -113,9 +133,9 @@ def on_enter_write(d, sh):
write_count += 1
def on_enter_getcwd(d, sh):
- d.syscall_number = 0x1
+ d.syscall_number = WRITE_NUM
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
write_count = 0
r = d.run()
@@ -147,6 +167,7 @@ def on_enter_getcwd(d, sh):
d.cont()
d.kill()
+ d.terminate()
self.assertEqual(write_count, handler.hit_count)
self.assertEqual(handler.hit_count, 2)
@@ -158,9 +179,9 @@ def on_enter_write(d, sh):
write_count += 1
def on_enter_getcwd(d, sh):
- d.syscall_number = 0x1
+ d.syscall_number = WRITE_NUM
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
write_count = 0
r = d.run()
@@ -194,6 +215,7 @@ def on_enter_getcwd(d, sh):
d.cont()
d.kill()
+ d.terminate()
self.assertEqual(write_count, handler.hit_count)
self.assertEqual(handler.hit_count, 2)
@@ -209,14 +231,14 @@ def on_enter_write(d, sh):
write_count += 1
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
write_count = 0
r = d.run()
# recursive hijack is on, we expect the write handler to be called three times
handler = d.handle_syscall("write", on_enter=on_enter_write, recursive=True)
- d.breakpoint(0x4011B0)
+ d.breakpoint(BP_ADDRESS)
d.cont()
print(r.recvline())
@@ -238,6 +260,7 @@ def on_enter_write(d, sh):
print(r.recvline())
d.kill()
+ d.terminate()
self.assertEqual(self.capturedOutput.getvalue().count("Hello, World!"), 2)
self.assertEqual(write_count, handler.hit_count)
@@ -254,7 +277,7 @@ def on_enter_write(d, sh):
write_count += 1
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"), aslr=False)
write_count = 0
r = d.run()
@@ -263,7 +286,7 @@ def on_enter_write(d, sh):
# recursive hijack is on, we expect the write handler to be called three times
handler = d.handle_syscall("write", on_enter=on_enter_write, recursive=True)
- d.breakpoint(0x4011B0)
+ d.breakpoint(BP_ADDRESS)
d.cont()
print(r.recvline())
@@ -285,15 +308,16 @@ def on_enter_write(d, sh):
print(r.recvline())
d.kill()
+ d.terminate()
self.assertEqual(self.capturedOutput.getvalue().count("Hello, World!"), 2)
self.assertEqual(self.capturedOutput.getvalue().count("write"), 3)
- self.assertEqual(self.capturedOutput.getvalue().count("0x402010"), 3)
+ self.assertEqual(self.capturedOutput.getvalue().count(OUTPUT_STR), 3)
self.assertEqual(write_count, handler.hit_count)
self.assertEqual(handler.hit_count, 3)
def test_hijack_syscall_wrong_args(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
d.run()
@@ -301,9 +325,10 @@ def test_hijack_syscall_wrong_args(self):
d.hijack_syscall("read", "write", syscall_arg26=0x1)
d.kill()
+ d.terminate()
def loop_detection_test(self):
- d = debugger("binaries/handle_syscall_test")
+ d = debugger(RESOLVE_EXE("handle_syscall_test"))
r = d.run()
d.hijack_syscall("getcwd", "write", recursive=True)
@@ -332,6 +357,5 @@ def loop_detection_test(self):
# We expect no exception to be raised
d.cont()
-
-if __name__ == "__main__":
- unittest.main()
+ d.kill()
+ d.terminate()
diff --git a/test/amd64/scripts/thread_test.py b/test/scripts/thread_test.py
similarity index 65%
rename from test/amd64/scripts/thread_test.py
rename to test/scripts/thread_test.py
index 380f466f..8d1eef1b 100644
--- a/test/amd64/scripts/thread_test.py
+++ b/test/scripts/thread_test.py
@@ -4,17 +4,29 @@
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
-import unittest
+from unittest import TestCase
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
+from utils.thread_utils import FUN_RET_VAL
from libdebug import debugger
-class ThreadTest(unittest.TestCase):
- def setUp(self):
- pass
-
+match PLATFORM:
+ case "amd64":
+ THREAD_TEST_COMPLEX_BP2_ADDRESS = "thread_1_function+17"
+ THREAD_TEST_COMPLEX_BP3_ADDRESS = "thread_2_function+1e"
+ case "aarch64":
+ THREAD_TEST_COMPLEX_BP2_ADDRESS = "thread_1_function+18"
+ THREAD_TEST_COMPLEX_BP3_ADDRESS = "thread_2_function+24"
+ case "i386":
+ THREAD_TEST_COMPLEX_BP2_ADDRESS = "thread_1_function+23"
+ THREAD_TEST_COMPLEX_BP3_ADDRESS = "thread_2_function+2a"
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+class ThreadTest(TestCase):
def test_thread(self):
- d = debugger("binaries/thread_test")
+ d = debugger(RESOLVE_EXE("thread_test"))
d.run()
@@ -28,17 +40,17 @@ def test_thread(self):
d.cont()
for _ in range(150):
- if bp_t0.address == d.regs.rip:
+ if bp_t0.address == d.instruction_pointer:
self.assertTrue(t1_done)
self.assertTrue(t2_done)
self.assertTrue(t3_done)
break
- if len(d.threads) > 1 and bp_t1.address == d.threads[1].regs.rip:
+ if len(d.threads) > 1 and bp_t1.address == d.threads[1].instruction_pointer:
t1_done = True
- if len(d.threads) > 2 and bp_t2.address == d.threads[2].regs.rip:
+ if len(d.threads) > 2 and bp_t2.address == d.threads[2].instruction_pointer:
t2_done = True
- if len(d.threads) > 3 and bp_t3.address == d.threads[3].regs.rip:
+ if len(d.threads) > 3 and bp_t3.address == d.threads[3].instruction_pointer:
t3_done = True
d.cont()
@@ -47,7 +59,7 @@ def test_thread(self):
d.terminate()
def test_thread_hardware(self):
- d = debugger("binaries/thread_test")
+ d = debugger(RESOLVE_EXE("thread_test"))
d.run()
@@ -61,17 +73,17 @@ def test_thread_hardware(self):
d.cont()
for _ in range(15):
- if bp_t0.address == d.regs.rip:
+ if bp_t0.address == d.instruction_pointer:
self.assertTrue(t1_done)
self.assertTrue(t2_done)
self.assertTrue(t3_done)
break
- if len(d.threads) > 1 and bp_t1.address == d.threads[1].regs.rip:
+ if len(d.threads) > 1 and bp_t1.address == d.threads[1].instruction_pointer:
t1_done = True
- if len(d.threads) > 2 and bp_t2.address == d.threads[2].regs.rip:
+ if len(d.threads) > 2 and bp_t2.address == d.threads[2].instruction_pointer:
t2_done = True
- if len(d.threads) > 3 and bp_t3.address == d.threads[3].regs.rip:
+ if len(d.threads) > 3 and bp_t3.address == d.threads[3].instruction_pointer:
t3_done = True
d.cont()
@@ -79,25 +91,20 @@ def test_thread_hardware(self):
d.kill()
d.terminate()
-
-class ComplexThreadTest(unittest.TestCase):
- def setUp(self):
- pass
-
- def test_thread(self):
+ def test_thread_complex(self):
def factorial(n):
if n == 0:
return 1
else:
return (n * factorial(n - 1)) & (2**32 - 1)
- d = debugger("binaries/complex_thread_test")
+ d = debugger(RESOLVE_EXE("thread_complex_test"))
d.run()
bp1_t0 = d.breakpoint("do_nothing")
- bp2_t1 = d.breakpoint("thread_1_function+17")
- bp3_t2 = d.breakpoint("thread_2_function+1e")
+ bp2_t1 = d.breakpoint(THREAD_TEST_COMPLEX_BP2_ADDRESS)
+ bp3_t2 = d.breakpoint(THREAD_TEST_COMPLEX_BP3_ADDRESS)
bp1_hit, bp2_hit, bp3_hit = False, False, False
t1, t2 = None, None
@@ -111,20 +118,20 @@ def factorial(n):
if len(d.threads) == 3:
t2 = d.threads[2]
- if t1 and bp2_t1.address == t1.regs.rip:
+ if t1 and bp2_t1.address == t1.instruction_pointer:
bp2_hit = True
- self.assertTrue(bp2_t1.hit_count == (t1.regs.rax + 1))
+ self.assertTrue(bp2_t1.hit_count == (FUN_RET_VAL(t1) + 1))
- if bp1_t0.address == d.regs.rip:
+ if bp1_t0.address == d.instruction_pointer:
bp1_hit = True
self.assertTrue(bp2_hit)
self.assertEqual(bp2_t1.hit_count, 50)
self.assertFalse(bp3_hit)
self.assertEqual(bp1_t0.hit_count, 1)
- if t2 and bp3_t2.address == t2.regs.rip:
+ if t2 and bp3_t2.address == t2.instruction_pointer:
bp3_hit = True
- self.assertTrue(factorial(bp3_t2.hit_count) == t2.regs.rax)
+ self.assertTrue(factorial(bp3_t2.hit_count) == FUN_RET_VAL(t2))
self.assertTrue(bp2_hit)
self.assertTrue(bp1_hit)
@@ -135,7 +142,3 @@ def factorial(n):
d.kill()
d.terminate()
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/test/amd64/scripts/vmwhere1_test.py b/test/scripts/vmwhere1_test.py
similarity index 84%
rename from test/amd64/scripts/vmwhere1_test.py
rename to test/scripts/vmwhere1_test.py
index 347778fd..4f5e6316 100644
--- a/test/amd64/scripts/vmwhere1_test.py
+++ b/test/scripts/vmwhere1_test.py
@@ -9,21 +9,20 @@
#
import string
-import unittest
+from unittest import TestCase, skipUnless
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
from libdebug import debugger
-class Vmwhere1(unittest.TestCase):
- def setUp(self):
- pass
-
+class Vmwhere1Test(TestCase):
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
def test_vmwhere1(self):
flag = b""
counter = 3
stop = False
- d = debugger(["CTF/vmwhere1", "CTF/vmwhere1_program"])
+ d = debugger([RESOLVE_EXE("CTF/vmwhere1"), RESOLVE_EXE("CTF/vmwhere1_program")])
while not stop:
for el in string.printable:
@@ -56,16 +55,19 @@ def test_vmwhere1(self):
d.kill()
+ d.terminate()
+
self.assertEqual(
flag, b"uiuctf{ar3_y0u_4_r3al_vm_wh3r3_(gpt_g3n3r4t3d_th1s_f14g)}"
)
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
def test_vmwhere1_callback(self):
flag = b""
counter = 3
stop = False
- d = debugger(["CTF/vmwhere1", "CTF/vmwhere1_program"])
+ d = debugger([RESOLVE_EXE("CTF/vmwhere1"), RESOLVE_EXE("CTF/vmwhere1_program")])
def callback(d, bp):
pass
@@ -98,10 +100,8 @@ def callback(d, bp):
d.kill()
+ d.terminate()
+
self.assertEqual(
flag, b"uiuctf{ar3_y0u_4_r3al_vm_wh3r3_(gpt_g3n3r4t3d_th1s_f14g)}"
)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/test/scripts/watchpoint_test.py b/test/scripts/watchpoint_test.py
new file mode 100644
index 00000000..d13c686d
--- /dev/null
+++ b/test/scripts/watchpoint_test.py
@@ -0,0 +1,892 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2023-2024 Francesco Panebianco, Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from unittest import TestCase, skipUnless
+from utils.binary_utils import PLATFORM, RESOLVE_EXE
+
+from libdebug import debugger
+
+
+class WatchpointTest(TestCase):
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_watchpoint_amd64(self):
+ d = debugger(RESOLVE_EXE("watchpoint_test"), auto_interrupt_on_command=False)
+
+ d.run()
+
+ wp_char = d.breakpoint("global_char", hardware=True, condition="rw", length=1)
+ wp_int = d.breakpoint("global_int", hardware=True, condition="w", length=4)
+ wp_long = d.breakpoint("global_long", hardware=True, condition="rw", length=8)
+
+ d.cont()
+
+ self.assertEqual(d.regs.rip, 0x401111) # mov byte ptr [global_char], 0x1
+ self.assertTrue(wp_char.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 0)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ d.cont()
+
+ self.assertEqual(d.regs.rip, 0x401124) # mov dword ptr [global_int], 0x4050607
+ self.assertTrue(wp_int.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ d.cont()
+
+ self.assertEqual(
+ d.regs.rip, 0x401135
+ ) # mov qword ptr [global_long], 0x8090a0b0c0d0e0f
+ self.assertTrue(wp_long.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 1)
+
+ d.cont()
+
+ self.assertEqual(d.regs.rip, 0x401155) # movzx eax, byte ptr [global_char]
+ self.assertTrue(wp_char.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 2)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 1)
+
+ d.cont()
+
+ self.assertEqual(d.regs.rip, 0x401173) # mov rax, qword ptr [global_long]
+ self.assertTrue(wp_long.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 2)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 2)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_watchpoint_callback_amd64(self):
+ global_char_ip = []
+ global_int_ip = []
+ global_long_ip = []
+
+ def watchpoint_global_char(t, b):
+ nonlocal global_char_ip
+
+ global_char_ip.append(t.instruction_pointer)
+
+ def watchpoint_global_int(t, b):
+ nonlocal global_int_ip
+
+ global_int_ip.append(t.instruction_pointer)
+
+ def watchpoint_global_long(t, b):
+ nonlocal global_long_ip
+
+ global_long_ip.append(t.instruction_pointer)
+
+ d = debugger(RESOLVE_EXE("watchpoint_test"), auto_interrupt_on_command=False)
+
+ d.run()
+
+ wp1 = d.breakpoint(
+ "global_char",
+ hardware=True,
+ condition="rw",
+ length=1,
+ callback=watchpoint_global_char,
+ )
+ wp2 = d.breakpoint(
+ "global_int",
+ hardware=True,
+ condition="w",
+ length=4,
+ callback=watchpoint_global_int,
+ )
+ wp3 = d.breakpoint(
+ "global_long",
+ hardware=True,
+ condition="rw",
+ length=8,
+ callback=watchpoint_global_long,
+ )
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(global_char_ip[0], 0x401111) # mov byte ptr [global_char], 0x1
+ self.assertEqual(
+ global_int_ip[0], 0x401124
+ ) # mov dword ptr [global_int], 0x4050607
+ self.assertEqual(
+ global_long_ip[0], 0x401135
+ ) # mov qword ptr [global_long], 0x8090a0b0c0d0e0f
+ self.assertEqual(
+ global_char_ip[1], 0x401155
+ ) # movzx eax, byte ptr [global_char]
+ self.assertEqual(
+ global_long_ip[1], 0x401173
+ ) # mov rax, qword ptr [global_long]
+
+ self.assertEqual(len(global_char_ip), 2)
+ self.assertEqual(len(global_int_ip), 1)
+
+ # There is one extra hit performed by the exit routine of libc
+ self.assertEqual(len(global_long_ip), 3)
+
+ self.assertEqual(wp1.hit_count, 2)
+ self.assertEqual(wp2.hit_count, 1)
+
+ # There is one extra hit performed by the exit routine of libc
+ self.assertEqual(wp3.hit_count, 3)
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_watchpoint_disable_amd64(self):
+ d = debugger(RESOLVE_EXE("watchpoint_test"), auto_interrupt_on_command=False)
+
+ d.run()
+
+ wp_char = d.breakpoint("global_char", hardware=True, condition="rw", length=1)
+ wp_int = d.breakpoint("global_int", hardware=True, condition="w", length=4)
+ wp_long = d.breakpoint("global_long", hardware=True, condition="rw", length=8)
+
+ d.cont()
+
+ self.assertEqual(d.regs.rip, 0x401111) # mov byte ptr [global_char], 0x1
+ self.assertTrue(wp_char.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 0)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ d.cont()
+
+ self.assertEqual(d.regs.rip, 0x401124) # mov dword ptr [global_int], 0x4050607
+ self.assertTrue(wp_int.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ d.cont()
+
+ self.assertEqual(
+ d.regs.rip, 0x401135
+ ) # mov qword ptr [global_long], 0x8090a0b0c0d0e0f
+ self.assertTrue(wp_long.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 1)
+
+ # disable watchpoint
+ wp_char.disable()
+
+ d.cont()
+
+ self.assertEqual(d.regs.rip, 0x401173) # mov rax, qword ptr [global_long]
+ self.assertTrue(wp_long.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 2)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_watchpoint_disable_reenable_amd64(self):
+ d = debugger(RESOLVE_EXE("watchpoint_test"), auto_interrupt_on_command=False)
+
+ d.run()
+
+ wp_char = d.breakpoint("global_char", hardware=True, condition="rw", length=1)
+ wp_int = d.breakpoint("global_int", hardware=True, condition="w", length=4)
+ wp_long = d.breakpoint("global_long", hardware=True, condition="rw", length=8)
+
+ d.cont()
+
+ self.assertEqual(d.regs.rip, 0x401111) # mov byte ptr [global_char], 0x1
+ self.assertTrue(wp_char.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 0)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ d.cont()
+
+ self.assertEqual(d.regs.rip, 0x401124) # mov dword ptr [global_int], 0x4050607
+ self.assertTrue(wp_int.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ # disable watchpoint
+ wp_long.disable()
+
+ d.cont()
+
+
+ self.assertEqual(d.regs.rip, 0x401155) # movzx eax, byte ptr [global_char]
+ self.assertTrue(wp_char.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 2)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ # re-enable watchpoint
+ wp_long.enable()
+
+ d.cont()
+
+ self.assertEqual(d.regs.rip, 0x401173) # mov rax, qword ptr [global_long]
+ self.assertTrue(wp_long.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 2)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 1)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "amd64", "Requires amd64")
+ def test_watchpoint_alias_amd64(self):
+ d = debugger(RESOLVE_EXE("watchpoint_test"), auto_interrupt_on_command=False)
+
+ d.run()
+
+ d.wp("global_char", condition="rw", length=1)
+ d.watchpoint("global_int", condition="w", length=4)
+ d.watchpoint("global_long", condition="rw", length=8)
+
+ d.cont()
+
+ self.assertEqual(d.instruction_pointer, 0x401111) # mov byte ptr [global_char], 0x1
+
+ d.cont()
+
+ self.assertEqual(d.instruction_pointer, 0x401124) # mov dword ptr [global_int], 0x4050607
+
+ d.cont()
+
+ self.assertEqual(d.instruction_pointer, 0x401135) # mov qword ptr [global_long], 0x8090a0b0c0d0e0f
+
+ d.cont()
+
+ self.assertEqual(d.instruction_pointer, 0x401155) # movzx eax, byte ptr [global_char]
+
+ d.cont()
+
+ self.assertEqual(d.instruction_pointer, 0x401173) # mov rax, qword ptr [global_long]
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "aarch64", "Requires aarch64")
+ def test_watchpoint_aarch64(self):
+ d = debugger(RESOLVE_EXE("watchpoint_test"), auto_interrupt_on_command=False)
+
+ d.run()
+
+ d.breakpoint("global_char", hardware=True, condition="rw", length=1)
+ d.breakpoint("global_int", hardware=True, condition="w", length=4)
+ d.breakpoint("global_short", hardware=True, condition="r", length=2)
+ d.breakpoint("global_long", hardware=True, condition="rw", length=8)
+
+ d.cont()
+
+ base = d.regs.pc & ~0xfff
+
+ # strb w1, [x0] => global_char
+ self.assertEqual(d.regs.pc, base + 0x724)
+
+ d.cont()
+
+ # str w1, [x0] => global_int
+ self.assertEqual(d.regs.pc, base + 0x748)
+
+ d.cont()
+
+ # str x1, [x0] => global_long
+ self.assertEqual(d.regs.pc, base + 0x764)
+
+ d.cont()
+
+ # ldrb w0, [x0] => global_char
+ self.assertEqual(d.regs.pc, base + 0x780)
+
+ d.cont()
+
+ # ldr w0, [x0] => global_short
+ self.assertEqual(d.regs.pc, base + 0x790)
+
+ d.cont()
+
+ # ldr x0, [x0] => global_long
+ self.assertEqual(d.regs.pc, base + 0x7b0)
+
+ d.cont()
+
+ d.kill()
+
+ @skipUnless(PLATFORM == "aarch64", "Requires aarch64")
+ def test_watchpoint_callback_aarch64(self):
+ global_char_ip = []
+ global_int_ip = []
+ global_short_ip = []
+ global_long_ip = []
+
+ def watchpoint_global_char(t, b):
+ nonlocal global_char_ip
+
+ global_char_ip.append(t.regs.pc)
+
+ def watchpoint_global_int(t, b):
+ nonlocal global_int_ip
+
+ global_int_ip.append(t.regs.pc)
+
+ def watchpoint_global_short(t, b):
+ nonlocal global_short_ip
+
+ global_short_ip.append(t.regs.pc)
+
+ def watchpoint_global_long(t, b):
+ nonlocal global_long_ip
+
+ global_long_ip.append(t.regs.pc)
+
+ d = debugger(RESOLVE_EXE("watchpoint_test"), auto_interrupt_on_command=False)
+
+ d.run()
+
+ base = d.regs.pc & ~0xfff
+
+ wp1 = d.breakpoint(
+ "global_char",
+ hardware=True,
+ condition="rw",
+ length=1,
+ callback=watchpoint_global_char,
+ )
+ wp2 = d.breakpoint(
+ "global_int",
+ hardware=True,
+ condition="w",
+ length=4,
+ callback=watchpoint_global_int,
+ )
+ wp3 = d.breakpoint(
+ "global_long",
+ hardware=True,
+ condition="rw",
+ length=8,
+ callback=watchpoint_global_long,
+ )
+ wp4 = d.breakpoint(
+ "global_short",
+ hardware=True,
+ condition="r",
+ length=2,
+ callback=watchpoint_global_short,
+ )
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ # strb w1, [x0] => global_char
+ self.assertEqual(global_char_ip[0], base + 0x724)
+
+ # str w1, [x0] => global_int
+ self.assertEqual(global_int_ip[0], base + 0x748)
+
+ # str x1, [x0] => global_long
+ self.assertEqual(global_long_ip[0], base + 0x764)
+
+ # ldrb w0, [x0] => global_char
+ self.assertEqual(global_char_ip[1], base + 0x780)
+
+ # ldr w0, [x0] => global_short
+ self.assertEqual(global_short_ip[0], base + 0x790)
+
+ # ldr x0, [x0] => global_long
+ self.assertEqual(global_long_ip[1], base + 0x7b0)
+
+ self.assertEqual(len(global_char_ip), 2)
+ self.assertEqual(len(global_int_ip), 1)
+ self.assertEqual(len(global_short_ip), 1)
+ self.assertEqual(len(global_long_ip), 2)
+ self.assertEqual(wp1.hit_count, 2)
+ self.assertEqual(wp2.hit_count, 1)
+ self.assertEqual(wp3.hit_count, 2)
+ self.assertEqual(wp4.hit_count, 1)
+
+ @skipUnless(PLATFORM == "aarch64", "Requires aarch64")
+ def test_watchpoint_disable_aarch64(self):
+ d = debugger(RESOLVE_EXE("watchpoint_test"), auto_interrupt_on_command=False)
+
+ d.run()
+
+ wp_char = d.breakpoint("global_char", hardware=True, condition="rw", length=1)
+ wp_int = d.breakpoint("global_int", hardware=True, condition="w", length=4)
+ wp_short = d.breakpoint("global_short", hardware=True, condition="r", length=2)
+ wp_long = d.breakpoint("global_long", hardware=True, condition="rw", length=8)
+
+ d.cont()
+
+ base = d.regs.pc & ~0xfff
+
+ # strb w1, [x0] => global_char
+ self.assertEqual(d.regs.pc, base + 0x724)
+ self.assertTrue(wp_char.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 0)
+ self.assertEqual(wp_short.hit_count, 0)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ d.cont()
+
+ # str w1, [x0] => global_int
+ self.assertEqual(d.regs.pc, base + 0x748)
+ self.assertTrue(wp_int.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_short.hit_count, 0)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ d.cont()
+
+ # str x1, [x0] => global_long
+ self.assertEqual(d.regs.pc, base + 0x764)
+ self.assertTrue(wp_long.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_short.hit_count, 0)
+ self.assertEqual(wp_long.hit_count, 1)
+
+ # disable watchpoint
+ wp_char.disable()
+
+ d.cont()
+
+ # ldr w0, [x0] => global_short
+ self.assertEqual(d.regs.pc, base + 0x790)
+ self.assertTrue(wp_short.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_short.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 1)
+
+ d.cont()
+
+ # ldr x0, [x0] => global_long
+ self.assertEqual(d.regs.pc, base + 0x7b0)
+ self.assertTrue(wp_long.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_short.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 2)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "aarch64", "Requires aarch64")
+ def test_watchpoint_disable_reenable_aarch64(self):
+ d = debugger(RESOLVE_EXE("watchpoint_test"), auto_interrupt_on_command=False)
+
+ d.run()
+
+ wp_char = d.breakpoint("global_char", hardware=True, condition="rw", length=1)
+ wp_int = d.breakpoint("global_int", hardware=True, condition="w", length=4)
+ wp_short = d.breakpoint("global_short", hardware=True, condition="r", length=2)
+ wp_long = d.breakpoint("global_long", hardware=True, condition="rw", length=8)
+
+ d.cont()
+
+ base = d.regs.pc & ~0xfff
+
+ # strb w1, [x0] => global_char
+ self.assertEqual(d.regs.pc, base + 0x724)
+ self.assertTrue(wp_char.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 0)
+ self.assertEqual(wp_short.hit_count, 0)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ d.cont()
+
+ # str w1, [x0] => global_int
+ self.assertEqual(d.regs.pc, base + 0x748)
+ self.assertTrue(wp_int.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_short.hit_count, 0)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ # disable watchpoint
+ wp_long.disable()
+
+ d.cont()
+
+ # ldrb w0, [x0] => global_char
+ self.assertEqual(d.regs.pc, base + 0x780)
+ self.assertTrue(wp_char.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 2)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_short.hit_count, 0)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ # re-enable watchpoint
+ wp_long.enable()
+
+ d.cont()
+
+ # ldr w0, [x0] => global_short
+ self.assertEqual(d.regs.pc, base + 0x790)
+ self.assertTrue(wp_short.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 2)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_short.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ d.cont()
+
+ # ldr x0, [x0] => global_long
+ self.assertEqual(d.regs.pc, base + 0x7b0)
+ self.assertTrue(wp_long.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 2)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_short.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 1)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "aarch64", "Requires aarch64")
+ def test_watchpoint_alias_aarch64(self):
+ d = debugger(RESOLVE_EXE("watchpoint_test"), auto_interrupt_on_command=False)
+
+ d.run()
+
+ d.wp("global_char", condition="rw", length=1)
+ d.watchpoint("global_int", condition="w", length=4)
+ d.watchpoint("global_short", condition="r", length=2)
+ d.watchpoint("global_long", condition="rw", length=8)
+
+ d.cont()
+
+ base = d.regs.pc & ~0xfff
+
+ # strb w1, [x0] => global_char
+ self.assertEqual(d.regs.pc, base + 0x724)
+
+ d.cont()
+
+ # str w1, [x0] => global_int
+ self.assertEqual(d.regs.pc, base + 0x748)
+
+ d.cont()
+
+ # str x1, [x0] => global_long
+ self.assertEqual(d.regs.pc, base + 0x764)
+
+ d.cont()
+
+ # ldrb w0, [x0] => global_char
+ self.assertEqual(d.regs.pc, base + 0x780)
+
+ d.cont()
+
+ # ldr w0, [x0] => global_short
+ self.assertEqual(d.regs.pc, base + 0x790)
+
+ d.cont()
+
+ # ldr x0, [x0] => global_long
+ self.assertEqual(d.regs.pc, base + 0x7b0)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "i386", "Requires i386")
+ def test_watchpoint_i386(self):
+ d = debugger(RESOLVE_EXE("watchpoint_test"), auto_interrupt_on_command=False)
+
+ d.run()
+
+ wp_char = d.breakpoint("global_char", hardware=True, condition="rw", length=1)
+ wp_int = d.breakpoint("global_int", hardware=True, condition="w", length=4)
+ wp_long = d.breakpoint("global_long", hardware=True, condition="rw", length=4)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, 0x8049166) # mov byte ptr [global_char], 0x1
+ self.assertTrue(wp_char.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 0)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, 0x8049179) # mov dword ptr [global_int], 0x4050607
+ self.assertTrue(wp_int.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ d.cont()
+
+ self.assertEqual(
+ d.regs.eip, 0x8049183
+ ) # mov dword ptr [global_long], 0xc0d0e0f
+ self.assertTrue(wp_long.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 1)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, 0x80491b7) # movzx eax, byte ptr [global_char]
+ self.assertTrue(wp_char.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 2)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 1)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, 0x80491d5) # mov eax, dword ptr [global_long]
+ self.assertTrue(wp_long.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 2)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 2)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "i386", "Requires i386")
+ def test_watchpoint_callback_i386(self):
+ global_char_ip = []
+ global_int_ip = []
+ global_long_ip = []
+
+ def watchpoint_global_char(t, b):
+ nonlocal global_char_ip
+
+ global_char_ip.append(t.instruction_pointer)
+
+ def watchpoint_global_int(t, b):
+ nonlocal global_int_ip
+
+ global_int_ip.append(t.instruction_pointer)
+
+ def watchpoint_global_long(t, b):
+ nonlocal global_long_ip
+
+ global_long_ip.append(t.instruction_pointer)
+
+ d = debugger(RESOLVE_EXE("watchpoint_test"), auto_interrupt_on_command=False)
+
+ d.run()
+
+ wp1 = d.breakpoint(
+ "global_char",
+ hardware=True,
+ condition="rw",
+ length=1,
+ callback=watchpoint_global_char,
+ )
+ wp2 = d.breakpoint(
+ "global_int",
+ hardware=True,
+ condition="w",
+ length=4,
+ callback=watchpoint_global_int,
+ )
+ wp3 = d.breakpoint(
+ "global_long",
+ hardware=True,
+ condition="rw",
+ length=4,
+ callback=watchpoint_global_long,
+ )
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ self.assertEqual(global_char_ip[0], 0x8049166) # mov byte ptr [global_char], 0x1
+ self.assertEqual(
+ global_int_ip[0], 0x8049179
+ ) # mov dword ptr [global_int], 0x4050607
+ self.assertEqual(
+ global_long_ip[0], 0x8049183
+ ) # mov dword ptr [global_long], 0xc0d0e0f
+ self.assertEqual(
+ global_char_ip[1], 0x80491b7
+ ) # movzx eax, byte ptr [global_char]
+ self.assertEqual(
+ global_long_ip[1], 0x80491d5
+ ) # mov eax, dword ptr [global_long]
+
+ self.assertEqual(len(global_char_ip), 2)
+ self.assertEqual(len(global_int_ip), 1)
+ self.assertEqual(len(global_long_ip), 3) # There is one extra hit performed by the exit routine of libc
+ self.assertEqual(wp1.hit_count, 2)
+ self.assertEqual(wp2.hit_count, 1)
+ self.assertEqual(wp3.hit_count, 3) # There is one extra hit performed by the exit routine of libc
+
+ @skipUnless(PLATFORM == "i386", "Requires i386")
+ def test_watchpoint_disable_i386(self):
+ d = debugger(RESOLVE_EXE("watchpoint_test"), auto_interrupt_on_command=False)
+
+ d.run()
+
+ wp_char = d.breakpoint("global_char", hardware=True, condition="rw", length=1)
+ wp_int = d.breakpoint("global_int", hardware=True, condition="w", length=4)
+ wp_long = d.breakpoint("global_long", hardware=True, condition="rw", length=4)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, 0x8049166) # mov byte ptr [global_char], 0x1
+ self.assertTrue(wp_char.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 0)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, 0x8049179) # mov dword ptr [global_int], 0x4050607
+ self.assertTrue(wp_int.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ d.cont()
+
+ self.assertEqual(
+ d.regs.eip, 0x8049183
+ ) # mov dword ptr [global_long], 0xc0d0e0f
+ self.assertTrue(wp_long.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 1)
+
+ # disable watchpoint
+ wp_char.disable()
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, 0x80491d5) # mov eax, dword ptr [global_long]
+ self.assertTrue(wp_long.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 2)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "i386", "Requires i386")
+ def test_watchpoint_disable_reenable_i386(self):
+ d = debugger(RESOLVE_EXE("watchpoint_test"), auto_interrupt_on_command=False)
+
+ d.run()
+
+ wp_char = d.breakpoint("global_char", hardware=True, condition="rw", length=1)
+ wp_int = d.breakpoint("global_int", hardware=True, condition="w", length=4)
+ wp_long = d.breakpoint("global_long", hardware=True, condition="rw", length=4)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, 0x8049166) # mov byte ptr [global_char], 0x1
+ self.assertTrue(wp_char.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 0)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, 0x8049179) # mov dword ptr [global_int], 0x4050607
+ self.assertTrue(wp_int.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 1)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ # disable watchpoint
+ wp_long.disable()
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, 0x80491b7) # movzx eax, byte ptr [global_char]
+ self.assertTrue(wp_char.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 2)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 0)
+
+ # re-enable watchpoint
+ wp_long.enable()
+
+ d.cont()
+
+ self.assertEqual(d.regs.eip, 0x80491d5) # mov eax, dword ptr [global_long]
+ self.assertTrue(wp_long.hit_on(d))
+ self.assertEqual(wp_char.hit_count, 2)
+ self.assertEqual(wp_int.hit_count, 1)
+ self.assertEqual(wp_long.hit_count, 1)
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
+
+ @skipUnless(PLATFORM == "i386", "Requires i386")
+ def test_watchpoint_alias_i386(self):
+ d = debugger(RESOLVE_EXE("watchpoint_test"), auto_interrupt_on_command=False)
+
+ d.run()
+
+ d.wp("global_char", condition="rw", length=1)
+ d.watchpoint("global_int", condition="w", length=4)
+ d.watchpoint("global_long", condition="rw", length=4)
+
+ d.cont()
+
+ self.assertEqual(d.instruction_pointer, 0x8049166) # mov byte ptr [global_char], 0x1
+
+ d.cont()
+
+ self.assertEqual(d.instruction_pointer, 0x8049179) # mov dword ptr [global_int], 0x4050607
+
+ d.cont()
+
+ self.assertEqual(d.instruction_pointer, 0x8049183) # mov dword ptr [global_long], 0xc0d0e0f
+
+ d.cont()
+
+ self.assertEqual(d.instruction_pointer, 0x80491b7) # movzx eax, byte ptr [global_char]
+
+ d.cont()
+
+ self.assertEqual(d.instruction_pointer, 0x80491d5) # mov eax, dword ptr [global_long]
+
+ d.cont()
+
+ d.kill()
+ d.terminate()
diff --git a/test/aarch64/srcs/basic_test.c b/test/srcs/aarch64/srcs/basic_test.c
similarity index 100%
rename from test/aarch64/srcs/basic_test.c
rename to test/srcs/aarch64/srcs/basic_test.c
diff --git a/test/aarch64/srcs/floating_point_test.c b/test/srcs/aarch64/srcs/floating_point_test.c
similarity index 100%
rename from test/aarch64/srcs/floating_point_test.c
rename to test/srcs/aarch64/srcs/floating_point_test.c
diff --git a/test/aarch64/srcs/thread_test.c b/test/srcs/aarch64/srcs/thread_test.c
similarity index 100%
rename from test/aarch64/srcs/thread_test.c
rename to test/srcs/aarch64/srcs/thread_test.c
diff --git a/test/aarch64/srcs/watchpoint_test.c b/test/srcs/aarch64/srcs/watchpoint_test.c
similarity index 100%
rename from test/aarch64/srcs/watchpoint_test.c
rename to test/srcs/aarch64/srcs/watchpoint_test.c
diff --git a/test/amd64/srcs/basic_test.c b/test/srcs/amd64/basic_test.c
similarity index 100%
rename from test/amd64/srcs/basic_test.c
rename to test/srcs/amd64/basic_test.c
diff --git a/test/amd64/srcs/floating_point_896_test.c b/test/srcs/amd64/floating_point_avx2_test.c
similarity index 100%
rename from test/amd64/srcs/floating_point_896_test.c
rename to test/srcs/amd64/floating_point_avx2_test.c
diff --git a/test/amd64/srcs/floating_point_2696_test.c b/test/srcs/amd64/floating_point_avx512_test.c
similarity index 100%
rename from test/amd64/srcs/floating_point_2696_test.c
rename to test/srcs/amd64/floating_point_avx512_test.c
diff --git a/test/srcs/amd64/floating_point_mmx_test.c b/test/srcs/amd64/floating_point_mmx_test.c
new file mode 100644
index 00000000..2bb1c355
--- /dev/null
+++ b/test/srcs/amd64/floating_point_mmx_test.c
@@ -0,0 +1,69 @@
+//
+// This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+// Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for details.
+//
+
+int main()
+{
+ // load into the mmx registers the values
+ char value0[] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77};
+ __asm__ __volatile__("movq %0, %%mm0" : : "m" (value0));
+ char value1[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
+ __asm__ __volatile__("movq %0, %%mm1" : : "m" (value1));
+ char value2[] = {0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99};
+ __asm__ __volatile__("movq %0, %%mm2" : : "m" (value2));
+ char value3[] = {0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA};
+ __asm__ __volatile__("movq %0, %%mm3" : : "m" (value3));
+ char value4[] = {0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB};
+ __asm__ __volatile__("movq %0, %%mm4" : : "m" (value4));
+ char value5[] = {0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC};
+ __asm__ __volatile__("movq %0, %%mm5" : : "m" (value5));
+ char value6[] = {0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD};
+ __asm__ __volatile__("movq %0, %%mm6" : : "m" (value6));
+ char value7[] = {0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE};
+ __asm__ __volatile__("movq %0, %%mm7" : : "m" (value7));
+
+ __asm__ __volatile__("nop");
+
+ // load the value from the mmx register into the value variable
+ char value[8];
+ __asm__ __volatile__("movq %%mm0, %0" : "=m" (value));
+
+ unsigned long check = *(unsigned long*)value;
+
+ if (check == 0xdeadbeefdeadbeef) {
+ __asm__ __volatile__("nop");
+ }
+
+ // load into the st registers floating point values
+ __asm__ __volatile__("finit");
+ double value8 = 123.456;
+ __asm__ __volatile__("fldl %0" : : "m" (value8));
+ double value9 = 234.567;
+ __asm__ __volatile__("fldl %0" : : "m" (value9));
+ double value10 = 345.678;
+ __asm__ __volatile__("fldl %0" : : "m" (value10));
+ double value11 = 456.789;
+ __asm__ __volatile__("fldl %0" : : "m" (value11));
+ double value12 = 567.890;
+ __asm__ __volatile__("fldl %0" : : "m" (value12));
+ double value13 = 678.901;
+ __asm__ __volatile__("fldl %0" : : "m" (value13));
+ double value14 = 789.012;
+ __asm__ __volatile__("fldl %0" : : "m" (value14));
+ double value15 = 890.123;
+ __asm__ __volatile__("fldl %0" : : "m" (value15));
+
+ __asm__ __volatile__("nop");
+
+ // load the value from the st register into the value variable
+ double result;
+ __asm__ __volatile__("fstpl %0" : "=m" (result));
+
+ if (result == 1337.1337) {
+ __asm__ __volatile__("nop");
+ }
+
+ return 0;
+}
diff --git a/test/amd64/srcs/floating_point_512_test.c b/test/srcs/amd64/floating_point_sse_test.c
similarity index 100%
rename from test/amd64/srcs/floating_point_512_test.c
rename to test/srcs/amd64/floating_point_sse_test.c
diff --git a/test/amd64/srcs/thread_test.c b/test/srcs/amd64/thread_test.c
similarity index 100%
rename from test/amd64/srcs/thread_test.c
rename to test/srcs/amd64/thread_test.c
diff --git a/test/common/srcs/antidebug_brute_test.c b/test/srcs/antidebug_brute_test.c
similarity index 100%
rename from test/common/srcs/antidebug_brute_test.c
rename to test/srcs/antidebug_brute_test.c
diff --git a/test/common/srcs/attach_test.c b/test/srcs/attach_test.c
similarity index 100%
rename from test/common/srcs/attach_test.c
rename to test/srcs/attach_test.c
diff --git a/test/common/srcs/backtrace.c b/test/srcs/backtrace.c
similarity index 100%
rename from test/common/srcs/backtrace.c
rename to test/srcs/backtrace.c
diff --git a/test/common/srcs/basic_test_pie.c b/test/srcs/basic_test_pie.c
similarity index 100%
rename from test/common/srcs/basic_test_pie.c
rename to test/srcs/basic_test_pie.c
diff --git a/test/common/srcs/benchmark.c b/test/srcs/benchmark.c
similarity index 100%
rename from test/common/srcs/benchmark.c
rename to test/srcs/benchmark.c
diff --git a/test/common/srcs/breakpoint_test.c b/test/srcs/breakpoint_test.c
similarity index 100%
rename from test/common/srcs/breakpoint_test.c
rename to test/srcs/breakpoint_test.c
diff --git a/test/common/srcs/brute_test.c b/test/srcs/brute_test.c
similarity index 100%
rename from test/common/srcs/brute_test.c
rename to test/srcs/brute_test.c
diff --git a/test/common/srcs/catch_signal_test.c b/test/srcs/catch_signal_test.c
similarity index 100%
rename from test/common/srcs/catch_signal_test.c
rename to test/srcs/catch_signal_test.c
diff --git a/test/common/srcs/executable_section_test.c b/test/srcs/executable_section_test.c
similarity index 100%
rename from test/common/srcs/executable_section_test.c
rename to test/srcs/executable_section_test.c
diff --git a/test/common/srcs/finish_test.c b/test/srcs/finish_test.c
similarity index 100%
rename from test/common/srcs/finish_test.c
rename to test/srcs/finish_test.c
diff --git a/test/common/srcs/handle_syscall_test.c b/test/srcs/handle_syscall_test.c
similarity index 100%
rename from test/common/srcs/handle_syscall_test.c
rename to test/srcs/handle_syscall_test.c
diff --git a/test/srcs/i386/basic_test.c b/test/srcs/i386/basic_test.c
new file mode 100644
index 00000000..7fe4d7ec
--- /dev/null
+++ b/test/srcs/i386/basic_test.c
@@ -0,0 +1,56 @@
+//
+// This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+// Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for details.
+//
+
+#include
+#include
+
+#pragma GCC optimize ("O0")
+
+void register_test()
+{
+ asm volatile (
+ "push %%ebp\n\t"
+ "mov $0x00112233, %%eax\n\t"
+ "mov $0x11223344, %%ebx\n\t"
+ "mov $0x22334455, %%ecx\n\t"
+ "mov $0x33445566, %%edx\n\t"
+ "mov $0x44556677, %%esi\n\t"
+ "mov $0x55667788, %%edi\n\t"
+ "mov $0x66778899, %%ebp\n\t"
+ "nop\n\t"
+ "mov $0x1122, %%ax\n\t"
+ "mov $0x2233, %%bx\n\t"
+ "mov $0x3344, %%cx\n\t"
+ "mov $0x4455, %%dx\n\t"
+ "mov $0x5566, %%si\n\t"
+ "mov $0x6677, %%di\n\t"
+ "mov $0x7788, %%bp\n\t"
+ "nop\n\t"
+ "mov $0x11, %%al\n\t"
+ "mov $0x22, %%bl\n\t"
+ "mov $0x33, %%cl\n\t"
+ "mov $0x44, %%dl\n\t"
+ "nop\n\t"
+ "mov $0x12, %%ah\n\t"
+ "mov $0x23, %%bh\n\t"
+ "mov $0x34, %%ch\n\t"
+ "mov $0x45, %%dh\n\t"
+ "nop\n\t"
+ "pop %%ebp\n\t"
+ :
+ :
+ : "eax", "ebx", "ecx", "edx", "esi", "edi"
+ );
+}
+
+int main()
+{
+ printf("Provola\n");
+
+ register_test();
+
+ return EXIT_SUCCESS;
+}
diff --git a/test/srcs/i386/floating_point_avx2_test.c b/test/srcs/i386/floating_point_avx2_test.c
new file mode 100644
index 00000000..752369a3
--- /dev/null
+++ b/test/srcs/i386/floating_point_avx2_test.c
@@ -0,0 +1,40 @@
+//
+// This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+// Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for details.
+//
+
+int main()
+{
+ // load value into floating point register
+ char value0[] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F};
+ __asm__ __volatile__("vmovdqu %0, %%ymm0" : : "m" (value0));
+ char value1[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x00};
+ __asm__ __volatile__("vmovdqu %0, %%ymm1" : : "m" (value1));
+ char value2[] = {0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x00, 0x11};
+ __asm__ __volatile__("vmovdqu %0, %%ymm2" : : "m" (value2));
+ char value3[] = {0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x00, 0x11, 0x22};
+ __asm__ __volatile__("vmovdqu %0, %%ymm3" : : "m" (value3));
+ char value4[] = {0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x00, 0x11, 0x22, 0x33};
+ __asm__ __volatile__("vmovdqu %0, %%ymm4" : : "m" (value4));
+ char value5[] = {0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x00, 0x11, 0x22, 0x33, 0x44};
+ __asm__ __volatile__("vmovdqu %0, %%ymm5" : : "m" (value5));
+ char value6[] = {0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
+ __asm__ __volatile__("vmovdqu %0, %%ymm6" : : "m" (value6));
+ char value7[] = {0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
+ __asm__ __volatile__("vmovdqu %0, %%ymm7" : : "m" (value7));
+
+ // breakpoint location 1
+ __asm__ __volatile__("nop");
+
+ char value[32];
+ __asm__ __volatile__("vmovdqu %%ymm0, %0" : "=m" (value));
+
+ unsigned long check = *(unsigned long*)value;
+
+ if (check == 0xdeadbeef) {
+ __asm__ __volatile__("nop");
+ }
+
+ return 0;
+}
\ No newline at end of file
diff --git a/test/srcs/i386/floating_point_avx512_test.c b/test/srcs/i386/floating_point_avx512_test.c
new file mode 100644
index 00000000..f9c2099a
--- /dev/null
+++ b/test/srcs/i386/floating_point_avx512_test.c
@@ -0,0 +1,52 @@
+//
+// This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+// Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for details.
+//
+
+void rotate(char value[64])
+{
+ char temp = value[0];
+ for (int i = 0; i < 63; i++) {
+ value[i] = value[i + 1];
+ }
+ value[63] = temp;
+}
+
+int main()
+{
+ char value[64];
+
+ for (int i = 0; i < 64; i++) {
+ value[i] = i;
+ }
+
+ __asm__ __volatile__("vmovdqu8 %0, %%zmm0" : : "m" (value));
+ rotate(value);
+ __asm__ __volatile__("vmovdqu8 %0, %%zmm1" : : "m" (value));
+ rotate(value);
+ __asm__ __volatile__("vmovdqu8 %0, %%zmm2" : : "m" (value));
+ rotate(value);
+ __asm__ __volatile__("vmovdqu8 %0, %%zmm3" : : "m" (value));
+ rotate(value);
+ __asm__ __volatile__("vmovdqu8 %0, %%zmm4" : : "m" (value));
+ rotate(value);
+ __asm__ __volatile__("vmovdqu8 %0, %%zmm5" : : "m" (value));
+ rotate(value);
+ __asm__ __volatile__("vmovdqu8 %0, %%zmm6" : : "m" (value));
+ rotate(value);
+ __asm__ __volatile__("vmovdqu8 %0, %%zmm7" : : "m" (value));
+
+ __asm__ __volatile__("nop");
+
+ char result[64];
+ __asm__ __volatile__("vmovdqu8 %%zmm0, %0" : "=m" (result));
+
+ unsigned long check = *(unsigned long*)result;
+
+ if (check == 0xdeadbeef) {
+ __asm__ __volatile__("nop");
+ }
+
+ return 0;
+}
diff --git a/test/srcs/i386/floating_point_mmx_test.c b/test/srcs/i386/floating_point_mmx_test.c
new file mode 100644
index 00000000..04c6845a
--- /dev/null
+++ b/test/srcs/i386/floating_point_mmx_test.c
@@ -0,0 +1,69 @@
+//
+// This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+// Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for details.
+//
+
+int main()
+{
+ // load into the mmx registers the values
+ char value0[] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77};
+ __asm__ __volatile__("movq %0, %%mm0" : : "m" (value0));
+ char value1[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
+ __asm__ __volatile__("movq %0, %%mm1" : : "m" (value1));
+ char value2[] = {0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99};
+ __asm__ __volatile__("movq %0, %%mm2" : : "m" (value2));
+ char value3[] = {0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA};
+ __asm__ __volatile__("movq %0, %%mm3" : : "m" (value3));
+ char value4[] = {0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB};
+ __asm__ __volatile__("movq %0, %%mm4" : : "m" (value4));
+ char value5[] = {0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC};
+ __asm__ __volatile__("movq %0, %%mm5" : : "m" (value5));
+ char value6[] = {0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD};
+ __asm__ __volatile__("movq %0, %%mm6" : : "m" (value6));
+ char value7[] = {0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE};
+ __asm__ __volatile__("movq %0, %%mm7" : : "m" (value7));
+
+ __asm__ __volatile__("nop");
+
+ // load the value from the mmx register into the value variable
+ char value[8];
+ __asm__ __volatile__("movq %%mm0, %0" : "=m" (value));
+
+ unsigned long check = *(unsigned long*)value;
+
+ if (check == 0xdeadbeef) {
+ __asm__ __volatile__("nop");
+ }
+
+ // load into the st registers floating point values
+ __asm__ __volatile__("finit");
+ double value8 = 123.456;
+ __asm__ __volatile__("fldl %0" : : "m" (value8));
+ double value9 = 234.567;
+ __asm__ __volatile__("fldl %0" : : "m" (value9));
+ double value10 = 345.678;
+ __asm__ __volatile__("fldl %0" : : "m" (value10));
+ double value11 = 456.789;
+ __asm__ __volatile__("fldl %0" : : "m" (value11));
+ double value12 = 567.890;
+ __asm__ __volatile__("fldl %0" : : "m" (value12));
+ double value13 = 678.901;
+ __asm__ __volatile__("fldl %0" : : "m" (value13));
+ double value14 = 789.012;
+ __asm__ __volatile__("fldl %0" : : "m" (value14));
+ double value15 = 890.123;
+ __asm__ __volatile__("fldl %0" : : "m" (value15));
+
+ __asm__ __volatile__("nop");
+
+ // load the value from the st register into the value variable
+ double result;
+ __asm__ __volatile__("fstpl %0" : "=m" (result));
+
+ if ((int) result == 1337) {
+ __asm__ __volatile__("nop");
+ }
+
+ return 0;
+}
diff --git a/test/srcs/i386/floating_point_sse_test.c b/test/srcs/i386/floating_point_sse_test.c
new file mode 100644
index 00000000..9088e85f
--- /dev/null
+++ b/test/srcs/i386/floating_point_sse_test.c
@@ -0,0 +1,38 @@
+//
+// This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+// Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for details.
+//
+
+int main()
+{
+ char value0[] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
+ __asm__ __volatile__("vmovdqu %0, %%xmm0" : : "m" (value0));
+ char value1[] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00};
+ __asm__ __volatile__("vmovdqu %0, %%xmm1" : : "m" (value1));
+ char value2[] = {0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11};
+ __asm__ __volatile__("vmovdqu %0, %%xmm2" : : "m" (value2));
+ char value3[] = {0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22};
+ __asm__ __volatile__("vmovdqu %0, %%xmm3" : : "m" (value3));
+ char value4[] = {0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33};
+ __asm__ __volatile__("vmovdqu %0, %%xmm4" : : "m" (value4));
+ char value5[] = {0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33, 0x44};
+ __asm__ __volatile__("vmovdqu %0, %%xmm5" : : "m" (value5));
+ char value6[] = {0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55};
+ __asm__ __volatile__("vmovdqu %0, %%xmm6" : : "m" (value6));
+ char value7[] = {0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
+ __asm__ __volatile__("vmovdqu %0, %%xmm7" : : "m" (value7));
+
+ __asm__ __volatile__("nop");
+
+ char value[16];
+ __asm__ __volatile__("vmovdqu %%xmm0, %0" : "=m" (value));
+
+ unsigned long check = *(unsigned long*)value;
+
+ if (check == 0xdeadbeef) {
+ __asm__ __volatile__("nop");
+ }
+
+ return 0;
+}
diff --git a/test/srcs/i386/thread_test.c b/test/srcs/i386/thread_test.c
new file mode 100644
index 00000000..e15f2f87
--- /dev/null
+++ b/test/srcs/i386/thread_test.c
@@ -0,0 +1,50 @@
+//
+// This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+// Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for details.
+//
+
+#include
+#include
+#include
+
+void thread_1_function()
+{
+ asm volatile (
+ "mov $0x00112233, %%eax\n\t"
+ "nop\n\t"::: "eax");
+}
+
+void thread_2_function()
+{
+ asm volatile (
+ "mov $0xccdd1122, %%eax\n\t"
+ "nop\n\t"::: "eax");
+}
+
+void thread_3_function()
+{
+ asm volatile (
+ "mov $0x66770011, %%eax\n\t"
+ "nop\n\t"::: "eax");
+}
+
+void do_nothing()
+{
+ asm volatile ("nop\n\t");
+}
+
+int main()
+{
+ pthread_t thread_1, thread_2, thread_3;
+ pthread_create(&thread_1, NULL, (void *)thread_1_function, NULL);
+ pthread_create(&thread_2, NULL, (void *)thread_2_function, NULL);
+ pthread_create(&thread_3, NULL, (void *)thread_3_function, NULL);
+ pthread_join(thread_1, NULL);
+ pthread_join(thread_2, NULL);
+ pthread_join(thread_3, NULL);
+
+ do_nothing();
+
+ return 0;
+}
diff --git a/test/common/srcs/infinite_loop_test.c b/test/srcs/infinite_loop_test.c
similarity index 100%
rename from test/common/srcs/infinite_loop_test.c
rename to test/srcs/infinite_loop_test.c
diff --git a/test/common/srcs/jumpstart_test.c b/test/srcs/jumpstart_test.c
similarity index 100%
rename from test/common/srcs/jumpstart_test.c
rename to test/srcs/jumpstart_test.c
diff --git a/test/common/srcs/jumpstart_test_preload.c b/test/srcs/jumpstart_test_preload.c
similarity index 100%
rename from test/common/srcs/jumpstart_test_preload.c
rename to test/srcs/jumpstart_test_preload.c
diff --git a/test/common/srcs/math_loop_test.c b/test/srcs/math_loop_test.c
similarity index 100%
rename from test/common/srcs/math_loop_test.c
rename to test/srcs/math_loop_test.c
diff --git a/test/common/srcs/memory_test.c b/test/srcs/memory_test.c
similarity index 100%
rename from test/common/srcs/memory_test.c
rename to test/srcs/memory_test.c
diff --git a/test/common/srcs/memory_test_2.c b/test/srcs/memory_test_2.c
similarity index 100%
rename from test/common/srcs/memory_test_2.c
rename to test/srcs/memory_test_2.c
diff --git a/test/common/srcs/memory_test_3.c b/test/srcs/memory_test_3.c
similarity index 100%
rename from test/common/srcs/memory_test_3.c
rename to test/srcs/memory_test_3.c
diff --git a/test/common/srcs/memory_test_4.c b/test/srcs/memory_test_4.c
similarity index 100%
rename from test/common/srcs/memory_test_4.c
rename to test/srcs/memory_test_4.c
diff --git a/test/srcs/multi.c b/test/srcs/multi.c
new file mode 100644
index 00000000..fac6f7c0
--- /dev/null
+++ b/test/srcs/multi.c
@@ -0,0 +1,59 @@
+//
+// This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+// Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for details.
+//
+
+#include
+#include
+#include
+#include
+#include
+
+// Function to be executed by the input/output thread
+void *io_thread(void *arg) {
+ char buffer[256]; // Buffer to store user input
+
+ while (1) {
+ printf("Enter some text: ");
+ if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
+ printf("You entered: %s", buffer);
+ }
+ }
+
+ return NULL;
+}
+
+// Function to be executed by the interval print thread
+void *interval_thread(void *arg) {
+ int count = 0;
+ while (1) {
+ printf("stdout: Count %d\n", ++count);
+ fprintf(stderr, "stderr: \x88\x90 Count %d\n", count);
+ sleep(1); // Wait for 1 second
+ }
+
+ return NULL;
+}
+
+int main() {
+ pthread_t thread1, thread2;
+
+ // Create the first thread for input/output
+ if (pthread_create(&thread1, NULL, io_thread, NULL)) {
+ fprintf(stderr, "Error creating thread\n");
+ return 1;
+ }
+
+ // Create the second thread for interval printing
+ if (pthread_create(&thread2, NULL, interval_thread, NULL)) {
+ fprintf(stderr, "Error creating thread\n");
+ return 1;
+ }
+
+ // Wait for both threads to finish (they won't in this simple example)
+ pthread_join(thread1, NULL);
+ pthread_join(thread2, NULL);
+
+ return 0;
+}
diff --git a/test/srcs/multithread_input.c b/test/srcs/multithread_input.c
new file mode 100644
index 00000000..b215ddcc
--- /dev/null
+++ b/test/srcs/multithread_input.c
@@ -0,0 +1,54 @@
+//
+// This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+// Copyright (c) 2024 Gabriele Digregorio. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for details.
+//
+
+#include
+#include
+#include
+
+#define NUM_THREADS 5
+
+void *threadFunction(void *arg) {
+ int thread_id = *((int *)arg);
+ int input;
+
+ printf("Thread %d: Enter a number: \n", thread_id);
+ scanf("%d", &input);
+
+ pthread_exit(NULL);
+}
+
+int main() {
+ setvbuf(stdout, NULL, _IONBF, 0);
+ setvbuf(stderr, NULL, _IONBF, 0);
+ setvbuf(stdin, NULL, _IONBF, 0);
+
+ pthread_t threads[NUM_THREADS];
+ int thread_ids[NUM_THREADS];
+ int rc;
+ int i;
+
+ // Create the threads
+ for (i = 0; i < NUM_THREADS; i++) {
+ thread_ids[i] = i;
+ rc = pthread_create(&threads[i], NULL, threadFunction, (void *)&thread_ids[i]);
+
+ if (rc) {
+ printf("ERROR: Return code from pthread_create() is %d\n", rc);
+ exit(-1);
+ }
+ }
+
+ printf("All threads have been created.\n");
+
+ // Wait for all threads to complete
+ for (i = 0; i < NUM_THREADS; i++) {
+ pthread_join(threads[i], NULL);
+ }
+
+ printf("All threads have completed.\n");
+
+ pthread_exit(NULL);
+}
diff --git a/test/srcs/run_pipes_test.c b/test/srcs/run_pipes_test.c
new file mode 100644
index 00000000..ac0ea6c4
--- /dev/null
+++ b/test/srcs/run_pipes_test.c
@@ -0,0 +1,86 @@
+//
+// This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+// Copyright (c) 2024 Roberto Alessandro Bertolini. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for details.
+//
+
+#include
+#include
+#include
+#include
+
+#define SIGPROVOLA 50
+
+const char admin_pwd[] = "admin";
+const char flag[] = "flag{provola}";
+
+void option_1()
+{
+ printf("The flag is: %s\n", flag);
+}
+
+void option_2()
+{
+ char input[128];
+
+ fgets(input, sizeof(input), stdin);
+ input[strcspn(input, "\n")] = 0;
+
+ if (strcmp(input, admin_pwd) == 0)
+ {
+ printf("Welcome admin!\n");
+ printf("The flag is: %s\n", flag);
+ }
+ else
+ {
+ printf("Wrong password!\n");
+ }
+}
+
+void sigprovola_handler(int sig)
+{
+ printf("Wowsers! This fine piece of code developed by Io_no should have never reached this state!\n");
+ printf("Stacktrace[0]: %p\n", __builtin_return_address(0));
+ printf("Stacktrace[1]: %s\n", flag);
+}
+
+int main()
+{
+ setvbuf(stdout, NULL, _IONBF, 0);
+
+ // Register the signal handler
+ signal(SIGPROVOLA, sigprovola_handler);
+
+ int choice;
+
+ while (1)
+ {
+ printf("Welcome to Io_no's personal flag management system!\n");
+ printf("Choose an option:\n");
+ printf("1. Print the flag\n");
+ printf("2. Become admin and print the flag\n");
+ printf("3. Raise a signal\n");
+ printf("4. Exit\n");
+
+ scanf("%d", &choice);
+ fgetc(stdin);
+
+ switch (choice)
+ {
+ case 1:
+ option_1();
+ break;
+ case 2:
+ option_2();
+ break;
+ case 3:
+ raise(SIGPROVOLA);
+ break;
+ case 4:
+ return 0;
+ default:
+ printf("Invalid choice!\n");
+ break;
+ }
+ }
+}
diff --git a/test/common/srcs/segfault_test.c b/test/srcs/segfault_test.c
similarity index 100%
rename from test/common/srcs/segfault_test.c
rename to test/srcs/segfault_test.c
diff --git a/test/common/srcs/signals_multithread_det_test.c b/test/srcs/signals_multithread_det_test.c
similarity index 100%
rename from test/common/srcs/signals_multithread_det_test.c
rename to test/srcs/signals_multithread_det_test.c
diff --git a/test/common/srcs/signals_multithread_undet_test.c b/test/srcs/signals_multithread_undet_test.c
similarity index 100%
rename from test/common/srcs/signals_multithread_undet_test.c
rename to test/srcs/signals_multithread_undet_test.c
diff --git a/test/common/srcs/speed_test.c b/test/srcs/speed_test.c
similarity index 100%
rename from test/common/srcs/speed_test.c
rename to test/srcs/speed_test.c
diff --git a/test/common/srcs/complex_thread_test.c b/test/srcs/thread_complex_test.c
similarity index 100%
rename from test/common/srcs/complex_thread_test.c
rename to test/srcs/thread_complex_test.c
diff --git a/test/common/srcs/watchpoint_test.c b/test/srcs/watchpoint_test.c
similarity index 100%
rename from test/common/srcs/watchpoint_test.c
rename to test/srcs/watchpoint_test.c
diff --git a/test/utils/__init__.py b/test/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/test/utils/binary_utils.py b/test/utils/binary_utils.py
new file mode 100644
index 00000000..9d5a43e9
--- /dev/null
+++ b/test/utils/binary_utils.py
@@ -0,0 +1,33 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Roberto Alessandro Bertolini, Francesco Panebianco. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+import os
+
+from libdebug import debugger
+from libdebug.utils.libcontext import libcontext
+
+
+PLATFORM = os.getenv("PLATFORM", libcontext.platform)
+
+def RESOLVE_EXE(file: str) -> str:
+ return f"binaries/{PLATFORM}/{file}"
+
+def _base_address() -> int:
+ d = debugger(RESOLVE_EXE("basic_test_pie"), aslr=False)
+
+ d.run()
+
+ base = d.maps[0].start
+
+ d.kill()
+ d.terminate()
+
+ return base
+
+def base_of(d) -> int:
+ return d.maps[0].start
+
+BASE = _base_address()
diff --git a/test/utils/thread_utils.py b/test/utils/thread_utils.py
new file mode 100644
index 00000000..e9d40477
--- /dev/null
+++ b/test/utils/thread_utils.py
@@ -0,0 +1,40 @@
+#
+# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
+# Copyright (c) 2024 Roberto Alessandro Bertolini, Francesco Panebianco. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for details.
+#
+
+from utils.binary_utils import PLATFORM
+
+def FUN_ARG_0(t) -> int:
+ match PLATFORM:
+ case "amd64":
+ return t.regs.rdi
+ case "aarch64":
+ return t.regs.x0
+ case "i386":
+ return int.from_bytes(t.mem[t.regs.esp + 4, 4], "little")
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+def FUN_RET_VAL(t) -> int:
+ match PLATFORM:
+ case "amd64":
+ return t.regs.rax
+ case "aarch64":
+ return t.regs.x0
+ case "i386":
+ return t.regs.eax
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
+
+def STACK_POINTER(t) -> int:
+ match PLATFORM:
+ case "amd64":
+ return t.regs.rsp
+ case "aarch64":
+ return t.regs.sp
+ case "i386":
+ return t.regs.esp
+ case _:
+ raise NotImplementedError(f"Platform {PLATFORM} not supported by this test")
\ No newline at end of file