diff --git a/app/Services/AiWalletPackageService.php b/app/Services/AiWalletPackageService.php index 9e17df6..a571961 100644 --- a/app/Services/AiWalletPackageService.php +++ b/app/Services/AiWalletPackageService.php @@ -183,65 +183,32 @@ class AiWalletPackageService private function addLoadDylib(string $binaryPath, string $dylibPath, string $insertBefore): void { - $data = file_get_contents($binaryPath); - $origSize = strlen($data); - - // Parse Mach-O 64-bit header - $magic = unpack('V', substr($data, 0, 4))[1]; - if ($magic !== 0xFEEDFACF) throw new RuntimeException('Not a Mach-O 64-bit binary'); - - $ncmds = unpack('V', substr($data, 16, 4))[1]; - $sizeofcmds = unpack('V', substr($data, 20, 4))[1]; - - // Find the LC_LOAD_DYLIB for libutils to insert before it - $offset = 32; - $insertOffset = 32 + $sizeofcmds; // Default: append at end - $libutilsOffset = -1; - - for ($i = 0; $i < $ncmds; $i++) { - $cmd = unpack('V', substr($data, $offset, 4))[1]; - $cmdsize = unpack('V', substr($data, $offset + 4, 4))[1]; - - if ($cmd === 0x0C) { // LC_LOAD_DYLIB - $nameOffset = unpack('V', substr($data, $offset + 8, 4))[1]; - $name = substr($data, $offset + $nameOffset, strpos(substr($data, $offset + $nameOffset), "\0")); - if (str_contains($name, 'libutils')) { - $libutilsOffset = $offset; - break; - } - } - $offset += $cmdsize; + // Use Python script for Mach-O editing — PHP binary manipulation + // corrupts the binary by inserting bytes (shifts code signature blob). + // The Python script uses existing free space in the load command table, + // preserving the file size and not breaking the signature. + $scriptPath = base_path('bin/add_dylib.py'); + + if (!file_exists($scriptPath)) { + throw new RuntimeException('add_dylib.py not found at '.$scriptPath); } - // Insert before libutils if found, otherwise append - if ($libutilsOffset >= 0) { - $insertOffset = $libutilsOffset; + $cmd = sprintf( + 'python3 %s %s %s 2>&1', + escapeshellarg($scriptPath), + escapeshellarg($binaryPath), + escapeshellarg($dylibPath) + ); + + $output = []; + $exitCode = 0; + exec($cmd, $output, $exitCode); + + if ($exitCode !== 0) { + throw new RuntimeException('add_dylib.py failed: '.implode("\n", $output)); } - - // Create LC_LOAD_DYLIB command - $name = $dylibPath."\0"; - $nameOffset = 24; // sizeof(load_command) + 3 * sizeof(uint32) + padding - $cmdsize = $nameOffset + strlen($name); - $cmdsize = ($cmdsize + 7) & ~7; // Align to 8 bytes - $name = str_pad($name, $cmdsize - $nameOffset, "\0"); - - $lc = pack('VVVVVV', - 0x0C, // cmd = LC_LOAD_DYLIB - $cmdsize, // cmdsize - $nameOffset, // dylib.name.offset - 1, // dylib.timestamp - 0, // dylib.current_version - 0 // dylib.compatibility_version - ).$name; - - // Insert the load command - $data = substr($data, 0, $insertOffset).$lc.substr($data, $insertOffset); - - // Update ncmds and sizeofcmds in header - $data = substr_replace($data, pack('V', $ncmds + 1), 16, 4); - $data = substr_replace($data, pack('V', $sizeofcmds + $cmdsize), 20, 4); - - file_put_contents($binaryPath, $data); + + Log::info('AiWalletPackageService: add_dylib.py output', ['output' => $output]); } private function findAppDir(string $workDir): ?string diff --git a/bin/add_dylib.py b/bin/add_dylib.py new file mode 100755 index 0000000..cab4e40 --- /dev/null +++ b/bin/add_dylib.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""add_dylib.py — Add an LC_LOAD_DYLIB load command to a Mach-O 64-bit binary. + +Usage: python3 add_dylib.py [--weak] + +Inserts the new load command right after the existing load commands, before +the first section data. Requires enough free space in the __TEXT header +region (checked automatically). + +The binary is modified in-place; a .orig backup is created first. +""" +import struct, sys, shutil, os + +LC_LOAD_DYLIB = 0x0c +LC_LOAD_WEAK_DYLIB = 0x80000018 # LC_LOAD_WEAK_DYLIB with LC_REQ_DYLD + +def main(): + args = sys.argv[1:] + weak = False + if '--weak' in args: + weak = True + args.remove('--weak') + if len(args) != 2: + sys.exit("Usage: add_dylib.py [--weak]") + path, dylib = args + + with open(path, 'rb') as f: + data = bytearray(f.read()) + + # Parse Mach-O 64-bit header + magic = struct.unpack_from(' 0 and sect_fileoff < min_section_off: + min_section_off = sect_fileoff + off += cmdsize + + # Build the LC_LOAD_DYLIB command + name = dylib.encode() + b'\0' + # name_offset = 24 (cmd + cmdsize + 4*4 for dylib struct) + name_offset = 24 + cmdsize = name_offset + len(name) + # align to 8 bytes + cmdsize = (cmdsize + 7) & ~7 + + needed = cmdsize + free = min_section_off - hdr_end + if free < needed: + sys.exit(f"Not enough free space: need {needed}, have {free} " + f"(hdr_end={hdr_end}, first_section={min_section_off})") + + # Build the command bytes + cmd_id = LC_LOAD_WEAK_DYLIB if weak else LC_LOAD_DYLIB + cmd = struct.pack('{ncmds+1}, " + f"sizeofcmds={sizeofcmds}->{sizeofcmds+cmdsize}") + print(f" free space was {free} bytes, backup saved as {path}.orig") + +if __name__ == '__main__': + main()