Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ ARG BASE_IMAGE="ubuntu:22.04"
ARG DOWNLOAD_TOKEN="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"
ARG PANDA_VERSION="1.8.57"
ARG BUSYBOX_VERSION="0.0.8"
ARG LINUX_VERSION="2.4.23"
ARG LINUX_VERSION="2.4.24"
ARG LIBNVRAM_VERSION="0.0.16"
ARG CONSOLE_VERSION="1.0.5"
ARG PENGUIN_PLUGINS_VERSION="1.5.15"
Expand Down
4 changes: 2 additions & 2 deletions docs/schema_doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ How to handle writes to the file
|||
|-|-|
|__Type__|string or null|
|__Default__|`read`|
|__Default__|`write`|


##### `pseudofiles.<string>.write.<model=discard>` Discard write
Expand Down Expand Up @@ -790,7 +790,7 @@ plugin: my_plugin
|||
|-|-|
|__Type__|string or null|
|__Default__|`read`|
|__Default__|`ioctl`|


## `nvram` NVRAM
Expand Down
52 changes: 50 additions & 2 deletions pyplugins/interventions/hyperfile.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
import struct
from queue import Queue
from pandare import PyPlugin


try:
from penguin import yaml
except ImportError:
import yaml


# Make sure these match hyperfs

HYPER_MAGIC = 0x51EC3692

HYPER_FILE_OP = 0
HYPER_GET_NUM_HYPERFILES = 1
HYPER_GET_HYPERFILE_PATHS = 2
HYPER_IOCTL_OP = 3
HYPER_IOCTL_END = 4
HYPER_IOCTL_READ_RESPONSE = 5

HYPER_READ = 0
HYPER_WRITE = 1
HYPER_IOCTL = 2
HYPER_GETATTR = 3

HYPER_IOCTL_OP_READ = 0
HYPER_IOCTL_OP_WRITE = 1
HYPER_IOCTL_OP_RETURN = 2

RETRY = 0xDEADBEEF


Expand Down Expand Up @@ -50,6 +64,10 @@ def __init__(self, panda):
self.files = self.get_arg("models")
self.logger = self.get_arg("logger")

# Queues for communicating with thread for processing an ioctl
self.ioctl_command_queue = Queue()
self.ioctl_response_queue = Queue()

# Struct format strings for endianness and word size
self.endian = '<' if panda.endianness == 'little' else '>'
self.s_word, self.u_word = 'iI' if panda.bits == 32 else 'qQ'
Expand Down Expand Up @@ -93,6 +111,12 @@ def before_hypercall(cpu):
self.handle_get_num_hyperfiles(cpu)
elif hc_type == HYPER_GET_HYPERFILE_PATHS:
self.handle_get_hyperfile_paths(cpu)
elif hc_type == HYPER_IOCTL_OP:
self.handle_ioctl_op(cpu)
elif hc_type == HYPER_IOCTL_END:
self.handle_ioctl_end(cpu)
elif hc_type == HYPER_IOCTL_READ_RESPONSE:
self.handle_ioctl_read_response(cpu)

def handle_get_num_hyperfiles(self, cpu):
num_hyperfiles_addr = self.panda.arch.get_arg(cpu, 2, convention="syscall")
Expand Down Expand Up @@ -131,6 +155,30 @@ def handle_get_hyperfile_paths(self, cpu):
self.logger.debug("Failed to write hyperfile path to guest - retry")
return

def handle_ioctl_op(self, cpu):
req_addr = self.panda.arch.get_arg(cpu, 2, convention="syscall")
cmd = self.ioctl_command_queue.get()
match cmd:
case ("read", size, ptr):
req_bytes = struct.pack(f"{self.endian} i {self.u_word} {self.u_word}", HYPER_IOCTL_OP_READ, size, ptr)
case ("write", ptr, data):
req_bytes = struct.pack(f"{self.endian} i {self.u_word} {self.u_word} {len(data)}s", HYPER_IOCTL_OP_WRITE, len(data), ptr, data)
case ("ret", status):
req_bytes = struct.pack(f"{self.endian} i {self.s_word}", HYPER_IOCTL_OP_RETURN, status)
case _:
assert False
self.panda.virtual_memory_write(cpu, req_addr, req_bytes)

def handle_ioctl_end(self, cpu):
self.ioctl_response_queue.put(("end",))
self.ioctl_command_queue = Queue()
self.ioctl_response_queue = Queue()

