Skip to content

Commit 8a832b4

Browse files
HuuDunggdungth
authored andcommitted
fix: support injection into Unity apps with dynamically loaded frameworks
Unity-based apps (e.g. Arena of Valor) do not statically link their frameworks via LC_LOAD_DYLIB. Instead, they load frameworks at runtime using dlopen(). This caused frameworkMachOsInBundle() to return an empty intersection, leaving only the main executable (which is typically encrypted) as a candidate. Changes: - Add fallback in frameworkMachOsInBundle(): when the intersection of linked dylibs and enumerated Frameworks/ is empty, use all valid Mach-O files found in Frameworks/ as candidates. - Also scan bare .dylib files at level 1 in Frameworks/. - Add detailed logging in locateAvailableMachO() to report each candidate's encryption status and file size for easier debugging.
1 parent cb3f0a8 commit 8a832b4

File tree

2 files changed

+54
-3
lines changed

2 files changed

+54
-3
lines changed

TrollFools/InjectorV3+Bundle.swift

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,16 @@ extension InjectorV3 {
4343
precondition(isMachO(executableURL), "Not a Mach-O: \(executableURL.path)")
4444

4545
let frameworksURL = target.appendingPathComponent("Frameworks")
46+
let frameworksExist = FileManager.default.fileExists(atPath: frameworksURL.path)
47+
48+
DDLogInfo("Scanning Mach-Os in \(target.lastPathComponent), Frameworks exists: \(frameworksExist)", ddlog: logger)
49+
4650
let linkedDylibs = try linkedDylibsRecursivelyOfMachO(executableURL)
51+
DDLogInfo("Linked dylibs (\(linkedDylibs.count)): \(linkedDylibs.map { $0.lastPathComponent })", ddlog: logger)
4752

4853
var enumeratedURLs = OrderedSet<URL>()
54+
var allMachOsInFrameworks = OrderedSet<URL>()
55+
4956
if let enumerator = FileManager.default.enumerator(
5057
at: frameworksURL,
5158
includingPropertiesForKeys: [.fileSizeKey],
@@ -58,11 +65,30 @@ extension InjectorV3 {
5865
}
5966
if enumerator.level == 2 {
6067
enumeratedURLs.append(itemURL)
68+
if isMachO(itemURL) {
69+
allMachOsInFrameworks.append(itemURL)
70+
}
71+
}
72+
// Scan bare dylibs at level 1 (directly in Frameworks/)
73+
if enumerator.level == 1 && itemURL.pathExtension.lowercased() == "dylib" && isMachO(itemURL) {
74+
allMachOsInFrameworks.append(itemURL)
75+
enumeratedURLs.append(itemURL)
6176
}
6277
}
6378
}
6479

65-
let machOs = linkedDylibs.intersection(enumeratedURLs)
80+
DDLogInfo("Enumerated \(enumeratedURLs.count) items, \(allMachOsInFrameworks.count) Mach-Os in Frameworks/", ddlog: logger)
81+
82+
var machOs = linkedDylibs.intersection(enumeratedURLs)
83+
DDLogInfo("Intersection: \(machOs.count) linked Mach-Os in Frameworks/", ddlog: logger)
84+
85+
// Fallback: if none of the Mach-Os in Frameworks/ are statically linked
86+
// by the main binary (e.g. Unity apps use dlopen), use all available Mach-Os.
87+
if machOs.isEmpty && !allMachOsInFrameworks.isEmpty {
88+
DDLogWarn("No statically linked Mach-Os found, falling back to all \(allMachOsInFrameworks.count) Mach-Os in Frameworks/", ddlog: logger)
89+
machOs = allMachOsInFrameworks
90+
}
91+
6692
var sortedMachOs: [URL] =
6793
switch injectStrategy {
6894
case .lexicographic:

TrollFools/InjectorV3+Inject.swift

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,33 @@ extension InjectorV3 {
205205
// MARK: - Path Finder
206206

207207
fileprivate func locateAvailableMachO() throws -> URL? {
208-
try frameworkMachOsInBundle(bundleURL)
209-
.first { try !isProtectedMachO($0) }
208+
let allMachOs = try frameworkMachOsInBundle(bundleURL)
209+
210+
DDLogInfo("Mach-O scan: \(allMachOs.count) candidates in \(bundleURL.lastPathComponent)", ddlog: logger)
211+
212+
var selectedMachO: URL?
213+
for (index, machO) in allMachOs.enumerated() {
214+
let isProtected = (try? isProtectedMachO(machO)) ?? true
215+
let fileSize = (try? machO.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0
216+
let sizeStr = ByteCountFormatter.string(fromByteCount: Int64(fileSize), countStyle: .file)
217+
218+
if isProtected {
219+
DDLogInfo(" [\(index + 1)/\(allMachOs.count)] ENCRYPTED \(machO.lastPathComponent) (\(sizeStr))", ddlog: logger)
220+
} else {
221+
DDLogInfo(" [\(index + 1)/\(allMachOs.count)] AVAILABLE \(machO.lastPathComponent) (\(sizeStr))", ddlog: logger)
222+
if selectedMachO == nil {
223+
selectedMachO = machO
224+
}
225+
}
226+
}
227+
228+
if let selected = selectedMachO {
229+
DDLogInfo("Selected Mach-O: \(selected.lastPathComponent)", ddlog: logger)
230+
} else {
231+
DDLogError("No available Mach-O found, all \(allMachOs.count) candidates are encrypted", ddlog: logger)
232+
}
233+
234+
return selectedMachO
210235
}
211236

212237
fileprivate static func findResource(_ name: String, fileExtension: String) -> URL {

0 commit comments

Comments
 (0)