├── .gitignore ├── Headers ├── RPCodesignHandler.h ├── RPControlHandler.h ├── RPConversionHandler.h ├── RPDirectoryScanner.h ├── RPLoadCommandsHandler.h ├── RPMachOMerger.h ├── RPMachOModifier.h ├── RPMachOParser.h ├── RPMachOThinner.h ├── RPOldABIChecker.h ├── RPPlistHandler.h ├── RPRepackHandler.h ├── RPScriptHandler.h ├── RPSpawnHandler.h ├── RPStringPatcher.h ├── RPStringScanner.h └── assembler.h ├── LICENSE ├── Makefile ├── README.md ├── WRITEUP.md ├── control ├── entitlements.plist ├── layout ├── DEBIAN │ ├── postinst │ └── postrm └── Library │ └── Application Support │ └── rootless-patcher │ ├── ConversionRuleset.json │ └── repack-rootless.sh ├── main.m └── src ├── RPCodesignHandler.m ├── RPControlHandler.m ├── RPConversionHandler.m ├── RPDirectoryScanner.m ├── RPLoadCommandsHandler.m ├── RPMachOMerger.m ├── RPMachOModifier.m ├── RPMachOParser.m ├── RPMachOThinner.m ├── RPOldABIChecker.m ├── RPPlistHandler.m ├── RPRepackHandler.m ├── RPScriptHandler.m ├── RPSpawnHandler.m ├── RPStringPatcher.m ├── RPStringScanner.m └── assembler.c /.gitignore: -------------------------------------------------------------------------------- 1 | .theos/ 2 | packages/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /Headers/RPCodesignHandler.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | 6 | @interface RPCodesignHandler : NSObject 7 | + (int)removeCodesignFromFile:(nonnull NSString *)file; 8 | + (int)addCodesignToFile:(nonnull NSString *)file; 9 | + (int)addCodesignToFile:(nonnull NSString *)file entitlementsPath:(nonnull NSString *)entitlementsPath; 10 | @end -------------------------------------------------------------------------------- /Headers/RPControlHandler.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | 4 | #import 5 | #import 6 | #import 7 | 8 | @interface RPControlHandler : NSObject 9 | + (nullable instancetype)handlerWithControlFile:(nonnull NSString *)controlFile; 10 | - (nonnull NSString *)controlFileAsString; 11 | - (void)setControlValue:(nonnull id)value forKey:(nonnull NSString *)key; 12 | - (nonnull id)controlValueForKey:(nonnull NSString *)key; 13 | 14 | + (nullable instancetype)new __attribute__((unavailable("Use +handlerWithControlFile: instead"))); 15 | - (nullable instancetype)init __attribute__((unavailable("Use +handlerWithControlFile: instead"))); 16 | @end -------------------------------------------------------------------------------- /Headers/RPConversionHandler.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | 6 | @interface RPConversionHandler : NSObject 7 | + (nullable instancetype)handlerWithConversionRuleset:(nonnull NSDictionary *)conversionRuleset; 8 | - (BOOL)shouldConvertString:(nonnull NSString *)string; 9 | - (nonnull NSString *)convertedStringForString:(nonnull NSString *)string; 10 | 11 | + (nullable instancetype)new __attribute__((unavailable("Use +handlerWithConversionRuleset: instead"))); 12 | - (nullable instancetype)init __attribute__((unavailable("Use +handlerWithConversionRuleset: instead"))); 13 | @end -------------------------------------------------------------------------------- /Headers/RPDirectoryScanner.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | #import 6 | 7 | @interface RPDirectoryScanner : NSObject 8 | + (nullable instancetype)scannerWithDirectory:(nonnull NSString *)directory; 9 | - (nullable NSArray *)machOFiles; 10 | - (nullable NSArray *)plistFiles; 11 | - (nullable NSArray *)scriptFiles; 12 | - (nullable NSString *)controlFile; 13 | 14 | + (nullable instancetype)new __attribute__((unavailable("Use +scannerWithDirectory: instead"))); 15 | - (nullable instancetype)init __attribute__((unavailable("Use +scannerWithDirectory: instead"))); 16 | @end -------------------------------------------------------------------------------- /Headers/RPLoadCommandsHandler.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | 6 | @interface RPLoadCommandsHandler : NSObject 7 | + (BOOL)handleLoadCommandsForFile:(nonnull NSString *)file; 8 | @end -------------------------------------------------------------------------------- /Headers/RPMachOMerger.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | 6 | @interface RPMachOMerger : NSObject 7 | + (int)mergeMachOsAtPaths:(nonnull NSArray *)paths outputPath:(nonnull NSString *)outputPath; 8 | @end -------------------------------------------------------------------------------- /Headers/RPMachOModifier.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | 6 | @interface RPMachOModifier : NSObject 7 | + (nullable instancetype)modifierWithFile:(nonnull NSString *)file; 8 | - (void)addSegment:(nonnull NSString *)segname section:(nonnull NSString *)sectname stringMap:(nonnull NSDictionary *)stringMap; 9 | - (void)rebaseStringsWithStringMap:(nonnull NSDictionary *)stringMap originalOffsetMap:(nonnull NSDictionary *)originalOffsetMap; 10 | - (nonnull NSData *)data; 11 | 12 | + (nullable instancetype)new __attribute__((unavailable("Use +modifierWithFile: instead"))); 13 | - (nullable instancetype)init __attribute__((unavailable("Use +modifierWithFile: instead"))); 14 | @end -------------------------------------------------------------------------------- /Headers/RPMachOParser.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | #import 6 | 7 | @interface RPMachOParser : NSObject 8 | + (nullable instancetype)parserWithHeader:(nonnull struct mach_header_64 *)header; 9 | - (nullable struct segment_command_64 *)segmentWithName:(nonnull NSString *)segname; 10 | - (nullable struct section_64 *)sectionInSegment:(nullable struct segment_command_64 *)segment withName:(nonnull NSString *)sectname; 11 | - (uint64_t)vmEnd; 12 | - (nonnull struct mach_header_64 *)header; 13 | 14 | + (nullable instancetype)new __attribute__((unavailable("Use +parserWithHeader: instead"))); 15 | - (nullable instancetype)init __attribute__((unavailable("Use +parserWithHeader: instead"))); 16 | @end -------------------------------------------------------------------------------- /Headers/RPMachOThinner.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | #import 6 | #import 7 | 8 | @interface RPMachOThinner : NSObject 9 | + (nonnull NSDictionary *)thinnedMachOsFromPaths:(nonnull NSArray *)paths; 10 | @end -------------------------------------------------------------------------------- /Headers/RPOldABIChecker.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | 6 | @interface RPOldABIChecker : NSObject 7 | + (BOOL)containsOldABI:(nonnull NSData *)machO; 8 | @end -------------------------------------------------------------------------------- /Headers/RPPlistHandler.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | #import 6 | 7 | @interface RPPlistHandler : NSObject 8 | + (nullable instancetype)handlerWithPlistFile:(nonnull NSString *)file; 9 | - (void)convertStringsUsingConversionRuleset:(nonnull NSDictionary *)conversionRuleset; 10 | - (nonnull NSDictionary *)plistContainer; 11 | 12 | + (nullable instancetype)new __attribute__((unavailable("Use +handlerWithPlistFile: instead"))); 13 | - (nullable instancetype)init __attribute__((unavailable("Use +handlerWithPlistFile: instead"))); 14 | @end -------------------------------------------------------------------------------- /Headers/RPRepackHandler.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | 6 | @interface RPRepackHandler : NSObject 7 | + (BOOL)repackFile:(nonnull NSString *)file toDirectory:(nonnull NSString *)directory; 8 | @end -------------------------------------------------------------------------------- /Headers/RPScriptHandler.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | 6 | @interface RPScriptHandler : NSObject 7 | + (nullable instancetype)handlerWithScriptFile:(nonnull NSString *)scriptFile; 8 | - (void)convertStringsUsingConversionRuleset:(nonnull NSDictionary *)conversionRuleset; 9 | - (nonnull NSString *)fileContents; 10 | 11 | + (nullable instancetype)new __attribute__((unavailable("Use +handlerWithScriptFile: instead"))); 12 | - (nullable instancetype)init __attribute__((unavailable("Use +handlerWithScriptFile: instead"))); 13 | @end -------------------------------------------------------------------------------- /Headers/RPSpawnHandler.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | 6 | @interface RPSpawnHandler : NSObject 7 | + (int)spawnWithArguments:(nonnull NSArray *)arguments; 8 | + (int)spawnWithArguments:(nonnull NSArray *)arguments stdoutPath:(nullable NSString *)stdoutPath stderrPath:(nullable NSString *)stderrPath; 9 | @end -------------------------------------------------------------------------------- /Headers/RPStringPatcher.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | 6 | @interface RPStringPatcher : NSObject 7 | + (nullable instancetype)patcherWithData:(nonnull NSData *)data replacementOffsetMap:(nonnull NSDictionary *)offsetMap originalOffsetMap:(nonnull NSDictionary *)originalOffsetMap; 8 | - (void)patchString:(nonnull NSString *)string toString:(nonnull NSString *)string ; 9 | - (nonnull NSData *)data; 10 | 11 | + (nullable instancetype)new __attribute__((unavailable("Use +patcherWithData: instead"))); 12 | - (nullable instancetype)init __attribute__((unavailable("Use +patcherWithData: instead"))); 13 | @end -------------------------------------------------------------------------------- /Headers/RPStringScanner.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | #import 6 | 7 | @interface RPStringScanner : NSObject 8 | + (nullable instancetype)scannerWithFile:(nonnull NSString *)file conversionRuleset:(nonnull NSDictionary *)conversionRuleset; 9 | - (nonnull NSDictionary *)stringMap; 10 | - (nonnull NSDictionary *)offsetMap; 11 | 12 | + (nullable instancetype)new __attribute__((unavailable("Use +stringScannerForFile: instead"))); 13 | - (nullable instancetype)init __attribute__((unavailable("Use +stringScannerForFile: instead"))); 14 | @end -------------------------------------------------------------------------------- /Headers/assembler.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #ifndef ASSEMBLER_H 4 | #define ASSEMBLER_H 5 | 6 | #import 7 | 8 | #define INSTRUCTION_SIZE 0x4 9 | 10 | #define ADR_MAX_RANGE (1024 * 1024) 11 | 12 | #define REGISTER_MASK 0x1F 13 | #define PAGE_OFFSET_MASK 0xFFF 14 | #define ARM_PAGE_SIZE 0x1000 15 | 16 | #define IMAGE_BASE 0x100000000 17 | 18 | #define MOV_OPCODE 0b11010010100000000000000000000000 19 | #define NOP_OPCODE 0b11010101000000110010000000011111 20 | #define ADRP_OPCODE 0b10010000000000000000000000000000 21 | #define ADD_OPCODE 0b10010001000000000000000000000000 22 | #define ADR_OPCODE 0b00010000000000000000000000000000 23 | 24 | #define MOV_MASK 0xD2800000 25 | #define ADRP_MASK 0x9F000000 26 | #define ADD_MASK 0xFF000000 27 | #define ADR_MASK 0x9F000000 28 | 29 | #define get_adrp_value(instruction, i) (((int64_t)(((instruction & 0x60000000) >> 18) | ((instruction & 0xffffe0) << 8)) << 1) + (i & ~PAGE_OFFSET_MASK)) 30 | 31 | #define get_adr_value(instruction, i) (((int64_t)(((instruction & 0x60000000) >> 18) | ((instruction & 0xffffe0) << 8)) >> 11) + i) 32 | 33 | #define get_add_register(instruction) ((instruction >> 5) & REGISTER_MASK) 34 | #define get_add_value(instruction) ((instruction >> 10) & PAGE_OFFSET_MASK) 35 | #define get_add_shift(instruction) ((instruction >> 22) & 3) 36 | 37 | #define get_mov_value(instruction) ((instruction >> 5) & 0xFFFF) 38 | #define get_mov_register(instruction) (instruction & REGISTER_MASK) 39 | 40 | uint32_t generate_adrp(int destination_register, int immediate); 41 | uint32_t generate_adr(int destination_register, int immediate); 42 | uint32_t generate_add(int destination_register, int source_register, int immediate, int shift); 43 | uint32_t generate_mov(int destination_register, int immediate); 44 | 45 | #endif // ASSEMBLER_H -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Nightwind 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifeq ($(TARGET_OS), macos) 2 | 3 | TARGET := macosx:clang:11.3:11.0 4 | 5 | else ifeq ($(TARGET_OS), ios) 6 | 7 | TARGET := iphone:clang:latest:15.0 8 | ARCHS = arm64 9 | 10 | else 11 | 12 | $(error TARGET_OS is not defined. Please specify either "ios" or "macos") 13 | 14 | endif 15 | 16 | include $(THEOS)/makefiles/common.mk 17 | 18 | TOOL_NAME = rootless-patcher 19 | 20 | rootless-patcher_FILES = $(wildcard *.m */*.m */*.c) 21 | rootless-patcher_CFLAGS = -fobjc-arc 22 | 23 | ifeq ($(TARGET_OS), ios) 24 | rootless-patcher_CODESIGN_FLAGS = -Sentitlements.plist 25 | endif 26 | 27 | rootless-patcher_INSTALL_PATH = /usr/local/bin 28 | 29 | include $(THEOS_MAKE_PATH)/tool.mk -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rootless-patcher 2 | A CLI rootful to rootless patcher. Utilizes a technique that does not require Xina-style symlinks. 3 | ### How to download 4 | - Find the latest release in the [Releases](https://github.com/NightwindDev/rootless-patcher/releases) tab 5 | - Download the correct file for your device configuration: 6 | - iOS rootless: `com.nightwind.rootless-patcher__iphoneos-arm64.deb` 7 | - iOS rootful: `com.nightwind.rootless-patcher__iphoneos-arm.deb` 8 | - macOS: `com.nightwind.rootless-patcher_.pkg` 9 | - Install via a package manager on iOS or double click the `.pkg` file on macOS to install. 10 | ### How to use 11 | - iOS: 12 | - Open a terminal application and run `rootless-patcher `. This method is faster. 13 | - Locate the .deb in Filza and run the `rootless-patcher` script on it by long pressing on the file, pressing `Scripts`, and finding `rootless-patcher`. This method is slower. Please note that this method only works with the jailbroken version of Filza and does **not** work with the version for TrollStore. 14 | - macOS: 15 | - Open a terminal application and run `rootless-patcher `. 16 | ### How to build manually 17 | - Make sure [Theos](https://theos.dev) is installed 18 | - Clone the repo: `git clone https://github.com/NightwindDev/rootless-patcher.git` 19 | - `cd rootless-patcher` 20 | - Build via `make`: 21 | - iOS rootless: `make clean package FINALPACKAGE=1 THEOS_PACKAGE_SCHEME=rootless TARGET_OS=ios` 22 | - iOS rootful: `make clean package FINALPACKAGE=1 TARGET_OS=ios` 23 | - macOS: `make clean package FINALPACKAGE=1 TARGET_OS=macos` 24 | #### Writeup 25 | In order to document how this project works and the challenges that came along with developing it, I've written a [writeup](WRITEUP.md). 26 | #### License 27 | This project is licensed under [MIT](LICENSE). 28 | ###### Copyright (c) 2024 Nightwind -------------------------------------------------------------------------------- /WRITEUP.md: -------------------------------------------------------------------------------- 1 | # Writeup 2 | ## How does `rootless-patcher` work? 3 | - Takes the inputted rootful .deb. 4 | - Repacks rootful .deb into a temporary directory, changing the jbroot structure of the .deb to be positioned in the `/var/jb` folder. 5 | - Scans through all Mach-O, `.plist`, and script files (`postinst`, `extrainst`, regular script files, etc.). 6 | - Uses the [`ConversionRuleset.json`](layout/Library/Application%20Support/rootless-patcher/ConversionRuleset.json) file to understand what strings should get converted to what. 7 | - FAT Mach-O files are thinned into their single-slice counterparts. 8 | - Mach-O files' entitlements get saved and they proceed to be unsigned in order to do modifications. 9 | - The strings are scanned from `__TEXT,__cstring` and `__DATA,__data` sections of the Mach-O files. 10 | - A new `__PATCH_ROOTLESS` segment is added to the Mach-O before the `__LINKEDIT` segment (and `__LLVM` segment if the Mach-O contains it). This segment contains its own `__cstring` section. 11 | - The string xrefs in the Mach-O's are patched to point to the new strings in the `__PATCH_ROOTLESS,__cstring` section. 12 | - There are four different categories of strings which need to be handled: 13 | - `CFString`s (located in `__DATA,__cfstring`/`__DATA_CONST,__cfstring`) 14 | - Global C strings (located in `__DATA,__data`) 15 | - Regular C strings (located in `__TEXT,__cstring`) 16 | - Certain Swift strings (more detail on these later) 17 | - If any binary is compiled with the old arm64e ABI, `rootless-patcher` takes note of that for later. 18 | - Make sure to keep track of the original file permissions, and merge the patched thinned Mach-O's into a FAT file, overwriting the original. 19 | - The load commands are fixed via the [`repack-rootless.sh`](layout/Library/Application%20Support/rootless-patcher/repack-rootless.sh) script to correspond to their rootless ones. 20 | - The patched Mach-O is then resigned and its old entitlements are applied if needed. 21 | - The `control` file gets modified, keeping its old permissions in memory. 22 | - The old ABI dependency gets added to the `control` file if the previous search for a binary compiled with the old arm64e ABI found one. 23 | - The `Icon:` path gets converted to the rootless one. 24 | - The `Architecture:` gets changed to `iphoneos-arm64` instead of the rootful one - `iphoneos-arm`. 25 | - The package ID and version get saved for later use. 26 | - The patched `control` file gets saved and its permissions are restored to the original ones. 27 | - All of the relevant `.plist` files have their paths converted. Same steps as before, the file permissions are stored, the file's paths get converted, the file is saved and its permissions are restored. 28 | - All of the non-Mach-O script files have their paths converted. Same steps as before, the file permissions are stored, the file's paths get converted, the file is saved and its permissions are restored. 29 | - Some special handling is done for certain tweaks (e.g. XenHTML). These tweaks have an improper `DEBIAN` directory structure, which causes `dpkg-deb` to not be able to repack th .deb correctly. `rootless-patcher` fixes this structure to correspond to `dpkg-deb`'s format. 30 | - `dpkg-deb` is used to repack the .deb file with the converted name. 31 | - The temporary working directory is deleted. 32 | ## Development challenges 33 | I wanted to list some of the challenges that came with developing `rootless-patcher`. There aren't many projects which edit Mach-O's to the level of `rootless-patcher`, so I discovered some things that people hadn't stumped onto before. 34 | ### The `__LLVM` segment 35 | The [`__LLVM` segment](https://www.graalvm.org/latest/reference-manual/llvm/Compiling/) on certain Mach-O's proved to cause a challenge when attempting to add the `__PATCH_ROOTLESS` segment. Initially, the approach that I used to add the segment was to add it right before the `__LINKEDIT` segment. The original approach was based on the ["Adding a segment to an existing macOS Mach-O binary"](https://alexomara.com/blog/adding-a-segment-to-an-existing-macos-mach-o-binary/) writeup by Alexander O'Mara, which was an incredibly helpful starting point. Most Mach-O's do not contain the `__LLVM` bitcode segment. The issue when adding a custom segment is that the `__LLVM` segment needs to be placed directly before the `__LINKEDIT` segment. Therefore, it needed to be handled separately. You can see this special case in the [MachOModifier.m](src/RPMachOModifier.m) file. Big thanks to [Leptos](https://github.com/leptos-null) and [objc](https://github.com/EthanArbuckle) in figuring out how to handle this special case correctly. 36 | ### Rebasing string xrefs 37 | This is the core of the entire patcher. First of all, huge thanks to [staturnz](https://github.com/staturnzz) for extraordinary amount of help in this area. This project would not be possible without him. 38 | While changing the references for `CFString`s is relatively straightforward (see [`-[RPStringPatcher _patchCFString:replacementAddress:newLength:]`](src/RPStringPatcher.m#L88-L100)), other types of strings are more difficult to handle. Global C string references are somewhat simple to patch as well. Regular C string references are more complicated to rebase, however. In order to correctly rebase them, a patchfinder needs to be written which would keep track of the references to the C strings. These strings can be referenced through the `ADRP` + `ADD`, and `ADR` ARM64 assembly instructions. Each one of these needs to be correctly handled. 39 | #### Max range of the `ADR` instruction 40 | The `ADR` instruction has a range of +/- 1MB from the current program counter (`PC`). This is why `ADRP` + `ADD` is used, which allows a +/- 4GB range from the `PC`, plus the page offset. There is a special case when dealing with `ADR`, however. The original implementation may work for normal string xrefs, however since the `__PATCH_ROOTLESS,__cstring` section with the converted strings may physically be farther from the `ADR`, the patched xrefs may fail. There are two options to remedy this: 41 | - The first option involves checking the next instruction. If it is a `NOP`, we can freely overwrite it. The `NOP` is really useful because `ADRP` + `ADD` takes two instructions, which can be put in the space of the old `ADR` and `NOP` instructions — [see code](src/RPStringPatcher.m#L163-L180). 42 | - Another option involves more code and overhead. If we don't have space for `ADRP` + `ADD`, we may be able to use a trampoline. Essentially, the idea is this: we have a small function which we define, which can allow us to properly reference the converted string in `__PATCH_ROOTLESS,__cstring`. The idea is that in the space that was previously occupied by the `ADR`, we may be able to branch to the function we defined, run our code there, and then return back to regular execution. 43 | #### Swift strings 44 | For optimization purposes, Swift strings have some special behavior which makes it difficult to statically patch their xrefs. For short strings (<=30 characters?), the size is stored statically in `__TEXT,__text`, somewhere near an xref to the string. This causes an issue, let's say we have a string: `/Library/MobileSubstrate/` (25 characters long) and we want to patch it to `/var/jb/Library/MobileSubstrate/` (32 characters long). The old length of 25 would be stored in the binary, causing the patched string to be truncated to `/var/jb/Library/MobileSubs` when referenced in the binary. This behavior is not well-defined and it is not trivial to patch the length. The patcher implements a [`-[RPStringPatcher _patchSwiftInstructionForLengthAt:oldLength:newLength:]`](src/RPStringPatcher.m#L187-L202) method which aims to fix one of the special cases, however there are many more, some of which are extremely difficult to patch due to there not being enough free space in the assembly. In these cases, a solution which would convert the strings at runtime may cause less headaches and overhead. 45 | ### Improper FAT files 46 | FAT files with multiple slices of the same `cpusubtype & ~CPU_SUBTYPE_MASK` are considered invalid. XenHTML has this structure for some of its Mach-O files in order to support both the old and new arm64e ABIs. However, this causes Mach-O tools to fail because these types of FAT files are considered improper and are not supported. `rootless-patcher` remedies this ([see code](main.m#L130-L134)) by adhering to the regular FAT format. 47 | ### Improper `DEBIAN` directory format 48 | XenHTML also has an improper `DEBIAN` directory format, which was discovered during testing. `rootless-patcher` handles this by fixing the format ([see code](main.m#L293-L325)) for `dpkg-deb` to properly repack it. 49 | ### Limitations 50 | - Binaries with obfuscated strings will not be patched properly. In order to convert the strings, `rootless-patcher` needs to have the unobfuscated strings so it can convert them and rebase the xrefs. 51 | - There may be some false positives. With [ConversionRuleset.json](layout/Library/Application%20Support/rootless-patcher/ConversionRuleset.json), I've done my best to rule out most false positives, but some may slip past. 52 | - Swift strings may be patched or interpreted incorrectly, as explained previously. 53 | ### Credits 54 | - Big thanks to [staturnz](https://github.com/staturnzz) for help in the [RPStringPatcher.m](src/RPStringPatcher.m) part of the project. Without him, this project would not have been possible. 55 | - Thanks to [opa334](https://github.com/opa334) for the initial idea on how to implement the patcher. 56 | - Thanks to [Leptos](https://github.com/leptos-null) for reviewing the code to help make it as reliable as possible. 57 | - Thanks to [tuancc](https://github.com/roothider) for help in regard to some of the technicalities. 58 | - Thanks to the testers who tested the patcher with many tweaks, helping round out some of the edge cases! -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | Package: com.nightwind.rootless-patcher 2 | Name: rootless-patcher 3 | Version: 0.0.1 4 | Architecture: iphoneos-arm 5 | Description: A CLI rootful to rootless patcher. 6 | Utilizes a technique that does not require Xina-style symlinks. 7 | Maintainer: Nightwind 8 | Author: Nightwind and staturnz 9 | Section: System 10 | Tag: role::hacker 11 | Depends: odcctools, ldid 12 | -------------------------------------------------------------------------------- /entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | platform-application 5 | 6 | com.apple.private.security.no-container 7 | 8 | com.apple.private.security.container-required 9 | 10 | com.apple.private.security.storage.AppDataContainers 11 | 12 | com.apple.private.spawn-subsystem-root 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /layout/DEBIAN/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -d /var/mobile/Library/Filza/scripts ]; then 3 | echo "Adding rootless-patcher.script to Filza's scripts directory." 4 | echo "rootless-patcher \"\$1\"" > /var/mobile/Library/Filza/scripts/rootless-patcher.script 5 | fi -------------------------------------------------------------------------------- /layout/DEBIAN/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -f /var/mobile/Library/Filza/scripts/rootless-patcher.script ]; then 3 | echo "Removing rootless-patcher.script from Filza's scripts directory." 4 | rm /var/mobile/Library/Filza/scripts/rootless-patcher.script 5 | fi -------------------------------------------------------------------------------- /layout/Library/Application Support/rootless-patcher/ConversionRuleset.json: -------------------------------------------------------------------------------- 1 | { 2 | "Blacklist": [ 3 | "/System", 4 | "/usr/lib/libobjc", 5 | "/usr/lib/libc++", 6 | "/usr/lib/libstdc++", 7 | "/usr/lib/dyld", 8 | "/usr/lib/libMobileGestalt.dylib", 9 | "/usr/lib/system", 10 | "/usr/lib/swift", 11 | "/usr/lib/libswift", 12 | "/usr/lib/libsql", 13 | "/usr/lib/libz", 14 | "/var/jb", 15 | "/var/mobile/Library/Preferences/com.apple.", 16 | "/var/mobile/Library/Preferences/systemgroup.com.apple.", 17 | "/var/mobile/Library/Preferences/group.com.apple.", 18 | "/var/mobile/Library/Preferences/.GlobalPreferences.plist", 19 | "/var/mobile/Library/Preferences/.GlobalPreferences_m.plist", 20 | "/var/mobile/Library/Preferences/bluetoothaudiod.plist", 21 | "/var/mobile/Library/Preferences/NetworkInterfaces.plist", 22 | "/var/mobile/Library/Preferences/OSThermalStatus.plist", 23 | "/var/mobile/Library/Preferences/preferences.plist", 24 | "/var/mobile/Library/Preferences/osanalyticshelper.plist", 25 | "/var/mobile/Library/Preferences/UserEventAgent.plist", 26 | "/var/mobile/Library/Preferences/wifid.plist", 27 | "/var/mobile/Library/Preferences/dprivacyd.plist", 28 | "/var/mobile/Library/Preferences/silhouette.plist", 29 | "/var/mobile/Library/Preferences/nfcd.plist", 30 | "/var/mobile/Library/Preferences/kNPProgressTrackerDomain.plist", 31 | "/var/mobile/Library/Preferences/siriknowledged.plist", 32 | "/var/mobile/Library/Preferences/UITextInputContextIdentifiers.plist", 33 | "/var/mobile/Library/Preferences/mobile_storage_proxy.plist", 34 | "/var/mobile/Library/Preferences/splashboardd.plist", 35 | "/var/mobile/Library/Preferences/mobile_installation_proxy.plist", 36 | "/var/mobile/Library/Preferences/languageassetd.plist", 37 | "/var/mobile/Library/Preferences/ptpcamerad.plist", 38 | "/var/mobile/Library/Preferences/com.google.gmp.measurement.monitor.plist", 39 | "/var/mobile/Library/Preferences/com.google.gmp.measurement.plist", 40 | "/var/mobile/Library/SpringBoard", 41 | "/var/mobile/Library/WebClips", 42 | "/Library/Wallpaper", 43 | "/Library/Application Support/AggregateDictionary", 44 | "/var/mobile/Library/Caches/com.apple.", 45 | "/var/checkra1n.dmg", 46 | "/private/etc/apt/undecimus", 47 | "/var/Liy/.procursus_strapped", 48 | "/Library/Ringtones", 49 | "://" 50 | ], 51 | "SpecialCases": { 52 | "private/var/": "var/", 53 | "var/tmp": "tmp", 54 | "~": "~" 55 | } 56 | } -------------------------------------------------------------------------------- /layout/Library/Application Support/rootless-patcher/repack-rootless.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | FILE="${1}" 6 | WORKING_DIR="$(dirname "${FILE}")" 7 | 8 | echo "[+] Script handling file: ${FILE}" 9 | 10 | # Fakesign in order to be able to fix load commands 11 | ldid -S "${FILE}" 12 | 13 | LOAD_COMMANDS="$(otool -L "${FILE}")" 14 | INSTALL_NAME="$(otool -D "${FILE}" | grep -v -e ":$" -e "^Archive :" | head -n1)" 15 | 16 | LIB_CACHE="$(echo "${LOAD_COMMANDS}" | tail -n +2 | grep /usr/lib/'[^/]'\*.dylib | cut -d' ' -f1 | tr -d "[:blank:]")" 17 | LIB_CACHE="${LIB_CACHE}$(echo "${LOAD_COMMANDS}" | tail -n +2 | grep /usr/local/lib/'[^/]'\*.dylib | cut -d' ' -f1 | tr -d "[:blank:]")" 18 | 19 | if [ -n "${INSTALL_NAME}" ]; then 20 | install_name_tool -id @rpath/"$(basename "${INSTALL_NAME}")" "${FILE}" > /dev/null 2>&1 21 | fi 22 | 23 | if echo "${LOAD_COMMANDS}" | grep -q CydiaSubstrate; then 24 | install_name_tool -change /Library/Frameworks/CydiaSubstrate.framework/CydiaSubstrate @rpath/libsubstrate.dylib "${FILE}" > /dev/null 2>&1 25 | fi 26 | 27 | if echo "${LOAD_COMMANDS}" | grep -q CepheiPrefs.framework; then 28 | install_name_tool -change /usr/lib/CepheiPrefs.framework/CepheiPrefs @rpath/CepheiPrefs.framework/CepheiPrefs "${FILE}" > /dev/null 2>&1 29 | fi 30 | 31 | if echo "${LOAD_COMMANDS}" | grep -q Cephei.framework; then 32 | install_name_tool -change /usr/lib/Cephei.framework/Cephei @rpath/Cephei.framework/Cephei "${FILE}" > /dev/null 2>&1 33 | fi 34 | 35 | echo "${LOAD_COMMANDS}" | grep -F "/Library/PreferenceBundles" | grep -E '/[^/]*/[^/]*\.bundle' | cut -d' ' -f1 | cut -d':' -f1 | while read -r BUNDLE_PATH; do 36 | BUNDLE_NAME=$(basename "${BUNDLE_PATH}") 37 | install_name_tool -change "${BUNDLE_PATH}" "@rpath/${BUNDLE_NAME}.bundle/${BUNDLE_NAME}" "${FILE}" > /dev/null 2>&1 38 | done 39 | 40 | echo "${LOAD_COMMANDS}" | grep -F "/Library/MobileSubstrate/DynamicLibraries" | grep -E '/[^/]*' | cut -d' ' -f1 | cut -d':' -f1 | while read -r DYLIB_PATH; do 41 | DYLIB_NAME=$(basename "${DYLIB_PATH}") 42 | install_name_tool -change "${DYLIB_PATH}" "@rpath/${DYLIB_NAME}" "${FILE}" > /dev/null 2>&1 43 | done 44 | 45 | echo "${LOAD_COMMANDS}" | grep -F "/Library/Frameworks" | grep -E '/[^/]*/[^/]*\.framework' | grep -v "/System/Library/Frameworks" | cut -d' ' -f1 | cut -d':' -f1 | while read -r FRAMEWORK_PATH; do 46 | FRAMEWORK_NAME=$(basename "${FRAMEWORK_PATH}") 47 | install_name_tool -change "${FRAMEWORK_PATH}" "@rpath/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" "${FILE}" > /dev/null 2>&1 48 | done 49 | 50 | echo "${LIB_CACHE}" | while read LINE; do 51 | install_name_tool -change "${LINE}" @rpath/"$(basename "${LINE}")" "${FILE}" > /dev/null 2>&1 52 | done 53 | 54 | install_name_tool -add_rpath "/usr/local/lib" "${FILE}" > /dev/null 2>&1 55 | install_name_tool -add_rpath "/usr/lib" "${FILE}" > /dev/null 2>&1 56 | install_name_tool -add_rpath "/var/jb/usr/lib" "${FILE}" > /dev/null 2>&1 57 | install_name_tool -add_rpath "/var/jb/usr/local/lib" "${FILE}" > /dev/null 2>&1 58 | install_name_tool -add_rpath "/var/jb/Library/Frameworks" "${FILE}" > /dev/null 2>&1 59 | install_name_tool -add_rpath "/var/jb/Library/PreferenceBundles" "${FILE}" > /dev/null 2>&1 60 | install_name_tool -add_rpath "/var/jb/Library/MobileSubstrate/DynamicLibraries" "${FILE}" > /dev/null 2>&1 61 | 62 | echo "[+] Success!" -------------------------------------------------------------------------------- /main.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | #import 6 | #import "Headers/RPRepackHandler.h" 7 | #import "Headers/RPLoadCommandsHandler.h" 8 | #import "Headers/RPDirectoryScanner.h" 9 | #import "Headers/RPMachOThinner.h" 10 | #import "Headers/RPStringScanner.h" 11 | #import "Headers/RPMachOModifier.h" 12 | #import "Headers/RPOldABIChecker.h" 13 | #import "Headers/RPPlistHandler.h" 14 | #import "Headers/RPConversionHandler.h" 15 | #import "Headers/RPScriptHandler.h" 16 | #import "Headers/RPControlHandler.h" 17 | #import "Headers/RPCodesignHandler.h" 18 | #import "Headers/RPMachOMerger.h" 19 | #import "Headers/RPSpawnHandler.h" 20 | 21 | int main(int argc, char *argv[]) { 22 | @autoreleasepool { 23 | if (argc != 2) { 24 | fprintf(stdout, "\n"); 25 | fprintf(stdout, "Usage:\n"); 26 | fprintf(stdout, "rootless-patcher \n"); 27 | fprintf(stdout, "\n"); 28 | 29 | return EXIT_FAILURE; 30 | } 31 | 32 | fprintf(stdout, "\n[+] Starting rootless-patcher...\n\n"); 33 | 34 | NSError *error = nil; 35 | 36 | NSString *const temporaryDirectory = [NSTemporaryDirectory() stringByAppendingPathComponent:@"rootless-patcher"]; 37 | NSFileManager *const fileManager = [NSFileManager defaultManager]; 38 | 39 | BOOL isDirectory; 40 | if (![fileManager fileExistsAtPath:temporaryDirectory isDirectory:&isDirectory] || !isDirectory) { 41 | const BOOL tempDirectoryCreateSuccess = [fileManager createDirectoryAtPath:temporaryDirectory withIntermediateDirectories:YES attributes:nil error:&error]; 42 | if (!tempDirectoryCreateSuccess) { 43 | fprintf(stderr, "[-] Failed to temporary directory at %s. Error: %s\n", temporaryDirectory.fileSystemRepresentation, error.localizedDescription.UTF8String); 44 | return EXIT_FAILURE; 45 | } 46 | } 47 | 48 | isDirectory = NO; 49 | NSString *const debPath = [NSString stringWithUTF8String:argv[1]]; 50 | if (!debPath || ![fileManager fileExistsAtPath:debPath isDirectory:&isDirectory] || isDirectory) { 51 | fprintf(stderr, "[-] Cannot find file at path: %s\n", debPath.fileSystemRepresentation); 52 | return EXIT_FAILURE; 53 | } 54 | 55 | NSString *const patchWorkingDirectory = [temporaryDirectory stringByAppendingPathComponent:[[debPath lastPathComponent] stringByDeletingPathExtension]]; 56 | const BOOL repackSuccess = [RPRepackHandler repackFile:debPath toDirectory:patchWorkingDirectory]; 57 | if (!repackSuccess) { 58 | fprintf(stderr, "[-] Failed to repack .deb.\n"); 59 | return EXIT_FAILURE; 60 | } 61 | 62 | RPDirectoryScanner *const directoryScanner = [RPDirectoryScanner scannerWithDirectory:patchWorkingDirectory]; 63 | NSArray *const machOFiles = [directoryScanner machOFiles]; 64 | NSArray *const plistFiles = [directoryScanner plistFiles]; 65 | NSArray *const scriptFiles = [directoryScanner scriptFiles]; 66 | NSString *const controlFile = [directoryScanner controlFile]; 67 | 68 | NSDictionary *const allThinnedMachOs = [RPMachOThinner thinnedMachOsFromPaths:machOFiles]; 69 | 70 | NSString *const conversionRulesetPath = ROOT_PATH_NS(@"/Library/Application Support/rootless-patcher/ConversionRuleset.json"); 71 | NSData *const conversionRulesetData = [NSData dataWithContentsOfFile:conversionRulesetPath]; 72 | 73 | error = nil; 74 | NSDictionary *const conversionRuleset = [NSJSONSerialization JSONObjectWithData:conversionRulesetData options:kNilOptions error:&error]; 75 | if (!conversionRuleset) { 76 | fprintf(stderr, "[-] Could not find ConversionRuleset.json at path: %s. Error: %s\n", conversionRulesetPath.fileSystemRepresentation, error.localizedDescription.UTF8String); 77 | return EXIT_FAILURE; 78 | } 79 | 80 | fprintf(stdout, "[+] Starting string conversion portion...\n"); 81 | 82 | BOOL containsOldABI = NO; 83 | 84 | error = nil; 85 | for (NSString *fatMachO in allThinnedMachOs) { 86 | NSString *const entitlementsPath = [[fatMachO stringByDeletingPathExtension] stringByAppendingString:@"_ents.plist"]; 87 | 88 | const int entitlementsSuccess = [RPSpawnHandler spawnWithArguments:@[ 89 | @"ldid", 90 | @"-e", 91 | fatMachO 92 | ] stdoutPath:entitlementsPath stderrPath:nil]; 93 | 94 | if (entitlementsSuccess != 0) { 95 | fprintf(stderr, "[-] Failed to get entitlements of path: %s\n", fatMachO.fileSystemRepresentation); 96 | } 97 | 98 | const BOOL hasEntitlements = [[NSString stringWithContentsOfFile:entitlementsPath encoding:NSUTF8StringEncoding error:nil] length] > 0; 99 | 100 | NSArray *const thinnedMachOs = [allThinnedMachOs objectForKey:fatMachO]; 101 | 102 | for (NSString *file in thinnedMachOs) { 103 | [RPCodesignHandler removeCodesignFromFile:file]; 104 | 105 | RPStringScanner *const stringScanner = [RPStringScanner scannerWithFile:file conversionRuleset:conversionRuleset]; 106 | NSDictionary *const stringMap = [stringScanner stringMap]; 107 | 108 | RPMachOModifier *const modifier = [RPMachOModifier modifierWithFile:file]; 109 | if ([stringMap count] > 0) { 110 | NSDictionary *const offsetMap = [stringScanner offsetMap]; 111 | [modifier addSegment:@"__PATCH_ROOTLESS" section:@"__cstring" stringMap:stringMap]; 112 | [modifier rebaseStringsWithStringMap:stringMap originalOffsetMap:offsetMap]; 113 | } 114 | 115 | NSData *const data = [modifier data]; 116 | [data writeToFile:file options:NSDataWritingAtomic error:nil]; 117 | 118 | if (!containsOldABI && [RPOldABIChecker containsOldABI:data]) { 119 | containsOldABI = YES; 120 | } 121 | } 122 | 123 | error = nil; 124 | NSDictionary *const fileAttributes = [fileManager attributesOfItemAtPath:fatMachO error:&error]; 125 | if (!fileAttributes) { 126 | fprintf(stderr, "[-] Failed to get attributes for file at path: %s. Error: %s\n", fatMachO.fileSystemRepresentation, error.localizedDescription.UTF8String); 127 | break; 128 | } 129 | 130 | NSMutableDictionary *const uniqueThinnedMachOs = [NSMutableDictionary dictionary]; 131 | for (NSString *thinnedMachO in thinnedMachOs) { 132 | const struct mach_header_64 *header = (struct mach_header_64 *)[[NSData dataWithContentsOfFile:thinnedMachO] bytes]; 133 | [uniqueThinnedMachOs setObject:thinnedMachO forKey:@(header->cpusubtype & ~CPU_SUBTYPE_MASK)]; 134 | } 135 | 136 | const BOOL machOMergeStatus = [RPMachOMerger mergeMachOsAtPaths:[uniqueThinnedMachOs allValues] outputPath:fatMachO]; 137 | if (machOMergeStatus != 0) { 138 | fprintf(stderr, "[-] Failed to merge Mach-O's: %s\n", uniqueThinnedMachOs.description.UTF8String); 139 | break; 140 | } 141 | 142 | error = nil; 143 | const BOOL attributeSetSuccess = [fileManager setAttributes:fileAttributes ofItemAtPath:fatMachO error:&error]; 144 | if (!attributeSetSuccess) { 145 | fprintf(stderr, "[-] Failed to set attributes for file at path: %s. Error: %s\n", fatMachO.fileSystemRepresentation, error.localizedDescription.UTF8String); 146 | break; 147 | } 148 | 149 | fprintf(stdout, "[+] Checking if %s has entitlements... %s\n", fatMachO.fileSystemRepresentation, hasEntitlements ? "YES" : "NO"); 150 | 151 | const BOOL handleScript = [RPLoadCommandsHandler handleLoadCommandsForFile:fatMachO]; 152 | if (!handleScript) { 153 | fprintf(stderr, "[-] Failed to handle script for file: %s\n", fatMachO.fileSystemRepresentation); 154 | break; 155 | } 156 | 157 | const int addCodesignStatus = hasEntitlements ? [RPCodesignHandler addCodesignToFile:fatMachO entitlementsPath:entitlementsPath] : [RPCodesignHandler addCodesignToFile:fatMachO]; 158 | if (addCodesignStatus != 0) { 159 | fprintf(stderr, "[-] Failed to add code signature to file at path: %s\n", fatMachO.fileSystemRepresentation); 160 | break; 161 | } 162 | 163 | for (NSString *file in thinnedMachOs) { 164 | error = nil; 165 | const BOOL removeFileSuccess = [fileManager removeItemAtPath:file error:&error]; 166 | if (!removeFileSuccess) { 167 | fprintf(stderr, "[-] Failed to remove file at path: %s\n", file.fileSystemRepresentation); 168 | break; 169 | } 170 | } 171 | 172 | const BOOL removeEntitlementsFileSuccess = [fileManager removeItemAtPath:entitlementsPath error:&error]; 173 | if (!removeEntitlementsFileSuccess) { 174 | fprintf(stderr, "[-] Failed to remove file at path: %s\n", entitlementsPath.fileSystemRepresentation); 175 | break; 176 | } 177 | } 178 | 179 | fprintf(stdout, "[+] Finishing string conversion portion...\n"); 180 | 181 | fprintf(stdout, "[+] Contains Old ABI - %s\n", containsOldABI ? "YES" : "NO"); 182 | 183 | fprintf(stdout, "[+] Starting control file portion...\n"); 184 | 185 | error = nil; 186 | NSDictionary *const fileAttributes = [fileManager attributesOfItemAtPath:controlFile error:&error]; 187 | if (!fileAttributes) { 188 | fprintf(stderr, "[-] Failed to get file attributes for control file: %s. Error: %s\n", controlFile.fileSystemRepresentation, error.localizedDescription.UTF8String); 189 | return EXIT_FAILURE; 190 | } 191 | 192 | RPControlHandler *const controlHandler = [RPControlHandler handlerWithControlFile:controlFile]; 193 | if (containsOldABI) { 194 | NSMutableArray *const dependencies = [controlHandler controlValueForKey:@"Depends"]; 195 | if ([dependencies isKindOfClass:[NSArray class]]) { 196 | [dependencies addObject:@"cy+cpu.arm64v8 | oldabi-xina | oldabi"]; 197 | } else if ([dependencies isKindOfClass:[NSString class]]) { 198 | [controlHandler setControlValue:@[dependencies, @"cy+cpu.arm64v8 | oldabi-xina | oldabi"] forKey:@"Depends"]; 199 | } 200 | } 201 | 202 | NSString *const packageIconPath = [controlHandler controlValueForKey:@"Icon"]; 203 | if (packageIconPath) { 204 | RPConversionHandler *const conversionHandler = [RPConversionHandler handlerWithConversionRuleset:conversionRuleset]; 205 | if ([conversionHandler shouldConvertString:packageIconPath]) { 206 | NSString *const convertedPackageIconPath = [conversionHandler convertedStringForString:packageIconPath]; 207 | fprintf(stdout, "[+] Converting icon path in control file...\n\t%s -> %s\n", packageIconPath.UTF8String, convertedPackageIconPath.UTF8String); 208 | [controlHandler setControlValue:packageIconPath forKey:@"Icon"]; 209 | } 210 | } 211 | 212 | [controlHandler setControlValue:@"iphoneos-arm64" forKey:@"Architecture"]; 213 | 214 | NSString *const packageID = [controlHandler controlValueForKey:@"Package"]; 215 | NSString *const packageVersion = [controlHandler controlValueForKey:@"Version"]; 216 | 217 | error = nil; 218 | const BOOL writeSuccess = [[controlHandler controlFileAsString] writeToFile:controlFile atomically:YES encoding:NSUTF8StringEncoding error:&error]; 219 | if (!writeSuccess) { 220 | fprintf(stderr, "[-] Failed to write to %s. Error: %s\n", controlFile.fileSystemRepresentation, error.localizedDescription.UTF8String); 221 | return EXIT_FAILURE; 222 | } 223 | 224 | error = nil; 225 | const BOOL attributeSetSuccess = [fileManager setAttributes:fileAttributes ofItemAtPath:controlFile error:&error]; 226 | if (!attributeSetSuccess) { 227 | fprintf(stderr, "[-] Failed to set file attributes for control file: %s. Error: %s\n", controlFile.fileSystemRepresentation, error.localizedDescription.UTF8String); 228 | return EXIT_FAILURE; 229 | } 230 | 231 | fprintf(stdout, "[+] Finishing control file portion...\n"); 232 | 233 | fprintf(stdout, "[+] Starting plist file portion...\n"); 234 | 235 | error = nil; 236 | for (NSString *plist in plistFiles) { 237 | NSDictionary *const fileAttributes = [fileManager attributesOfItemAtPath:plist error:&error]; 238 | if (!fileAttributes) { 239 | fprintf(stderr, "[-] Failed to get file attributes for plist file: %s. Error: %s\n", plist.fileSystemRepresentation, error.localizedDescription.UTF8String); 240 | break; 241 | } 242 | 243 | RPPlistHandler *const handler = [RPPlistHandler handlerWithPlistFile:plist]; 244 | [handler convertStringsUsingConversionRuleset:conversionRuleset]; 245 | 246 | [[handler plistContainer] writeToFile:plist atomically:YES]; 247 | 248 | error = nil; 249 | const BOOL attributeSetSuccess = [fileManager setAttributes:fileAttributes ofItemAtPath:plist error:&error]; 250 | if (!attributeSetSuccess) { 251 | fprintf(stderr, "[-] Failed to set file attributes for plist file: %s. Error: %s\n", plist.fileSystemRepresentation, error.localizedDescription.UTF8String); 252 | break; 253 | } 254 | } 255 | 256 | fprintf(stdout, "[+] Finishing plist file portion...\n"); 257 | 258 | fprintf(stdout, "[+] Starting script file portion...\n"); 259 | 260 | error = nil; 261 | for (NSString *scriptFile in scriptFiles) { 262 | NSDictionary *const fileAttributes = [fileManager attributesOfItemAtPath:scriptFile error:&error]; 263 | if (!fileAttributes) { 264 | fprintf(stderr, "[-] Failed to get file attributes for script file: %s. Error: %s\n", scriptFile.fileSystemRepresentation, error.localizedDescription.UTF8String); 265 | break; 266 | } 267 | 268 | RPScriptHandler *const handler = [RPScriptHandler handlerWithScriptFile:scriptFile]; 269 | [handler convertStringsUsingConversionRuleset:conversionRuleset]; 270 | 271 | error = nil; 272 | NSString *const convertedFileContents = [handler fileContents]; 273 | const BOOL writeSuccess = [convertedFileContents writeToFile:scriptFile atomically:YES encoding:NSUTF8StringEncoding error:&error]; 274 | if (!writeSuccess) { 275 | fprintf(stderr, "[-] Failed to write to file: %s. Error: %s\n", scriptFile.fileSystemRepresentation, error.localizedDescription.UTF8String); 276 | break; 277 | } 278 | 279 | error = nil; 280 | const BOOL attributeSetSuccess = [fileManager setAttributes:fileAttributes ofItemAtPath:scriptFile error:&error]; 281 | if (!attributeSetSuccess) { 282 | fprintf(stderr, "[-] Failed to set file attributes for script file: %s. Error: %s\n", scriptFile.fileSystemRepresentation, error.localizedDescription.UTF8String); 283 | break; 284 | } 285 | } 286 | 287 | fprintf(stdout, "[+] Finishing script file portion...\n"); 288 | 289 | NSArray *const controlPathComponents = [controlFile pathComponents]; 290 | NSString *const debianPath = [patchWorkingDirectory stringByAppendingPathComponent:@"DEBIAN"]; 291 | NSString *const controlContainer = [[controlPathComponents componentsJoinedByString:@"/"] stringByDeletingLastPathComponent]; 292 | 293 | if (![[controlContainer lastPathComponent] isEqualToString:@"DEBIAN"]) { 294 | fprintf(stdout, "\n[!] IMPORTANT: Starting a hacky fix for certain tweaks that contain an improper DEBIAN directory structure. This is done in order for dpkg-deb to properly build the .deb.\nThe control container (which should end with DEBIAN), is actually: %s\n\n", controlContainer.fileSystemRepresentation); 295 | error = nil; 296 | 297 | for (NSString *scriptFile in scriptFiles) { 298 | error = nil; 299 | const BOOL moveSuccess = [fileManager moveItemAtPath:scriptFile toPath:[debianPath stringByAppendingPathComponent:[scriptFile lastPathComponent]] error:&error]; 300 | if (!moveSuccess) { 301 | fprintf(stderr, "[-] Failed to move script to path: %s. Error: %s\n", debianPath.fileSystemRepresentation, error.localizedDescription.UTF8String); 302 | } 303 | } 304 | 305 | NSString *const improperStructurePath = [[controlContainer stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"improper_control_directory_structure"]; 306 | 307 | error = nil; 308 | const BOOL renameSuccess = [fileManager moveItemAtPath:controlContainer toPath:improperStructurePath error:&error]; 309 | if (!renameSuccess) { 310 | fprintf(stderr, "[-] Failed to rename control container. Error: %s\n", error.localizedDescription.UTF8String); 311 | } 312 | 313 | error = nil; 314 | NSString *const newControlFilePath = [improperStructurePath stringByAppendingPathComponent:[controlFile lastPathComponent]]; 315 | const BOOL moveSuccess = [fileManager moveItemAtPath:newControlFilePath toPath:[debianPath stringByAppendingPathComponent:[controlFile lastPathComponent]] error:&error]; 316 | if (!moveSuccess) { 317 | fprintf(stderr, "[-] Failed to move control file to path: %s. Error: %s\n", debianPath.fileSystemRepresentation, error.localizedDescription.UTF8String); 318 | } 319 | 320 | error = nil; 321 | const BOOL improperDirectoryRemoveSuccess = [fileManager removeItemAtPath:improperStructurePath error:&error]; 322 | if (!improperDirectoryRemoveSuccess) { 323 | fprintf(stderr, "[-] Failed to remove improper directory. Error: %s\n", error.localizedDescription.UTF8String); 324 | } 325 | } 326 | 327 | NSString *const newPath = [[debPath stringByDeletingLastPathComponent] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@_%@_iphoneos-arm64.deb", packageID, packageVersion]]; 328 | 329 | const int buildStatus = [RPSpawnHandler spawnWithArguments:@[ 330 | @"dpkg-deb", 331 | @"-b", 332 | patchWorkingDirectory, 333 | newPath 334 | ]]; 335 | 336 | error = nil; 337 | const BOOL patchWorkingDirectoryRemoveSuccess = [fileManager removeItemAtPath:patchWorkingDirectory error:&error]; 338 | if (!patchWorkingDirectoryRemoveSuccess) { 339 | fprintf(stderr, "[-] Error removing patch working directory: %s. Error: %s\n", patchWorkingDirectory.fileSystemRepresentation, error.localizedDescription.UTF8String); 340 | return EXIT_FAILURE; 341 | } 342 | 343 | if (buildStatus != 0) { 344 | fprintf(stderr, "[-] Failed to build .deb using dpkg-deb\n"); 345 | return EXIT_FAILURE; 346 | } 347 | 348 | fprintf(stdout, "\n[+] Done! New .deb path: %s\n\n", newPath.fileSystemRepresentation); 349 | 350 | return EXIT_SUCCESS; 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/RPCodesignHandler.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import "Headers/RPCodesignHandler.h" 5 | #import "Headers/RPSpawnHandler.h" 6 | 7 | @implementation RPCodesignHandler 8 | 9 | + (int)removeCodesignFromFile:(NSString *)file { 10 | return [RPSpawnHandler spawnWithArguments:@[ 11 | @"ldid", 12 | @"-r", 13 | file 14 | ]]; 15 | } 16 | 17 | + (int)addCodesignToFile:(NSString *)file { 18 | return [RPSpawnHandler spawnWithArguments:@[ 19 | @"ldid", 20 | @"-S", 21 | file 22 | ]]; 23 | } 24 | 25 | + (int)addCodesignToFile:(NSString *)file entitlementsPath:(NSString *)entitlementsPath { 26 | return [RPSpawnHandler spawnWithArguments:@[ 27 | @"ldid", 28 | [@"-S" stringByAppendingString:entitlementsPath], 29 | file 30 | ]]; 31 | } 32 | 33 | @end -------------------------------------------------------------------------------- /src/RPControlHandler.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import "Headers/RPControlHandler.h" 5 | 6 | @implementation RPControlHandler { 7 | NSMutableArray *_keys; 8 | NSMutableDictionary *_dictionary; 9 | } 10 | 11 | + (instancetype)handlerWithControlFile:(NSString *)controlFile { 12 | RPControlHandler *const parser = [RPControlHandler new]; 13 | 14 | if (parser) { 15 | NSError *error = nil; 16 | 17 | NSString *const fileContents = [NSString stringWithContentsOfFile:controlFile encoding:NSUTF8StringEncoding error:&error]; 18 | if (!fileContents) { 19 | fprintf(stderr, "[-] Failed to get control file contents at path: %s. Error: %s\n", controlFile.fileSystemRepresentation, error.localizedDescription.UTF8String); 20 | return nil; 21 | } 22 | 23 | NSMutableDictionary *const dictionary = [NSMutableDictionary dictionary]; 24 | NSMutableArray *const keys = [NSMutableArray array]; 25 | 26 | NSArray *const lines = [fileContents componentsSeparatedByString:@": "]; 27 | 28 | const NSUInteger linesCount = [lines count]; 29 | 30 | for (NSUInteger i = 1; i < linesCount; i++) { 31 | NSArray *const previousTokens = [lines[i - 1] componentsSeparatedByString:@"\n"]; 32 | NSArray *const currentTokens = [lines[i] componentsSeparatedByString:@"\n"]; 33 | 34 | NSString *const key = [previousTokens lastObject]; 35 | NSString *const value = [[currentTokens subarrayWithRange:NSMakeRange(0, currentTokens.count - 1)] componentsJoinedByString:@"\n"]; 36 | 37 | const NSUInteger keyLength = [key length]; 38 | const NSUInteger valueLength = [value length]; 39 | 40 | if (keyLength > 0 && valueLength > 0) { 41 | if ([value containsString:@", "]) { 42 | [dictionary setObject:[value componentsSeparatedByString:@", "] forKey:key]; 43 | } else { 44 | [dictionary setObject:value forKey:key]; 45 | } 46 | 47 | [keys addObject:key]; 48 | } 49 | } 50 | 51 | parser->_dictionary = dictionary; 52 | parser->_keys = keys; 53 | } 54 | 55 | return parser; 56 | } 57 | 58 | - (void)setControlValue:(id)value forKey:(NSString *)key { 59 | if (![_dictionary valueForKey:key]) { 60 | return; 61 | } 62 | 63 | [_dictionary setValue:value forKey:key]; 64 | } 65 | 66 | - (id)controlValueForKey:(NSString *)key { 67 | return [_dictionary valueForKey:key]; 68 | } 69 | 70 | - (NSString *)controlFileAsString { 71 | NSString *string = @""; 72 | 73 | for (NSString *key in _keys) { 74 | const id value = [self controlValueForKey:key]; 75 | 76 | if ([value isKindOfClass:[NSString class]]) { 77 | string = [string stringByAppendingString:[NSString stringWithFormat:@"%@: %@\n", key, value]]; 78 | } else if ([value isKindOfClass:[NSArray class]]) { 79 | string = [string stringByAppendingString:[NSString stringWithFormat:@"%@: %@\n", key, [value componentsJoinedByString:@", "]]]; 80 | } 81 | } 82 | 83 | return string; 84 | } 85 | 86 | @end -------------------------------------------------------------------------------- /src/RPConversionHandler.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import "Headers/RPConversionHandler.h" 5 | 6 | @implementation RPConversionHandler { 7 | NSArray *_blacklist; 8 | NSDictionary *_specialCases; 9 | NSArray *_bootstrapList; 10 | } 11 | 12 | + (instancetype)handlerWithConversionRuleset:(NSDictionary *)conversionRuleset { 13 | RPConversionHandler *const handler = [RPConversionHandler new]; 14 | 15 | if (handler) { 16 | handler->_blacklist = [conversionRuleset objectForKey:@"Blacklist"]; 17 | handler->_specialCases = [conversionRuleset objectForKey:@"SpecialCases"]; 18 | handler->_bootstrapList = @[@"Applications", @"bin", @"boot", @"etc", @"lib", @"Library", @"mnt", @"sbin", @"tmp", @"User", @"usr", @"var"]; 19 | } 20 | 21 | return handler; 22 | } 23 | 24 | - (BOOL)shouldConvertString:(NSString *)string { 25 | if ([string length] == 0) { 26 | return NO; 27 | } 28 | 29 | NSArray *const pathComponents = [[self _string:string byStrippingPrefix:@"file://"] pathComponents]; 30 | if (!pathComponents || [pathComponents count] == 1) { 31 | return NO; 32 | } 33 | 34 | NSString *firstPathComponent = pathComponents.firstObject; 35 | 36 | for (NSString *pathComponent in pathComponents) { 37 | if (![pathComponent isEqualToString:@"/"]) { 38 | firstPathComponent = pathComponent; 39 | break; 40 | } 41 | } 42 | 43 | if (![_bootstrapList containsObject:firstPathComponent]) { 44 | return NO; 45 | } 46 | 47 | for (NSString *specialCase in _specialCases) { 48 | if ([string containsString:specialCase]) { 49 | return YES; 50 | } 51 | } 52 | 53 | if ([string hasPrefix:@"file://"]) { 54 | return YES; 55 | } 56 | 57 | for (NSString *blacklistedString in _blacklist) { 58 | if ([string containsString:blacklistedString]) { 59 | return NO; 60 | } 61 | } 62 | 63 | return YES; 64 | } 65 | 66 | - (NSString *)convertedStringForString:(NSString *)string { 67 | NSString *convertedString = string; 68 | 69 | for (NSString *specialCase in _specialCases) { 70 | if ([string containsString:specialCase]) { 71 | convertedString = [convertedString stringByReplacingOccurrencesOfString:specialCase withString:[_specialCases valueForKey:specialCase]]; 72 | } 73 | } 74 | 75 | /** 76 | * Specifically using stringByAppendingString: instead of stringByAppendingPathComponent: to handle cases with a trailing /. 77 | * Example: 78 | * /Applications/ -> /var/jb/Applications | stringByAppendingPathComponent: 79 | * /Applications/ -> /var/jb/Applications/ | stringByAppendingString: 80 | */ 81 | if ([string hasPrefix:@"file://"]) { 82 | return [@"file:///var/jb" stringByAppendingString:[self _string:string byStrippingPrefix:@"file://"]]; 83 | } else if ([string hasPrefix:@"/"]) { 84 | return [@"/var/jb" stringByAppendingString:convertedString]; 85 | } else { 86 | return [@"var/jb/" stringByAppendingString:convertedString]; 87 | } 88 | } 89 | 90 | - (NSString *)_string:(NSString *)string byStrippingPrefix:(NSString *)prefix { 91 | return [string hasPrefix:prefix] ? [string substringFromIndex:[prefix length]] : string; 92 | } 93 | 94 | @end -------------------------------------------------------------------------------- /src/RPDirectoryScanner.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | #import 6 | #import "Headers/RPDirectoryScanner.h" 7 | 8 | @implementation RPDirectoryScanner { 9 | NSString *_directory; 10 | } 11 | 12 | + (instancetype)scannerWithDirectory:(NSString *)directory { 13 | RPDirectoryScanner *const scanner = [RPDirectoryScanner new]; 14 | 15 | if (scanner) { 16 | scanner->_directory = directory; 17 | } 18 | 19 | return scanner; 20 | } 21 | 22 | - (NSArray *)machOFiles { 23 | __weak typeof(self) weakSelf = self; 24 | __block NSMutableArray *const files = [NSMutableArray array]; 25 | 26 | [self _recursivelyScanDirectory:_directory withBlock:^(NSString *filePath) { 27 | NSData *const data = [NSData dataWithContentsOfFile:filePath]; 28 | 29 | const struct mach_header_64 *header = (const struct mach_header_64 *)[data bytes]; 30 | if (header && [weakSelf _magicMatchesMachO:header->magic]) { 31 | [files addObject:filePath]; 32 | } 33 | }]; 34 | 35 | return [files count] ? files : nil; 36 | } 37 | 38 | - (NSArray *)plistFiles { 39 | __block NSMutableArray *const files = [NSMutableArray array]; 40 | 41 | [self _recursivelyScanDirectory:_directory withBlock:^(NSString *filePath) { 42 | if ([[filePath pathExtension] isEqualToString:@"plist"]) { 43 | [files addObject:filePath]; 44 | } 45 | }]; 46 | 47 | return [files count] ? files : nil; 48 | } 49 | 50 | - (NSArray *)scriptFiles { 51 | __weak typeof(self) weakSelf = self; 52 | 53 | __block NSMutableArray *const files = [NSMutableArray array]; 54 | 55 | [self _recursivelyScanDirectory:_directory withBlock:^(NSString *filePath) { 56 | NSError *error = nil; 57 | NSString *const fileContents = [NSString stringWithContentsOfFile:filePath encoding:NSASCIIStringEncoding error:&error]; 58 | if (!fileContents) { 59 | fprintf(stderr, "[-] Failed to get contents of file: %s. Error: %s\n", filePath.fileSystemRepresentation, error.localizedDescription.UTF8String); 60 | return; 61 | } 62 | if ([fileContents hasPrefix:@"#!"]) { 63 | [files addObject:filePath]; 64 | } 65 | }]; 66 | 67 | NSArray *const controlScriptNames = @[@"preinst", @"postinst", @"prerm", @"postrm", @"postrmv", @"extrainst", @"triggers"]; 68 | 69 | [self _recursivelyScanDirectory:[_directory stringByAppendingPathComponent:@"DEBIAN"] withBlock:^(NSString *filePath) { 70 | if (![files containsObject:filePath] && [controlScriptNames containsObject:[filePath lastPathComponent]]) { 71 | NSData *const data = [NSData dataWithContentsOfFile:filePath]; 72 | 73 | const struct mach_header_64 *const header = (const struct mach_header_64 *)[data bytes]; 74 | 75 | if (header && ![weakSelf _magicMatchesMachO:header->magic]) { 76 | [files addObject:filePath]; 77 | } 78 | } 79 | }]; 80 | 81 | return files; 82 | } 83 | 84 | - (NSString *)controlFile { 85 | NSFileManager *const fileManager = [NSFileManager defaultManager]; 86 | __block NSString *controlFilePath = @""; 87 | 88 | [self _recursivelyScanDirectory:[_directory stringByAppendingPathComponent:@"DEBIAN"] withBlock:^(NSString *filePath) { 89 | BOOL isDirectory = NO; 90 | 91 | if ([[filePath lastPathComponent] isEqualToString:@"control"] && [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory] && !isDirectory) { 92 | controlFilePath = filePath; 93 | } 94 | }]; 95 | 96 | return controlFilePath; 97 | } 98 | 99 | - (void)_recursivelyScanDirectory:(NSString *)directory withBlock:(void (^)(NSString *))block { 100 | NSFileManager *const fileManager = [NSFileManager defaultManager]; 101 | 102 | NSError *error = nil; 103 | NSArray *const subpaths = [fileManager subpathsOfDirectoryAtPath:directory error:&error]; 104 | if (error) { 105 | fprintf(stderr, "[-] Failed to get subpaths of directory at path: %s. Error: %s\n", directory.fileSystemRepresentation, error.localizedDescription.UTF8String); 106 | return; 107 | } 108 | 109 | for (NSString *subpath in subpaths) { 110 | NSString *const fullPath = [directory stringByAppendingPathComponent:subpath]; 111 | 112 | BOOL isDirectory; 113 | const BOOL fileExists = [fileManager fileExistsAtPath:fullPath isDirectory:&isDirectory]; 114 | 115 | if (fileExists && !isDirectory) { 116 | block(fullPath); 117 | } 118 | } 119 | } 120 | 121 | - (BOOL)_magicMatchesMachO:(uint32_t)magic { 122 | return magic == MH_MAGIC_64 || magic == MH_CIGAM_64 || magic == FAT_MAGIC || magic == FAT_CIGAM; 123 | } 124 | 125 | @end -------------------------------------------------------------------------------- /src/RPLoadCommandsHandler.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | #import "Headers/RPLoadCommandsHandler.h" 6 | #import "Headers/RPSpawnHandler.h" 7 | 8 | @implementation RPLoadCommandsHandler 9 | 10 | + (BOOL)handleLoadCommandsForFile:(NSString *)file { 11 | NSString *const scriptPath = ROOT_PATH_NS(@"/Library/Application Support/rootless-patcher/repack-rootless.sh"); 12 | 13 | const int scriptStatus = [RPSpawnHandler spawnWithArguments:@[ 14 | @"sh", 15 | scriptPath, 16 | file 17 | ]]; 18 | 19 | if (scriptStatus != 0) { 20 | fprintf(stderr, "[-] Failed to execute script: %s\n", scriptPath.fileSystemRepresentation); 21 | return NO; 22 | } 23 | 24 | return YES; 25 | } 26 | 27 | @end -------------------------------------------------------------------------------- /src/RPMachOMerger.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import "Headers/RPMachOMerger.h" 5 | #import "Headers/RPSpawnHandler.h" 6 | 7 | @implementation RPMachOMerger 8 | 9 | + (int)mergeMachOsAtPaths:(NSArray *)paths outputPath:(NSString *)outputPath { 10 | NSMutableArray *const args = [NSMutableArray array]; 11 | [args addObject:@"lipo"]; 12 | [args addObjectsFromArray:paths]; 13 | [args addObject:@"-output"]; 14 | [args addObject:outputPath]; 15 | [args addObject:@"-create"]; 16 | 17 | return [RPSpawnHandler spawnWithArguments:args]; 18 | } 19 | 20 | @end -------------------------------------------------------------------------------- /src/RPMachOModifier.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | #import 6 | #import "Headers/RPMachOModifier.h" 7 | #import "Headers/RPMachOParser.h" 8 | #import "Headers/RPStringPatcher.h" 9 | 10 | @implementation RPMachOModifier { 11 | NSMutableData *_fileData; 12 | RPMachOParser *_parser; 13 | NSString *_filePath; 14 | NSMutableDictionary *_replacementOffsetMap; 15 | } 16 | 17 | + (instancetype)modifierWithFile:(NSString *)file { 18 | RPMachOModifier *const modifier = [RPMachOModifier new]; 19 | 20 | if (modifier) { 21 | NSMutableData *const data = [NSMutableData dataWithContentsOfFile:file]; 22 | 23 | modifier->_fileData = data; 24 | modifier->_parser = [RPMachOParser parserWithHeader:(struct mach_header_64 *)[data bytes]]; 25 | modifier->_filePath = file; 26 | 27 | modifier->_replacementOffsetMap = [NSMutableDictionary dictionary]; 28 | } 29 | 30 | return modifier; 31 | } 32 | 33 | - (void)addSegment:(NSString *)segname section:(NSString *)sectname stringMap:(NSDictionary *)stringMap { 34 | struct mach_header_64 *header = (struct mach_header_64 *)[_parser header]; 35 | 36 | struct segment_command_64 *llvmSegment = [_parser segmentWithName:@"__LLVM"]; 37 | struct segment_command_64 *linkeditSegment = [_parser segmentWithName:@"__LINKEDIT"]; 38 | if (!linkeditSegment) { 39 | return; 40 | } 41 | 42 | const uint64_t vmEnd = [_parser vmEnd]; 43 | 44 | const NSRange endRange = llvmSegment ? NSMakeRange(llvmSegment->fileoff, llvmSegment->filesize + linkeditSegment->filesize) : NSMakeRange(linkeditSegment->fileoff, linkeditSegment->filesize); 45 | NSData *const endData = [_fileData subdataWithRange:endRange]; 46 | [_fileData replaceBytesInRange:endRange withBytes:nil length:0]; 47 | 48 | const NSUInteger pagesNeeded = [self _pagesNeededForStringMap:stringMap]; 49 | 50 | const struct segment_command_64 newSegment = { 51 | .cmd = LC_SEGMENT_64, 52 | .cmdsize = sizeof(struct segment_command_64) + sizeof(struct section_64), 53 | .vmaddr = llvmSegment ? llvmSegment->vmaddr : vmEnd, 54 | .vmsize = PAGE_SIZE * pagesNeeded, 55 | .fileoff = llvmSegment ? llvmSegment->fileoff : _fileData.length, 56 | .filesize = PAGE_SIZE * pagesNeeded, 57 | .maxprot = VM_PROT_READ, 58 | .initprot = VM_PROT_READ, 59 | .nsects = 1 60 | }; 61 | 62 | strncpy((char *)newSegment.segname, segname.UTF8String, sizeof(newSegment.segname)); 63 | 64 | const struct section_64 newSection = { 65 | .addr = newSegment.vmaddr, 66 | .size = newSegment.vmsize, 67 | .offset = (uint32_t)(newSegment.fileoff), 68 | .align = 0, 69 | .reloff = 0, 70 | .nreloc = 0, 71 | .flags = S_CSTRING_LITERALS, 72 | .reserved1 = 0, 73 | .reserved2 = 0, 74 | .reserved3 = 0 75 | }; 76 | 77 | strncpy((char *)newSection.segname, segname.UTF8String, sizeof(newSection.segname)); 78 | strncpy((char *)newSection.sectname, sectname.UTF8String, sizeof(newSection.sectname)); 79 | 80 | const uint64_t insertOffset = (uint64_t)(llvmSegment ?: linkeditSegment) - (uint64_t)(header + 1); 81 | 82 | unsigned char *cmds = (unsigned char *)malloc(header->sizeofcmds); 83 | 84 | memcpy(cmds, header + 1, header->sizeofcmds); 85 | 86 | unsigned char *patch = (unsigned char *)(header + 1) + insertOffset; 87 | 88 | memcpy(patch, &newSegment, sizeof(newSegment)); 89 | patch += sizeof(newSegment); 90 | 91 | memcpy(patch, &newSection, sizeof(newSection)); 92 | patch += sizeof(newSection); 93 | 94 | memcpy(patch, cmds + insertOffset, header->sizeofcmds - insertOffset); 95 | 96 | free((void *)cmds); 97 | 98 | _parser = [RPMachOParser parserWithHeader:header]; 99 | 100 | llvmSegment = [_parser segmentWithName:@"__LLVM"]; 101 | linkeditSegment = [_parser segmentWithName:@"__LINKEDIT"]; 102 | 103 | header->ncmds += 1; 104 | header->sizeofcmds += newSegment.cmdsize; 105 | 106 | if (llvmSegment) { 107 | struct section_64 *sect = (struct section_64 *)(llvmSegment + 1); 108 | for (uint32_t i = 0; i < llvmSegment->nsects; i++) { 109 | sect->offset = sect->offset - llvmSegment->fileoff + newSegment.fileoff + newSegment.filesize; 110 | sect->addr = sect->addr - llvmSegment->vmaddr + newSegment.vmaddr + newSegment.vmsize; 111 | sect = (struct section_64 *)(sect + 1); 112 | } 113 | 114 | llvmSegment->fileoff = newSegment.fileoff + newSegment.filesize; 115 | llvmSegment->vmaddr = newSegment.vmaddr + newSegment.vmsize; 116 | 117 | linkeditSegment->fileoff = llvmSegment->filesize + newSegment.fileoff + newSegment.filesize; 118 | linkeditSegment->vmaddr = llvmSegment->vmsize + newSegment.vmaddr + newSegment.vmsize; 119 | } else { 120 | linkeditSegment->fileoff = newSegment.fileoff + newSegment.filesize; 121 | linkeditSegment->vmaddr = newSegment.vmaddr + newSegment.vmsize; 122 | } 123 | 124 | struct linkedit_data_command *chainedFixups = nil; 125 | [self _shiftCommandsWithOffset:newSegment.filesize chainedFixups:&chainedFixups]; 126 | 127 | unsigned char *codepage = (unsigned char *)calloc(newSegment.vmsize, 1); 128 | [self _addPatchedStringsFromStringMap:stringMap toCodepage:codepage mappedOffset:newSegment.vmaddr - newSegment.fileoff sectionOffset:newSection.offset]; 129 | [_fileData appendBytes:codepage length:newSegment.vmsize]; 130 | free((void *)codepage); 131 | 132 | [_fileData appendData:endData]; 133 | 134 | if (chainedFixups) { 135 | [self _fixChainedFixups:chainedFixups linkeditSegment:linkeditSegment]; 136 | } 137 | } 138 | 139 | - (void)rebaseStringsWithStringMap:(NSDictionary *)stringMap originalOffsetMap:(NSDictionary *)originalOffsetMap { 140 | RPStringPatcher *const patcher = [RPStringPatcher patcherWithData:_fileData replacementOffsetMap:_replacementOffsetMap originalOffsetMap:originalOffsetMap]; 141 | 142 | for (NSString *originalString in stringMap) { 143 | NSString *const patchedString = [stringMap objectForKey:originalString]; 144 | [patcher patchString:originalString toString:patchedString]; 145 | } 146 | 147 | _fileData = [[patcher data] mutableCopy]; 148 | } 149 | 150 | - (void)_addPatchedStringsFromStringMap:(NSDictionary *)stringMap toCodepage:(unsigned char *)codepage mappedOffset:(uint64_t)mappedOffset sectionOffset:(uint64_t)sectionOffset { 151 | NSArray *const patchedStrings = [stringMap allValues]; 152 | uint32_t offset = 0; 153 | 154 | const NSUInteger stringCount = [stringMap count]; 155 | for (NSUInteger i = 0; i < stringCount; i++) { 156 | const char *string = [patchedStrings[i] UTF8String]; 157 | strcpy((char *)codepage + offset, string); 158 | [_replacementOffsetMap setObject:@(sectionOffset + offset + mappedOffset) forKey:patchedStrings[i]]; 159 | offset += strlen(string) + 1; 160 | } 161 | } 162 | 163 | - (NSUInteger)_pagesNeededForStringMap:(NSDictionary *)stringMap { 164 | NSArray *const patchedStrings = [stringMap allValues]; 165 | const NSUInteger stringCount = [stringMap count]; 166 | 167 | uint64_t totalSize = 0; 168 | for (NSUInteger i = 0; i < stringCount; i++) { 169 | totalSize += strlen([patchedStrings[i] UTF8String]) + 1; 170 | } 171 | 172 | return (totalSize + PAGE_SIZE - 1) / PAGE_SIZE; 173 | } 174 | 175 | - (void)_shiftCommandsWithOffset:(uint64_t)fixOffset chainedFixups:(struct linkedit_data_command **)chainedFixups { 176 | const struct mach_header_64 *header = (struct mach_header_64 *)[_fileData bytes]; 177 | 178 | struct load_command *lc = (struct load_command *)(header + 1); 179 | 180 | for (uint32_t i = 0; i < header->ncmds; i++) { 181 | const uint32_t cmd = lc->cmd; 182 | 183 | if (cmd == LC_DYLD_INFO || cmd == LC_DYLD_INFO_ONLY) { 184 | 185 | struct dyld_info_command *dyldInfoCommand = (struct dyld_info_command *)lc; 186 | 187 | if (dyldInfoCommand->rebase_size > 0) { 188 | dyldInfoCommand->rebase_off += fixOffset; 189 | } 190 | 191 | if (dyldInfoCommand->bind_size > 0 && dyldInfoCommand->bind_off) { 192 | dyldInfoCommand->bind_off += fixOffset; 193 | } 194 | 195 | if (dyldInfoCommand->weak_bind_size > 0 && dyldInfoCommand->weak_bind_off) { 196 | dyldInfoCommand->weak_bind_off += fixOffset; 197 | } 198 | 199 | if (dyldInfoCommand->lazy_bind_size > 0 && dyldInfoCommand->lazy_bind_off) { 200 | dyldInfoCommand->lazy_bind_off += fixOffset; 201 | } 202 | 203 | if (dyldInfoCommand->export_size > 0 && dyldInfoCommand->export_off) { 204 | dyldInfoCommand->export_off += fixOffset; 205 | } 206 | 207 | } else if (cmd == LC_SYMTAB) { 208 | 209 | struct symtab_command *symtabCommand = (struct symtab_command *)lc; 210 | 211 | if (symtabCommand->symoff) symtabCommand->symoff += fixOffset; 212 | if (symtabCommand->stroff) symtabCommand->stroff += fixOffset; 213 | 214 | } else if (cmd == LC_DYSYMTAB) { 215 | 216 | struct dysymtab_command *dysymtabCommand = (struct dysymtab_command *)lc; 217 | 218 | if (dysymtabCommand->tocoff) dysymtabCommand->tocoff += fixOffset; 219 | if (dysymtabCommand->modtaboff) dysymtabCommand->modtaboff += fixOffset; 220 | if (dysymtabCommand->extrefsymoff) dysymtabCommand->extrefsymoff += fixOffset; 221 | if (dysymtabCommand->indirectsymoff) dysymtabCommand->indirectsymoff += fixOffset; 222 | if (dysymtabCommand->extreloff) dysymtabCommand->extreloff += fixOffset; 223 | if (dysymtabCommand->locreloff) dysymtabCommand->locreloff += fixOffset; 224 | 225 | } else if ( 226 | cmd == LC_FUNCTION_STARTS || 227 | cmd == LC_DATA_IN_CODE || 228 | cmd == LC_CODE_SIGNATURE || 229 | cmd == LC_SEGMENT_SPLIT_INFO || 230 | cmd == LC_DYLIB_CODE_SIGN_DRS || 231 | cmd == LC_LINKER_OPTIMIZATION_HINT || 232 | cmd == LC_DYLD_EXPORTS_TRIE || 233 | cmd == LC_DYLD_CHAINED_FIXUPS) { 234 | 235 | struct linkedit_data_command *dataCommand = (struct linkedit_data_command *)lc; 236 | 237 | if (cmd == LC_DYLD_CHAINED_FIXUPS) { 238 | *chainedFixups = dataCommand; 239 | } 240 | 241 | if (dataCommand->dataoff) dataCommand->dataoff += fixOffset; 242 | } 243 | 244 | lc = (struct load_command *)((uint64_t)lc + (uint64_t)lc->cmdsize); 245 | } 246 | } 247 | 248 | - (void)_fixChainedFixups:(struct linkedit_data_command *)chainedFixups linkeditSegment:(struct segment_command_64 *)linkeditSegment { 249 | struct mach_header_64 *header = (struct mach_header_64 *)[_fileData mutableBytes]; 250 | 251 | const uint32_t offsetInLinkedit = (uint32_t)chainedFixups->dataoff - (uint32_t)linkeditSegment->fileoff; 252 | const uintptr_t linkeditStartAddress = (uint64_t)header + linkeditSegment->fileoff; 253 | 254 | const struct dyld_chained_fixups_header *chainsHeader = (const struct dyld_chained_fixups_header *)(linkeditStartAddress + offsetInLinkedit); 255 | const struct dyld_chained_starts_in_image *startsInfo = (const struct dyld_chained_starts_in_image *)((uint8_t *)chainsHeader + chainsHeader->starts_offset); 256 | 257 | const uint64_t startsInfoSizeNew = sizeof(struct dyld_chained_starts_in_image) + sizeof(startsInfo->seg_info_offset) * (startsInfo->seg_count + 1); 258 | 259 | NSMutableData *const append = [NSMutableData dataWithLength:startsInfoSizeNew]; 260 | struct dyld_chained_starts_in_image *startsInfoNew = (struct dyld_chained_starts_in_image *)[append mutableBytes]; 261 | bzero(startsInfoNew, startsInfoSizeNew); 262 | *startsInfoNew = *startsInfo; 263 | startsInfoNew->seg_count += 1; 264 | 265 | for (uint32_t i = 0; i < startsInfo->seg_count; i++) { 266 | const uint32_t segmentInfoOffset = startsInfo->seg_info_offset[i]; 267 | if (!segmentInfoOffset) continue; 268 | 269 | startsInfoNew->seg_info_offset[i] = (uint32_t)[append length]; 270 | 271 | const struct dyld_chained_starts_in_segment *segmentInfo = (struct dyld_chained_starts_in_segment *)((uint8_t *)startsInfo + segmentInfoOffset); 272 | 273 | int segmentInfoSize = sizeof(struct dyld_chained_starts_in_segment); 274 | 275 | if (segmentInfo->page_count) { 276 | segmentInfoSize += sizeof(segmentInfo->page_start) * (segmentInfo->page_count - 1); 277 | } 278 | 279 | [append appendBytes:segmentInfo length:segmentInfoSize]; 280 | } 281 | 282 | [_fileData appendData:append]; 283 | linkeditSegment->filesize += [append length]; 284 | linkeditSegment->vmsize += ([append length] + PAGE_SIZE - 1) & (~(PAGE_SIZE - 1)); 285 | } 286 | 287 | - (NSData *)data { 288 | return [_fileData copy]; 289 | } 290 | 291 | @end -------------------------------------------------------------------------------- /src/RPMachOParser.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import "Headers/RPMachOParser.h" 5 | 6 | @implementation RPMachOParser { 7 | struct mach_header_64 *_header; 8 | } 9 | 10 | + (instancetype)parserWithHeader:(struct mach_header_64 *)header { 11 | RPMachOParser *const parser = [RPMachOParser new]; 12 | 13 | if (parser) { 14 | parser->_header = header; 15 | } 16 | 17 | return parser; 18 | } 19 | 20 | - (struct segment_command_64 *)segmentWithName:(NSString *)segname { 21 | struct load_command *lc = (struct load_command *)(_header + 1); 22 | 23 | for (uint32_t i = 0; i < _header->ncmds; i++) { 24 | if (lc->cmd == LC_SEGMENT_64) { 25 | struct segment_command_64 *segment = (struct segment_command_64 *)lc; 26 | 27 | if (strcmp(segment->segname, segname.UTF8String) == 0) { 28 | return segment; 29 | } 30 | } 31 | 32 | lc = (struct load_command *)((uintptr_t)lc + (uint64_t)lc->cmdsize); 33 | } 34 | 35 | return nil; 36 | } 37 | 38 | - (struct section_64 *)sectionInSegment:(struct segment_command_64 *)segment withName:(NSString *)sectname { 39 | if (!segment) { 40 | return nil; 41 | } 42 | 43 | struct section_64 *sect = (struct section_64 *)(segment + 1); 44 | 45 | for (uint32_t i = 0; i < segment->nsects; i++) { 46 | if (strcmp(sect->sectname, sectname.UTF8String) == 0) { 47 | return sect; 48 | } 49 | 50 | sect = (struct section_64 *)(sect + 1); 51 | } 52 | 53 | return nil; 54 | } 55 | 56 | - (uint64_t)vmEnd { 57 | uint64_t vmEnd = 0; 58 | 59 | struct load_command *lc = (struct load_command *)(_header + 1); 60 | 61 | for (uint32_t i = 0; i < _header->ncmds; i++) { 62 | if (lc->cmd == LC_SEGMENT_64) { 63 | struct segment_command_64 *segment = (struct segment_command_64 *)lc; 64 | 65 | if (segment->vmsize && vmEnd < segment->vmaddr + segment->vmsize) { 66 | vmEnd = segment->vmaddr + segment->vmsize; 67 | } 68 | } 69 | 70 | lc = (struct load_command *)((uint64_t)lc + (uint64_t)lc->cmdsize); 71 | } 72 | 73 | return vmEnd; 74 | } 75 | 76 | - (struct mach_header_64 *)header { 77 | return _header; 78 | } 79 | 80 | @end -------------------------------------------------------------------------------- /src/RPMachOThinner.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | #import 6 | #import "Headers/RPMachOThinner.h" 7 | 8 | @implementation RPMachOThinner 9 | 10 | + (NSDictionary *)thinnedMachOsFromPaths:(NSArray *)paths { 11 | NSMutableDictionary *const thinnedMachOs = [NSMutableDictionary dictionary]; 12 | 13 | for (NSString *path in paths) { 14 | NSArray *const thinnedMachO = [RPMachOThinner _thinnedMachOsFromPath:path]; 15 | [thinnedMachOs setObject:thinnedMachO forKey:path]; 16 | } 17 | 18 | return [thinnedMachOs copy]; 19 | } 20 | 21 | + (NSArray *)_thinnedMachOsFromPath:(NSString *)path { 22 | NSMutableArray *const thinnedMachOs = [NSMutableArray array]; 23 | 24 | NSData *const data = [NSData dataWithContentsOfFile:path]; 25 | 26 | const struct mach_header_64 *header = (const struct mach_header_64 *)[data bytes]; 27 | 28 | if (header->magic == FAT_MAGIC || header->magic == FAT_CIGAM) { 29 | NSArray *const thinnedMachOsFromFAT = [RPMachOThinner _thinnedMachOsFromFAT:path]; 30 | for (NSString *thinnedMachO in thinnedMachOsFromFAT) { 31 | [thinnedMachOs addObject:thinnedMachO]; 32 | } 33 | } else if (header->magic == MH_MAGIC_64 || header->magic == MH_CIGAM_64) { 34 | NSString *const thinnedPath = [RPMachOThinner _thinnedPathForPath:path cpusubtype:header->cpusubtype]; 35 | 36 | NSError *error = nil; 37 | const BOOL success = [data writeToFile:thinnedPath options:NSDataWritingAtomic error:&error]; 38 | 39 | if (success) { 40 | [thinnedMachOs addObject:thinnedPath]; 41 | } else { 42 | fprintf(stderr, "[-] Failed to write to %s. Error: %s\n", thinnedPath.fileSystemRepresentation, error.localizedDescription.UTF8String); 43 | } 44 | } 45 | 46 | return [thinnedMachOs copy]; 47 | } 48 | 49 | + (NSArray *)_thinnedMachOsFromFAT:(NSString *)path { 50 | NSMutableArray *const thinnedMachOs = [NSMutableArray array]; 51 | 52 | NSData *const data = [NSData dataWithContentsOfFile:path]; 53 | 54 | const struct fat_header *header = (const struct fat_header *)[data bytes]; 55 | struct fat_arch *arch = (struct fat_arch *)([data bytes] + sizeof(struct fat_header)); 56 | 57 | for (uint32_t i = 0; i < OSSwapBigToHostInt32(header->nfat_arch); i++) { 58 | const struct mach_header_64 *header = (const struct mach_header_64 *)([data bytes] + OSSwapBigToHostInt32(arch->offset)); 59 | 60 | if (header->cputype == CPU_TYPE_ARM64) { 61 | const NSRange sliceRange = NSMakeRange(OSSwapBigToHostInt32(arch->offset), OSSwapBigToHostInt32(arch->size)); 62 | NSData *const subdata = [data subdataWithRange:sliceRange]; 63 | 64 | NSString *const thinnedPath = [RPMachOThinner _thinnedPathForPath:path cpusubtype:header->cpusubtype]; 65 | 66 | NSError *error = nil; 67 | const BOOL success = [subdata writeToFile:thinnedPath options:NSDataWritingAtomic error:&error]; 68 | 69 | if (success) { 70 | [thinnedMachOs addObject:thinnedPath]; 71 | } else { 72 | fprintf(stderr, "[-] Failed to write to %s. Error: %s\n", thinnedPath.fileSystemRepresentation, error.localizedDescription.UTF8String); 73 | } 74 | } 75 | 76 | arch++; 77 | } 78 | 79 | return thinnedMachOs; 80 | } 81 | 82 | + (NSString *)_thinnedPathForPath:(NSString *)path cpusubtype:(cpu_subtype_t)cpusubtype { 83 | NSString *const name = [path lastPathComponent]; 84 | return [[path stringByDeletingLastPathComponent] stringByAppendingPathComponent:[NSString stringWithFormat:@"%x_%@", cpusubtype, name]]; 85 | } 86 | 87 | @end -------------------------------------------------------------------------------- /src/RPOldABIChecker.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | #import 6 | #import "Headers/RPOldABIChecker.h" 7 | 8 | @implementation RPOldABIChecker 9 | 10 | + (BOOL)containsOldABI:(NSData *)machO { 11 | const struct mach_header_64 *header = (const struct mach_header_64 *)[machO bytes]; 12 | 13 | if (header->magic == FAT_MAGIC || header->magic == FAT_CIGAM) { 14 | const struct fat_header *fatHeader = (const struct fat_header *)header; 15 | struct fat_arch *arch = (struct fat_arch *)(fatHeader + sizeof(struct fat_header)); 16 | 17 | for (uint32_t i = 0; i < OSSwapBigToHostInt32(fatHeader->nfat_arch); i++) { 18 | const struct mach_header_64 *thinHeader = (const struct mach_header_64 *)(fatHeader + OSSwapBigToHostInt32(arch->offset)); 19 | 20 | if ((thinHeader->cpusubtype & ~CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64E) { 21 | return !(thinHeader->cpusubtype & CPU_SUBTYPE_PTRAUTH_ABI); 22 | } 23 | 24 | arch += 1; 25 | } 26 | } else if (header->magic == MH_MAGIC_64 || header->magic == MH_CIGAM_64) { 27 | if ((header->cpusubtype & ~CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64E) { 28 | return !(header->cpusubtype & CPU_SUBTYPE_PTRAUTH_ABI); 29 | } 30 | } 31 | 32 | return NO; 33 | } 34 | 35 | @end -------------------------------------------------------------------------------- /src/RPPlistHandler.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import "Headers/RPPlistHandler.h" 5 | #import "Headers/RPConversionHandler.h" 6 | 7 | @implementation RPPlistHandler { 8 | id _plistContainer; 9 | RPConversionHandler *_conversionHandler; 10 | } 11 | 12 | + (instancetype)handlerWithPlistFile:(NSString *)file { 13 | RPPlistHandler *const handler = [RPPlistHandler new]; 14 | 15 | if (handler) { 16 | NSData *const data = [NSData dataWithContentsOfFile:file]; 17 | handler->_plistContainer = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListMutableContainersAndLeaves format:nil error:nil]; 18 | } 19 | 20 | return handler; 21 | } 22 | 23 | - (void)convertStringsUsingConversionRuleset:(NSDictionary *)conversionRuleset { 24 | _conversionHandler = [RPConversionHandler handlerWithConversionRuleset:conversionRuleset]; 25 | [self _patchStringValues:_plistContainer]; 26 | } 27 | 28 | - (void)_patchStringValues:(id)container { 29 | if ([container isKindOfClass:[NSMutableDictionary class]]) { 30 | NSMutableDictionary *const dictionary = container; 31 | 32 | for (NSUInteger i = 0; i < dictionary.count; i++) { 33 | id key = [dictionary allKeys][i]; 34 | id value = [dictionary objectForKey:key]; 35 | 36 | if ([value isKindOfClass:[NSString class]]) { 37 | if ([_conversionHandler shouldConvertString:value]) { 38 | NSString *const convertedString = [_conversionHandler convertedStringForString:value]; 39 | [dictionary setObject:convertedString forKey:key]; 40 | fprintf(stdout, "\t%s -> %s\n", ((NSString *)value).UTF8String, convertedString.UTF8String); 41 | } 42 | } else if ([value isKindOfClass:[NSMutableDictionary class]] || [value isKindOfClass:[NSMutableArray class]]) { 43 | [self _patchStringValues:value]; 44 | } 45 | } 46 | } else if ([container isKindOfClass:[NSMutableArray class]]) { 47 | NSMutableArray *const array = container; 48 | 49 | for (NSUInteger i = 0; i < array.count; i++) { 50 | id value = [array objectAtIndex:i]; 51 | 52 | if ([value isKindOfClass:[NSString class]]) { 53 | if ([_conversionHandler shouldConvertString:value]) { 54 | NSString *const convertedString = [_conversionHandler convertedStringForString:value]; 55 | array[i] = convertedString; 56 | fprintf(stdout, "\t%s -> %s\n", ((NSString *)value).UTF8String, convertedString.UTF8String); 57 | } 58 | } else if ([value isKindOfClass:[NSMutableDictionary class]] || [value isKindOfClass:[NSMutableArray class]]) { 59 | [self _patchStringValues:value]; 60 | } 61 | } 62 | } 63 | } 64 | 65 | - (id)plistContainer { 66 | return [_plistContainer copy]; 67 | } 68 | 69 | @end -------------------------------------------------------------------------------- /src/RPRepackHandler.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import "Headers/RPRepackHandler.h" 5 | #import "Headers/RPSpawnHandler.h" 6 | 7 | @implementation RPRepackHandler 8 | 9 | + (BOOL)repackFile:(NSString *)file toDirectory:(NSString *)directory { 10 | NSFileManager *const fileManager = [NSFileManager defaultManager]; 11 | 12 | NSString *const oldWorkingDirectory = [[directory stringByDeletingLastPathComponent] stringByAppendingPathComponent:[NSString stringWithFormat:@"old_%@", [directory lastPathComponent]]]; 13 | 14 | const int unpackStatus = [RPSpawnHandler spawnWithArguments:@[ 15 | @"dpkg-deb", 16 | @"-R", 17 | file, 18 | oldWorkingDirectory 19 | ]]; 20 | 21 | if (unpackStatus != 0) { 22 | fprintf(stderr, "[-] Failed to unpack .deb using dpkg-deb\n"); 23 | return NO; 24 | } 25 | 26 | NSError *error = nil; 27 | 28 | if ([fileManager fileExistsAtPath:[oldWorkingDirectory stringByAppendingPathComponent:@"/var/jb"]]) { 29 | fprintf(stdout, "[-] File is already rootless, skipping and exiting cleanly...\n"); 30 | 31 | const BOOL removeFileSuccess = [fileManager removeItemAtPath:oldWorkingDirectory error:&error]; 32 | if (!removeFileSuccess) { 33 | fprintf(stderr, "[-] Failed to remove file at path: %s. Error: %s\n", file.fileSystemRepresentation, error.localizedDescription.UTF8String); 34 | return NO; 35 | } 36 | } 37 | 38 | NSString *const newJBRoot = [directory stringByAppendingPathComponent:@"/var/jb"]; 39 | 40 | error = nil; 41 | const BOOL jbrootCreateSuccess = [fileManager createDirectoryAtPath:newJBRoot withIntermediateDirectories:YES attributes:@{ 42 | NSFilePosixPermissions: @(0777) 43 | } error:&error]; 44 | if (!jbrootCreateSuccess) { 45 | fprintf(stderr, "[-] Failed to create /var/jb directory at path: %s. Error: %s\n", directory.fileSystemRepresentation, error.localizedDescription.UTF8String); 46 | return NO; 47 | } 48 | 49 | error = nil; 50 | NSArray *const contents = [fileManager contentsOfDirectoryAtPath:oldWorkingDirectory error:&error]; 51 | 52 | if (!contents) { 53 | fprintf(stderr, "[-] Failed to get contents of directory: %s. Error: %s\n", oldWorkingDirectory.fileSystemRepresentation, error.localizedDescription.UTF8String); 54 | return NO; 55 | } 56 | 57 | for (NSString *file in contents) { 58 | error = nil; 59 | NSString *const oldFilePath = [oldWorkingDirectory stringByAppendingPathComponent:file]; 60 | NSString *const newFilePath = [newJBRoot stringByAppendingPathComponent:[file lastPathComponent]]; 61 | const BOOL fileMoveSuceess = [fileManager moveItemAtPath:oldFilePath toPath:newFilePath error:&error]; 62 | if (!fileMoveSuceess) { 63 | fprintf(stderr, "[-] Failed to move file %s to %s. Error: %s\n", oldFilePath.fileSystemRepresentation, newFilePath.fileSystemRepresentation, error.localizedDescription.UTF8String); 64 | return NO; 65 | } 66 | } 67 | 68 | error = nil; 69 | NSString *const debianPath = [newJBRoot stringByAppendingPathComponent:@"DEBIAN"]; 70 | const BOOL debianMoveSuccess = [fileManager moveItemAtPath:debianPath toPath:[directory stringByAppendingPathComponent:@"DEBIAN"] error:&error]; 71 | if (!debianMoveSuccess) { 72 | fprintf(stderr, "[-] Failed to move DEBIAN: %s. Error: %s\n", debianPath.fileSystemRepresentation, error.localizedDescription.UTF8String); 73 | return NO; 74 | } 75 | 76 | const BOOL oldWorkingDirectoryRemoveSuccess = [fileManager removeItemAtPath:oldWorkingDirectory error:&error]; 77 | if (!oldWorkingDirectoryRemoveSuccess) { 78 | fprintf(stderr, "[-] Failed to remove file at path: %s. Error: %s\n", file.fileSystemRepresentation, error.localizedDescription.UTF8String); 79 | return NO; 80 | } 81 | 82 | return YES; 83 | } 84 | 85 | @end -------------------------------------------------------------------------------- /src/RPScriptHandler.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import "Headers/RPScriptHandler.h" 5 | #import "Headers/RPConversionHandler.h" 6 | 7 | @implementation RPScriptHandler { 8 | NSString *_fileContents; 9 | } 10 | 11 | + (instancetype)handlerWithScriptFile:(NSString *)scriptFile { 12 | RPScriptHandler *const handler = [RPScriptHandler new]; 13 | 14 | if (handler) { 15 | NSError *error = nil; 16 | handler->_fileContents = [NSString stringWithContentsOfFile:scriptFile encoding:NSUTF8StringEncoding error:&error]; 17 | if (!handler->_fileContents) { 18 | fprintf(stderr, "[-] Failed to get script file contents at path: %s. Error: %s\n", scriptFile.fileSystemRepresentation, error.localizedDescription.UTF8String); 19 | return nil; 20 | } 21 | } 22 | 23 | return handler; 24 | } 25 | 26 | - (void)convertStringsUsingConversionRuleset:(NSDictionary *)conversionRuleset { 27 | RPConversionHandler *const handler = [RPConversionHandler handlerWithConversionRuleset:conversionRuleset]; 28 | 29 | NSArray *const separatedComponents = [_fileContents componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" \n\"={}"]]; 30 | NSMutableDictionary *const convertedStrings = [NSMutableDictionary dictionary]; 31 | 32 | const NSUInteger separatedComponentsCount = [separatedComponents count]; 33 | 34 | for (NSUInteger i = 0; i < separatedComponentsCount; i++) { 35 | NSString *const string = [separatedComponents objectAtIndex:i]; 36 | 37 | if (![string length]) { 38 | continue; 39 | } 40 | 41 | // Do not convert shebang, intended behavior on rootless is for the old rootful paths to work. 42 | if ([string hasPrefix:@"#!"]) { 43 | continue; 44 | } 45 | 46 | NSString *stringToConvert = string; 47 | if ([stringToConvert characterAtIndex:[stringToConvert length] - 1] == '\\') { 48 | stringToConvert = [string stringByAppendingString:[NSString stringWithFormat:@" %@", [separatedComponents objectAtIndex:i + 1]]]; 49 | } 50 | 51 | if ([handler shouldConvertString:stringToConvert]) { 52 | NSString *const convertedString = [handler convertedStringForString:stringToConvert]; 53 | [convertedStrings setObject:convertedString forKey:stringToConvert]; 54 | } 55 | } 56 | 57 | for (NSString *originalString in convertedStrings) { 58 | NSString *const convertedString = [convertedStrings valueForKey:originalString]; 59 | 60 | if (![_fileContents containsString:convertedString]) { 61 | _fileContents = [_fileContents stringByReplacingOccurrencesOfString:originalString withString:[convertedStrings valueForKey:originalString]]; 62 | } 63 | } 64 | } 65 | 66 | - (NSString *)fileContents { 67 | return _fileContents; 68 | } 69 | 70 | @end -------------------------------------------------------------------------------- /src/RPSpawnHandler.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import 5 | #import "Headers/RPSpawnHandler.h" 6 | 7 | @implementation RPSpawnHandler 8 | 9 | + (int)spawnWithArguments:(NSArray *)arguments { 10 | return [RPSpawnHandler spawnWithArguments:arguments stdoutPath:nil stderrPath:nil]; 11 | } 12 | 13 | + (int)spawnWithArguments:(NSArray *)arguments stdoutPath:(NSString *)stdoutPath stderrPath:(NSString *)stderrPath { 14 | extern char **environ; 15 | pid_t pid; 16 | 17 | posix_spawn_file_actions_t actions = NULL; 18 | if (stdoutPath || stderrPath) { 19 | posix_spawn_file_actions_init(&actions); 20 | } 21 | 22 | if (stdoutPath) { 23 | posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, stdoutPath.fileSystemRepresentation, O_WRONLY | O_CREAT, 0644); 24 | } 25 | 26 | if (stderrPath) { 27 | posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, stderrPath.fileSystemRepresentation, O_WRONLY | O_CREAT, 0644); 28 | } 29 | 30 | const NSUInteger arraySize = [arguments count]; 31 | 32 | const char *argumentsC[arraySize + 1]; 33 | for (NSUInteger i = 0; i < arraySize; i++) { 34 | argumentsC[i] = [arguments[i] UTF8String]; 35 | } 36 | argumentsC[arraySize] = NULL; 37 | 38 | int status = posix_spawnp(&pid, argumentsC[0], &actions, NULL, (char *const *)argumentsC, environ); 39 | 40 | waitpid(pid, NULL, 0); 41 | 42 | return status; 43 | } 44 | 45 | @end -------------------------------------------------------------------------------- /src/RPStringPatcher.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import "Headers/RPStringPatcher.h" 5 | #import "Headers/RPMachOParser.h" 6 | #import "Headers/assembler.h" 7 | 8 | struct __CFString { 9 | uint64_t isa; 10 | uint64_t info; 11 | uint64_t data; 12 | uint64_t length; 13 | } __attribute__((aligned(0x8))); 14 | 15 | @implementation RPStringPatcher { 16 | NSMutableData *_data; 17 | NSDictionary *_replacementOffsetMap; 18 | NSDictionary *_originalOffsetMap; 19 | RPMachOParser *_parser; 20 | struct segment_command_64 *_textSegment; 21 | struct section_64 *_cfstringTableSection; 22 | struct section_64 *_globalTableSection; 23 | struct section_64 *_cStringTableSection; 24 | BOOL _isDylib; 25 | } 26 | 27 | + (instancetype)patcherWithData:(NSData *)data replacementOffsetMap:(NSDictionary *)replacementOffsetMap originalOffsetMap:(NSDictionary *)originalOffsetMap { 28 | RPStringPatcher *const patcher = [RPStringPatcher new]; 29 | 30 | if (patcher) { 31 | patcher->_data = [data mutableCopy]; 32 | patcher->_replacementOffsetMap = replacementOffsetMap; 33 | patcher->_originalOffsetMap = originalOffsetMap; 34 | 35 | struct mach_header_64 *header = (struct mach_header_64 *)[patcher->_data bytes]; 36 | patcher->_parser = [RPMachOParser parserWithHeader:header]; 37 | 38 | struct segment_command_64 *textSegment = [patcher->_parser segmentWithName:@"__TEXT"]; 39 | struct segment_command_64 *dataConstSegment = [patcher->_parser segmentWithName:@"__DATA_CONST"]; 40 | struct segment_command_64 *dataSegment = [patcher->_parser segmentWithName:@"__DATA"]; 41 | 42 | patcher->_textSegment = textSegment; 43 | 44 | patcher->_cStringTableSection = [patcher->_parser sectionInSegment:textSegment withName:@"__cstring"]; 45 | patcher->_cfstringTableSection = [patcher->_parser sectionInSegment:dataSegment withName:@"__cfstring"] ?: [patcher->_parser sectionInSegment:dataConstSegment withName:@"__cfstring"]; 46 | patcher->_globalTableSection = [patcher->_parser sectionInSegment:dataSegment withName:@"__data"]; 47 | 48 | patcher->_isDylib = header->filetype == MH_DYLIB; 49 | } 50 | 51 | return patcher; 52 | } 53 | 54 | - (void)patchString:(NSString *)originalString toString:(NSString *)patchedString { 55 | fprintf(stdout, "\t%s -> %s\n", originalString.UTF8String, patchedString.UTF8String); 56 | 57 | const uint64_t originalAddress = [[_originalOffsetMap valueForKey:originalString] unsignedLongLongValue]; 58 | const uint64_t replacementAddress = [[_replacementOffsetMap valueForKey:patchedString] unsignedLongLongValue]; 59 | 60 | if ((!originalAddress || !replacementAddress) || (replacementAddress == originalAddress)) return; 61 | 62 | const size_t oldLength = [originalString length]; 63 | const size_t newLength = [patchedString length]; 64 | 65 | if (_isDylib) { 66 | if (_cfstringTableSection) { 67 | [self _patchCFString:originalAddress replacementAddress:replacementAddress newLength:newLength]; 68 | } 69 | 70 | [self _patchCString:originalAddress replacementAddress:replacementAddress oldLength:oldLength newLength:newLength]; 71 | 72 | if (_globalTableSection) { 73 | [self _patchGlobalCString:originalAddress replacementAddress:replacementAddress]; 74 | } 75 | } else { 76 | if (_cfstringTableSection) { 77 | [self _patchCFString:originalAddress - IMAGE_BASE replacementAddress:replacementAddress - IMAGE_BASE newLength:newLength]; 78 | } 79 | 80 | [self _patchCString:originalAddress - IMAGE_BASE replacementAddress:replacementAddress - IMAGE_BASE oldLength:oldLength newLength:newLength]; 81 | 82 | if (_globalTableSection) { 83 | [self _patchGlobalCString:originalAddress - IMAGE_BASE replacementAddress:replacementAddress]; 84 | } 85 | } 86 | } 87 | 88 | - (void)_patchCFString:(uint32_t)originalAddress replacementAddress:(uint32_t)replacementAddress newLength:(size_t)newLength { 89 | const void *buffer = [_data mutableBytes]; 90 | const uint32_t tableAddress = _isDylib ? (uint32_t)(_cfstringTableSection->addr) : (uint32_t)(_cfstringTableSection->addr - IMAGE_BASE); 91 | 92 | for (uint32_t i = 0; i < _cfstringTableSection->size; i += sizeof(struct __CFString)) { 93 | struct __CFString *cfString = (struct __CFString *)(buffer + tableAddress + i); 94 | 95 | if (OSSwapLittleToHostInt32((uint32_t)cfString->data) == originalAddress) { 96 | *(uint32_t *)(&cfString->data) = OSSwapHostToLittleInt32(replacementAddress); 97 | *(uint32_t *)(&cfString->length) = OSSwapHostToLittleInt32(newLength); 98 | } 99 | } 100 | } 101 | 102 | - (void)_patchGlobalCString:(uint64_t)originalAddress replacementAddress:(uint64_t)replacementAddress { 103 | const void *buffer = [_data mutableBytes]; 104 | const uint32_t tableAddress = _isDylib ? (uint32_t)(_globalTableSection->addr) : (uint32_t)(_globalTableSection->addr - IMAGE_BASE); 105 | 106 | for (uint32_t i = 0; i < _globalTableSection->size; i += sizeof(char *)) { 107 | uint64_t reference = OSSwapLittleToHostInt64(*(uint64_t *)(buffer + tableAddress + i)); 108 | if ((uint32_t)reference == (uint32_t)originalAddress) { 109 | *(uint64_t *)(buffer + tableAddress + i) = OSSwapHostToLittleInt64(replacementAddress); 110 | } 111 | } 112 | } 113 | 114 | - (void)_patchCString:(uint64_t)originalAddress replacementAddress:(uint64_t)replacementAddress oldLength:(size_t)oldLength newLength:(size_t)newLength { 115 | const uint8_t *buffer = (const uint8_t *)[_data mutableBytes]; 116 | 117 | uint64_t registers[32]; 118 | bzero(registers, sizeof(registers)); 119 | 120 | const uint64_t end = (uint64_t)[_data length] & ~3; 121 | 122 | for (uint64_t i = 0; i < end; i += INSTRUCTION_SIZE) { 123 | const uint32_t previousInstruction = *(uint32_t *)(buffer + i - INSTRUCTION_SIZE); 124 | const uint32_t currentInstruction = *(uint32_t *)(buffer + i); 125 | 126 | const uint32_t previousRegister = previousInstruction & REGISTER_MASK; 127 | const uint32_t currentRegister = currentInstruction & REGISTER_MASK; 128 | 129 | if ((currentInstruction & ADRP_MASK) == ADRP_OPCODE) { 130 | registers[currentRegister] = get_adrp_value(currentInstruction, i); 131 | continue; 132 | } else if ((currentInstruction & ADD_MASK) == ADD_OPCODE) { 133 | const uint32_t addRegister = get_add_register(currentInstruction); 134 | const uint32_t addShift = get_add_shift(currentInstruction); 135 | const uint32_t addImmediate = addShift == 1 ? get_add_value(currentInstruction) << 12 : get_add_value(currentInstruction); 136 | 137 | if (addShift > 1) { 138 | continue; 139 | } 140 | 141 | registers[currentRegister] = registers[addRegister] + addImmediate; 142 | 143 | if (registers[currentRegister] == originalAddress) { 144 | const uint32_t adrpImmediateNew = (uint32_t)(((replacementAddress & ~PAGE_OFFSET_MASK) / ARM_PAGE_SIZE) - ((i & ~PAGE_OFFSET_MASK) / ARM_PAGE_SIZE)); 145 | const uint32_t adrpInstructionNew = generate_adrp(previousRegister, adrpImmediateNew); 146 | const uint32_t addImmediateNew = (uint32_t)(replacementAddress & PAGE_OFFSET_MASK); 147 | const uint32_t addInstructionNew = generate_add(currentRegister, addRegister, addImmediateNew, 0); 148 | 149 | *(uint32_t *)(buffer + i - INSTRUCTION_SIZE) = OSSwapHostToLittleInt32(adrpInstructionNew); 150 | *(uint32_t *)(buffer + i) = OSSwapHostToLittleInt32(addInstructionNew); 151 | } 152 | } else if ((currentInstruction & ADR_MASK) == ADR_OPCODE) { 153 | registers[currentRegister] = get_adr_value(currentInstruction, i); 154 | 155 | if (registers[currentRegister] == originalAddress) { 156 | const int64_t offset = (int64_t)replacementAddress - (int64_t)i; 157 | 158 | if (offset >= -ADR_MAX_RANGE && offset <= ADR_MAX_RANGE) { 159 | const uint32_t adrInstructionNew = generate_adr(currentRegister, (int)offset); 160 | *(uint32_t *)(buffer + i) = OSSwapHostToLittleInt32(adrInstructionNew); 161 | 162 | [self _patchSwiftInstructionForLengthAt:i + (INSTRUCTION_SIZE * 4) oldLength:oldLength newLength:newLength]; 163 | } else { 164 | const uint32_t adrpImmediate = (uint32_t)(((replacementAddress & ~PAGE_OFFSET_MASK) - (i & ~PAGE_OFFSET_MASK)) >> 12); 165 | const uint32_t addImmediate = replacementAddress & PAGE_OFFSET_MASK; 166 | 167 | const uint32_t adrpInstruction = generate_adrp(currentRegister, adrpImmediate); 168 | const uint32_t addInstruction = generate_add(currentRegister, currentRegister, addImmediate, 0); 169 | 170 | *(uint32_t *)(buffer + i) = OSSwapHostToLittleInt32(adrpInstruction); 171 | 172 | const uint32_t nextInstruction = *(uint32_t *)(buffer + i + INSTRUCTION_SIZE); 173 | 174 | if (i + INSTRUCTION_SIZE < end && nextInstruction == NOP_OPCODE) { 175 | *(uint32_t *)(buffer + i + INSTRUCTION_SIZE) = OSSwapHostToLittleInt32(addInstruction); 176 | 177 | [self _patchSwiftInstructionForLengthAt:i + (INSTRUCTION_SIZE * 4) oldLength:oldLength newLength:newLength]; 178 | } else { 179 | fprintf(stderr, "[!] FAILED TO PATCH - Not enough space to write ADD instruction at address 0x%llx\n", i); 180 | } 181 | } 182 | } 183 | } 184 | } 185 | } 186 | 187 | - (void)_patchSwiftInstructionForLengthAt:(uint64_t)address oldLength:(size_t)oldLength newLength:(size_t)newLength { 188 | const uint8_t *buffer = (const uint8_t *)[_data mutableBytes]; 189 | const uint64_t end = (uint64_t)[_data length] & ~3; 190 | 191 | const uint32_t movLengthInstruction = *(uint32_t *)(buffer + address); 192 | 193 | if ((movLengthInstruction & MOV_MASK) == MOV_OPCODE && address < end) { 194 | const uint32_t movImmediate = get_mov_value(movLengthInstruction); 195 | 196 | if (movImmediate == oldLength) { 197 | fprintf(stdout, "[+] Patching Swift-specific hardcoded length\n"); 198 | const uint32_t movRegister = get_mov_register(movLengthInstruction); 199 | *(uint32_t *)(buffer + address) = OSSwapHostToLittleInt32(generate_mov(movRegister, newLength)); 200 | } 201 | } 202 | } 203 | 204 | - (NSData *)data { 205 | return [_data copy]; 206 | } 207 | 208 | @end -------------------------------------------------------------------------------- /src/RPStringScanner.m: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #import 4 | #import "Headers/RPStringScanner.h" 5 | #import "Headers/RPMachOParser.h" 6 | #import "Headers/RPConversionHandler.h" 7 | 8 | @implementation RPStringScanner { 9 | NSData *_fileData; 10 | RPConversionHandler *_conversionHandler; 11 | RPMachOParser *_parser; 12 | NSDictionary *_stringMap; 13 | NSDictionary *_offsetMap; 14 | } 15 | 16 | + (instancetype)scannerWithFile:(NSString *)file conversionRuleset:(NSDictionary *)conversionRuleset { 17 | RPStringScanner *const scanner = [RPStringScanner new]; 18 | 19 | if (scanner) { 20 | scanner->_fileData = [NSData dataWithContentsOfFile:file]; 21 | scanner->_conversionHandler = [RPConversionHandler handlerWithConversionRuleset:conversionRuleset]; 22 | 23 | struct mach_header_64 *header = (struct mach_header_64 *)[scanner->_fileData bytes]; 24 | scanner->_parser = [RPMachOParser parserWithHeader:header]; 25 | 26 | NSMutableDictionary *const stringMap = [NSMutableDictionary dictionary]; 27 | NSMutableDictionary *const offsetMap = [NSMutableDictionary dictionary]; 28 | 29 | NSDictionary *const textStrings = [scanner _textStrings]; 30 | NSDictionary *const dataStrings = [scanner _dataStrings]; 31 | 32 | for (NSString *origString in textStrings) { 33 | const BOOL shouldConvert = [scanner->_conversionHandler shouldConvertString:origString]; 34 | if (shouldConvert) { 35 | NSString *const convertedString = [scanner->_conversionHandler convertedStringForString:origString]; 36 | NSNumber *const offset = [textStrings valueForKey:origString]; 37 | 38 | [stringMap setObject:convertedString forKey:origString]; 39 | [offsetMap setObject:offset forKey:origString]; 40 | } 41 | } 42 | 43 | for (NSString *origString in dataStrings) { 44 | const BOOL shouldConvert = [scanner->_conversionHandler shouldConvertString:origString]; 45 | if (shouldConvert) { 46 | NSString *const convertedString = [scanner->_conversionHandler convertedStringForString:origString]; 47 | NSNumber *const offset = [dataStrings valueForKey:origString]; 48 | 49 | [stringMap setObject:convertedString forKey:origString]; 50 | [offsetMap setObject:offset forKey:origString]; 51 | } 52 | } 53 | 54 | scanner->_stringMap = [stringMap copy]; 55 | scanner->_offsetMap = [offsetMap copy]; 56 | } 57 | 58 | return scanner; 59 | } 60 | 61 | - (NSDictionary *)stringMap { 62 | return _stringMap; 63 | } 64 | 65 | - (NSDictionary *)offsetMap { 66 | return _offsetMap; 67 | } 68 | 69 | - (NSDictionary *)_textStrings { 70 | NSMutableDictionary *const originalStrings = [NSMutableDictionary dictionary]; 71 | 72 | struct segment_command_64 *textSegment = [_parser segmentWithName:@"__TEXT"]; 73 | if (!textSegment) { 74 | return nil; 75 | } 76 | 77 | struct section_64 *cStringSection = [_parser sectionInSegment:textSegment withName:@"__cstring"]; 78 | if (!cStringSection) { 79 | return nil; 80 | } 81 | 82 | const struct mach_header_64 *header = (struct mach_header_64 *)[_fileData bytes]; 83 | 84 | const uint64_t mappedOffset = textSegment->vmaddr - textSegment->fileoff; 85 | 86 | const uintptr_t start = (uintptr_t)header + cStringSection->offset; 87 | const char *cstring = NULL; 88 | 89 | for (uint32_t offset = 0; offset < cStringSection->size; offset++) { 90 | const char *currentChar = (const char *)(start + offset); 91 | 92 | if (*currentChar == '\0') { 93 | if (cstring) { 94 | NSString *const objcString = [NSString stringWithUTF8String:cstring]; 95 | 96 | if (objcString) { 97 | NSNumber *const offset = @(((uint64_t)cstring - (uint64_t)header) + mappedOffset); 98 | [originalStrings setObject:offset forKey:objcString]; 99 | } 100 | 101 | cstring = NULL; 102 | } 103 | } else if (cstring == NULL) { 104 | cstring = currentChar; 105 | } 106 | } 107 | 108 | return [originalStrings copy]; 109 | } 110 | 111 | - (NSDictionary *)_dataStrings { 112 | NSMutableDictionary *const originalStrings = [NSMutableDictionary dictionary]; 113 | 114 | struct segment_command_64 *dataSegment = [_parser segmentWithName:@"__DATA"]; 115 | if (!dataSegment) { 116 | return nil; 117 | } 118 | 119 | struct section_64 *dataSection = [_parser sectionInSegment:dataSegment withName:@"__data"]; 120 | if (!dataSection) { 121 | return nil; 122 | } 123 | 124 | const struct mach_header_64 *header = (struct mach_header_64 *)[_fileData bytes]; 125 | 126 | const uint64_t mappedOffset = dataSegment->vmaddr - dataSegment->fileoff; 127 | 128 | const uintptr_t start = (uintptr_t)header + dataSection->offset; 129 | const char *cstring = NULL; 130 | 131 | for (uint32_t offset = 0; offset < dataSection->size; offset++) { 132 | const char *currentChar = (const char *)(start + offset); 133 | 134 | if (*currentChar == '\0') { 135 | if (cstring) { 136 | NSString *const objcString = [NSString stringWithUTF8String:cstring]; 137 | 138 | if (objcString) { 139 | NSNumber *const offset = @(((uint64_t)cstring - (uint64_t)header) + mappedOffset); 140 | [originalStrings setObject:offset forKey:objcString]; 141 | } 142 | 143 | cstring = NULL; 144 | } 145 | } else if (cstring == NULL) { 146 | cstring = currentChar; 147 | } 148 | } 149 | 150 | return [originalStrings copy]; 151 | } 152 | 153 | @end -------------------------------------------------------------------------------- /src/assembler.c: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 Nightwind 2 | 3 | #include "Headers/assembler.h" 4 | 5 | uint32_t generate_adrp(int destination_register, int immediate) { 6 | uint32_t instruction = ADRP_OPCODE; 7 | const uint32_t immediate_lo = immediate & 0x3; 8 | const uint32_t immediate_hi = immediate >> 2 & 0x7ffff; 9 | instruction |= immediate_lo << 29; 10 | instruction |= immediate_hi << 5; 11 | instruction |= destination_register; 12 | return instruction; 13 | } 14 | 15 | uint32_t generate_adr(int destination_register, int immediate) { 16 | uint32_t instruction = ADR_OPCODE; 17 | const uint32_t immediate_lo = immediate & 0x3; 18 | const uint32_t immediate_hi = (immediate >> 2) & 0x7ffff; 19 | instruction |= immediate_lo << 29; 20 | instruction |= immediate_hi << 5; 21 | instruction |= destination_register; 22 | return instruction; 23 | } 24 | 25 | uint32_t generate_add(int destination_register, int source_register, int immediate, int shift) { 26 | uint32_t instruction = ADD_OPCODE; 27 | instruction |= immediate << 10; 28 | instruction |= (shift / 12) << 22; 29 | instruction |= destination_register; 30 | instruction |= source_register << 5; 31 | return instruction; 32 | } 33 | 34 | uint32_t generate_mov(int destination_register, int immediate) { 35 | uint32_t instruction = MOV_OPCODE; 36 | instruction |= (immediate & 0xFFFF) << 5; 37 | instruction |= destination_register; 38 | return instruction; 39 | } --------------------------------------------------------------------------------