def handle_ioctl_read_response(self, cpu):
addr = self.panda.arch.get_arg(cpu, 2, convention="syscall")
resp = self.panda.virtual_memory_read(cpu, addr, 128)
self.ioctl_response_queue.put(("read_response", resp))

def handle_file_op(self, cpu):
header_fmt = f"{self.endian} i {self.u_word}"
read_fmt = write_fmt = f"{self.endian} {self.u_word} {self.u_word} q"
Expand Down Expand Up @@ -216,8 +264,8 @@ def handle_file_op(self, cpu):

elif type_val == HYPER_IOCTL:
cmd, arg = struct.unpack_from(ioctl_fmt, buf, sub_offset)
retval = model[type_val](device_name, cmd, arg)
self.handle_result(device_name, "ioctl", retval, cmd, arg)
model[type_val](self.ioctl_command_queue, self.ioctl_response_queue, device_name, cmd, arg)
retval = 0

elif type_val == HYPER_GETATTR:
retval, size_data = model[type_val](device_name, model)
Expand Down
76 changes: 58 additions & 18 deletions pyplugins/interventions/pseudofiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pickle
import re
import struct
import threading
from collections import Counter
from copy import deepcopy
from os.path import dirname, isfile, isabs
Expand Down Expand Up @@ -676,18 +677,7 @@ def read_default(self, filename, buffer, length, offset, details=None):
self.centralized_log(filename, "read")
return (b"", -22) # -EINVAL - we don't support reads

# IOCTL is more complicated than read/write.
# default is a bit of a misnomer, it's our default ioctl handler which
# implements default behavior (i.e., error) on issue of unspecified ioctls,
# but implements what it's told for others
def ioctl_default(self, filename, cmd, arg, ioctl_details):
"""
Given a cmd and arg, return a value
filename is device path
ioctl_details is a dict of:
cmd -> {'model': 'return_const'|'symex',
'val': X}
"""
def do_ioctl(self, filename, cmd, ioctl_details):
# Try to use cmd as our key, but '*' is a fallback
# is_wildcard = False
if cmd in ioctl_details:
Expand All @@ -697,13 +687,13 @@ def ioctl_default(self, filename, cmd, arg, ioctl_details):
# is_wildcard = True
else:
self.log_ioctl_failure(filename, cmd)
return -25 # -ENOTTY
return (None, -25) # -ENOTTY

model = cmd_details["model"]

if model == "return_const":
rv = cmd_details["val"]
return rv
return (cmd_details, rv)

elif model == "symex":
# Symex is tricky and different from normal models.
Expand All @@ -717,20 +707,70 @@ def ioctl_default(self, filename, cmd, arg, ioctl_details):
# ignore? But we probably could?
# raise NotImplementedError("Uhhhh nested symex")
# self.last_symex = filename
return MAGIC_SYMEX_RETVAL # We'll detect this on the return and know what to do. I think?
return (cmd_details, MAGIC_SYMEX_RETVAL) # We'll detect this on the return and know what to do. I think?
elif model == "from_plugin":
plugin_name = cmd_details["plugin"]
plugin = getattr(plugins, plugin_name)
func = cmd_details.get("function", "ioctl")
if hasattr(plugin, func):
fn = getattr(plugin, func)
return (cmd_details, getattr(plugin, func))
else:
raise ValueError(f"Hyperfile {filename} depends on plugin {plugin} which does not have function {func}")
return fn(filename, cmd, arg, cmd_details)
else:
# This is an actual error - config is malformed. Bail
raise ValueError(f"Unsupported ioctl model {model} for cmd {cmd}")
# return -25 # -ENOTTY

# IOCTL is more complicated than read/write.
# default is a bit of a misnomer, it's our default ioctl handler which
# implements default behavior (i.e., error) on issue of unspecified ioctls,
# but implements what it's told for others
def ioctl_default(self, command_queue, response_queue, filename, cmd, arg, ioctl_details):
"""
Given a cmd and arg, return a value
filename is device path
ioctl_details is a dict of:
cmd -> {'model': 'return_const'|'symex',
'val': X}
"""

cmd_details, res = self.do_ioctl(filename, cmd, ioctl_details)

class PseudofileIoctlContext:
def __init__(self, panda):
self.panda = panda

