mirror of https://github.com/pwndbg/pwndbg.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
242 lines
6.9 KiB
Python
242 lines
6.9 KiB
Python
"""
|
|
This file should consist of global test fixtures.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import typing
|
|
from typing import Dict
|
|
from typing import Literal
|
|
from typing import Tuple
|
|
|
|
import gdb
|
|
import pytest
|
|
import ziglang
|
|
|
|
from pwndbg.lib import tempfile
|
|
|
|
_start_binary_called = False
|
|
|
|
QEMU_PORT: str | None = None
|
|
|
|
COMPILATION_TARGETS_TYPE = Literal[
|
|
"aarch64",
|
|
"arm",
|
|
"riscv32",
|
|
"riscv64",
|
|
"loongarch64",
|
|
"powerpc32",
|
|
"powerpc64",
|
|
"mips32",
|
|
"mipsel32",
|
|
"mips64",
|
|
"s390x",
|
|
"sparc",
|
|
]
|
|
|
|
COMPILATION_TARGETS: list[COMPILATION_TARGETS_TYPE] = list(
|
|
typing.get_args(COMPILATION_TARGETS_TYPE)
|
|
)
|
|
|
|
# Tuple contains (Zig target,extra_cli_args,qemu_suffix),
|
|
COMPILE_AND_RUN_INFO: Dict[COMPILATION_TARGETS_TYPE, Tuple[str, Tuple[str, ...], str]] = {
|
|
"aarch64": ("aarch64-freestanding", (), "aarch64"),
|
|
"arm": ("arm-freestanding", (), "arm"),
|
|
"riscv32": ("riscv32-freestanding", (), "riscv32"),
|
|
"riscv64": ("riscv64-freestanding", (), "riscv64"),
|
|
"mips32": ("mips-freestanding", (), "mips"),
|
|
"mipsel32": ("mipsel-freestanding", (), "mipsel"),
|
|
"mips64": ("mips64-freestanding", (), "mips64"),
|
|
"loongarch64": ("loongarch64-freestanding", (), "loongarch64"),
|
|
"s390x": ("s390x-freestanding", (), "s390x"),
|
|
"sparc": ("sparc64-freestanding", (), "sparc64"),
|
|
"powerpc32": ("powerpc-freestanding", (), "ppc"),
|
|
"powerpc64": ("powerpc64-freestanding", (), "ppc64"),
|
|
}
|
|
|
|
|
|
def reserve_port(ip: str = "127.0.0.1", port: int = 0) -> str:
|
|
"""
|
|
https://github.com/Yelp/ephemeral-port-reserve/blob/master/ephemeral_port_reserve.py
|
|
|
|
Bind to an ephemeral port, force it into the TIME_WAIT state, and unbind it.
|
|
|
|
This means that further ephemeral port alloctions won't pick this "reserved" port,
|
|
but subprocesses can still bind to it explicitly, given that they use SO_REUSEADDR.
|
|
By default on linux you have a grace period of 60 seconds to reuse this port.
|
|
To check your own particular value:
|
|
$ cat /proc/sys/net/ipv4/tcp_fin_timeout
|
|
60
|
|
|
|
By default, the port will be reserved for localhost (aka 127.0.0.1).
|
|
To reserve a port for a different ip, provide the ip as the first argument.
|
|
Note that IP 0.0.0.0 is interpreted as localhost.
|
|
"""
|
|
import contextlib
|
|
import errno
|
|
from socket import SO_REUSEADDR
|
|
from socket import SOL_SOCKET
|
|
from socket import error as SocketError
|
|
from socket import socket
|
|
|
|
port = int(port)
|
|
with contextlib.closing(socket()) as s:
|
|
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
|
try:
|
|
s.bind((ip, port))
|
|
except SocketError as e:
|
|
# socket.error: EADDRINUSE Address already in use
|
|
if e.errno == errno.EADDRINUSE and port != 0:
|
|
s.bind((ip, 0))
|
|
else:
|
|
raise
|
|
|
|
# the connect below deadlocks on kernel >= 4.4.0 unless this arg is greater than zero
|
|
s.listen(1)
|
|
|
|
sockname = s.getsockname()
|
|
|
|
# these three are necessary just to get the port into a TIME_WAIT state
|
|
with contextlib.closing(socket()) as s2:
|
|
s2.connect(sockname)
|
|
sock, _ = s.accept()
|
|
with contextlib.closing(sock):
|
|
return sockname[1]
|
|
|
|
|
|
def ensure_qemu_port():
|
|
"""
|
|
Ensures that QEMU_PORT is set to a valid, usable port.
|
|
"""
|
|
global QEMU_PORT
|
|
if QEMU_PORT is None:
|
|
QEMU_PORT = reserve_port()
|
|
print(f"Reserved port {QEMU_PORT} for QEMU")
|
|
|
|
|
|
@pytest.fixture
|
|
def qemu_assembly_run():
|
|
"""
|
|
Returns function that launches given binary with 'starti' command
|
|
|
|
The `path` is returned from `make_elf_from_assembly` (provided by pwntools)
|
|
"""
|
|
|
|
ensure_qemu_port()
|
|
|
|
qemu: subprocess.Popen = None
|
|
|
|
def _start_binary(asm: str, arch: COMPILATION_TARGETS_TYPE):
|
|
nonlocal qemu
|
|
|
|
if arch not in COMPILATION_TARGETS or arch not in COMPILE_AND_RUN_INFO:
|
|
raise Exception(f"Unknown compilation target: {arch}")
|
|
|
|
zig_target, extra_cli_args, qemu_suffix = COMPILE_AND_RUN_INFO[arch]
|
|
|
|
# Place assembly and compiled binary in a temporary folder
|
|
# named /tmp/pwndbg-*
|
|
tmpdir = tempfile.tempdir()
|
|
|
|
asm_file = os.path.join(tmpdir, "input.S")
|
|
|
|
with open(asm_file, "w") as f:
|
|
f.write(asm)
|
|
|
|
compiled_file = os.path.join(tmpdir, "out.elf")
|
|
|
|
# Build the binary with Zig
|
|
compile_process = subprocess.run(
|
|
[
|
|
os.path.join(os.path.dirname(ziglang.__file__), "zig"),
|
|
"cc",
|
|
*extra_cli_args,
|
|
f"--target={zig_target}",
|
|
asm_file,
|
|
"-o",
|
|
compiled_file,
|
|
],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
universal_newlines=True,
|
|
)
|
|
|
|
if compile_process.returncode != 0:
|
|
raise Exception("Compilation error", compile_process.stdout, compile_process.stderr)
|
|
|
|
qemu = subprocess.Popen(
|
|
[
|
|
f"qemu-{qemu_suffix}",
|
|
"-g",
|
|
f"{QEMU_PORT}",
|
|
f"{compiled_file}",
|
|
]
|
|
)
|
|
|
|
os.environ["PWNDBG_IN_TEST"] = "1"
|
|
os.environ["COLUMNS"] = "80"
|
|
gdb.execute("set exception-verbose on")
|
|
gdb.execute("set context-reserve-lines never")
|
|
gdb.execute("set width 80")
|
|
gdb.execute(f"target remote :{QEMU_PORT}")
|
|
|
|
global _start_binary_called
|
|
# if _start_binary_called:
|
|
# raise Exception('Starting more than one binary is not supported in pwndbg tests.')
|
|
|
|
_start_binary_called = True
|
|
|
|
yield _start_binary
|
|
|
|
qemu.kill()
|
|
|
|
|
|
@pytest.fixture
|
|
def qemu_start_binary():
|
|
"""
|
|
Returns function that launches given binary with 'starti' command
|
|
|
|
Argument `path` is the path to the binary
|
|
"""
|
|
|
|
qemu: subprocess.Popen = None
|
|
|
|
ensure_qemu_port()
|
|
|
|
def _start_binary(path: str, arch: COMPILATION_TARGETS_TYPE):
|
|
nonlocal qemu
|
|
|
|
if arch not in COMPILATION_TARGETS or arch not in COMPILE_AND_RUN_INFO:
|
|
raise Exception(f"Unknown compilation target: {arch}")
|
|
|
|
_, _, qemu_suffix = COMPILE_AND_RUN_INFO[arch]
|
|
|
|
qemu = subprocess.Popen(
|
|
[
|
|
f"qemu-{qemu_suffix}",
|
|
"-g",
|
|
f"{QEMU_PORT}",
|
|
f"{path}",
|
|
]
|
|
)
|
|
|
|
os.environ["PWNDBG_IN_TEST"] = "1"
|
|
os.environ["COLUMNS"] = "80"
|
|
gdb.execute("set exception-verbose on")
|
|
gdb.execute("set context-reserve-lines never")
|
|
gdb.execute("set width 80")
|
|
gdb.execute(f"target remote :{QEMU_PORT}")
|
|
|
|
global _start_binary_called
|
|
# if _start_binary_called:
|
|
# raise Exception('Starting more than one binary is not supported in pwndbg tests.')
|
|
|
|
_start_binary_called = True
|
|
|
|
yield _start_binary
|
|
|
|
qemu.kill()
|