diff --git a/Dockerfile b/Dockerfile index ece9ca9d0..b8d36e5ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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" diff --git a/docs/schema_doc.md b/docs/schema_doc.md index 34c1333f7..9a42078fa 100644 --- a/docs/schema_doc.md +++ b/docs/schema_doc.md @@ -683,7 +683,7 @@ How to handle writes to the file ||| |-|-| |__Type__|string or null| -|__Default__|`read`| +|__Default__|`write`| ##### `pseudofiles..write.` Discard write @@ -790,7 +790,7 @@ plugin: my_plugin ||| |-|-| |__Type__|string or null| -|__Default__|`read`| +|__Default__|`ioctl`| ## `nvram` NVRAM diff --git a/pyplugins/interventions/hyperfile.py b/pyplugins/interventions/hyperfile.py index 021326954..0cb6c9b48 100644 --- a/pyplugins/interventions/hyperfile.py +++ b/pyplugins/interventions/hyperfile.py @@ -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 @@ -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' @@ -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") @@ -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" @@ -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) diff --git a/pyplugins/interventions/pseudofiles.py b/pyplugins/interventions/pseudofiles.py index 2bb541018..be3c9e655 100644 --- a/pyplugins/interventions/pseudofiles.py +++ b/pyplugins/interventions/pseudofiles.py @@ -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 @@ -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: @@ -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. @@ -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 diff --git a/src/penguin/penguin_config/structure.py b/src/penguin/penguin_config/structure.py index 73486c32b..d1464ec80 100644 --- a/src/penguin/penguin_config/structure.py +++ b/src/penguin/penguin_config/structure.py @@ -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( @@ -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")), ), ) ), diff --git a/tests/unit_tests/test_target/base_config.yaml b/tests/unit_tests/test_target/base_config.yaml index 60bc4f373..98e942085 100644 --- a/tests/unit_tests/test_target/base_config.yaml +++ b/tests/unit_tests/test_target/base_config.yaml @@ -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 @@ -67,4 +68,4 @@ static_files: /igloo/utils/sh: type: symlink - target: /igloo/utils/busybox \ No newline at end of file + target: /igloo/utils/busybox diff --git a/tests/unit_tests/test_target/patches/tests/custom_ioctl.yaml b/tests/unit_tests/test_target/patches/tests/custom_ioctl.yaml new file mode 100644 index 000000000..17377ecc6 --- /dev/null +++ b/tests/unit_tests/test_target/patches/tests/custom_ioctl.yaml @@ -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 + #include + #include + #include + #include + #include + + 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 diff --git a/tests/unit_tests/test_target/pyplugins/custom_ioctl.py b/tests/unit_tests/test_target/pyplugins/custom_ioctl.py new file mode 100644 index 000000000..75399c555 --- /dev/null +++ b/tests/unit_tests/test_target/pyplugins/custom_ioctl.py @@ -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