Skip to content

feat: inject more frameworks #85

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions TrollFools.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
61EFA37B2D31165700159442 /* InjectorV3+Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61EFA37A2D31165000159442 /* InjectorV3+Backup.swift */; };
61EFA37D2D311DA800159442 /* InjectorV3+Inject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61EFA37C2D311DA300159442 /* InjectorV3+Inject.swift */; };
61F595622D6B42340034DD83 /* SwiftUIIntrospect-Static in Frameworks */ = {isa = PBXBuildFile; productRef = 61F595612D6B42340034DD83 /* SwiftUIIntrospect-Static */; };
86B80B3B2E4CE82500543291 /* LibraryManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86B80B3A2E4CE82500543291 /* LibraryManagerView.swift */; };
CC0D662D2D7F11A2000EADED /* ArArchiveKit in Frameworks */ = {isa = PBXBuildFile; productRef = CC0D662C2D7F11A2000EADED /* ArArchiveKit */; };
CC0D662F2D7F11AC000EADED /* ArArchiveKit in Frameworks */ = {isa = PBXBuildFile; productRef = CC0D662E2D7F11AC000EADED /* ArArchiveKit */; };
CC0D66322D7F13A9000EADED /* SWCompression in Frameworks */ = {isa = PBXBuildFile; productRef = CC0D66312D7F13A9000EADED /* SWCompression */; };
Expand Down Expand Up @@ -171,6 +172,7 @@
61EFA3782D3108B700159442 /* InjectorV3+Eject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InjectorV3+Eject.swift"; sourceTree = "<group>"; };
61EFA37A2D31165000159442 /* InjectorV3+Backup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InjectorV3+Backup.swift"; sourceTree = "<group>"; };
61EFA37C2D311DA300159442 /* InjectorV3+Inject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InjectorV3+Inject.swift"; sourceTree = "<group>"; };
86B80B3A2E4CE82500543291 /* LibraryManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryManagerView.swift; sourceTree = "<group>"; };
CC0E80FA2C54F84000B137B4 /* mv-15 */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = "mv-15"; sourceTree = "<group>"; };
CC1548CF2C4A6B8200A4173E /* ct_bypass */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = ct_bypass; sourceTree = "<group>"; };
CC1548D22C4A743200A4173E /* SuccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuccessView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -280,6 +282,7 @@
6124A0722CD2327E00C52253 /* View */ = {
isa = PBXGroup;
children = (
86B80B3A2E4CE82500543291 /* LibraryManagerView.swift */,
6124A0732CD2328600C52253 /* AppListCell.swift */,
CCF4706D2C4A4BAB008D8197 /* AppListView.swift */,
61C7D54B2D82DBAC0064D626 /* DisclaimerView.swift */,
Expand Down Expand Up @@ -673,6 +676,7 @@
61EFA3732D30347A00159442 /* InjectorV3+MachO.swift in Sources */,
614C0C682D7C72AD007E9184 /* AppListSearchModel.swift in Sources */,
614C0C622D7C2EC0007E9184 /* Constants.swift in Sources */,
86B80B3B2E4CE82500543291 /* LibraryManagerView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
14 changes: 14 additions & 0 deletions TrollFools/AppListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct AppListView: View {

@State var selectorOpenedURL: URLIdentifiable? = nil
@State var selectedIndex: String? = nil
@State private var isLibraryManagerPresented: Bool = false

@State var isWarningPresented = false
@State var temporaryOpenedURL: URLIdentifiable? = nil
Expand Down Expand Up @@ -259,6 +260,14 @@ struct AppListView: View {
}
}
}
ToolbarItem(placement: .navigationBarLeading) {
Button {
isLibraryManagerPresented = true
} label: {
Image(systemName: "shippingbox")
}
.accessibilityLabel(NSLocalizedString("Libraries", comment: ""))
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
appList.filter.showPatchedOnly.toggle()
Expand All @@ -276,6 +285,11 @@ struct AppListView: View {
.accessibilityLabel(NSLocalizedString("Show Patched Only", comment: ""))
}
}
.sheet(isPresented: $isLibraryManagerPresented) {
NavigationView {
LibraryManagerView()
}
}
}

var allAppGroup: some View {
Expand Down
174 changes: 157 additions & 17 deletions TrollFools/InjectorV3+Inject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@
import CocoaLumberjackSwift
import Foundation

fileprivate var gCachedLibraryIndex: [String: InjectorV3.LibraryModuleEntry] = [:]
fileprivate var gPreparedLibraryURLs: [ObjectIdentifier: [String: URL]] = [:]
fileprivate let gLibraryAliasMap: [String: String] = [
"ellekit": "CydiaSubstrate",
"ellekit.framework": "CydiaSubstrate",
"libellekit.dylib": "CydiaSubstrate",
"libsubstitute.dylib": "CydiaSubstrate",
"libsubstrate.dylib": "CydiaSubstrate",
"cydiasubstrate": "CydiaSubstrate",
"cydiasubstrate.framework": "CydiaSubstrate",
]

extension InjectorV3 {
enum Strategy: String, CaseIterable {
case lexicographic
Expand Down Expand Up @@ -60,12 +72,29 @@ extension InjectorV3 {
return
}

Self.cachedLibraryIndex = [:]
Self.buildLibraryIndexIfNeeded()

try assetURLs.forEach {
try standardizeLoadCommandDylibToSubstrate($0)
try standardizeLoadCommandDylibToLocalLibrary($0)
try applyCoreTrustBypass($0)
}

let substrateFwkURL = try prepareSubstrate()
var allNeededKeys: Set<String> = []
for assetURL in assetURLs {
let machO: URL = try checkIsBundle(assetURL) ? locateExecutableInBundle(assetURL) : assetURL
let dylibs = try loadedDylibsOfMachO(machO)
for imported in dylibs {
if let (rawKey, _) = libraryKey(fromImportedPath: imported) {
let lowered = rawKey.lowercased()
let destKey = Self.libraryAliasMap[lowered] ?? rawKey
if Self.cachedLibraryIndex[destKey.lowercased()] != nil {
allNeededKeys.insert(destKey)
}
}
}
}
let preparedLibs = try prepareLibraryModulesIfNeeded(keys: allNeededKeys)
guard let targetMachO = try locateAvailableMachO() else {
DDLogError("All Mach-Os are protected", ddlog: logger)

Expand All @@ -74,7 +103,7 @@ extension InjectorV3 {

DDLogInfo("Best matched Mach-O is \(targetMachO.path)", ddlog: logger)

let resourceURLs: [URL] = [substrateFwkURL] + assetURLs
let resourceURLs: [URL] = preparedLibs + assetURLs
try makeAlternate(targetMachO)
do {
try copyfiles(resourceURLs)
Expand Down Expand Up @@ -105,25 +134,117 @@ extension InjectorV3 {
try cmdChangeOwnerToInstalld(target, recursively: isFramework)
}

// MARK: - Cydia Substrate
// MARK: - Library Replace

fileprivate struct LibraryModuleEntry {
enum Kind { case framework, dylib }
let kind: Kind
let key: String
let zipURL: URL
}

fileprivate static var cachedLibraryIndex: [String: LibraryModuleEntry] {
get { gCachedLibraryIndex }
set { gCachedLibraryIndex = newValue }
}
fileprivate var preparedLibraryURLs: [String: URL] {
get { gPreparedLibraryURLs[ObjectIdentifier(self)] ?? [:] }
set { gPreparedLibraryURLs[ObjectIdentifier(self)] = newValue }
}

fileprivate static let substrateZipURL = findResource(substrateFwkName, fileExtension: "zip")
fileprivate static var libraryAliasMap: [String: String] { gLibraryAliasMap }

fileprivate func prepareSubstrate() throws -> URL {
try FileManager.default.unzipItem(at: Self.substrateZipURL, to: temporaryDirectoryURL)
fileprivate static func buildLibraryIndexIfNeeded() {
if !cachedLibraryIndex.isEmpty { return }

let fwkURL = temporaryDirectoryURL.appendingPathComponent(Self.substrateFwkName)
try markBundlesAsInjected([fwkURL], privileged: false)
var index: [String: LibraryModuleEntry] = [:]

let machO = fwkURL.appendingPathComponent(Self.substrateName)
let searchRoots: [URL] = [Bundle.main.bundleURL, userLibrariesDirectoryURL]
for root in searchRoots {
if root == userLibrariesDirectoryURL {
// 确保用户库目录存在
try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
}
guard let enumerator = FileManager.default.enumerator(at: root, includingPropertiesForKeys: [.isRegularFileKey]) else { continue }
for case let fileURL as URL in enumerator {
guard let isRegular = try? fileURL.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile, isRegular == true else { continue }
let name = fileURL.lastPathComponent
if name.hasSuffix(".framework.zip") {
let moduleName = String(name.dropLast(".framework.zip".count))
// 用户库优先覆盖内置
index[moduleName.lowercased()] = LibraryModuleEntry(kind: .framework, key: moduleName, zipURL: fileURL)
} else if name.hasSuffix(".dylib.zip") {
let dylibName = String(name.dropLast(".zip".count))
index[dylibName.lowercased()] = LibraryModuleEntry(kind: .dylib, key: dylibName, zipURL: fileURL)
}
}
}

try cmdCoreTrustBypass(machO, teamID: teamID)
try cmdChangeOwnerToInstalld(fwkURL, recursively: true)
cachedLibraryIndex = index
}

/// 用户自定义库目录:App Support/<bundle-id>/Libraries
fileprivate static var userLibrariesDirectoryURL: URL {
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
return base.appendingPathComponent(gTrollFoolsIdentifier, isDirectory: true)
.appendingPathComponent("Libraries", isDirectory: true)
}

fileprivate func libraryKey(fromImportedPath imported: String) -> (key: String, kind: LibraryModuleEntry.Kind)? {
let lower = imported.lowercased()
if let range = lower.range(of: ".framework/") {
let prefix = lower[..<range.lowerBound]
if let lastSlash = prefix.lastIndex(of: "/") {
let start = lower.index(after: lastSlash)
let name = lower[start..<range.lowerBound]
return (String(name), .framework)
} else {
let name = lower[..<range.lowerBound]
return (String(name), .framework)
}
}
if let lastSlash = lower.lastIndex(of: "/") {
let fileName = String(lower[lower.index(after: lastSlash)...])
if fileName.hasSuffix(".dylib") { return (fileName, .dylib) }
} else if lower.hasSuffix(".dylib") {
return (lower, .dylib)
}
return nil
}

return fwkURL
fileprivate func prepareLibraryModulesIfNeeded(keys: Set<String>) throws -> [URL] {
Self.buildLibraryIndexIfNeeded()
var prepared: [URL] = []
for rawKey in keys {
let key = rawKey.lowercased()
guard let entry = Self.cachedLibraryIndex[key] else { continue }
if let existing = preparedLibraryURLs[key] {
prepared.append(existing)
continue
}
try FileManager.default.unzipItem(at: entry.zipURL, to: temporaryDirectoryURL)
let targetURL: URL
switch entry.kind {
case .framework:
let fwkURL = temporaryDirectoryURL.appendingPathComponent("\(entry.key).framework")
targetURL = fwkURL
try markBundlesAsInjected([fwkURL], privileged: false)
let macho = fwkURL.appendingPathComponent(entry.key)
try cmdCoreTrustBypass(macho, teamID: teamID)
try cmdChangeOwnerToInstalld(fwkURL, recursively: true)
case .dylib:
let dylibURL = temporaryDirectoryURL.appendingPathComponent(entry.key)
targetURL = dylibURL
try cmdCoreTrustBypass(dylibURL, teamID: teamID)
try cmdChangeOwnerToInstalld(dylibURL, recursively: false)
}
preparedLibraryURLs[key] = targetURL
prepared.append(targetURL)
}
return prepared
}

fileprivate func standardizeLoadCommandDylibToSubstrate(_ assetURL: URL) throws {
fileprivate func standardizeLoadCommandDylibToLocalLibrary(_ assetURL: URL) throws {
let machO: URL
if checkIsBundle(assetURL) {
machO = try locateExecutableInBundle(assetURL)
Expand All @@ -132,10 +253,29 @@ extension InjectorV3 {
}

let dylibs = try loadedDylibsOfMachO(machO)
for dylib in dylibs {
if Self.ignoredDylibAndFrameworkNames.firstIndex(where: { dylib.lowercased().hasSuffix("/\($0)") }) != nil {
try cmdChangeLoadCommandDylib(machO, from: dylib, to: "@executable_path/Frameworks/\(Self.substrateFwkName)/\(Self.substrateName)")
var neededKeys: Set<String> = []
for imported in dylibs {
if let (rawKey, _) = libraryKey(fromImportedPath: imported) {
let lower = rawKey.lowercased()
let destKey = Self.libraryAliasMap[lower] ?? rawKey
if Self.cachedLibraryIndex[destKey.lowercased()] != nil {
neededKeys.insert(destKey)
}
}
}
let _ = try prepareLibraryModulesIfNeeded(keys: neededKeys)
for imported in dylibs {
guard let (rawKey, _) = libraryKey(fromImportedPath: imported) else { continue }
let destKey = Self.libraryAliasMap[rawKey.lowercased()] ?? rawKey
guard let entry = Self.cachedLibraryIndex[destKey.lowercased()] else { continue }
let newName: String
switch entry.kind {
case .framework:
newName = "@executable_path/Frameworks/\(entry.key).framework/\(entry.key)"
case .dylib:
newName = "@executable_path/Frameworks/\(entry.key)"
}
try cmdChangeLoadCommandDylib(machO, from: imported, to: newName)
}
}

Expand Down
Loading