Skip to content

Commit

Permalink
Merge pull request libdebug#136 from libdebug/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
io-no authored Oct 14, 2024
2 parents 3699dcf + b12556c commit b39441c
Show file tree
Hide file tree
Showing 406 changed files with 14,258 additions and 12,439 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
timeout-minutes: 5
strategy:
matrix:
python-version: ["3.12"]
python-version: ["3.13"]

steps:
- uses: actions/checkout@v4
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ venv.bak/

# mkdocs documentation
/site
docs/from_pydoc/

# mypy
.mypy_cache/
Expand Down
58 changes: 54 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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)

Expand All @@ -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()

Expand All @@ -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()
```
Expand Down
20 changes: 0 additions & 20 deletions docs/Makefile

This file was deleted.

Binary file added docs/assets/backtrace_plot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/backtrace_plot_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
Binary file added docs/assets/hijack-dark.webp
Binary file not shown.
Binary file added docs/assets/hijack.webp
Binary file not shown.
Binary file added docs/assets/libdebug_header.webp
Binary file not shown.
Binary file added docs/assets/libdebug_logo.webp
Binary file not shown.
Binary file added docs/assets/libdebug_logo_horiz_dark.webp
Binary file not shown.
Binary file added docs/assets/libdebug_logo_horiz_light.webp
Binary file not shown.
Binary file added docs/assets/pipe_logging.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/pprint_backtrace.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/pprint_maps.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/pprint_regs.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/pprint_syscalls.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/sync_async.webp
Binary file not shown.
Binary file added docs/assets/sync_async_dark.webp
Binary file not shown.
84 changes: 84 additions & 0 deletions docs/basics/command_queue.md
Original file line number Diff line number Diff line change
@@ -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.
114 changes: 114 additions & 0 deletions docs/basics/control_flow_commands.md
Original file line number Diff line number Diff line change
@@ -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. |
Loading

0 comments on commit b39441c

Please sign in to comment.