diff --git a/README.md b/README.md index bfebaf349..68a8b292a 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ GitHub Actions + + website +

@@ -91,7 +94,7 @@ Important Notes: - OTA Update Disabler - Screen Time Disabler - App Decrypt - +- Clean Cache ### Coming Soon - FTP Server diff --git a/lara/kexploit/pe/sbx.h b/lara/kexploit/pe/sbx.h index 7956e8c49..fc139486d 100644 --- a/lara/kexploit/pe/sbx.h +++ b/lara/kexploit/pe/sbx.h @@ -10,6 +10,7 @@ #include +uint64_t sbx_ucredbyproc(uint64_t proc); int sbx_escape(uint64_t self_proc); void sbx_setlogcallback(void (*callback)(const char *message)); uint64_t sbx_gettoken(pid_t pid); diff --git a/lara/kexploit/utils.h b/lara/kexploit/utils.h index 19a7f6c9c..f82fad67d 100644 --- a/lara/kexploit/utils.h +++ b/lara/kexploit/utils.h @@ -68,6 +68,8 @@ uint64_t proc_self(void); uint64_t task_self(void); int crashproc(const char* pid); +int proc_pause_resume(const char *name, bool resume); +int count_pids(uint64_t allproc); #ifdef __cplusplus } diff --git a/lara/kexploit/utils.m b/lara/kexploit/utils.m index ec7f8d26a..c0f4d9c26 100644 --- a/lara/kexploit/utils.m +++ b/lara/kexploit/utils.m @@ -853,3 +853,50 @@ int crashproc(const char* name) { ds_kwrite64(state + offsetof(struct arm_saved_state64, sp), 0x1337133713371337); return 0; } + +int proc_pause_resume(const char *name, bool resume) { + if (!name) { + return -1; + } + + uint64_t proc = procbyname(name); + if (!proc) { + printf("(signal) process not found: %s\n", name); + return -1; + } + + uint32_t pid = ds_kread32(proc + PROC_PID_OFFSET); + int result; + + if (resume) { + result = kill(pid, SIGCONT); + } else { + result = kill(pid, SIGSTOP); + } + + if (result != 0) { + perror("(signal) kill failed"); + return -1; + } + + printf("(signal) %s %s\n", + name, + resume ? "resumed" : "paused"); + return 0; +} + +int count_pids(uint64_t allproc) { + int count = 0; + uint64_t proc = allproc; + + for (int i = 0; i < 12000 && proc; i++) { + int pid = ds_kread32(proc + off_proc_p_pid); + if (pid > 0 && pid < 99999) + count++; + uint64_t next = ds_kread64(proc + off_proc_p_list_le_next); + if (next == 0 || next == proc) + break; + proc = next; + } + return count; +} diff --git a/lara/views/tweaks/ToolsView.swift b/lara/views/tweaks/ToolsView.swift index d69d8eb77..1141bb075 100644 --- a/lara/views/tweaks/ToolsView.swift +++ b/lara/views/tweaks/ToolsView.swift @@ -24,6 +24,8 @@ struct ToolsView: View { @State private var pid: pid_t = getpid() @State private var status: String? @State private var crashname: String = "SpringBoard" + @State private var pausedProcesses: Set = [] + @State private var proc_sbx: UInt64 = 0 private enum tokenclass: String, CaseIterable, Identifiable { case read = "com.apple.app-sandbox.read" @@ -156,10 +158,37 @@ struct ToolsView: View { } } .disabled(crashname.isEmpty) + Button("Pause") { + crashname.withCString { _ = proc_pause_resume($0, false) } + pausedProcesses.insert(crashname) + } + .disabled(crashname.isEmpty || pausedProcesses.contains(crashname)) + + Button("Resume") { + crashname.withCString { _ = proc_pause_resume($0, true) } + pausedProcesses.remove(crashname) + } + .disabled(crashname.isEmpty || !pausedProcesses.contains(crashname)) + + Button("SBX Escape Helper") { + crashname.withCString { cstr in + proc_sbx = procbyname(cstr) + } + + if proc_sbx == 0 { + status = "Failed to get proc" + return + } + + let errorcheck = sbx_escape(proc_sbx) + status = errorcheck == 0 ? nil : "Failure" + } + .disabled(crashname.isEmpty) + } header: { - Text("Crasher") + Text("Task Manager") } footer: { - Text("Crashes the selected process") + Text("Manages The Selected Process") } Section { diff --git a/lara/views/tweaks/TweaksView.swift b/lara/views/tweaks/TweaksView.swift index 443948457..80e01a160 100644 --- a/lara/views/tweaks/TweaksView.swift +++ b/lara/views/tweaks/TweaksView.swift @@ -59,6 +59,7 @@ struct TweaksView: View { .disabled(!mgr.vfsready) NavigationLink("OTA Updates", destination: OTAView(mgr: mgr)) NavigationLink("Screen Time", destination: ScreenTimeView(mgr: mgr)) + NavigationLink("Clean Cache", destination: CacheView()) } Section(header: HeaderLabel(text: "Broken", icon: "exclamationmark.triangle.fill")) { diff --git a/lara/views/tweaks/cacheclean/BundleResolver.swift b/lara/views/tweaks/cacheclean/BundleResolver.swift new file mode 100644 index 000000000..931e3a6dd --- /dev/null +++ b/lara/views/tweaks/cacheclean/BundleResolver.swift @@ -0,0 +1,222 @@ +// +// BundleResolver.swift +// lara +// + +import Foundation +import UIKit + +// MARK: - Model + +struct ResolvedApp { + let dataUUID: String + let bundleID: String + let bundlePath: String + let name: String + let icon: UIImage? +} + +// MARK: - Resolver + +final class BundleResolver { + + private let fm = FileManager.default + + private let bundleRoot = "/var/containers/Bundle/Application" + private let dataRoot = "/var/mobile/Containers/Data/Application" + + // MARK: Public + + func resolveAll() -> [ResolvedApp] { + + let bundleMap = buildBundleMap() + + guard let dataContainers = try? fm.contentsOfDirectory(atPath: dataRoot) else { + return [] + } + + var results: [ResolvedApp] = [] + + for dataUUID in dataContainers { + + let dataPath = dataRoot + "/" + dataUUID + let metaPath = dataPath + "/.com.apple.mobile_container_manager.metadata.plist" + + // MARK: STEP 1 - metadata + var bundleID = + NSDictionary(contentsOfFile: metaPath)?["MCMMetadataIdentifier"] as? String + + // MARK: STEP 2 - fallback scan inside container + if bundleID == nil { + bundleID = findBundleIDInDataContainer(dataPath) + } + + // ❗ DO NOT DROP APP + let finalBundleID = bundleID ?? "unknown.\(dataUUID)" + + // MARK: STEP 3 - resolve bundle path + let resolvedBundlePath = + bundleID != nil + ? (bundleMap[finalBundleID] ?? findBundlePathFallback(bundleID: finalBundleID)) + : nil + + let finalBundlePath = resolvedBundlePath ?? "" + + // MARK: STEP 4 - name resolution (NEVER FAIL) + let finalName = readSafeName( + bundlePath: finalBundlePath.isEmpty ? dataPath : finalBundlePath, + fallback: dataUUID + ) + + // MARK: STEP 5 - icon resolution (NEVER FAIL) + let finalIcon = + finalBundlePath.isEmpty + ? UIImage(systemName: "app") + : readIcon(finalBundlePath) + + results.append( + ResolvedApp( + dataUUID: dataUUID, + bundleID: finalBundleID, + bundlePath: finalBundlePath, + name: finalName, + icon: finalIcon + ) + ) + } + + return results + } + + // MARK: Bundle Map + + private func buildBundleMap() -> [String: String] { + + var map: [String: String] = [:] + + guard let roots = try? fm.contentsOfDirectory(atPath: bundleRoot) else { + return map + } + + for root in roots { + + let rootPath = bundleRoot + "/" + root + + guard let items = try? fm.contentsOfDirectory(atPath: rootPath) else { + continue + } + + for item in items where item.hasSuffix(".app") { + + let appPath = rootPath + "/" + item + + // PRIMARY + if let meta = NSDictionary(contentsOfFile: appPath + "/.com.apple.mobile_container_manager.metadata.plist"), + let bundleID = meta["MCMMetadataIdentifier"] as? String { + map[bundleID] = appPath + continue + } + + // FALLBACK + if let info = NSDictionary(contentsOfFile: appPath + "/Info.plist"), + let bundleID = info["CFBundleIdentifier"] as? String { + map[bundleID] = appPath + } + } + } + + return map + } + + // MARK: Data container scan + + private func findBundleIDInDataContainer(_ dataPath: String) -> String? { + + guard let items = try? fm.contentsOfDirectory(atPath: dataPath) else { + return nil + } + + for item in items where item.hasSuffix(".app") { + + let infoPath = dataPath + "/" + item + "/Info.plist" + + if let info = NSDictionary(contentsOfFile: infoPath), + let bundleID = info["CFBundleIdentifier"] as? String { + return bundleID + } + } + + return nil + } + + // MARK: Bundle fallback + + private func findBundlePathFallback(bundleID: String) -> String? { + + guard let roots = try? fm.contentsOfDirectory(atPath: bundleRoot) else { + return nil + } + + for root in roots { + + let rootPath = bundleRoot + "/" + root + + guard let items = try? fm.contentsOfDirectory(atPath: rootPath) else { + continue + } + + for item in items where item.hasSuffix(".app") { + + let appPath = rootPath + "/" + item + + if let info = NSDictionary(contentsOfFile: appPath + "/Info.plist"), + let id = info["CFBundleIdentifier"] as? String, + id == bundleID { + return appPath + } + } + } + + return nil + } + + // MARK: SAFE NAME (never empty) + + private func readSafeName(bundlePath: String, fallback: String) -> String { + + let infoPath = bundlePath + "/Info.plist" + + guard let info = NSDictionary(contentsOfFile: infoPath) else { + return fallback + } + + return info["CFBundleDisplayName"] as? String ?? + info["CFBundleName"] as? String ?? + fallback + } + + // MARK: Icon + + private func readIcon(_ bundlePath: String) -> UIImage? { + + let infoPath = bundlePath + "/Info.plist" + + guard let info = NSDictionary(contentsOfFile: infoPath) else { + return UIImage(systemName: "app") + } + + if let icons = info["CFBundleIcons"] as? [String: Any], + let primary = icons["CFBundlePrimaryIcon"] as? [String: Any], + let files = primary["CFBundleIconFiles"] as? [String], + let iconName = files.last { + + let path = bundlePath + "/" + iconName + + return UIImage(contentsOfFile: path) + ?? UIImage(contentsOfFile: path + "@2x.png") + ?? UIImage(contentsOfFile: path + ".png") + } + + return UIImage(systemName: "app") + } +} diff --git a/lara/views/tweaks/cacheclean/CacheView.swift b/lara/views/tweaks/cacheclean/CacheView.swift new file mode 100644 index 000000000..f7294dad6 --- /dev/null +++ b/lara/views/tweaks/cacheclean/CacheView.swift @@ -0,0 +1,356 @@ +// +// +// CacheView.swift +// +// lara +// + +import SwiftUI +import UIKit +import WebKit +import Combine + +// MARK: - Models + +struct CacheApp: Identifiable { + let id: String + let name: String + let bundleID: String? + + let appBundlePath: String? + let dataContainerPath: String + + let icon: UIImage? + + let cacheSize: Int64 + let tmpSize: Int64 + let documentsSize: Int64 + + let cachePath: String + let tmpPath: String + let documentsPath: String +} + +struct StorageSnapshot: Identifiable { + let id = UUID() + let date = Date() + let totalBytes: Int64 +} + +struct AppRecord { + let dataUUID: String + let bundleID: String + let bundlePath: String + let name: String + let icon: UIImage? +} + +// MARK: - Manager + +final class CleanerManager: ObservableObject { + + @Published var apps: [CacheApp] = [] + @Published var snapshots: [StorageSnapshot] = [] + + @Published var isScanning = false + @Published var scanProgress: Double = 0 + @Published var statusText = "" + + @Published var totalCacheBytes: Int64 = 0 + + private let fm = FileManager.default + private let dataRoot = "/var/mobile/Containers/Data/Application" + + // Resolver (optional enrichment layer) + private let resolver = BundleResolver() + private var appDB: [String: AppRecord] = [:] + + // MARK: Build DB (SAFE) + private func buildDatabase() { + let resolved = resolver.resolveAll() + + var db: [String: AppRecord] = [:] + for app in resolved { + db[app.dataUUID] = AppRecord( + dataUUID: app.dataUUID, + bundleID: app.bundleID, + bundlePath: app.bundlePath, + name: app.name, + icon: app.icon + ) + } + + self.appDB = db + } + + // MARK: Scan + + func startScan(minSizeMB: Int64 = 1) { + + guard !isScanning else { return } + + isScanning = true + apps.removeAll() + scanProgress = 0 + totalCacheBytes = 0 + statusText = "Scanning apps..." + + DispatchQueue.global(qos: .userInitiated).async { + + self.buildDatabase() + + let containers = (try? self.fm.contentsOfDirectory(atPath: self.dataRoot)) ?? [] + + let total = max(containers.count, 1) + var processed = 0 + + var results: [CacheApp] = [] + + for uuid in containers { + + let dataPath = self.dataRoot + "/" + uuid + + let cachePath = dataPath + "/Library/Caches" + let tmpPath = dataPath + "/tmp" + let docsPath = dataPath + "/Documents" + + let cacheSize = self.folderSize(cachePath) + let tmpSize = self.folderSize(tmpPath) + let docsSize = self.folderSize(docsPath) + + let totalSize = cacheSize + tmpSize + docsSize + + // Only skip extremely small containers + if totalSize < minSizeMB * 1024 * 1024 { + processed += 1 + continue + } + + let appInfo = self.appDB[uuid] + + let name = appInfo?.name ?? "App \(uuid.suffix(6))" + let bundleID = appInfo?.bundleID + let bundlePath = appInfo?.bundlePath + let icon = appInfo?.icon + + results.append(CacheApp( + id: uuid, + name: name, + bundleID: bundleID, + appBundlePath: bundlePath, + dataContainerPath: dataPath, + icon: icon, + cacheSize: cacheSize, + tmpSize: tmpSize, + documentsSize: docsSize, + cachePath: cachePath, + tmpPath: tmpPath, + documentsPath: docsPath + )) + + processed += 1 + + DispatchQueue.main.async { + self.scanProgress = Double(processed) / Double(total) + self.statusText = "Scanning \(processed)/\(total)" + } + } + + let totalBytes = results.reduce(0) { + $0 + $1.cacheSize + $1.tmpSize + $1.documentsSize + } + + DispatchQueue.main.async { + self.apps = results.sorted { $0.cacheSize > $1.cacheSize } + self.totalCacheBytes = totalBytes + + self.snapshots.append(StorageSnapshot(totalBytes: totalBytes)) + + self.isScanning = false + self.scanProgress = 1.0 + self.statusText = "Completed (\(results.count) apps)" + } + } + } + + // MARK: Delete + + func deleteCache(_ app: CacheApp) { + try? fm.removeItem(atPath: app.cachePath) + try? fm.removeItem(atPath: app.tmpPath) + } + + func deleteAll() { + for app in apps { + deleteCache(app) + } + } + + // MARK: Web cleanup + + func cleanWKWebView() { + + let types: Set = [ + WKWebsiteDataTypeDiskCache, + WKWebsiteDataTypeMemoryCache, + WKWebsiteDataTypeCookies, + WKWebsiteDataTypeLocalStorage, + WKWebsiteDataTypeSessionStorage, + WKWebsiteDataTypeIndexedDBDatabases, + WKWebsiteDataTypeWebSQLDatabases + ] + + WKWebsiteDataStore.default().removeData( + ofTypes: types, + modifiedSince: Date(timeIntervalSince1970: 0) + ) { + DispatchQueue.main.async { + self.statusText = "WKWebView cleared" + } + } + } + + func cleanURLCache() { + URLCache.shared.removeAllCachedResponses() + } + + // MARK: Size + + private func folderSize(_ path: String) -> Int64 { + guard let e = fm.enumerator(atPath: path) else { return 0 } + + var size: Int64 = 0 + + for case let file as String in e { + + let full = (path as NSString).appendingPathComponent(file) + + if let attrs = try? fm.attributesOfItem(atPath: full), + let fileSize = attrs[.size] as? NSNumber { + size += fileSize.int64Value + } + } + + return size + } +} + +// MARK: - UI + +struct CacheView: View { + + @StateObject var mgr = CleanerManager() + + var body: some View { + + NavigationStack { + + VStack { + + Text("Clean Cache") + .font(.title2).bold() + + Text("\(mgr.totalCacheBytes / 1024 / 1024) MB Total") + .font(.title) + + ProgressView(value: mgr.scanProgress) + + Text(mgr.statusText) + .font(.caption) + + List { + + Section("Apps") { + + ForEach(mgr.apps) { app in + + HStack { + + if let icon = app.icon { + Image(uiImage: icon) + .resizable() + .frame(width: 40, height: 40) + .cornerRadius(8) + } else { + Image(systemName: "app") + } + + VStack(alignment: .leading) { + Text(app.name).bold() + Text("Cache \(app.cacheSize / 1024 / 1024) MB") + Text("Tmp \(app.tmpSize / 1024 / 1024) MB") + Text("Docs \(app.documentsSize / 1024 / 1024) MB") + .font(.caption) + .foregroundStyle(.secondary) + } + } + .swipeActions { + Button(role: .destructive) { + mgr.deleteCache(app) + } label: { + Text("Delete") + } + } + .swipeActions(edge: .trailing) { + Button(role: .destructive) { + mgr.deleteCache(app) + } label: { + Text("Cache") + } + } + .swipeActions(edge: .leading) { + Button(role: .destructive) { + try? FileManager.default.removeItem(atPath: app.documentsPath) + if let index = mgr.apps.firstIndex(where: { $0.id == app.id }) { + mgr.apps[index] = CacheApp( + id: app.id, + name: app.name, + bundleID: app.bundleID, + appBundlePath: app.appBundlePath, + dataContainerPath: app.dataContainerPath, + icon: app.icon, + cacheSize: app.cacheSize, + tmpSize: app.tmpSize, + documentsSize: 0, + cachePath: app.cachePath, + tmpPath: app.tmpPath, + documentsPath: app.documentsPath + + ) + } + + } label: { + Text("Docs") + } + .tint(.orange) + } + } + } + + Section("Tools") { + + Button("Delete ALL Cache") { + mgr.deleteAll() + } + .foregroundStyle(.red) + + Button("Clear WKWebView") { + mgr.cleanWKWebView() + } + + Button("Clear URLCache") { + mgr.cleanURLCache() + } + + Button("Rescan") { + mgr.startScan() + } + } + } + } + .onAppear { + mgr.startScan() + } + } + } +}