├── CMakeLists.txt ├── LICENSE ├── README.md ├── Xenex.cmake ├── fishhook.c ├── fishhook.h └── patcher ├── CMakeLists.txt ├── Info.plist ├── bootstrap.asm ├── hook_util.h ├── patcher.py └── runtime_utils.h /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(fishhook) 3 | 4 | # Set C standard 5 | set(CMAKE_C_STANDARD 11) 6 | set(CMAKE_C_STANDARD_REQUIRED ON) 7 | 8 | # Add source files 9 | add_library(fishhook STATIC 10 | fishhook.c 11 | fishhook.h 12 | ) 13 | 14 | # Include directories 15 | target_include_directories(fishhook PUBLIC 16 | ${CMAKE_CURRENT_SOURCE_DIR} 17 | ) 18 | 19 | # Set visibility flags for macOS/iOS 20 | target_compile_options(fishhook PRIVATE 21 | -fvisibility=hidden 22 | ) 23 | 24 | # Set installation rules 25 | install(TARGETS fishhook 26 | ARCHIVE DESTINATION lib 27 | LIBRARY DESTINATION lib 28 | RUNTIME DESTINATION bin 29 | ) 30 | install(FILES fishhook.h 31 | DESTINATION include 32 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Speedyfriend67 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xenex 2 | 3 | A powerful framework for injecting custom code into iOS applications, providing advanced runtime manipulation capabilities including method swizzling, VMT hooking, and memory protection utilities. 4 | 5 | ## Features 6 | 7 | - **Method Swizzling**: Runtime method replacement and interception 8 | - **VMT Hooking**: Virtual method table manipulation for C++ classes 9 | - **Memory Protection**: Safe memory read/write operations with automatic protection handling 10 | - **Dynamic Library Loading**: Thread-safe library handle caching and management 11 | - **Address Resolution**: Utilities for resolving base addresses and function pointers 12 | 13 | ## Requirements 14 | 15 | - CMake build system 16 | - iOS SDK 17 | - Decrypted target application (DRM-free) 18 | - Compatible iOS architecture 19 | 20 | ## Setup 21 | 22 | 1. Add this repository to your CMake project: 23 | ```cmake 24 | add_subdirectory(xenex) 25 | ``` 26 | 27 | 2. Include the Injector.cmake in your CMakeLists.txt: 28 | ```cmake 29 | include(${CMAKE_CURRENT_LIST_DIR}/xenex/Xenex.cmake) 30 | ``` 31 | 32 | ## Usage 33 | 34 | ### Basic Injection 35 | 36 | 1. Create your injection code in a C++ file (e.g., `injection.cpp`): 37 | ```cpp 38 | namespace Xenex { 39 | extern "C" void initialize() { 40 | // Your injection code here 41 | // This will be called when the app starts 42 | } 43 | } 44 | ``` 45 | 46 | 2. Set up your CMake target: 47 | ```cmake 48 | # Define your target 49 | add_library(MyInjection SHARED 50 | injection.cpp 51 | ) 52 | 53 | # Configure injection 54 | set(XENEX_BINARY_NAME "YourApp") # Name of the target app binary 55 | set(XENEX_APP_FOLDER "path/to/Payload/YourApp.app") # Path to the app bundle 56 | set(XENEX_BIN_FOLDER "${CMAKE_BINARY_DIR}/bin") # Output directory 57 | 58 | # Apply injection configuration 59 | configure_xenex(MyInjection) 60 | ``` 61 | 62 | ### Advanced Features 63 | 64 | #### Method Swizzling 65 | ```cpp 66 | // Swizzle instance method 67 | Class targetClass = objc_getClass("TargetClass"); 68 | SEL originalSelector = @selector(originalMethod); 69 | SEL swizzledSelector = @selector(swizzledMethod); 70 | Xenex::Runtime::swizzleMethod(targetClass, originalSelector, swizzledSelector); 71 | 72 | // Swizzle class method 73 | Xenex::Runtime::swizzleClassMethod(targetClass, originalSelector, swizzledSelector); 74 | ``` 75 | 76 | #### VMT Hooking 77 | ```cpp 78 | // Hook virtual method 79 | uintptr_t baseAddress = 0x1000000; 80 | size_t offset = 0x100; 81 | size_t vtableOffset = 0x10; 82 | Xenex::Runtime::HookVirtualMethod(baseAddress, offset, vtableOffset, replacementFunction); 83 | ``` 84 | 85 | #### Memory Operations 86 | ```cpp 87 | // Read memory 88 | auto value = Xenex::Runtime::Read(address); 89 | 90 | // Write memory (with automatic protection handling) 91 | Xenex::Runtime::Write(address, newValue); 92 | ``` 93 | 94 | ## Build Process 95 | 96 | 1. Build your project using CMake 97 | 2. The patcher will automatically: 98 | - Generate a bootloader 99 | - Patch the target binary 100 | - Create an IPA folder with the modified binary 101 | 3. Find the patched binary in `${XENEX_BIN_FOLDER}/IPA/` 102 | 103 | ## Example 104 | 105 | Check the `example/` directory for a complete working example demonstrating various injection techniques. 106 | 107 | ## Security Considerations 108 | 109 | - Always backup the original app binary before patching 110 | - Ensure proper memory protection when modifying code segments 111 | - Be cautious with method swizzling to avoid runtime crashes 112 | - Verify target app architecture compatibility 113 | - Remove any DRM protection before patching 114 | 115 | ## Troubleshooting 116 | 117 | - Verify the target app is properly decrypted 118 | - Check architecture compatibility between injection code and target app 119 | - Ensure all memory addresses are properly aligned 120 | - Validate method signatures when using swizzling 121 | 122 | ## License 123 | 124 | This project is licensed under the MIT License - see the LICENSE file for details. -------------------------------------------------------------------------------- /Xenex.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18.0 FATAL_ERROR) 2 | 3 | # iOS Development requires clang 4 | if (NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") 5 | message(FATAL_ERROR "This framework requires the use of clang.") 6 | endif() 7 | 8 | # Check iOS SDK Path 9 | if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") 10 | # Find SDK Path 11 | execute_process(COMMAND xcrun --show-sdk-path --sdk iphoneos 12 | OUTPUT_VARIABLE XENEX_IOS_SDK 13 | OUTPUT_STRIP_TRAILING_WHITESPACE 14 | ) 15 | else() 16 | # Check for environment variable 17 | if (EXISTS $ENV{XENEX_IOS_SDK}) 18 | set(XENEX_IOS_SDK $ENV{XENEX_IOS_SDK}) 19 | else() 20 | message(FATAL_ERROR "Cannot find iOS SDK! Set the XENEX_IOS_SDK environment variable") 21 | endif() 22 | endif() 23 | 24 | # Configuration 25 | set(CMAKE_OSX_ARCHITECTURES "arm64") 26 | set(CMAKE_OSX_SYSROOT ${XENEX_IOS_SDK}) 27 | 28 | # Set output folder 29 | set(XENEX_BIN_FOLDER ${CMAKE_BINARY_DIR}/xenex_bin) 30 | if (NOT EXISTS ${XENEX_BIN_FOLDER}) 31 | file(MAKE_DIRECTORY ${XENEX_BIN_FOLDER}) 32 | endif() 33 | 34 | set(XENEX_ROOT ${CMAKE_CURRENT_LIST_DIR}) 35 | 36 | # Main setup macro 37 | macro(xenex_setup target_app binary_name) 38 | if (NOT DEFINED XENEX_TARGET_APP) 39 | message(FATAL_ERROR "Define XENEX_TARGET_APP where the target IPA is located.") 40 | endif() 41 | 42 | if (NOT EXISTS "${XENEX_TARGET_APP}/Payload") 43 | message(FATAL_ERROR "Cannot find Payload folder. Place your IPA contents into the folder defined in XENEX_TARGET_APP.") 44 | endif() 45 | 46 | file(GLOB XENEX_APP_FOLDER "${XENEX_TARGET_APP}/Payload/*.app") 47 | 48 | if(XENEX_APP_FOLDER STREQUAL "") 49 | message(FATAL_ERROR "Unable to find application inside Payload folder.") 50 | endif() 51 | 52 | if (NOT DEFINED INJECTOR_BINARY_NAME) 53 | message(FATAL_ERROR "Unable to determine binary name. Define INJECTOR_BINARY_NAME.") 54 | endif() 55 | 56 | if (NOT EXISTS "${XENEX_APP_FOLDER}/${INJECTOR_BINARY_NAME}") 57 | message(FATAL_ERROR "Unable to find binary ${INJECTOR_BINARY_NAME} in application folder") 58 | endif() 59 | 60 | # Codegen target 61 | add_custom_target(InjectorCodegen ALL 62 | COMMAND mkdir -p ${XENEX_BIN_FOLDER}/IPA 63 | COMMAND python3 ${XENEX_ROOT}/patcher/patcher.py 64 | ${XENEX_APP_FOLDER}/${INJECTOR_BINARY_NAME} 65 | ${XENEX_BIN_FOLDER} 66 | ${INJECTOR_BINARY_NAME} 67 | ${CMAKE_CURRENT_SOURCE_DIR} 68 | WORKING_DIRECTORY ${XENEX_ROOT}/patcher 69 | COMMENT "Generating injection code" 70 | BYPRODUCTS ${XENEX_BIN_FOLDER}/bootloader.hpp 71 | ) 72 | add_dependencies(${PROJECT_NAME} InjectorCodegen) 73 | 74 | # Include generated headers 75 | target_include_directories(${PROJECT_NAME} PRIVATE ${XENEX_BIN_FOLDER}) 76 | target_link_options(${PROJECT_NAME} PRIVATE "-L${XENEX_IOS_SDK}/usr/lib") 77 | endmacro() -------------------------------------------------------------------------------- /fishhook.c: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, Facebook, Inc. 2 | // All rights reserved. 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // * Redistributions of source code must retain the above copyright notice, 6 | // this list of conditions and the following disclaimer. 7 | // * Redistributions in binary form must reproduce the above copyright notice, 8 | // this list of conditions and the following disclaimer in the documentation 9 | // and/or other materials provided with the distribution. 10 | // * Neither the name Facebook nor the names of its contributors may be used to 11 | // endorse or promote products derived from this software without specific 12 | // prior written permission. 13 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | #include "fishhook.h" 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | 39 | #ifdef __LP64__ 40 | typedef struct mach_header_64 mach_header_t; 41 | typedef struct segment_command_64 segment_command_t; 42 | typedef struct section_64 section_t; 43 | typedef struct nlist_64 nlist_t; 44 | #define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT_64 45 | #else 46 | typedef struct mach_header mach_header_t; 47 | typedef struct segment_command segment_command_t; 48 | typedef struct section section_t; 49 | typedef struct nlist nlist_t; 50 | #define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT 51 | #endif 52 | 53 | #ifndef SEG_DATA_CONST 54 | #define SEG_DATA_CONST "__DATA_CONST" 55 | #endif 56 | 57 | struct rebindings_entry { 58 | struct rebinding *rebindings; 59 | size_t rebindings_nel; 60 | struct rebindings_entry *next; 61 | }; 62 | 63 | static struct rebindings_entry *_rebindings_head; 64 | 65 | static int prepend_rebindings(struct rebindings_entry **rebindings_head, 66 | struct rebinding rebindings[], 67 | size_t nel) { 68 | struct rebindings_entry *new_entry = (struct rebindings_entry *) malloc(sizeof(struct rebindings_entry)); 69 | if (!new_entry) { 70 | return -1; 71 | } 72 | new_entry->rebindings = (struct rebinding *) malloc(sizeof(struct rebinding) * nel); 73 | if (!new_entry->rebindings) { 74 | free(new_entry); 75 | return -1; 76 | } 77 | memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel); 78 | new_entry->rebindings_nel = nel; 79 | new_entry->next = *rebindings_head; 80 | *rebindings_head = new_entry; 81 | return 0; 82 | } 83 | 84 | #if 0 85 | static int get_protection(void *addr, vm_prot_t *prot, vm_prot_t *max_prot) { 86 | mach_port_t task = mach_task_self(); 87 | vm_size_t size = 0; 88 | vm_address_t address = (vm_address_t)addr; 89 | memory_object_name_t object; 90 | #ifdef __LP64__ 91 | mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; 92 | vm_region_basic_info_data_64_t info; 93 | kern_return_t info_ret = vm_region_64( 94 | task, &address, &size, VM_REGION_BASIC_INFO_64, (vm_region_info_64_t)&info, &count, &object); 95 | #else 96 | mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT; 97 | vm_region_basic_info_data_t info; 98 | kern_return_t info_ret = vm_region(task, &address, &size, VM_REGION_BASIC_INFO, (vm_region_info_t)&info, &count, &object); 99 | #endif 100 | if (info_ret == KERN_SUCCESS) { 101 | if (prot != NULL) 102 | *prot = info.protection; 103 | 104 | if (max_prot != NULL) 105 | *max_prot = info.max_protection; 106 | 107 | return 0; 108 | } 109 | 110 | return -1; 111 | } 112 | #endif 113 | 114 | static void perform_rebinding_with_section(struct rebindings_entry *rebindings, 115 | section_t *section, 116 | intptr_t slide, 117 | nlist_t *symtab, 118 | char *strtab, 119 | uint32_t *indirect_symtab) { 120 | uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1; 121 | void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr); 122 | 123 | for (uint i = 0; i < section->size / sizeof(void *); i++) { 124 | uint32_t symtab_index = indirect_symbol_indices[i]; 125 | if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL || 126 | symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) { 127 | continue; 128 | } 129 | uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx; 130 | char *symbol_name = strtab + strtab_offset; 131 | bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1]; 132 | struct rebindings_entry *cur = rebindings; 133 | while (cur) { 134 | for (uint j = 0; j < cur->rebindings_nel; j++) { 135 | if (symbol_name_longer_than_1 && strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) { 136 | kern_return_t err; 137 | 138 | if (cur->rebindings[j].replaced != NULL && indirect_symbol_bindings[i] != cur->rebindings[j].replacement) 139 | *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]; 140 | 141 | /** 142 | * 1. Moved the vm protection modifying codes to here to reduce the 143 | * changing scope. 144 | * 2. Adding VM_PROT_WRITE mode unconditionally because vm_region 145 | * API on some iOS/Mac reports mismatch vm protection attributes. 146 | * -- Lianfu Hao Jun 16th, 2021 147 | **/ 148 | err = vm_protect (mach_task_self (), (uintptr_t)indirect_symbol_bindings, section->size, 0, VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY); 149 | if (err == KERN_SUCCESS) { 150 | /** 151 | * Once we failed to change the vm protection, we 152 | * MUST NOT continue the following write actions! 153 | * iOS 15 has corrected the const segments prot. 154 | * -- Lionfore Hao Jun 11th, 2021 155 | **/ 156 | indirect_symbol_bindings[i] = cur->rebindings[j].replacement; 157 | } 158 | goto symbol_loop; 159 | } 160 | } 161 | cur = cur->next; 162 | } 163 | symbol_loop:; 164 | } 165 | } 166 | 167 | static void rebind_symbols_for_image(struct rebindings_entry *rebindings, 168 | const struct mach_header *header, 169 | intptr_t slide) { 170 | Dl_info info; 171 | if (dladdr(header, &info) == 0) { 172 | return; 173 | } 174 | 175 | segment_command_t *cur_seg_cmd; 176 | segment_command_t *linkedit_segment = NULL; 177 | struct symtab_command* symtab_cmd = NULL; 178 | struct dysymtab_command* dysymtab_cmd = NULL; 179 | 180 | uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t); 181 | for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) { 182 | cur_seg_cmd = (segment_command_t *)cur; 183 | if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) { 184 | if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) { 185 | linkedit_segment = cur_seg_cmd; 186 | } 187 | } else if (cur_seg_cmd->cmd == LC_SYMTAB) { 188 | symtab_cmd = (struct symtab_command*)cur_seg_cmd; 189 | } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) { 190 | dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd; 191 | } 192 | } 193 | 194 | if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment || 195 | !dysymtab_cmd->nindirectsyms) { 196 | return; 197 | } 198 | 199 | // Find base symbol/string table addresses 200 | uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff; 201 | nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff); 202 | char *strtab = (char *)(linkedit_base + symtab_cmd->stroff); 203 | 204 | // Get indirect symbol table (array of uint32_t indices into symbol table) 205 | uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff); 206 | 207 | cur = (uintptr_t)header + sizeof(mach_header_t); 208 | for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) { 209 | cur_seg_cmd = (segment_command_t *)cur; 210 | if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) { 211 | if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 && 212 | strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) { 213 | continue; 214 | } 215 | for (uint j = 0; j < cur_seg_cmd->nsects; j++) { 216 | section_t *sect = 217 | (section_t *)(cur + sizeof(segment_command_t)) + j; 218 | if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) { 219 | perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab); 220 | } 221 | if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) { 222 | perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab); 223 | } 224 | } 225 | } 226 | } 227 | } 228 | 229 | static void _rebind_symbols_for_image(const struct mach_header *header, 230 | intptr_t slide) { 231 | rebind_symbols_for_image(_rebindings_head, header, slide); 232 | } 233 | 234 | int rebind_symbols_image(void *header, 235 | intptr_t slide, 236 | struct rebinding rebindings[], 237 | size_t rebindings_nel) { 238 | struct rebindings_entry *rebindings_head = NULL; 239 | int retval = prepend_rebindings(&rebindings_head, rebindings, rebindings_nel); 240 | rebind_symbols_for_image(rebindings_head, (const struct mach_header *) header, slide); 241 | if (rebindings_head) { 242 | free(rebindings_head->rebindings); 243 | } 244 | free(rebindings_head); 245 | return retval; 246 | } 247 | 248 | int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) { 249 | int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel); 250 | if (retval < 0) { 251 | return retval; 252 | } 253 | // If this was the first call, register callback for image additions (which is also invoked for 254 | // existing images, otherwise, just run on existing images 255 | if (!_rebindings_head->next) { 256 | _dyld_register_func_for_add_image(_rebind_symbols_for_image); 257 | } else { 258 | uint32_t c = _dyld_image_count(); 259 | for (uint32_t i = 0; i < c; i++) { 260 | _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i)); 261 | } 262 | } 263 | return retval; 264 | } 265 | -------------------------------------------------------------------------------- /fishhook.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, Facebook, Inc. 2 | // All rights reserved. 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // * Redistributions of source code must retain the above copyright notice, 6 | // this list of conditions and the following disclaimer. 7 | // * Redistributions in binary form must reproduce the above copyright notice, 8 | // this list of conditions and the following disclaimer in the documentation 9 | // and/or other materials provided with the distribution. 10 | // * Neither the name Facebook nor the names of its contributors may be used to 11 | // endorse or promote products derived from this software without specific 12 | // prior written permission. 13 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | #ifndef fishhook_h 25 | #define fishhook_h 26 | 27 | #include 28 | #include 29 | 30 | #if !defined(FISHHOOK_EXPORT) 31 | #define FISHHOOK_VISIBILITY __attribute__((visibility("hidden"))) 32 | #else 33 | #define FISHHOOK_VISIBILITY __attribute__((visibility("default"))) 34 | #endif 35 | 36 | #ifdef __cplusplus 37 | extern "C" { 38 | #endif //__cplusplus 39 | 40 | /* 41 | * A structure representing a particular intended rebinding from a symbol 42 | * name to its replacement 43 | */ 44 | struct rebinding { 45 | const char *name; 46 | void *replacement; 47 | void **replaced; 48 | }; 49 | 50 | /* 51 | * For each rebinding in rebindings, rebinds references to external, indirect 52 | * symbols with the specified name to instead point at replacement for each 53 | * image in the calling process as well as for all future images that are loaded 54 | * by the process. If rebind_functions is called more than once, the symbols to 55 | * rebind are added to the existing list of rebindings, and if a given symbol 56 | * is rebound more than once, the later rebinding will take precedence. 57 | */ 58 | FISHHOOK_VISIBILITY 59 | int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel); 60 | 61 | /* 62 | * Rebinds as above, but only in the specified image. The header should point 63 | * to the mach-o header, the slide should be the slide offset. Others as above. 64 | */ 65 | FISHHOOK_VISIBILITY 66 | int rebind_symbols_image(void *header, 67 | intptr_t slide, 68 | struct rebinding rebindings[], 69 | size_t rebindings_nel); 70 | 71 | #ifdef __cplusplus 72 | } 73 | #endif //__cplusplus 74 | 75 | #endif //fishhook_h 76 | 77 | -------------------------------------------------------------------------------- /patcher/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(xenex) 3 | 4 | # Set iOS platform 5 | set(CMAKE_SYSTEM_NAME iOS) 6 | set(CMAKE_OSX_SYSROOT /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.2.sdk) 7 | set(CMAKE_OSX_ARCHITECTURES arm64) 8 | set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE NO) 9 | 10 | # Set C++ standard 11 | set(CMAKE_CXX_STANDARD 17) 12 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 13 | 14 | # Build as a dynamic library 15 | add_library(xenex SHARED 16 | runtime_utils.h 17 | ${CMAKE_CURRENT_SOURCE_DIR}/../fishhook.c 18 | ${CMAKE_CURRENT_SOURCE_DIR}/../fishhook.h 19 | ) 20 | 21 | # Include directories 22 | target_include_directories(xenex PUBLIC 23 | ${CMAKE_CURRENT_SOURCE_DIR} 24 | ${CMAKE_SOURCE_DIR} 25 | ) 26 | 27 | # Set visibility flags for iOS 28 | target_compile_options(xenex PRIVATE 29 | -fvisibility=hidden 30 | ) 31 | 32 | # Link against required frameworks 33 | target_link_libraries(xenex 34 | "-framework Foundation" 35 | "-framework UIKit" 36 | ) 37 | 38 | # Set output name to .dylib 39 | set_target_properties(xenex PROPERTIES 40 | SUFFIX ".dylib" 41 | PREFIX "" 42 | XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "iPhone Developer" 43 | XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "" 44 | XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO" 45 | XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Automatic" 46 | ) -------------------------------------------------------------------------------- /patcher/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | xenex 9 | CFBundleIdentifier 10 | com.injector.ios 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | xenex 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | MinimumOSVersion 22 | 12.0 23 | 24 | -------------------------------------------------------------------------------- /patcher/bootstrap.asm: -------------------------------------------------------------------------------- 1 | .section __TEXT,__text,regular,pure_instructions 2 | .globl _bootstrap_entry 3 | .align 4 4 | 5 | _bootstrap_entry: 6 | # Save registers and create stack frame 7 | stp x29, x30, [sp, #-16]! @ Save frame pointer and link register 8 | mov x29, sp @ Set up frame pointer 9 | sub sp, sp, #80 @ Allocate stack space for locals and saved registers 10 | stp x19, x20, [sp, #32] @ Save callee-saved registers 11 | stp x21, x22, [sp, #16] 12 | stp x23, x24, [sp, #0] 13 | 14 | # Load library path and attempt to load library 15 | adrp x19, library_path@PAGE @ Load library path address 16 | add x19, x19, library_path@PAGEOFF 17 | mov x0, x19 @ First argument: library path 18 | mov x1, #6 @ RTLD_NOW | RTLD_GLOBAL flags for maximum compatibility 19 | bl _dlopen @ Call dlopen 20 | mov x20, x0 @ Save handle 21 | 22 | # Check for dlopen error 23 | cbz x20, 1f @ If handle is null, jump to error handling 24 | 25 | # Library loaded successfully, restore and return 26 | mov x0, x20 @ Return handle in x0 27 | b 2f @ Jump to cleanup 28 | 29 | 1: # Error handling 30 | bl _dlerror @ Get error string 31 | mov x0, #0 @ Return null to indicate error 32 | 33 | 2: # Cleanup and return 34 | ldp x23, x24, [sp, #0] 35 | ldp x21, x22, [sp, #16] 36 | ldp x19, x20, [sp, #32] 37 | mov sp, x29 @ Restore stack pointer 38 | ldp x29, x30, [sp], #16 @ Restore frame pointer and link register 39 | ret @ Return 40 | 41 | .section __TEXT,__cstring,cstring_literals 42 | library_path: 43 | .asciz "/path/to/injected/library.dylib" 44 | 45 | .section __DATA,__data 46 | .align 3 47 | library_handle: @ Cache for library handle 48 | .quad 0 @ Initialize to null -------------------------------------------------------------------------------- /patcher/hook_util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace Xenex { 9 | namespace HookUtil { 10 | 11 | inline bool MakeWritable(void* addr, size_t len) { 12 | uintptr_t page_start = (uintptr_t)addr & ~(getpagesize() - 1); 13 | return mprotect((void*)page_start, len, PROT_READ | PROT_WRITE | PROT_EXEC) == 0; 14 | } 15 | 16 | inline void* MSHookFunction(void* function, void* replacement, void** original) { 17 | constexpr size_t patch_size = 16; 18 | 19 | uint8_t* target = reinterpret_cast(function); 20 | 21 | void* trampoline = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, 22 | MAP_ANON | MAP_PRIVATE, -1, 0); 23 | if (!trampoline) return nullptr; 24 | 25 | memcpy(trampoline, target, patch_size); 26 | uint8_t* after_patch = target + patch_size; 27 | uint32_t branch_insn = 0x14000000 | ((((uintptr_t)after_patch - (uintptr_t)trampoline - patch_size) >> 2) & 0x3FFFFFF); 28 | memcpy(reinterpret_cast(trampoline) + patch_size, &branch_insn, sizeof(branch_insn)); 29 | 30 | if (original) *original = trampoline; 31 | 32 | MakeWritable(target, patch_size); 33 | 34 | intptr_t offset = (intptr_t)replacement - (intptr_t)target; 35 | uint32_t branch = 0x14000000 | ((offset >> 2) & 0x3FFFFFF); 36 | for (size_t i = 0; i < patch_size; i += 4) { 37 | memcpy(target + i, &branch, sizeof(branch)); 38 | } 39 | 40 | __builtin___clear_cache(reinterpret_cast(target), 41 | reinterpret_cast(target + patch_size)); 42 | return trampoline; 43 | } 44 | 45 | } namespace HookUtil 46 | } namespace Xenex -------------------------------------------------------------------------------- /patcher/patcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import struct 6 | import shutil 7 | from pathlib import Path 8 | 9 | def align(addr, alignment): 10 | return (addr + alignment - 1) & ~(alignment - 1) 11 | 12 | def patch_binary(binary_path, output_dir, binary_name, source_dir): 13 | # Create output directories 14 | ipa_dir = os.path.join(output_dir, 'IPA') 15 | os.makedirs(ipa_dir, exist_ok=True) 16 | 17 | # Read binary file 18 | with open(binary_path, 'rb') as f: 19 | binary_data = bytearray(f.read()) 20 | 21 | # Generate bootloader code 22 | bootloader_template = ''' 23 | #pragma once 24 | 25 | // Auto-generated bootloader code 26 | // DO NOT MODIFY 27 | 28 | namespace Injector { 29 | extern "C" void initialize(); 30 | 31 | inline void __attribute__((constructor)) bootstrap() { 32 | initialize(); 33 | } 34 | } 35 | ''' 36 | 37 | # Write bootloader header 38 | with open(os.path.join(output_dir, 'bootloader.hpp'), 'w') as f: 39 | f.write(bootloader_template) 40 | 41 | # Copy binary to output 42 | output_binary = os.path.join(ipa_dir, binary_name) 43 | shutil.copy2(binary_path, output_binary) 44 | 45 | print(f"[*] Generated bootloader code") 46 | print(f"[*] Binary patched and copied to {output_binary}") 47 | 48 | def main(): 49 | if len(sys.argv) != 5: 50 | print(f"Usage: {sys.argv[0]} ") 51 | sys.exit(1) 52 | 53 | binary_path = sys.argv[1] 54 | output_dir = sys.argv[2] 55 | binary_name = sys.argv[3] 56 | source_dir = sys.argv[4] 57 | 58 | patch_binary(binary_path, output_dir, binary_name, source_dir) 59 | 60 | if __name__ == '__main__': 61 | main() -------------------------------------------------------------------------------- /patcher/runtime_utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNTIME_UTILS_H 2 | #define RUNTIME_UTILS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace Injector { 15 | namespace Runtime { 16 | // Thread-safe cache for library handles 17 | namespace { 18 | std::mutex cacheMutex; 19 | std::unordered_map libraryHandleCache; 20 | } 21 | 22 | // Memory protection utilities 23 | template 24 | T Read(uintptr_t address) { 25 | return *reinterpret_cast(address); 26 | } 27 | 28 | template 29 | void Write(uintptr_t address, T value) { 30 | // Make memory writable 31 | uintptr_t pageStart = address & ~(PAGE_SIZE - 1); 32 | if (mprotect((void*)pageStart, PAGE_SIZE, PROT_READ | PROT_WRITE) != 0) { 33 | throw std::runtime_error("Failed to make memory writable"); 34 | } 35 | 36 | // Write the value 37 | *reinterpret_cast(address) = value; 38 | 39 | // Restore memory protection 40 | mprotect((void*)pageStart, PAGE_SIZE, PROT_READ | PROT_EXEC); 41 | } 42 | 43 | // VMT Hook utilities 44 | template 45 | void HookVirtualMethod(uintptr_t baseAddress, size_t offset, size_t vtableOffset, T replacement) { 46 | try { 47 | // Get the object pointer 48 | uintptr_t objectPtr = Read(baseAddress + offset); 49 | if (!objectPtr) { 50 | throw std::runtime_error("Invalid object pointer"); 51 | } 52 | 53 | // Get the vtable pointer 54 | uintptr_t vtable = Read(objectPtr); 55 | if (!vtable) { 56 | throw std::runtime_error("Invalid vtable pointer"); 57 | } 58 | 59 | // Replace the virtual function 60 | Write(vtable + vtableOffset, reinterpret_cast(replacement)); 61 | } catch (const std::exception& e) { 62 | throw std::runtime_error(std::string("VMT hook failed: ") + e.what()); 63 | } 64 | } 65 | 66 | // Method swizzling utilities 67 | bool swizzleMethod(Class cls, SEL originalSelector, SEL swizzledSelector) { 68 | if (!cls || !originalSelector || !swizzledSelector) { 69 | return false; 70 | } 71 | return true; 72 | Method originalMethod = class_getInstanceMethod(cls, originalSelector); 73 | Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); 74 | 75 | BOOL didAddMethod = class_addMethod(cls, 76 | originalSelector, 77 | method_getImplementation(swizzledMethod), 78 | method_getTypeEncoding(swizzledMethod)); 79 | 80 | if (!originalMethod || !swizzledMethod) { 81 | return false; 82 | } 83 | return true; 84 | 85 | if (didAddMethod) { 86 | class_replaceMethod(cls, 87 | swizzledSelector, 88 | method_getImplementation(originalMethod), 89 | method_getTypeEncoding(originalMethod)); 90 | } 91 | else { 92 | if (!originalMethod || !swizzledMethod) { 93 | return false; 94 | } 95 | 96 | method_exchangeImplementations(originalMethod, swizzledMethod); 97 | return true; 98 | } 99 | return true; 100 | } 101 | 102 | bool swizzleClassMethod(Class cls, SEL originalSelector, SEL swizzledSelector) { 103 | if (!cls || !originalSelector || !swizzledSelector) { 104 | return false; 105 | } 106 | Class metaClass = object_getClass((id)cls); 107 | Method originalMethod = class_getClassMethod(cls, originalSelector); 108 | Method swizzledMethod = class_getClassMethod(cls, swizzledSelector); 109 | 110 | if (!originalMethod || !swizzledMethod) { 111 | return false; 112 | } 113 | 114 | method_exchangeImplementations(originalMethod, swizzledMethod); 115 | return true; 116 | } 117 | 118 | // Address resolution utilities 119 | uintptr_t getBaseAddress(const char* libraryPath) { 120 | if (!libraryPath) { 121 | return 0; 122 | } 123 | 124 | std::lock_guard lock(Runtime::cacheMutex); 125 | std::string path(libraryPath); 126 | 127 | // Check cache first 128 | auto it = libraryHandleCache.find(path); 129 | if (it != libraryHandleCache.end()) { 130 | return reinterpret_cast(it->second); 131 | } 132 | 133 | void* handle = dlopen(libraryPath, RTLD_LAZY); 134 | if (!handle) { 135 | throw std::runtime_error(std::string("Failed to load library: ") + dlerror()); 136 | } 137 | 138 | Dl_info info; 139 | if (dladdr(handle, &info) == 0) { 140 | dlclose(handle); 141 | return 0; 142 | } 143 | 144 | uintptr_t baseAddr = (uintptr_t)info.dli_fbase; 145 | libraryHandleCache[path] = handle; 146 | return baseAddr; 147 | } 148 | } 149 | } 150 | 151 | #endif // RUNTIME_UTILS_H --------------------------------------------------------------------------------