mirror of https://github.com/pwndbg/pwndbg.git
Add hijack-fd command to modify the file descriptor of a process (#2623)
* add hijack fd * fix comments * lint * lintpull/2627/head
parent
f0386821c8
commit
ab43ce572f
@ -0,0 +1,288 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import socket
|
||||
from typing import Literal
|
||||
from typing import NamedTuple
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from urllib.parse import ParseResult
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pwnlib import asm
|
||||
from pwnlib import constants
|
||||
from pwnlib import shellcraft
|
||||
from pwnlib.util.net import sockaddr
|
||||
|
||||
import pwndbg.aglib.memory
|
||||
import pwndbg.aglib.shellcode
|
||||
import pwndbg.commands
|
||||
import pwndbg.lib.abi
|
||||
import pwndbg.lib.memory
|
||||
import pwndbg.lib.regs
|
||||
from pwndbg.commands import CommandCategory
|
||||
|
||||
|
||||
class ShellcodeRegs(NamedTuple):
|
||||
newfd: str
|
||||
syscall_ret: str
|
||||
stack: str
|
||||
|
||||
|
||||
def get_shellcode_regs() -> ShellcodeRegs:
|
||||
register_set = pwndbg.lib.regs.reg_sets[pwndbg.aglib.arch.current]
|
||||
syscall_abi = pwndbg.lib.abi.ABI.syscall()
|
||||
|
||||
# pickup free register what is not used for syscall abi
|
||||
newfd_reg = next(
|
||||
(
|
||||
reg_name
|
||||
for reg_name in register_set.gpr
|
||||
if reg_name not in syscall_abi.register_arguments
|
||||
)
|
||||
)
|
||||
assert (
|
||||
newfd_reg is not None
|
||||
), f"architecture {pwndbg.aglib.arch.current} don't have unused register..."
|
||||
|
||||
return ShellcodeRegs(newfd_reg, register_set.retval, register_set.stack)
|
||||
|
||||
|
||||
def stack_size_alignment(s: int) -> int:
|
||||
syscall_abi = pwndbg.lib.abi.ABI.syscall()
|
||||
return s + (syscall_abi.arg_alignment - (s % syscall_abi.arg_alignment))
|
||||
|
||||
|
||||
def asm_replace_file(replace_fd: int, filename: str) -> Tuple[int, str]:
|
||||
filename = filename.encode() + b"\x00"
|
||||
|
||||
regs = get_shellcode_regs()
|
||||
stack_size = stack_size_alignment(len(filename))
|
||||
|
||||
open_asm = (
|
||||
shellcraft.syscall("SYS_open", regs.stack, "O_CREAT|O_RDWR", 0o666)
|
||||
if hasattr(constants, "SYS_open")
|
||||
else shellcraft.syscall("SYS_openat", "AT_FDCWD", regs.stack, "O_CREAT|O_RDWR", 0o666)
|
||||
)
|
||||
|
||||
dup_asm = (
|
||||
shellcraft.syscall("SYS_dup2", regs.newfd, replace_fd)
|
||||
if hasattr(constants, "SYS_dup2")
|
||||
else shellcraft.syscall("SYS_dup3", regs.newfd, replace_fd, 0)
|
||||
)
|
||||
|
||||
return stack_size, asm.asm(
|
||||
"".join(
|
||||
[
|
||||
shellcraft.pushstr(filename, False),
|
||||
open_asm,
|
||||
shellcraft.mov(regs.newfd, regs.syscall_ret),
|
||||
dup_asm,
|
||||
shellcraft.syscall("SYS_close", regs.newfd),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def asm_replace_socket(replace_fd: int, socket_data: ParsedSocket) -> Tuple[int, str]:
|
||||
sockdata, addr_len, _ = sockaddr(socket_data.address, socket_data.port, socket_data.ip_version)
|
||||
socktype = {"tcp": "SOCK_STREAM", "udp": "SOCK_DGRAM"}[socket_data.protocol]
|
||||
family = {"ipv4": "AF_INET", "ipv6": "AF_INET6"}[socket_data.ip_version]
|
||||
|
||||
regs = get_shellcode_regs()
|
||||
stack_size = stack_size_alignment(len(sockdata))
|
||||
|
||||
dup_asm = (
|
||||
shellcraft.syscall("SYS_dup2", regs.newfd, replace_fd)
|
||||
if hasattr(constants, "SYS_dup2")
|
||||
else shellcraft.syscall("SYS_dup3", regs.newfd, replace_fd, 0)
|
||||
)
|
||||
|
||||
return stack_size, asm.asm(
|
||||
"".join(
|
||||
[
|
||||
shellcraft.syscall("SYS_socket", family, socktype, 0),
|
||||
shellcraft.mov(regs.newfd, regs.syscall_ret),
|
||||
shellcraft.pushstr(sockdata, False),
|
||||
shellcraft.syscall("SYS_connect", regs.newfd, regs.stack, addr_len),
|
||||
dup_asm,
|
||||
shellcraft.syscall("SYS_close", regs.newfd),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def exec_shellcode_with_stack(ec: pwndbg.dbg_mod.ExecutionController, blob, stack_size: int):
|
||||
# This function could be improved, for example:
|
||||
# - Run the shellcode inside an emulator like Unicorn
|
||||
# - Calculate the maximum stack size the shellcode would consume dynamically.
|
||||
|
||||
stack_start_diff = pwndbg.aglib.regs.sp
|
||||
stack_start = stack_start_diff - stack_size
|
||||
original_stack = pwndbg.aglib.memory.read(stack_start, stack_size)
|
||||
|
||||
try:
|
||||
async with pwndbg.aglib.shellcode.exec_shellcode(
|
||||
ec, blob, restore_context=True, disable_breakpoints=True
|
||||
):
|
||||
stack_diff_size = stack_start_diff - pwndbg.aglib.regs.sp
|
||||
|
||||
# Make sure stack is not corrupted somehow
|
||||
assert not (
|
||||
stack_diff_size > stack_size
|
||||
), f"stack is probably corrupted size_current=f{stack_diff_size} size_max_want={stack_size}"
|
||||
|
||||
yield
|
||||
finally:
|
||||
pwndbg.aglib.memory.write(stack_start, original_stack)
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
description="""Replace a file descriptor of a debugged process.
|
||||
|
||||
The new file descriptor can point to:
|
||||
- a file
|
||||
- a pipe
|
||||
- a socket
|
||||
- a device, etc.
|
||||
|
||||
Examples:
|
||||
1. Redirect STDOUT to a file:
|
||||
`hijack-fd 1 /dev/null`
|
||||
|
||||
2. Redirect STDERR to a socket:
|
||||
`hijack-fd 2 tcp://localhost:8888`
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"fdnum",
|
||||
help="File descriptor (FD) number to be replaced with the specified new socket or file.",
|
||||
type=int,
|
||||
)
|
||||
|
||||
|
||||
class ParsedSocket(NamedTuple):
|
||||
protocol: Literal["tcp", "udp"]
|
||||
ip_version: Literal["ipv4", "ipv6"]
|
||||
address: str
|
||||
port: int
|
||||
|
||||
|
||||
def parse_socket(url: str) -> ParsedSocket:
|
||||
if "://" in url:
|
||||
# For handling:
|
||||
# - `tcp://[::1]:80`
|
||||
# - `udp://example.com:80`
|
||||
# - `tcp+ipv6://example.com:80`
|
||||
parsed = urlparse(url)
|
||||
else:
|
||||
# For handling:
|
||||
# - `127.0.0.1:80`
|
||||
parsed = ParseResult("", url, "", "", "", "")
|
||||
|
||||
# Handling eg: `tcp+ipv6://example.com:80`
|
||||
scheme_info = parsed.scheme.split("+", 1)
|
||||
|
||||
selected_protocol: Literal["tcp", "udp"] = "tcp"
|
||||
selected_ip_protocol: Literal["ipv4", "ipv6"] | None = None
|
||||
if parsed.scheme:
|
||||
for any_value in scheme_info:
|
||||
if any_value in ("tcp", "udp"):
|
||||
selected_protocol = any_value
|
||||
elif any_value in ("ipv4", "ipv6"):
|
||||
selected_ip_protocol = any_value
|
||||
|
||||
domain_or_ip = parsed.hostname
|
||||
if not domain_or_ip:
|
||||
raise argparse.ArgumentTypeError("Domain or IP is required")
|
||||
|
||||
port = parsed.port
|
||||
if not port:
|
||||
raise argparse.ArgumentTypeError("Port is required")
|
||||
|
||||
protocol_ordered = (
|
||||
("ipv4", socket.AF_INET),
|
||||
("ipv6", socket.AF_INET6),
|
||||
)
|
||||
|
||||
found_ip_protocol: Literal["ipv4", "ipv6"] | None = None
|
||||
address_ipv4_or_ipv6: str = ""
|
||||
for family_name, family_const in protocol_ordered:
|
||||
if selected_ip_protocol and selected_ip_protocol != family_name:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Resolve the given domain or IP address to its corresponding IP address
|
||||
ips = socket.getaddrinfo(domain_or_ip, None, family_const)
|
||||
except socket.gaierror:
|
||||
# happen when domain not found
|
||||
continue
|
||||
|
||||
for _, _, _, _, ip in ips:
|
||||
address_ipv4_or_ipv6 = ip[0]
|
||||
found_ip_protocol = family_name
|
||||
|
||||
if found_ip_protocol:
|
||||
break
|
||||
|
||||
if not address_ipv4_or_ipv6:
|
||||
raise argparse.ArgumentTypeError(
|
||||
f"Could not resolve {domain_or_ip} to proper {selected_ip_protocol} address"
|
||||
)
|
||||
|
||||
if not found_ip_protocol:
|
||||
raise argparse.ArgumentTypeError("Protocol only accept: ipv4,ipv6")
|
||||
|
||||
return ParsedSocket(selected_protocol, found_ip_protocol, address_ipv4_or_ipv6, port)
|
||||
|
||||
|
||||
PARSED_FILE_ARG = Tuple[Optional[ParsedSocket], Optional[str]]
|
||||
|
||||
|
||||
def parse_file_or_socket(s: str) -> PARSED_FILE_ARG:
|
||||
# is file
|
||||
if s.startswith("/") or s.startswith("./"):
|
||||
return None, s
|
||||
return parse_socket(s), None
|
||||
|
||||
|
||||
parser.add_argument(
|
||||
"newfile",
|
||||
help="""Specify a file or a socket.
|
||||
|
||||
For files, the filename must start with `/` (e.g., `/etc/passwd`).
|
||||
|
||||
For sockets, the following formats are allowed:
|
||||
- `127.0.0.1:80` (default is TCP)
|
||||
- `tcp://[::1]:80`
|
||||
- `udp://example.com:80`
|
||||
- `tcp+ipv6://example.com:80`
|
||||
""",
|
||||
type=parse_file_or_socket,
|
||||
)
|
||||
|
||||
|
||||
@pwndbg.commands.ArgparsedCommand(parser, category=CommandCategory.MISC, command_name="hijack-fd")
|
||||
@pwndbg.commands.OnlyWhenRunning
|
||||
@pwndbg.commands.OnlyWhenUserspace
|
||||
def hijack_fd(fdnum: int, newfile: PARSED_FILE_ARG) -> None:
|
||||
socket_data, filename = newfile
|
||||
if filename:
|
||||
stack_size, asm_bin = asm_replace_file(fdnum, filename)
|
||||
elif socket_data:
|
||||
stack_size, asm_bin = asm_replace_socket(fdnum, socket_data)
|
||||
else:
|
||||
assert False
|
||||
|
||||
async def ctrl(ec: pwndbg.dbg_mod.ExecutionController):
|
||||
async with exec_shellcode_with_stack(ec, asm_bin, stack_size):
|
||||
print(
|
||||
"Operation succeeded. Errors are not captured.\n"
|
||||
"You can verify this with `procinfo` if the file descriptor has been replaced."
|
||||
)
|
||||
|
||||
pwndbg.dbg.selected_inferior().dispatch_execution_controller(ctrl)
|
||||
Loading…
Reference in new issue