def read_bytes(self, addr, size):
chunk_size = 128
data = b""
for i in range((size + chunk_size - 1) // chunk_size):
offset = i * chunk_size
chunk_addr = addr + offset
command_queue.put(("read", chunk_size, chunk_addr))
resp_type, chunk = response_queue.get()
assert resp_type == "read_response"
data += chunk
data = data[:size]
return data

def write_bytes(self, addr, data):
chunk_size = 128
for i in range((len(data) + chunk_size - 1) // chunk_size):
offset = i * chunk_size
chunk_addr = addr + offset
chunk_data = data[offset:offset + chunk_size]
command_queue.put(("write", chunk_addr, chunk_data))

def thread_fn(*args):
status = res if isinstance(res, int) else res(*args)
if status is None:
status = -22 # EINVAL
command_queue.put(("ret", status))
assert response_queue.get() == ("end",)

threading.Thread(
target=thread_fn,
args=(PseudofileIoctlContext(self.panda), filename, cmd, arg, cmd_details),
).start()

def dump_results(self):
# Dump all file failures to disk as yaml
Expand Down
4 changes: 2 additions & 2 deletions src/penguin/penguin_config/structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ def set_kernel_default(cls, values):
description=None,
fields=(
("plugin", str, Field(title="Name of the loaded PyPlugin")),
("function", Optional[str], Field(title="Function to call", default="read")),
("function", Optional[str], Field(title="Function to call", default="write")),
),
),
dict(
Expand Down Expand Up @@ -466,7 +466,7 @@ def set_kernel_default(cls, values):
description=None,
fields=(
("plugin", str, Field(title="Name of the loaded PyPlugin")),
("function", Optional[str], Field(title="Function to call", default="read")),
("function", Optional[str], Field(title="Function to call", default="ioctl")),
),
)
),
Expand Down
3 changes: 2 additions & 1 deletion tests/unit_tests/test_target/base_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ env:
patches:
- ./patches/tests/bash.yaml
- ./patches/tests/core_dumps.yaml
- ./patches/tests/custom_ioctl.yaml
- ./patches/tests/devfs_passthrough.yaml
- ./patches/tests/env_cmp.yaml
- ./patches/tests/env_unset.yaml
Expand Down Expand Up @@ -67,4 +68,4 @@ static_files:

/igloo/utils/sh:
type: symlink
target: /igloo/utils/busybox
target: /igloo/utils/busybox
62 changes: 62 additions & 0 deletions tests/unit_tests/test_target/patches/tests/custom_ioctl.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
plugins:
./pyplugins/custom_ioctl: {}
verifier:
conditions:
custom_ioctl:
type: file_contains
file: console.log
string: "/tests/custom_ioctl.sh PASS"

pseudofiles:
/dev/my_custom_ioctl:
ioctl:
"*":
model: from_plugin
plugin: CustomIoctl

lib_inject:
extra: |
#include <assert.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>

void libinject_custom_ioctl(void)
{
int fd, result;
char buf[128] = { 0 };

fd = open("/dev/my_custom_ioctl", O_RDONLY);
assert(fd >= 0);

result = ioctl(fd, _IO('f', 0x21), &(unsigned int){0x11223344});
assert(result == 0x11223344);

result = ioctl(fd, _IO('f', 0x21), NULL);
assert(result == -1);
assert(errno == EFAULT);

result = ioctl(fd, _IO('f', 0x22), &(char *){buf});
assert(result == 200);
assert(!strcmp(buf, "hello world"));

result = ioctl(fd, _IO('f', 0x23), NULL);
assert(result == -1);
assert(errno == 300);
}

static_files:
/tests/custom_ioctl.sh:
type: inline_file
contents: |
#!/igloo/utils/micropython

import ffi

ffi.open("lib_inject.so").func("v", "libinject_custom_ioctl", "")()

print("tests pass")

mode: 73
22 changes: 22 additions & 0 deletions tests/unit_tests/test_target/pyplugins/custom_ioctl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from pandare import PyPlugin


class CustomIoctl(PyPlugin):
def ioctl(self, ctx, path, cmd, arg, details):

# The _IO() macro sets an upper bit on MIPS but not on other targets.
# Mask the command so the same test works on all targets.
cmd &= 0xffff

match cmd:
case 0x6621:
data = ctx.read_bytes(arg, 4)
data = int.from_bytes(data, byteorder=ctx.panda.endianness)
return data
case 0x6622:
data = ctx.read_bytes(arg, ctx.panda.bits // 8)
addr = int.from_bytes(data, byteorder=ctx.panda.endianness)
ctx.write_bytes(addr, b"hello world\x00")
return 200
case 0x6623:
return -300
Loading