// // DylibInjector.swift // WeChatTweak // // Injects LC_LOAD_DYLIB into Mach-O binaries. // import Darwin import MachO import Foundation struct DylibInjector { enum Error: Swift.Error, LocalizedError { case invalidFile case not64BitMachO(magic: UInt32) case dylibAlreadyInjected case noSpaceForLoadCommand(available: Int, required: Int) var errorDescription: String? { switch self { case .invalidFile: return "Invalid Mach-O file" case .not64BitMachO(let magic): return "Not a 64-bit Mach-O (magic: \(String(format: "0x%x", magic)))" case .dylibAlreadyInjected: return "Dylib already injected" case .noSpaceForLoadCommand(let available, let required): return "No space for load command (available: \(available), required: \(required))" } } } static let dylibPath = "@executable_path/WeChatTweakPlugin.dylib" static func inject(binary: URL) throws { guard FileManager.default.fileExists(atPath: binary.path) else { throw Error.invalidFile } let fh = try FileHandle(forUpdating: binary) defer { try? fh.close() } guard let magicData = try fh.read(upToCount: 4), magicData.count == 4 else { throw Error.invalidFile } let magicBE = magicData.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian } let isSwappedFat = (magicBE == FAT_CIGAM) if magicBE == FAT_MAGIC || magicBE == FAT_CIGAM { guard let nfatData = try fh.read(upToCount: 4), nfatData.count == 4 else { throw Error.invalidFile } let rawNfat = nfatData.withUnsafeBytes { $0.load(as: UInt32.self) } let nfat = isSwappedFat ? UInt32(littleEndian: rawNfat) : UInt32(bigEndian: rawNfat) var sliceOffsets: [UInt64] = [] for _ in 0..= 0 && nameStart < rest.count { let nameData = rest[nameStart...] if let end = nameData.firstIndex(of: 0) { let name = String(data: nameData[nameStart.. 0 { minSectionFileOffset = min(minSectionFileOffset, UInt64(sectFileOff)) } } } lcOffset += UInt64(cmdsize) } // Calculate available space let endOfLC = UInt64(32 + sizeofcmds) // relative to slice start let available: Int if minSectionFileOffset != UInt64.max { available = Int(minSectionFileOffset) - Int(endOfLC) } else { available = 0 } // Build dylib_command let dylibCmd = buildDylibCommand(path: dylibPath) guard available >= dylibCmd.count else { throw Error.noSpaceForLoadCommand(available: available, required: dylibCmd.count) } // Write dylib_command at end of existing load commands try fh.seek(toOffset: sliceOffset + endOfLC) try fh.write(contentsOf: dylibCmd) // Update header: ncmds and sizeofcmds let newNcmds = ncmds + 1 let newSizeofcmds = sizeofcmds + UInt32(dylibCmd.count) var ncmdsBytes = newNcmds.littleEndian var sizeBytes = newSizeofcmds.littleEndian try fh.seek(toOffset: sliceOffset + 16) try fh.write(contentsOf: Data(bytes: &ncmdsBytes, count: 4)) try fh.write(contentsOf: Data(bytes: &sizeBytes, count: 4)) print("LC_LOAD_DYLIB injected at slice offset \(String(format: "0x%llx", sliceOffset))") } private static func buildDylibCommand(path: String) -> Data { let pathBytes = Array(path.utf8) + [0] // null-terminated let fixedSize = 24 // cmd(4) + cmdsize(4) + name_offset(4) + timestamp(4) + cur_ver(4) + compat_ver(4) let rawSize = fixedSize + pathBytes.count let cmdsize = (rawSize + 7) & ~7 // align to 8 var data = Data(capacity: cmdsize) // cmd: LC_LOAD_DYLIB = 0x0C var cmd: UInt32 = UInt32(LC_LOAD_DYLIB).littleEndian data.append(Data(bytes: &cmd, count: 4)) // cmdsize var size: UInt32 = UInt32(cmdsize).littleEndian data.append(Data(bytes: &size, count: 4)) // name offset (from start of command) var nameOffset: UInt32 = UInt32(24).littleEndian data.append(Data(bytes: &nameOffset, count: 4)) // timestamp var timestamp: UInt32 = 0 data.append(Data(bytes: ×tamp, count: 4)) // current_version var currentVersion: UInt32 = 0 data.append(Data(bytes: ¤tVersion, count: 4)) // compatibility_version var compatVersion: UInt32 = 0 data.append(Data(bytes: &compatVersion, count: 4)) // path string data.append(contentsOf: pathBytes) // padding let padding = cmdsize - rawSize if padding > 0 { data.append(Data(repeating: 0, count: padding)) } return data } }