From 34b5ed77d1697ca045f95f849b8677a801737f97 Mon Sep 17 00:00:00 2001 From: stuartjash Date: Wed, 13 Sep 2023 10:44:34 -0700 Subject: [PATCH 01/23] removed sudo from tcpdump as aftermath already has sudo, making it redundant --- network/NetworkConnections.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/NetworkConnections.swift b/network/NetworkConnections.swift index 801f545..cd54059 100644 --- a/network/NetworkConnections.swift +++ b/network/NetworkConnections.swift @@ -29,7 +29,7 @@ class NetworkConnections: NetworkModule { func pcapCapture(writeFile: URL) { var output = "" DispatchQueue.global(qos: .userInitiated).async { - let command = "sudo tcpdump -i en0 -w \(writeFile.relativePath)" + let command = "tcpdump -i en0 -w \(writeFile.relativePath)" output = Aftermath.shell("\(command)") return From b1c0dc478e04fc54ec026f2b3c40df03013c4a41 Mon Sep 17 00:00:00 2001 From: stuartjash Date: Mon, 18 Sep 2023 14:40:53 -0500 Subject: [PATCH 02/23] updated macos 13 requirement for eslogger --- aftermath/Command.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/aftermath/Command.swift b/aftermath/Command.swift index 84d4438..f6e833c 100644 --- a/aftermath/Command.swift +++ b/aftermath/Command.swift @@ -155,10 +155,14 @@ class Command { mainModule.addTextToFile(atUrl: CaseFiles.metadataFile, text: "file,birth,modified,accessed,permissions,uid,gid,xattr,downloadedFrom") - // Start logging Endpoint Security data - mainModule.log("Starting ES logging...") - let esModule = ESModule() - esModule.run() + if #available(macOS 13, *) { + // Start logging Endpoint Security data + mainModule.log("Starting ES logging...") + let esModule = ESModule() + esModule.run() + } else { + print("Unable to run eslogger due to unavailability on this OS") + } // tcpdump From e43998d45da0c40ac39f3a1977a939d904cef529 Mon Sep 17 00:00:00 2001 From: stuartjash Date: Thu, 28 Sep 2023 16:18:06 -0700 Subject: [PATCH 03/23] added ability to disable certain collection features that may collect personal data --- README.md | 10 ++-- aftermath/Command.swift | 65 ++++++++++++------------- analysis/LogParser.swift | 18 +++---- analysis/ProcessParser.swift | 61 ++++++++++++----------- artifacts/ArtifactsModule.swift | 19 ++++++-- endpointSecurity/ESLogs.swift | 8 +-- endpointSecurity/ESModule.swift | 9 +++- filesystem/FileSystemModule.swift | 55 +++++++++++++-------- filesystem/browsers/BrowserModule.swift | 6 ++- network/NetworkModule.swift | 6 ++- persistence/PersistenceModule.swift | 6 +++ processes/ProcessModule.swift | 25 +++++++--- systemRecon/SystemReconModule.swift | 5 ++ unifiedlogs/UnifiedLogModule.swift | 4 ++ 14 files changed, 177 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index 90b6367..a8af8ec 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![](https://github.com/jamf/aftermath/blob/main/AftermathLogo.png) -![](https://img.shields.io/badge/release-2.0.0-bright%20green) ![](https://img.shields.io/badge/macOS-12.0%2B-blue) ![](https://img.shields.io/badge/license-MIT-orange) +![](https://img.shields.io/badge/release-2.1.0-bright%20green) ![](https://img.shields.io/badge/macOS-12.0%2B-blue) ![](https://img.shields.io/badge/license-MIT-orange) ## About @@ -84,14 +84,16 @@ To uninstall the aftermath binary, run the `AftermathUninstaller.pkg` from the [ usage: --collect-dirs --deep or -d -> perform a deep scan of the file system for modified and accessed timestamped metadata WARNING: This will be a time-intensive, memory-consuming scan. - --es-logs -> specify which Endpoint Security events (space-separated) to collect (defaults are: create exec mmap). To disable, see --disable-es-logs +--disable -> disable a set of aftermath features that may collect personal user data + Available features to disable: browsers -> collecting browser information | browser-killswitch -> force-closes browers | -> databases -> tcc & lsquarantine databases | filesystem -> walking the filesystem for timestamps | proc-info -> collecting process information via TrueTree and eslogger | all -> all aforementioned options + usage: --disable browsers browser-killswitch databases filesystem proc-info + --disable all +--es-logs -> specify which Endpoint Security events (space-separated) to collect (defaults are: create exec mmap). To disable, see --disable es-logs usage: --es-logs setuid unmount write --logs -> specify an external text file with unified log predicates (as dictionary objects) to parse usage: --logs /Users//Desktop/myPredicates.txt -o or --output -> specify an output location for Aftermath collection results (defaults to /tmp) usage: -o Users/user/Desktop ---disable-browser-killswitch -> by default, browsers are force-closed during collection. This will disable the force-closing of browsers. ---disable-es-logs -> by default, es logs of create, exec, and mmap are collected. This will disable this default behavior --pretty -> colorize Terminal output --cleanup -> remove Aftermath folders from default locations ("/tmp", "/var/folders/zz/) ``` diff --git a/aftermath/Command.swift b/aftermath/Command.swift index f6e833c..fd12232 100644 --- a/aftermath/Command.swift +++ b/aftermath/Command.swift @@ -16,10 +16,8 @@ static let pretty = Options(rawValue: 1 << 3) static let collectDirs = Options(rawValue: 1 << 4) static let unifiedLogs = Options(rawValue: 1 << 5) - static let disableBrowserKillswitch = Options(rawValue: 1 << 6) - static let esLogs = Options(rawValue: 1 << 7) - static let disableESLogs = Options(rawValue: 1 << 8) - + static let esLogs = Options(rawValue: 1 << 6) + static let disable = Options(rawValue: 1 << 7) } @main @@ -30,7 +28,8 @@ class Command { static var collectDirs: [String] = [] static var unifiedLogsFile: String? = nil static var esLogs: [String] = ["create", "exec", "mmap"] - static let version: String = "2.0.0" + static let version: String = "2.1.0" + static var disableFeatures: [String:Bool] = ["all":false, "browsers":false, "browser-killswitch":false, "databases":false, "filesystem":false, "proc-info":false] static func main() { setup(with: CommandLine.arguments) @@ -53,7 +52,6 @@ class Command { case "--cleanup": Self.cleanup(defaultRun: false) case "-d", "--deep": Self.options.insert(.deep) case "--pretty": Self.options.insert(.pretty) - case "--disable-browser-killswitch": Self.options.insert(.disableBrowserKillswitch) case "--analyze": if let index = args.firstIndex(of: arg) { Self.options.insert(.analyze) @@ -68,7 +66,27 @@ class Command { i += 1 } } - case "--disable-es-logs": Self.options.insert(.disableESLogs) + case "--disable": + if let index = args.firstIndex(of: arg) { + Self.options.insert(.disable) + var i = 1 + while (index + i) < args.count && !args[index + i].starts(with: "-") { + for k in self.disableFeatures.keys { + if args[index + i] == "all" { + for k in self.disableFeatures.keys { + self.disableFeatures[k] = true + } + break + } + + if args[index + i] == k { + self.disableFeatures[k] = true + break + } + } + i += 1 + } + } case "--es-logs": if let index = args.firstIndex(of: arg) { Self.options.insert(.esLogs) @@ -155,77 +173,55 @@ class Command { mainModule.addTextToFile(atUrl: CaseFiles.metadataFile, text: "file,birth,modified,accessed,permissions,uid,gid,xattr,downloadedFrom") + // eslogger if #available(macOS 13, *) { - // Start logging Endpoint Security data - mainModule.log("Starting ES logging...") let esModule = ESModule() esModule.run() } else { - print("Unable to run eslogger due to unavailability on this OS") + print("Unable to run eslogger due to unavailability on this OS. Requires macOS 13 or higher.") } // tcpdump - mainModule.log("Running pcap...") let pcapModule = NetworkModule() pcapModule.pcapRun() // System Recon - mainModule.log("Started system recon") let systemReconModule = SystemReconModule() systemReconModule.run() - mainModule.log("Finished system recon") // Network - mainModule.log("Started gathering network information...") let networkModule = NetworkModule() networkModule.run() - mainModule.log("Finished gathering network information") // Processes - mainModule.log("Starting process dump...") let procModule = ProcessModule() procModule.run() - mainModule.log("Finished gathering process information") // Persistence - mainModule.log("Starting Persistence Module") let persistenceModule = PersistenceModule() persistenceModule.run() - mainModule.log("Finished logging persistence items") // FileSystem - mainModule.log("Started gathering file system information...") let fileSysModule = FileSystemModule() fileSysModule.run() - mainModule.log("Finished gathering file system information") // Artifacts - mainModule.log("Started gathering artifacts...") let artifactModule = ArtifactsModule() artifactModule.run() - mainModule.log("Finished gathering artifacts") // Logs - mainModule.log("Started logging unified logs") let unifiedLogModule = UnifiedLogModule(logFile: unifiedLogsFile) unifiedLogModule.run() - mainModule.log("Finished logging unified logs") - - - mainModule.log("Finished running pcap") - - - // End logging Endpoint Security data - mainModule.log("Finished ES logging") + mainModule.log("Finished running Aftermath collection") // Copy from cache to output @@ -268,12 +264,13 @@ class Command { print("--collect-dirs -> specify locations of (space-separated) directories to dump those raw files") print(" usage: --collect-dirs /Users//Downloads /tmp") print("--deep -> performs deep scan and captures metadata from Users entire directory (WARNING: this may be time-consuming)") + print("--disable -> disable a set of aftermath features that may collect personal user data") + print(" usage: --disable browsers browser-killswitch databases filesystem proc-info") + print(" --disable all") print("--es-logs -> specify which Endpoint Security events (space-separated) to collect (defaults are: create exec mmap)") print(" usage: --es-logs exec open rename") print("--logs -> specify an external text file with unified log predicates to parse") print(" usage: --logs /Users//Desktop/myPredicates.txt") - print("--disable-browser-killswitch -> by default, browsers are force-closed during collection. This will disable the force-closing of browsers.") - print("--disable-es-logs -> by default, es logs of create, exec, and mmap are collected. This will disable this default behavior") print("--pretty -> colorize Terminal output") print("--cleanup -> remove Aftermath Folders in default locations") exit(1) diff --git a/analysis/LogParser.swift b/analysis/LogParser.swift index 476d67e..ddf2ede 100644 --- a/analysis/LogParser.swift +++ b/analysis/LogParser.swift @@ -56,15 +56,10 @@ class LogParser: AftermathModule { self.addTextToFile(atUrl: self.storylineFile, text: text) } } catch { - print("Unable to parse contents") + self.log("Unable to parse install log contents") } } - - fileprivate func sanatizeInfo(_ info: inout String) { - info = info.replacingOccurrences(of: ",", with: "") - info = info.replacingOccurrences(of: "\"", with: "") - } - + func parseSysLog() { // system.log @@ -114,7 +109,7 @@ class LogParser: AftermathModule { self.addTextToFile(atUrl: storylineFile, text: text) } } catch { - print("Unable to parse contents") + self.log("Unable to parse syslog contents") } } @@ -154,10 +149,15 @@ class LogParser: AftermathModule { self.addTextToFile(atUrl: self.storylineFile, text: text) } } catch { - print("Unable to parse contents") + self.log("Unable to parse XPR contents") } } + fileprivate func sanatizeInfo(_ info: inout String) { + info = info.replacingOccurrences(of: ",", with: "") + info = info.replacingOccurrences(of: "\"", with: "") + } + func run() { self.log("Parsing install log...") parseInstallLog() diff --git a/analysis/ProcessParser.swift b/analysis/ProcessParser.swift index 9404f10..08d75ea 100644 --- a/analysis/ProcessParser.swift +++ b/analysis/ProcessParser.swift @@ -20,40 +20,45 @@ class ProcessParser: AftermathModule { func parseProcessDump() { let procPathRaw = "\(self.collectionDir)/Processes/process_dump.txt" - do { - - let data = try String(contentsOf: URL(fileURLWithPath: procPathRaw), encoding: .utf8) - let line = data.components(separatedBy: "\n") - - for ind in 1...line.count - 1 { - let splitLine = line[ind].components(separatedBy: " ") + if filemanager.fileExists(atPath: procPathRaw) { + do { - guard let date = splitLine[safe: 0] else { continue } - guard let time = splitLine[safe: 1] else { continue } - guard let zone = splitLine[safe: 2] else { continue } - let unformattedDate = date + "T" + time + zone // 2022-09-02T17:16:58 +0000 - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale(identifier: "en_US") - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" // 2022-09-02T17:16:58+0000 - dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + let data = try String(contentsOf: URL(fileURLWithPath: procPathRaw), encoding: .utf8) + let line = data.components(separatedBy: "\n") - var info = "" - for i in 3...splitLine.count - 1 { - info = info.appending(" " + splitLine[i]) + for ind in 1...line.count - 1 { + let splitLine = line[ind].components(separatedBy: " ") + + guard let date = splitLine[safe: 0] else { continue } + guard let time = splitLine[safe: 1] else { continue } + guard let zone = splitLine[safe: 2] else { continue } + let unformattedDate = date + "T" + time + zone // 2022-09-02T17:16:58 +0000 + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" // 2022-09-02T17:16:58+0000 + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + + var info = "" + for i in 3...splitLine.count - 1 { + info = info.appending(" " + splitLine[i]) + } + + sanatizeInfo(&info) + + guard let dateZone = dateFormatter.date(from: unformattedDate) else { continue } + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + let formattedDate = dateFormatter.string(from: dateZone) + let text = "\(formattedDate), PROCESS, \(info)" + self.addTextToFile(atUrl: self.storylineFile, text: text) } - - sanatizeInfo(&info) - - guard let dateZone = dateFormatter.date(from: unformattedDate) else { continue } - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" - let formattedDate = dateFormatter.string(from: dateZone) - let text = "\(formattedDate), PROCESS, \(info)" - self.addTextToFile(atUrl: self.storylineFile, text: text) + } catch { + print("Error parsing process dump raw file: \(error)") } - } catch { - print("Error parsing process dump raw file: \(error)") + } else { + self.log("Process data not available") } } + fileprivate func sanatizeInfo(_ info: inout String) { info = info.replacingOccurrences(of: ",", with: "") diff --git a/artifacts/ArtifactsModule.swift b/artifacts/ArtifactsModule.swift index 63df7b1..c0f3eb5 100644 --- a/artifacts/ArtifactsModule.swift +++ b/artifacts/ArtifactsModule.swift @@ -15,17 +15,24 @@ class ArtifactsModule: AftermathModule, AMProto { lazy var moduleDirRoot = self.createNewDirInRoot(dirName: dirName) func run() { + + self.log("Started gathering artifacts...") + let rawDir = self.createNewDir(dir: moduleDirRoot, dirname: "raw") let systemConfigDir = self.createNewDir(dir: rawDir, dirname: "ssh") let profilesDir = self.createNewDir(dir: rawDir, dirname: "profiles") let logFilesDir = self.createNewDir(dir: rawDir, dirname: "logs") let xbsDir = self.createNewDir(dir: rawDir, dirname: "xbs") - let tcc = TCC(tccDir: rawDir) - tcc.run() - - let lsquarantine = LSQuarantine(rawDir: rawDir) - lsquarantine.run() + if Command.disableFeatures["databases"] == false { + let tcc = TCC(tccDir: rawDir) + tcc.run() + + let lsquarantine = LSQuarantine(rawDir: rawDir) + lsquarantine.run() + } else { + self.log("Skipping collecting database information") + } let systemConf = SystemConfig(systemConfigDir: systemConfigDir) systemConf.run() @@ -46,5 +53,7 @@ class ArtifactsModule: AftermathModule, AMProto { } else { self.log("Unable to capture XPdb due to unavailability on this OS") } + + self.log("Finished gathering artifacts") } } diff --git a/endpointSecurity/ESLogs.swift b/endpointSecurity/ESLogs.swift index c5eaedc..45aa5cf 100644 --- a/endpointSecurity/ESLogs.swift +++ b/endpointSecurity/ESLogs.swift @@ -31,11 +31,7 @@ class ESLogs: ESModule { } override func run() { - if !Command.options.contains(.disableESLogs) { - self.log("Collecting ES logs...") - logESEvents(events: Command.esLogs.joined(separator: " ")) - } else { - self.log("Skipping ES logging") - } + self.log("Collecting ES logs...") + logESEvents(events: Command.esLogs.joined(separator: " ")) } } diff --git a/endpointSecurity/ESModule.swift b/endpointSecurity/ESModule.swift index 9d8753b..4ef1881 100644 --- a/endpointSecurity/ESModule.swift +++ b/endpointSecurity/ESModule.swift @@ -18,7 +18,12 @@ class ESModule: AftermathModule { lazy var esFile = self.createNewCaseFile(dirUrl: moduleDirRoot, filename: "es_logs.json") func run() { - let esLogs = ESLogs(outputDir: moduleDirRoot, outputFile: esFile) - esLogs.run() + if Command.disableFeatures["proc-info"] == false { + self.log("Starting ES logging...") + let esLogs = ESLogs(outputDir: moduleDirRoot, outputFile: esFile) + esLogs.run() + } else { + self.log("Skipping ES logging") + } } } diff --git a/filesystem/FileSystemModule.swift b/filesystem/FileSystemModule.swift index 91689bb..ae0df71 100644 --- a/filesystem/FileSystemModule.swift +++ b/filesystem/FileSystemModule.swift @@ -18,26 +18,39 @@ class FileSystemModule: AftermathModule, AMProto { func run() { - // run browser module - let browserModule = BrowserModule() - browserModule.run() - - // get slack data - let slackFile = self.createNewCaseFile(dirUrl: self.moduleDirRoot, filename: "slack_extract.json") - let slack = Slack(slackLoc: self.rawDir, writeFile: slackFile) - slack.run() - - // get data from common directories - let commonDirFile = self.createNewCaseFile(dirUrl: self.moduleDirRoot, filename: "common_directories.txt") - let common = CommonDirectories(writeFile: commonDirFile) - common.run() - - // get users on system - let sysUsers = self.createNewCaseFile(dirUrl: self.moduleDirRoot, filename: "users.txt") - for user in getUsersOnSystem() { self.addTextToFile(atUrl: sysUsers, text: "\nUsers\n\(user.username)\n\(user.homedir)\n") } - - // walk file system - let walker = FileWalker() - walker.run() + if Command.disableFeatures["filesystem"] == false { + self.log("Started gathering file system information...") + + if Command.disableFeatures["browsers"] == false { + // run browser module + let browserModule = BrowserModule() + browserModule.run() + } else { + self.log("Skipping collecting browser information") + } + + // get slack data + let slackFile = self.createNewCaseFile(dirUrl: self.moduleDirRoot, filename: "slack_extract.json") + let slack = Slack(slackLoc: self.rawDir, writeFile: slackFile) + slack.run() + + // get data from common directories + let commonDirFile = self.createNewCaseFile(dirUrl: self.moduleDirRoot, filename: "common_directories.txt") + let common = CommonDirectories(writeFile: commonDirFile) + common.run() + + // get users on system + let sysUsers = self.createNewCaseFile(dirUrl: self.moduleDirRoot, filename: "users.txt") + for user in getUsersOnSystem() { self.addTextToFile(atUrl: sysUsers, text: "\nUsers\n\(user.username)\n\(user.homedir)\n") } + + // walk file system + let walker = FileWalker() + walker.run() + + self.log("Finished gathering file system information...") + + } else { + self.log("Skipping filesystem collection") + } } } diff --git a/filesystem/browsers/BrowserModule.swift b/filesystem/browsers/BrowserModule.swift index 6758023..31239a6 100644 --- a/filesystem/browsers/BrowserModule.swift +++ b/filesystem/browsers/BrowserModule.swift @@ -26,9 +26,11 @@ class BrowserModule: AftermathModule, AMProto { self.log("Collecting browser information. Checking for open browsers. Closing any open browsers...") - // if the --force-browser-killswitch option is not added, force close the browsers - if !Command.options.contains(.disableBrowserKillswitch) { + // force close the browsers if it's not specified + if Command.disableFeatures["browser-killswitch"] == false { closeBrowsers() + } else { + self.log("Not force closing browsers") } // Check if Edge is installed diff --git a/network/NetworkModule.swift b/network/NetworkModule.swift index a648b09..4805e3b 100644 --- a/network/NetworkModule.swift +++ b/network/NetworkModule.swift @@ -14,13 +14,17 @@ class NetworkModule: AftermathModule, AMProto { lazy var moduleDirRoot = self.createNewDirInRoot(dirName: dirName) func run() { + self.log("Started gathering network information...") + let network = NetworkConnections() network.run() - + self.log("Finished gathering network information...") } func pcapRun() { + self.log("Running pcap...") + let pcapWriteFile = self.createNewCaseFile(dirUrl: moduleDirRoot, filename: "trace.pcap") let network = NetworkConnections() diff --git a/persistence/PersistenceModule.swift b/persistence/PersistenceModule.swift index 79d6fe1..7fe38a5 100644 --- a/persistence/PersistenceModule.swift +++ b/persistence/PersistenceModule.swift @@ -15,6 +15,9 @@ class PersistenceModule: AftermathModule, AMProto { lazy var moduleDirRoot = self.createNewDirInRoot(dirName: dirName) func run() { + + self.log("Starting Persistence Module") + let persistenceRawDir = self.createNewDirInRoot(dirName: "\(dirName)/raw") // capture the launch items @@ -42,5 +45,8 @@ class PersistenceModule: AftermathModule, AMProto { let loginItems = LoginItems(saveToRawDir: persistenceRawDir) loginItems.run() + + self.log("Finished gathering persistence mechanisms") + } } diff --git a/processes/ProcessModule.swift b/processes/ProcessModule.swift index 7f92082..5873d58 100644 --- a/processes/ProcessModule.swift +++ b/processes/ProcessModule.swift @@ -17,13 +17,22 @@ class ProcessModule: AftermathModule { lazy var processFile = self.createNewCaseFile(dirUrl: moduleDirRoot, filename: "process_dump.txt") func run() { - - let saveFile = self.createNewCaseFile(dirUrl: self.moduleDirRoot, filename: "true_tree_output.txt") - - let tree = Tree() - let nodePidDict = tree.createNodeDictionary() - let treeRootNode = tree.buildTrueTree(nodePidDict) - - treeRootNode.printTree(saveFile) + if Command.disableFeatures["proc-info"] == false { + self.log("Starting process dump...") + + let saveFile = self.createNewCaseFile(dirUrl: self.moduleDirRoot, filename: "true_tree_output.txt") + + let tree = Tree() + let nodePidDict = tree.createNodeDictionary() + let treeRootNode = tree.buildTrueTree(nodePidDict) + + treeRootNode.printTree(saveFile) + + self.log("Finished gathering process information") + + } else { + self.log("Skipping process collection") + } + } } diff --git a/systemRecon/SystemReconModule.swift b/systemRecon/SystemReconModule.swift index f1c98fa..0907bcc 100644 --- a/systemRecon/SystemReconModule.swift +++ b/systemRecon/SystemReconModule.swift @@ -228,6 +228,8 @@ class SystemReconModule: AftermathModule, AMProto { } func run() { + self.log("Started system recon") + let systemInformationFile = self.createNewCaseFile(dirUrl: moduleDirRoot, filename: "system_information.txt") let installedAppsFile = self.createNewCaseFile(dirUrl: moduleDirRoot, filename: "installed_apps.txt") let runningAppsFile = self.createNewCaseFile(dirUrl: moduleDirRoot, filename: "running_apps.txt") @@ -244,5 +246,8 @@ class SystemReconModule: AftermathModule, AMProto { environmentVariables(saveFile: environmentVariablesFile) securityAssessment(saveFile: systemInformationFile) installedUsers(saveFile: installedUsersFile) + + self.log("Finished system recon") + } } diff --git a/unifiedlogs/UnifiedLogModule.swift b/unifiedlogs/UnifiedLogModule.swift index ac87748..ce20a71 100644 --- a/unifiedlogs/UnifiedLogModule.swift +++ b/unifiedlogs/UnifiedLogModule.swift @@ -67,6 +67,7 @@ class UnifiedLogModule: AftermathModule, AMProto { } func run() { + self.log("Starting logging unified logs") self.log("Filtering Unified Log. Hang Tight!") // run the external input file of predicates @@ -83,5 +84,8 @@ class UnifiedLogModule: AftermathModule, AMProto { // run default predicates filterPredicates(predicates: self.defaultPredicates) self.log("Unified Log filtering complete.") + + self.log("Finished logging unified logs") + } } From 127e2571b60db8ac86aace38a3cbc7817f069f4e Mon Sep 17 00:00:00 2001 From: stuartjash Date: Fri, 29 Sep 2023 00:00:09 -0700 Subject: [PATCH 04/23] added support for brave browser --- aftermath.xcodeproj/project.pbxproj | 4 + aftermath/Command.swift | 3 +- analysis/Storyline.swift | 26 +++ filesystem/browsers/Brave.swift | 229 ++++++++++++++++++++++++ filesystem/browsers/BrowserModule.swift | 5 + filesystem/browsers/Chrome.swift | 1 - 6 files changed, 265 insertions(+), 3 deletions(-) create mode 100644 filesystem/browsers/Brave.swift diff --git a/aftermath.xcodeproj/project.pbxproj b/aftermath.xcodeproj/project.pbxproj index ce40c6c..9fad263 100644 --- a/aftermath.xcodeproj/project.pbxproj +++ b/aftermath.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 5E93B0AE2941608D009D2AB5 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E93B0AD2941608D009D2AB5 /* Data.swift */; }; 5E93B0B0294160B6009D2AB5 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E93B0AF294160B6009D2AB5 /* String.swift */; }; 5EA438FF2A7010FF00F3E2B9 /* XProtectBehavioralService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA438FE2A7010FF00F3E2B9 /* XProtectBehavioralService.swift */; }; + 5EFDDCD72AC6661A00EEF193 /* Brave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFDDCD62AC6661A00EEF193 /* Brave.swift */; }; 70A44403275707A90035F40E /* SystemReconModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A44402275707A90035F40E /* SystemReconModule.swift */; }; 70A44405275A76990035F40E /* LSQuarantine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A44404275A76990035F40E /* LSQuarantine.swift */; }; 70CF9E3A27611C6100FD884B /* ShellHistoryAndProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70CF9E3927611C6100FD884B /* ShellHistoryAndProfiles.swift */; }; @@ -90,6 +91,7 @@ 5E93B0AD2941608D009D2AB5 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; 5E93B0AF294160B6009D2AB5 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 5EA438FE2A7010FF00F3E2B9 /* XProtectBehavioralService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XProtectBehavioralService.swift; sourceTree = ""; }; + 5EFDDCD62AC6661A00EEF193 /* Brave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Brave.swift; sourceTree = ""; }; 70A44402275707A90035F40E /* SystemReconModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemReconModule.swift; sourceTree = ""; }; 70A44404275A76990035F40E /* LSQuarantine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LSQuarantine.swift; sourceTree = ""; }; 70CF9E3927611C6100FD884B /* ShellHistoryAndProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellHistoryAndProfiles.swift; sourceTree = ""; }; @@ -290,6 +292,7 @@ A0E1E3EE275EC810008D0DC6 /* Safari.swift */, 5E6780F12922E7E800BAF04B /* Edge.swift */, 5E4BC8FF29D75A8E0004DAA6 /* Arc.swift */, + 5EFDDCD62AC6661A00EEF193 /* Brave.swift */, ); path = browsers; sourceTree = ""; @@ -520,6 +523,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5EFDDCD72AC6661A00EEF193 /* Brave.swift in Sources */, A3CD4E56274434EE00869ECB /* Command.swift in Sources */, 5E494475293D50FE007FFBDD /* ConfigurationProfiles.swift in Sources */, 5E4BC90029D75A8E0004DAA6 /* Arc.swift in Sources */, diff --git a/aftermath/Command.swift b/aftermath/Command.swift index fd12232..b71b0ba 100644 --- a/aftermath/Command.swift +++ b/aftermath/Command.swift @@ -156,8 +156,7 @@ class Command { mainModule.log("Aftermath requires macOS 12 or later in order to analyze collection data.") print("Aftermath requires macOS 12 or later in order to analyze collection data.") } - - mainModule.log("Finished analysis module") + // Move analysis directory to output direcotry CaseFiles.MoveTemporaryCaseDir(outputLocation: self.outputLocation, isAnalysis: true) diff --git a/analysis/Storyline.swift b/analysis/Storyline.swift index ee08004..0f72d1a 100644 --- a/analysis/Storyline.swift +++ b/analysis/Storyline.swift @@ -171,6 +171,31 @@ class Storyline: AftermathModule { } } + func addBraveData() { + let bravePaths = ["history":"\(collectionDir)/Browser/Brave/history_output.csv","downloads":"\(collectionDir)/Browser/Brave/downloads_output.csv"] + + for (title,p) in bravePaths { + + if !filemanager.fileExists(atPath: p) { continue } + + var data = "" + + do { + data = try String(contentsOfFile: p) + } catch { + print(error) + } + + var rows = data.components(separatedBy: "\n") + rows.removeFirst() + for row in rows { + if row == "" { continue } + let columns = row.components(separatedBy: ",") + self.addTextToFile(atUrl: self.storylineFile, text: "\(columns[0]),brave_\(title),\(columns[3]))") + } + } + } + func sortStoryline() { self.log("Creating the storyline...Please wait...") @@ -248,6 +273,7 @@ class Storyline: AftermathModule { addChromeData() addEdgeData() addArcData() + addBraveData() sortStoryline() removeUnsorted() } diff --git a/filesystem/browsers/Brave.swift b/filesystem/browsers/Brave.swift new file mode 100644 index 0000000..d8adf75 --- /dev/null +++ b/filesystem/browsers/Brave.swift @@ -0,0 +1,229 @@ +// +// Brave.swift +// aftermath +// +// Copyright 2022 JAMF Software, LLC +// + +import Foundation +import SQLite3 + +class Brave: BrowserModule { + + let braveDir: URL + let writeFile: URL + + init(braveDir: URL, writeFile: URL) { + self.braveDir = braveDir + self.writeFile = writeFile + } + + func gatherHistory() { + + let historyOutput = self.createNewCaseFile(dirUrl: self.braveDir, filename: "history_output.csv") + self.addTextToFile(atUrl: historyOutput, text: "datetime,user,profile,url") + + for user in getBasicUsersOnSystem() { + for profile in getBraveProfilesForUser(user: user) { + + // Get the history file for the profile + var file: URL + if filemanager.fileExists(atPath: "\(user.homedir)/Library/Application Support/BraveSoftware/Brave-Browser/\(profile)/History") { + file = URL(fileURLWithPath: "\(user.homedir)/Library/Application Support/BraveSoftware/Brave-Browser/\(profile)/History") + self.copyFileToCase(fileToCopy: file, toLocation: self.braveDir, newFileName: "history_and_downloads_\(user.username)_\(profile).db") + } else { continue } + + // Open the history file + var db: OpaquePointer? + if sqlite3_open(file.path, &db) == SQLITE_OK { + + // Query the history file + var queryStatement: OpaquePointer? = nil + let queryString = "SELECT datetime(((v.visit_time/1000000)-11644473600), 'unixepoch'), u.url FROM visits v INNER JOIN urls u ON u.id = v.url;" + + if sqlite3_prepare_v2(db, queryString, -1, &queryStatement, nil) == SQLITE_OK { + var dateTime: String = "" + var url: String = "" + + // write the results to the historyOutput file + while sqlite3_step(queryStatement) == SQLITE_ROW { + if let col1 = sqlite3_column_text(queryStatement, 0) { + let unformattedDatetime = String(cString: col1) + dateTime = Aftermath.standardizeMetadataTimestamp(timeStamp: unformattedDatetime) + } + + let col2 = sqlite3_column_text(queryStatement, 1) + if col2 != nil { + url = String(cString: col2!) + } + + self.addTextToFile(atUrl: historyOutput, text: "\(dateTime),\(user.username),\(profile),\(url)") + } + } else { self.log("Unable to query the database. Please ensure that Brave is not running.") } + } else { self.log("Unable to open the database") } + } + } + } + + func dumpDownloads() { + self.addTextToFile(atUrl: self.writeFile, text: "----- Brave Downloads: -----\n") + + let downlaodsRaw = self.createNewCaseFile(dirUrl: self.braveDir, filename: "downloads_output.csv") + self.addTextToFile(atUrl: downlaodsRaw, text: "datetime,user,profile,url,target_path,danger_type,opened") + + for user in getBasicUsersOnSystem() { + for profile in getBraveProfilesForUser(user: user) { + var file: URL + if filemanager.fileExists(atPath: "\(user.homedir)/Library/Application Support/BraveSoftware/Brave-Browser/\(profile)/History") { + file = URL(fileURLWithPath: "\(user.homedir)/Library/Application Support/BraveSoftware/Brave-Browser/\(profile)/History") + } else { continue } + + var db: OpaquePointer? + if sqlite3_open(file.path, &db) == SQLITE_OK { + var queryStatement: OpaquePointer? = nil + let queryString = "SELECT datetime(d.start_time/1000000-11644473600, 'unixepoch'), dc.url, d.target_path, d.danger_type, d.opened FROM downloads d INNER JOIN downloads_url_chains dc ON dc.id = d.id;" + + if sqlite3_prepare_v2(db, queryString, -1, &queryStatement, nil) == SQLITE_OK { + var dateTime: String = "" + var url: String = "" + var targetPath: String = "" + var dangerType: String = "" + var opened: String = "" + + while sqlite3_step(queryStatement) == SQLITE_ROW { + if let col1 = sqlite3_column_text(queryStatement, 0) { + let unformattedDatetime = String(cString: col1) + dateTime = Aftermath.standardizeMetadataTimestamp(timeStamp: unformattedDatetime) + } + + let col2 = sqlite3_column_text(queryStatement, 1) + if let col2 = col2 { url = String(cString: col2) } + + let col3 = sqlite3_column_text(queryStatement, 2) + if let col3 = col3 { targetPath = String(cString: col3) } + + let col4 = sqlite3_column_text(queryStatement, 3) + if let col4 = col4 { dangerType = String(cString: col4) } + + let col5 = sqlite3_column_text(queryStatement, 4) + if let col5 = col5 { opened = String(cString: col5) } + + self.addTextToFile(atUrl: downlaodsRaw, text: "\(dateTime),\(user.username),\(profile),\(url),\(targetPath),\(dangerType),\(opened)") + } + } + } + } + } + + self.addTextToFile(atUrl: self.writeFile, text: "\n----- End of Brave Downloads -----\n") + } + + func dumpPreferences() { + for user in getBasicUsersOnSystem() { + for profile in getBraveProfilesForUser(user: user) { + var file: URL + if filemanager.fileExists(atPath: "\(user.homedir)/Library/Application Support/BraveSoftware/Brave-Browser/\(profile)/Preferences") { + file = URL(fileURLWithPath: "\(user.homedir)/Library/Application Support/BraveSoftware/Brave-Browser/\(profile)/Preferences") + self.copyFileToCase(fileToCopy: file, toLocation: self.braveDir, newFileName: "preferences_\(user.username)_\(profile)") + } else { continue } + + do { + let data = try Data(contentsOf: file, options: .mappedIfSafe) + if let json = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves) as? [String: Any] { + self.addTextToFile(atUrl: writeFile, text: "\nBrave Preferences -----\n\(String(describing: json))\n ----- End of Brave Preferences -----\n") + } + + } catch { self.log("Unable to capture Brave Preferenes") } + } + } + } + + func dumpCookies() { + self.addTextToFile(atUrl: self.writeFile, text: "----- Brave Cookies: -----\n") + + for user in getBasicUsersOnSystem() { + for profile in getBraveProfilesForUser(user: user) { + var file: URL + if filemanager.fileExists(atPath: "\(user.homedir)/Library/Application Support/BraveSoftware/Brave-Browser/\(profile)/Cookies") { + file = URL(fileURLWithPath: "\(user.homedir)/Library/Application Support/BraveSoftware/Brave-Browser/\(profile)/Cookies") + self.copyFileToCase(fileToCopy: file, toLocation: self.braveDir, newFileName: "cookies_\(user.username)_\(profile).db") + } else { continue } + + var db: OpaquePointer? + if sqlite3_open(file.path, &db) == SQLITE_OK { + var queryStatement: OpaquePointer? = nil + let queryString = "select datetime(creation_utc/100000 -11644473600, 'unixepoch'), name, host_key, path, datetime(expires_utc/100000-11644473600, 'unixepoch') from cookies;" + + if sqlite3_prepare_v2(db, queryString, -1, &queryStatement, nil) == SQLITE_OK { + var dateTime: String = "" + var name: String = "" + var hostKey: String = "" + var path: String = "" + var expireTime: String = "" + + while sqlite3_step(queryStatement) == SQLITE_ROW { + if let col1 = sqlite3_column_text(queryStatement, 0) { + dateTime = String(cString: col1) + } + + if let col2 = sqlite3_column_text(queryStatement, 1) { + name = String(cString: col2) + } + + if let col3 = sqlite3_column_text(queryStatement, 2) { + hostKey = String(cString: col3) + } + + if let col4 = sqlite3_column_text(queryStatement, 3) { + path = String(cString: col4) + } + + if let col5 = sqlite3_column_text(queryStatement, 4) { + expireTime = String(cString: col5) + } + + self.addTextToFile(atUrl: self.writeFile, text: "DateTime: \(dateTime)\nUser: \(user.username)\nProfile: \(profile)\nName: \(name)\nHostKey: \(hostKey)\nPath:\(path)\nExpireTime: \(expireTime)\n\n") + } + } + } + } + } + self.addTextToFile(atUrl: self.writeFile, text: "\n----- End of Brave Cookies -----\n") + } + + func captureExtensions() { + for user in getBasicUsersOnSystem() { + for profile in getBraveProfilesForUser(user: user) { + let braveExtensionDir = self.createNewDir(dir: self.braveDir, dirname: "extensions_\(user.username)_\(profile)") + let path = "\(user.homedir)/Library/Application Support/BraveSoftware/Brave-Browser/\(profile)/Extensions" + + for file in filemanager.filesInDirRecursive(path: path) { + self.copyFileToCase(fileToCopy: file, toLocation: braveExtensionDir) + } + } + } + } + + func getBraveProfilesForUser(user: User) -> [String] { + var profiles: [String] = [] + // Get the directory name if it contains the string "Profile" + if filemanager.fileExists(atPath: "\(user.homedir)/Library/Application Support/BraveSoftware/Brave-Browser") { + for file in filemanager.filesInDir(path: "\(user.homedir)/Library/Application Support/BraveSoftware/Brave-Browser") { + if file.lastPathComponent.starts(with: "Profile") || file.lastPathComponent == "Default" { + profiles.append(file.lastPathComponent) + } + } + } + + return profiles + } + + override func run() { + self.log("Collecting Brave browser information...") + gatherHistory() + dumpDownloads() + dumpPreferences() + dumpCookies() + captureExtensions() + } +} diff --git a/filesystem/browsers/BrowserModule.swift b/filesystem/browsers/BrowserModule.swift index 31239a6..15de98b 100644 --- a/filesystem/browsers/BrowserModule.swift +++ b/filesystem/browsers/BrowserModule.swift @@ -22,6 +22,7 @@ class BrowserModule: AftermathModule, AMProto { let chromeDir = self.createNewDir(dir: moduleDirRoot, dirname: "Chrome") let safariDir = self.createNewDir(dir: moduleDirRoot, dirname: "Safari") let arcDir = self.createNewDir(dir: moduleDirRoot, dirname: "Arc") + let braveDir = self.createNewDir(dir: moduleDirRoot, dirname: "Brave") let writeFile = self.createNewCaseFile(dirUrl: moduleDirRoot, filename: "browsers.txt") self.log("Collecting browser information. Checking for open browsers. Closing any open browsers...") @@ -52,6 +53,10 @@ class BrowserModule: AftermathModule, AMProto { // Check if Arc is installed let arc = Arc(arcDir: arcDir, writeFile: writeFile) arc.run() + + // Check if Brave is installed + let brave = Brave(braveDir: braveDir, writeFile: writeFile) + brave.run() } func closeBrowsers() { diff --git a/filesystem/browsers/Chrome.swift b/filesystem/browsers/Chrome.swift index c8dd800..08834cb 100644 --- a/filesystem/browsers/Chrome.swift +++ b/filesystem/browsers/Chrome.swift @@ -201,7 +201,6 @@ class Chrome: BrowserModule { self.copyFileToCase(fileToCopy: file, toLocation: chromeExtensionDir) } } - } } From fce8de7da7d116a7e873ee489b8de9bcc47aadd1 Mon Sep 17 00:00:00 2001 From: stuartjash Date: Tue, 3 Oct 2023 16:51:34 -0700 Subject: [PATCH 05/23] added slack to personal info disable --- README.md | 2 +- aftermath/Command.swift | 4 ++-- filesystem/Slack.swift | 9 +++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a8af8ec..c421ff8 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ To uninstall the aftermath binary, run the `AftermathUninstaller.pkg` from the [ WARNING: This will be a time-intensive, memory-consuming scan. --disable -> disable a set of aftermath features that may collect personal user data Available features to disable: browsers -> collecting browser information | browser-killswitch -> force-closes browers | -> databases -> tcc & lsquarantine databases | filesystem -> walking the filesystem for timestamps | proc-info -> collecting process information via TrueTree and eslogger | all -> all aforementioned options - usage: --disable browsers browser-killswitch databases filesystem proc-info + usage: --disable browsers browser-killswitch databases filesystem proc-info slack --disable all --es-logs -> specify which Endpoint Security events (space-separated) to collect (defaults are: create exec mmap). To disable, see --disable es-logs usage: --es-logs setuid unmount write diff --git a/aftermath/Command.swift b/aftermath/Command.swift index b71b0ba..862d4d8 100644 --- a/aftermath/Command.swift +++ b/aftermath/Command.swift @@ -29,7 +29,7 @@ class Command { static var unifiedLogsFile: String? = nil static var esLogs: [String] = ["create", "exec", "mmap"] static let version: String = "2.1.0" - static var disableFeatures: [String:Bool] = ["all":false, "browsers":false, "browser-killswitch":false, "databases":false, "filesystem":false, "proc-info":false] + static var disableFeatures: [String:Bool] = ["all": false, "browsers": false, "browser-killswitch": false, "databases": false, "filesystem": false, "proc-info": false, "slack": false] static func main() { setup(with: CommandLine.arguments) @@ -264,7 +264,7 @@ class Command { print(" usage: --collect-dirs /Users//Downloads /tmp") print("--deep -> performs deep scan and captures metadata from Users entire directory (WARNING: this may be time-consuming)") print("--disable -> disable a set of aftermath features that may collect personal user data") - print(" usage: --disable browsers browser-killswitch databases filesystem proc-info") + print(" usage: --disable browsers browser-killswitch databases filesystem proc-info slack") print(" --disable all") print("--es-logs -> specify which Endpoint Security events (space-separated) to collect (defaults are: create exec mmap)") print(" usage: --es-logs exec open rename") diff --git a/filesystem/Slack.swift b/filesystem/Slack.swift index 75111aa..2b8ca4d 100644 --- a/filesystem/Slack.swift +++ b/filesystem/Slack.swift @@ -35,7 +35,12 @@ class Slack: FileSystemModule { } override func run() { - self.log("Collecting Slack information") - extractSlackPrefs() + if Command.disableFeatures["slack"] == false { + self.log("Collecting Slack information") + extractSlackPrefs() + } else { + self.log("Skipping capturing Slack preferences") + } + } } From a9b23f5dde37d7de6bf2c98992cb3beb62952ca9 Mon Sep 17 00:00:00 2001 From: Stuart Ashenbrenner <72467868+stuartjash@users.noreply.github.com> Date: Tue, 3 Oct 2023 17:09:41 -0700 Subject: [PATCH 06/23] Update README.md version update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c421ff8..762e949 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![](https://github.com/jamf/aftermath/blob/main/AftermathLogo.png) -![](https://img.shields.io/badge/release-2.1.0-bright%20green) ![](https://img.shields.io/badge/macOS-12.0%2B-blue) ![](https://img.shields.io/badge/license-MIT-orange) +![](https://img.shields.io/badge/release-2.1.1-bright%20green) ![](https://img.shields.io/badge/macOS-12.0%2B-blue) ![](https://img.shields.io/badge/license-MIT-orange) ## About From 3ca02f2d2fd183b3b9dd0b9e0868c9d3084fc327 Mon Sep 17 00:00:00 2001 From: stuartjash Date: Tue, 17 Oct 2023 14:26:39 -0700 Subject: [PATCH 07/23] dump the btm file to capture other persistence items --- persistence/BTM.swift | 21 +++++++++++++++++++++ persistence/PersistenceModule.swift | 10 ++++++++++ 2 files changed, 31 insertions(+) create mode 100644 persistence/BTM.swift diff --git a/persistence/BTM.swift b/persistence/BTM.swift new file mode 100644 index 0000000..e067e4d --- /dev/null +++ b/persistence/BTM.swift @@ -0,0 +1,21 @@ +// +// BTM.swift +// aftermath +// +// Created by Stuart Ashenbrenner on 10/17/23. +// + +import Foundation + +class BTM: PersistenceModule { + + override func run() { + self.log("Dumping btm file") + + let command = "sfltool dumpbtm" + let output = Aftermath.shell(command) + + let btmDumpFile = self.createNewCaseFile(dirUrl: moduleDirRoot, filename: "btm.txt") + self.addTextToFile(atUrl: btmDumpFile, text: output) + } +} diff --git a/persistence/PersistenceModule.swift b/persistence/PersistenceModule.swift index 7fe38a5..ee230cc 100644 --- a/persistence/PersistenceModule.swift +++ b/persistence/PersistenceModule.swift @@ -28,24 +28,34 @@ class PersistenceModule: AftermathModule, AMProto { let hooks = LoginHooks(saveToRawDir: persistenceRawDir) hooks.run() + // capture all cron tabs let cron = Cron(saveToRawDir: persistenceRawDir) cron.run() + // collect overrides file let overrides = Overrides(saveToRawDir: persistenceRawDir) overrides.run() + // write out all system extensions let systemExtensions = SystemExtensions(saveToRawDir: persistenceRawDir) systemExtensions.run() + // collect any periodic scripts let periodicScripts = Periodic(saveToRawDir: persistenceRawDir) periodicScripts.run() + // on older OSs, collect emond let emond = Emond(saveToRawDir: persistenceRawDir) emond.run() + // gather all Login Items let loginItems = LoginItems(saveToRawDir: persistenceRawDir) loginItems.run() + // dump the BTM file + let btmParser = BTM() + btmParser.run() + self.log("Finished gathering persistence mechanisms") } From 94a90ec2f171d6aba7931f987ec6e433217d5d0a Mon Sep 17 00:00:00 2001 From: stuartjash Date: Tue, 17 Oct 2023 14:27:05 -0700 Subject: [PATCH 08/23] capture fish config file --- artifacts/ShellHistoryAndProfiles.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/artifacts/ShellHistoryAndProfiles.swift b/artifacts/ShellHistoryAndProfiles.swift index d230bb0..43a70e5 100644 --- a/artifacts/ShellHistoryAndProfiles.swift +++ b/artifacts/ShellHistoryAndProfiles.swift @@ -21,7 +21,7 @@ class BashProfiles: ArtifactsModule { let userFiles = [ ".bash_history", ".bash_profile", ".bashrc", ".bash_logout", ".zsh_history", ".zshenv", ".zprofile", ".zshrc", ".zlogin", ".zlogout", - ".sh_history" + ".sh_history", ".config/fish/config.fish" ] let globalFiles = ["/etc/profile", "/etc/zshenv", "/etc/zprofile", "/etc/zshrc", "/etc/zlogin", "/etc/zlogout"] @@ -31,7 +31,7 @@ class BashProfiles: ArtifactsModule { for filename in userFiles { let path = URL(fileURLWithPath: "\(user.homedir)/\(filename)") if (filemanager.fileExists(atPath: path.path)) { - let newFileName = "\(user.username)_\(filename)" + let newFileName = "\(user.username)_\(filename.replacingOccurrences(of: "/", with: ""))" self.copyFileToCase(fileToCopy: path, toLocation: self.profilesDir, newFileName: newFileName) } From 4231ee113ea13dda28f4c706c996a2608a69bd78 Mon Sep 17 00:00:00 2001 From: stuartjash Date: Tue, 17 Oct 2023 14:31:33 -0700 Subject: [PATCH 09/23] bump to v2.2.0 --- README.md | 2 +- aftermath.xcodeproj/project.pbxproj | 4 ++++ aftermath/Command.swift | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 762e949..b43f82f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![](https://github.com/jamf/aftermath/blob/main/AftermathLogo.png) -![](https://img.shields.io/badge/release-2.1.1-bright%20green) ![](https://img.shields.io/badge/macOS-12.0%2B-blue) ![](https://img.shields.io/badge/license-MIT-orange) +![](https://img.shields.io/badge/release-2.2.0-bright%20green) ![](https://img.shields.io/badge/macOS-12.0%2B-blue) ![](https://img.shields.io/badge/license-MIT-orange) ## About diff --git a/aftermath.xcodeproj/project.pbxproj b/aftermath.xcodeproj/project.pbxproj index 9fad263..d316e6d 100644 --- a/aftermath.xcodeproj/project.pbxproj +++ b/aftermath.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 5E93B0AE2941608D009D2AB5 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E93B0AD2941608D009D2AB5 /* Data.swift */; }; 5E93B0B0294160B6009D2AB5 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E93B0AF294160B6009D2AB5 /* String.swift */; }; 5EA438FF2A7010FF00F3E2B9 /* XProtectBehavioralService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA438FE2A7010FF00F3E2B9 /* XProtectBehavioralService.swift */; }; + 5ECE5DC12ADF2B4A00939BB0 /* BTM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ECE5DC02ADF2B4A00939BB0 /* BTM.swift */; }; 5EFDDCD72AC6661A00EEF193 /* Brave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFDDCD62AC6661A00EEF193 /* Brave.swift */; }; 70A44403275707A90035F40E /* SystemReconModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A44402275707A90035F40E /* SystemReconModule.swift */; }; 70A44405275A76990035F40E /* LSQuarantine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A44404275A76990035F40E /* LSQuarantine.swift */; }; @@ -91,6 +92,7 @@ 5E93B0AD2941608D009D2AB5 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; 5E93B0AF294160B6009D2AB5 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 5EA438FE2A7010FF00F3E2B9 /* XProtectBehavioralService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XProtectBehavioralService.swift; sourceTree = ""; }; + 5ECE5DC02ADF2B4A00939BB0 /* BTM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTM.swift; sourceTree = ""; }; 5EFDDCD62AC6661A00EEF193 /* Brave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Brave.swift; sourceTree = ""; }; 70A44402275707A90035F40E /* SystemReconModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemReconModule.swift; sourceTree = ""; }; 70A44404275A76990035F40E /* LSQuarantine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LSQuarantine.swift; sourceTree = ""; }; @@ -266,6 +268,7 @@ A09B239B2848F6050062D592 /* Periodic.swift */, A007834D28947D71008489EA /* Emond.swift */, A007834F28947E80008489EA /* LoginItems.swift */, + 5ECE5DC02ADF2B4A00939BB0 /* BTM.swift */, ); path = persistence; sourceTree = ""; @@ -538,6 +541,7 @@ A029AB1C28774CA400649701 /* Tree.swift in Sources */, A007835028947E80008489EA /* LoginItems.swift in Sources */, A0C930D428A4318F0011FB87 /* Timeline.swift in Sources */, + 5ECE5DC12ADF2B4A00939BB0 /* BTM.swift in Sources */, A374535A275735B40074B65C /* LoginHooks.swift in Sources */, 70CF9E3A27611C6100FD884B /* ShellHistoryAndProfiles.swift in Sources */, A0E1E3EB275EC800008D0DC6 /* Firefox.swift in Sources */, diff --git a/aftermath/Command.swift b/aftermath/Command.swift index 862d4d8..4aee627 100644 --- a/aftermath/Command.swift +++ b/aftermath/Command.swift @@ -28,7 +28,7 @@ class Command { static var collectDirs: [String] = [] static var unifiedLogsFile: String? = nil static var esLogs: [String] = ["create", "exec", "mmap"] - static let version: String = "2.1.0" + static let version: String = "2.2.0" static var disableFeatures: [String:Bool] = ["all": false, "browsers": false, "browser-killswitch": false, "databases": false, "filesystem": false, "proc-info": false, "slack": false] static func main() { From fcb5f75097b92851b22e82d029aee5d233dfd97f Mon Sep 17 00:00:00 2001 From: stuartjash Date: Tue, 24 Oct 2023 08:23:53 -0700 Subject: [PATCH 10/23] memory usage dump --- aftermath.xcodeproj/project.pbxproj | 16 +++++ aftermath/Command.swift | 5 ++ memory/MemoryModule.swift | 23 ++++++++ memory/Stat.swift | 92 +++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 memory/MemoryModule.swift create mode 100644 memory/Stat.swift diff --git a/aftermath.xcodeproj/project.pbxproj b/aftermath.xcodeproj/project.pbxproj index d316e6d..f8040fa 100644 --- a/aftermath.xcodeproj/project.pbxproj +++ b/aftermath.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ 5E93B0B0294160B6009D2AB5 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E93B0AF294160B6009D2AB5 /* String.swift */; }; 5EA438FF2A7010FF00F3E2B9 /* XProtectBehavioralService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA438FE2A7010FF00F3E2B9 /* XProtectBehavioralService.swift */; }; 5ECE5DC12ADF2B4A00939BB0 /* BTM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ECE5DC02ADF2B4A00939BB0 /* BTM.swift */; }; + 5ECE5DC42AE0406700939BB0 /* MemoryModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ECE5DC32AE0406700939BB0 /* MemoryModule.swift */; }; + 5ECE5DC62AE040B700939BB0 /* Stat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ECE5DC52AE040B700939BB0 /* Stat.swift */; }; 5EFDDCD72AC6661A00EEF193 /* Brave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFDDCD62AC6661A00EEF193 /* Brave.swift */; }; 70A44403275707A90035F40E /* SystemReconModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A44402275707A90035F40E /* SystemReconModule.swift */; }; 70A44405275A76990035F40E /* LSQuarantine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A44404275A76990035F40E /* LSQuarantine.swift */; }; @@ -93,6 +95,8 @@ 5E93B0AF294160B6009D2AB5 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 5EA438FE2A7010FF00F3E2B9 /* XProtectBehavioralService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XProtectBehavioralService.swift; sourceTree = ""; }; 5ECE5DC02ADF2B4A00939BB0 /* BTM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTM.swift; sourceTree = ""; }; + 5ECE5DC32AE0406700939BB0 /* MemoryModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryModule.swift; sourceTree = ""; }; + 5ECE5DC52AE040B700939BB0 /* Stat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stat.swift; sourceTree = ""; }; 5EFDDCD62AC6661A00EEF193 /* Brave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Brave.swift; sourceTree = ""; }; 70A44402275707A90035F40E /* SystemReconModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemReconModule.swift; sourceTree = ""; }; 70A44404275A76990035F40E /* LSQuarantine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LSQuarantine.swift; sourceTree = ""; }; @@ -179,6 +183,15 @@ path = endpointSecurity; sourceTree = ""; }; + 5ECE5DC22AE0405500939BB0 /* memory */ = { + isa = PBXGroup; + children = ( + 5ECE5DC32AE0406700939BB0 /* MemoryModule.swift */, + 5ECE5DC52AE040B700939BB0 /* Stat.swift */, + ); + path = memory; + sourceTree = ""; + }; 70A44401275707800035F40E /* systemRecon */ = { isa = PBXGroup; children = ( @@ -386,6 +399,7 @@ A374535B2757C1110074B65C /* extensions */, A0E1E3F9275ED4B7008D0DC6 /* filesystem */, A02509F228ADB1930030D6A7 /* helpers */, + 5ECE5DC22AE0405500939BB0 /* memory */, A0E1E3F4275ED2D6008D0DC6 /* network */, A0776CAC27482FF2007D18D8 /* persistence */, A029AB132876A01300649701 /* processes */, @@ -550,6 +564,7 @@ 5E93B0B0294160B6009D2AB5 /* String.swift in Sources */, A0E1E3E9275EC736008D0DC6 /* BrowserModule.swift in Sources */, A02509F428ADB1A80030D6A7 /* CHelpers.swift in Sources */, + 5ECE5DC62AE040B700939BB0 /* Stat.swift in Sources */, 70A44403275707A90035F40E /* SystemReconModule.swift in Sources */, A029AB2B2877F52D00649701 /* launchdXPC.m in Sources */, A0E1E3EF275EC810008D0DC6 /* Safari.swift in Sources */, @@ -578,6 +593,7 @@ A0D6D54927FE52C1002BB3C8 /* SystemExtensions.swift in Sources */, A08342D6284A8247005E437A /* AnalysisModule.swift in Sources */, A029AB192876A29600649701 /* Pids.swift in Sources */, + 5ECE5DC42AE0406700939BB0 /* MemoryModule.swift in Sources */, A08342D8284E48FC005E437A /* LogFiles.swift in Sources */, A0D6D54327F76C58002BB3C8 /* Cron.swift in Sources */, A02509F128AD93DA0030D6A7 /* Storyline.swift in Sources */, diff --git a/aftermath/Command.swift b/aftermath/Command.swift index 4aee627..5fe01e1 100644 --- a/aftermath/Command.swift +++ b/aftermath/Command.swift @@ -209,6 +209,11 @@ class Command { // FileSystem let fileSysModule = FileSystemModule() fileSysModule.run() + + + // Memory + let memoryModule = MemoryModule() + memoryModule.run() // Artifacts diff --git a/memory/MemoryModule.swift b/memory/MemoryModule.swift new file mode 100644 index 0000000..6c777ec --- /dev/null +++ b/memory/MemoryModule.swift @@ -0,0 +1,23 @@ +// +// MemoryModule.swift +// aftermath +// +// Created by Stuart Ashenbrenner on 10/18/23. +// + +import Foundation + +class MemoryModule: AftermathModule { + let name = "Memory Module" + let dirName = "Memory" + let description = "A module for collecting memory data" + lazy var moduleDirRoot = self.createNewDirInRoot(dirName: dirName) + + func run() { + self.log("Collecting available memory information") + + let stat = Stat() + stat.run() + } + +} diff --git a/memory/Stat.swift b/memory/Stat.swift new file mode 100644 index 0000000..14a2e5e --- /dev/null +++ b/memory/Stat.swift @@ -0,0 +1,92 @@ +// +// Stat.swift +// aftermath +// +// Created by Stuart Ashenbrenner on 10/18/23. +// + +import Foundation + +class Stat: MemoryModule { + + private func scannerModule(inputString: String, stringToFind: String) -> Double? { + let scanner = Scanner(string: inputString) + + if scanner.scanUpTo(stringToFind, into: nil), + scanner.scanString(stringToFind, into: nil) { + var result: Double = 0.0 + + if scanner.scanDouble(&result) { + return result + } + } + return nil + } + + override func run() { + + let writeFile = self.createNewCaseFile(dirUrl: moduleDirRoot, filename: "memory_usage.txt") + + // trim extra characters from output + var trimSet = CharacterSet.whitespacesAndNewlines + trimSet.insert(charactersIn: "\"") + + // create vm_stat shell + let command = "vm_stat" + let vmstatOutput = Aftermath.shell(command) + + // convert bytes to GiB + let byteConverter = 0.00000000093132257 + + // pagesize + var pagesizeOutput = Aftermath.shell("pagesize") + pagesizeOutput = pagesizeOutput.trimmingCharacters(in: trimSet) + let pagesizeDouble = Double(pagesizeOutput) + + // parse vm_stat output + var componentDict = [String:Double]() + let vmLines = vmstatOutput.split(separator: "\n") + for l in vmLines { + let components = l.split(separator: ":") + + if components.count == 2 { + let key = components[0].trimmingCharacters(in: trimSet) + let value = components[1].trimmingCharacters(in: trimSet) + let valueDouble = Double(value) + let updatedValue = valueDouble ?? 0 * pagesizeDouble! + componentDict[key] = updatedValue + } + } + + // app memory + let appMemory = (componentDict["Anonymous pages"] ?? 0.0) - (componentDict["Pages purgeable"] ?? 0.0) + let wired = Double(componentDict["Pages wired down"]!) + let active = Double(componentDict["Pages active"]!) + let inactive = Double(componentDict["Pages inactive"]!) + let spec = Double(componentDict["Pages speculative"]!) + let throttled = Double(componentDict["Pages throttled"]!) + let freeMemory = Double(componentDict["Pages free"]!) + let purgeable = Double(componentDict["Pages purgeable"]!) + let compressed = Double(componentDict["Pages occupied by compressor"]!) + let tradTotal = ((wired + active + inactive + spec + throttled + freeMemory + compressed) * pagesizeDouble!) * byteConverter + let fileBacked = Double(componentDict["File-backed pages"]!) + let physicalTotal = ((appMemory + wired + compressed + fileBacked + purgeable + freeMemory) * pagesizeDouble!) * byteConverter + + // swap usage + let vmSwapUsageOutput = Aftermath.shell("sysctl vm.swapusage") + + if let vmSwapUsage = scannerModule(inputString: vmSwapUsageOutput, stringToFind: "used = ") { + self.addTextToFile(atUrl: writeFile, text: "Swap used: \(vmSwapUsage * 0.0009765625)\n") + } + + // memory pressure + let memoryPressureOutput = Aftermath.shell("memory_pressure") + if let memoryPressure = scannerModule(inputString: memoryPressureOutput, stringToFind: "percentage: ") { + self.addTextToFile(atUrl: writeFile, text: "Memory Pressure: \(100 - memoryPressure)%") + } + + // write out + self.addTextToFile(atUrl: writeFile, text: "\nTraditional Memory:\nWired Memory: \(wired)\nActive Memory: \(active)\nInactive Memory: \(inactive)\nPages Speculative: \(spec)\nPages Throttled: \(throttled)\nPurgeable: \(purgeable)\nCompressed: \(compressed)\nFree Memory: \(freeMemory)\nTotal: \(String(format: "%.2f", tradTotal))GiB\n") + self.addTextToFile(atUrl: writeFile, text: "\nActivity Monitor Memory:\nApp Memory: \(appMemory)\nWired: \(wired)\nCompressed: \(compressed)\nMemory Used: \(appMemory + wired + compressed)\nCached files: \(fileBacked + purgeable)\nTotal Physical: \(String(format: "%.2f", physicalTotal))GiB\n") + } +} From 01388dcc50b7455b6d8802cd52e360d02f135a08 Mon Sep 17 00:00:00 2001 From: stuartjash Date: Tue, 24 Oct 2023 08:24:05 -0700 Subject: [PATCH 11/23] memory usage dump --- memory/MemoryModule.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/memory/MemoryModule.swift b/memory/MemoryModule.swift index 6c777ec..b943a50 100644 --- a/memory/MemoryModule.swift +++ b/memory/MemoryModule.swift @@ -19,5 +19,4 @@ class MemoryModule: AftermathModule { let stat = Stat() stat.run() } - } From ada5aaa307368b51b5dbc86f201f1d3fe2b1a65b Mon Sep 17 00:00:00 2001 From: stuartjash Date: Tue, 24 Oct 2023 08:26:05 -0700 Subject: [PATCH 12/23] formatting --- filesystem/browsers/Safari.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/filesystem/browsers/Safari.swift b/filesystem/browsers/Safari.swift index 4ec836b..e9f03c3 100644 --- a/filesystem/browsers/Safari.swift +++ b/filesystem/browsers/Safari.swift @@ -116,8 +116,8 @@ class Safari: BrowserModule { self.addTextToFile(atUrl: safariDownloads, text: "timestamp,url") for user in getBasicUsersOnSystem() { - let downloadsPlist = URL(fileURLWithPath: "\(user.homedir)/Library/Safari/Downloads.plist") - + let downloadsPlist = URL(fileURLWithPath: "\(user.homedir)/Library/Safari/Downloads.plist") + if filemanager.fileExists(atPath: downloadsPlist.path) { let plistDict = Aftermath.getPlistAsDict(atUrl: downloadsPlist) @@ -144,7 +144,6 @@ class Safari: BrowserModule { } } self.addTextToFile(atUrl: safariDownloads, text: "\(timestamp),\(url)") - } } } From e17da5020a33109b4cf79a03d891fe9dfaf5bbae Mon Sep 17 00:00:00 2001 From: stuartjash Date: Tue, 24 Oct 2023 08:26:25 -0700 Subject: [PATCH 13/23] added unified logging to ignore/disable list --- unifiedlogs/UnifiedLogModule.swift | 38 +++++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/unifiedlogs/UnifiedLogModule.swift b/unifiedlogs/UnifiedLogModule.swift index ce20a71..2ee199d 100644 --- a/unifiedlogs/UnifiedLogModule.swift +++ b/unifiedlogs/UnifiedLogModule.swift @@ -67,25 +67,29 @@ class UnifiedLogModule: AftermathModule, AMProto { } func run() { - self.log("Starting logging unified logs") - self.log("Filtering Unified Log. Hang Tight!") - - // run the external input file of predicates - if let externalLogFile = self.logFile { - if !filemanager.fileExists(atPath: externalLogFile) { - self.log("No external predicate file found at \(externalLogFile)") - } else { - let externalParsedPredicates = parsePredicateFile(path: externalLogFile) - print(externalParsedPredicates) - filterPredicates(predicates: externalParsedPredicates) + if Command.disableFeatures["ul"] == false { + self.log("Starting logging unified logs") + self.log("Filtering Unified Log. Hang Tight!") + + // run the external input file of predicates + if let externalLogFile = self.logFile { + if !filemanager.fileExists(atPath: externalLogFile) { + self.log("No external predicate file found at \(externalLogFile)") + } else { + let externalParsedPredicates = parsePredicateFile(path: externalLogFile) + print(externalParsedPredicates) + filterPredicates(predicates: externalParsedPredicates) + } } + + // run default predicates + filterPredicates(predicates: self.defaultPredicates) + self.log("Unified Log filtering complete.") + + self.log("Finished logging unified logs") + } else { + self.log("Skipping unified logging") } - - // run default predicates - filterPredicates(predicates: self.defaultPredicates) - self.log("Unified Log filtering complete.") - - self.log("Finished logging unified logs") } } From 5b87dfddc7288a4a918d4019f56099f7f296c02a Mon Sep 17 00:00:00 2001 From: stuartjash Date: Tue, 24 Oct 2023 08:26:37 -0700 Subject: [PATCH 14/23] unified log addition --- aftermath/Command.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aftermath/Command.swift b/aftermath/Command.swift index 5fe01e1..c4e21e8 100644 --- a/aftermath/Command.swift +++ b/aftermath/Command.swift @@ -29,7 +29,7 @@ class Command { static var unifiedLogsFile: String? = nil static var esLogs: [String] = ["create", "exec", "mmap"] static let version: String = "2.2.0" - static var disableFeatures: [String:Bool] = ["all": false, "browsers": false, "browser-killswitch": false, "databases": false, "filesystem": false, "proc-info": false, "slack": false] + static var disableFeatures: [String:Bool] = ["all": false, "browsers": false, "browser-killswitch": false, "databases": false, "filesystem": false, "proc-info": false, "slack": false, "ul": false] static func main() { setup(with: CommandLine.arguments) @@ -269,7 +269,7 @@ class Command { print(" usage: --collect-dirs /Users//Downloads /tmp") print("--deep -> performs deep scan and captures metadata from Users entire directory (WARNING: this may be time-consuming)") print("--disable -> disable a set of aftermath features that may collect personal user data") - print(" usage: --disable browsers browser-killswitch databases filesystem proc-info slack") + print(" usage: --disable browsers browser-killswitch databases filesystem proc-info slack ul") print(" --disable all") print("--es-logs -> specify which Endpoint Security events (space-separated) to collect (defaults are: create exec mmap)") print(" usage: --es-logs exec open rename") From 76c18c750d1bd1c6aa6990303c0c1f26482d79a7 Mon Sep 17 00:00:00 2001 From: stuartjash Date: Tue, 24 Oct 2023 08:26:49 -0700 Subject: [PATCH 15/23] bump to v2.2.0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b43f82f..e34d265 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ To uninstall the aftermath binary, run the `AftermathUninstaller.pkg` from the [ --deep or -d -> perform a deep scan of the file system for modified and accessed timestamped metadata WARNING: This will be a time-intensive, memory-consuming scan. --disable -> disable a set of aftermath features that may collect personal user data - Available features to disable: browsers -> collecting browser information | browser-killswitch -> force-closes browers | -> databases -> tcc & lsquarantine databases | filesystem -> walking the filesystem for timestamps | proc-info -> collecting process information via TrueTree and eslogger | all -> all aforementioned options + Available features to disable: browsers -> collecting browser information | browser-killswitch -> force-closes browers | -> databases -> tcc & lsquarantine databases | filesystem -> walking the filesystem for timestamps | proc-info -> collecting process information via TrueTree and eslogger | slack -> slack data | ul -> unified logging modules | all -> all aforementioned options usage: --disable browsers browser-killswitch databases filesystem proc-info slack --disable all --es-logs -> specify which Endpoint Security events (space-separated) to collect (defaults are: create exec mmap). To disable, see --disable es-logs From 0b89624067b369b536027d965665dddd78792fb0 Mon Sep 17 00:00:00 2001 From: stuartjash Date: Fri, 1 Dec 2023 16:31:25 -0800 Subject: [PATCH 16/23] collection of diagnosticsreports and crashreporter files --- artifacts/LogFiles.swift | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/artifacts/LogFiles.swift b/artifacts/LogFiles.swift index 84a7b3b..dd8abab 100644 --- a/artifacts/LogFiles.swift +++ b/artifacts/LogFiles.swift @@ -61,8 +61,44 @@ class LogFiles: ArtifactsModule { } } + func collectDiagnosticsReports() { + let diagReportsDir = self.createNewDir(dir: self.logFilesDir, dirname: "diagnostics_reports") + + let files = filemanager.filesInDirRecursive(path: "/Library/Logs/DiagnosticReports") + for file in files { + let filePath = URL(fileURLWithPath: file.relativePath) + if (filemanager.fileExists(atPath: filePath.path)) { + self.copyFileToCase(fileToCopy: filePath, toLocation: diagReportsDir) + } + } + + for user in getBasicUsersOnSystem() { + let files = filemanager.filesInDirRecursive(path: "\(user.homedir)/Library/Logs/DiagnosticReports") + for file in files { + let filePath = URL(fileURLWithPath: file.relativePath) + if (filemanager.fileExists(atPath: filePath.path)) { + self.copyFileToCase(fileToCopy: filePath, toLocation: diagReportsDir, newFileName: "\(user)_\(filePath.lastPathComponent)") + } + } + } + } + + func collectCrashReports() { + let crashReportsDir = self.createNewDir(dir: self.logFilesDir, dirname: "crash_reporter") + + let files = filemanager.filesInDirRecursive(path: "/Library/Logs/CrashReporter") + for file in files { + let filePath = URL(fileURLWithPath: file.relativePath) + if (filemanager.fileExists(atPath: filePath.path)) { + self.copyFileToCase(fileToCopy: filePath, toLocation: crashReportsDir) + } + } + } + override func run() { captureLogFiles() captureUserLogs() + collectDiagnosticsReports() + collectCrashReports() } } From d03f00d316fae0463c053e43587809767f063e82 Mon Sep 17 00:00:00 2001 From: stuartjash Date: Sat, 2 Dec 2023 14:55:23 -0800 Subject: [PATCH 17/23] fix username --- artifacts/LogFiles.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artifacts/LogFiles.swift b/artifacts/LogFiles.swift index dd8abab..445775e 100644 --- a/artifacts/LogFiles.swift +++ b/artifacts/LogFiles.swift @@ -77,7 +77,7 @@ class LogFiles: ArtifactsModule { for file in files { let filePath = URL(fileURLWithPath: file.relativePath) if (filemanager.fileExists(atPath: filePath.path)) { - self.copyFileToCase(fileToCopy: filePath, toLocation: diagReportsDir, newFileName: "\(user)_\(filePath.lastPathComponent)") + self.copyFileToCase(fileToCopy: filePath, toLocation: diagReportsDir, newFileName: "\(user.username)_\(filePath.lastPathComponent)") } } } From 49aab5a66f34125dd4ba12ba36112c7c673c3d27 Mon Sep 17 00:00:00 2001 From: Stuart Ashenbrenner <72467868+stuartjash@users.noreply.github.com> Date: Sat, 2 Dec 2023 15:11:10 -0800 Subject: [PATCH 18/23] v2.2.0 (#19) * dump the btm file to capture other persistence items * capture fish config file * bump to v2.2.0 * memory usage dump * formatting * added unified logging to ignore/disable list * unified log addition * bump to v2.2.0 * collection of diagnosticsreports and crashreporter files * fix username --- README.md | 4 +- aftermath.xcodeproj/project.pbxproj | 20 ++++++ aftermath/Command.swift | 11 ++- artifacts/LogFiles.swift | 36 ++++++++++ artifacts/ShellHistoryAndProfiles.swift | 4 +- filesystem/browsers/Safari.swift | 5 +- memory/MemoryModule.swift | 22 ++++++ memory/Stat.swift | 92 +++++++++++++++++++++++++ persistence/BTM.swift | 21 ++++++ persistence/PersistenceModule.swift | 10 +++ unifiedlogs/UnifiedLogModule.swift | 38 +++++----- 11 files changed, 236 insertions(+), 27 deletions(-) create mode 100644 memory/MemoryModule.swift create mode 100644 memory/Stat.swift create mode 100644 persistence/BTM.swift diff --git a/README.md b/README.md index 762e949..e34d265 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![](https://github.com/jamf/aftermath/blob/main/AftermathLogo.png) -![](https://img.shields.io/badge/release-2.1.1-bright%20green) ![](https://img.shields.io/badge/macOS-12.0%2B-blue) ![](https://img.shields.io/badge/license-MIT-orange) +![](https://img.shields.io/badge/release-2.2.0-bright%20green) ![](https://img.shields.io/badge/macOS-12.0%2B-blue) ![](https://img.shields.io/badge/license-MIT-orange) ## About @@ -85,7 +85,7 @@ To uninstall the aftermath binary, run the `AftermathUninstaller.pkg` from the [ --deep or -d -> perform a deep scan of the file system for modified and accessed timestamped metadata WARNING: This will be a time-intensive, memory-consuming scan. --disable -> disable a set of aftermath features that may collect personal user data - Available features to disable: browsers -> collecting browser information | browser-killswitch -> force-closes browers | -> databases -> tcc & lsquarantine databases | filesystem -> walking the filesystem for timestamps | proc-info -> collecting process information via TrueTree and eslogger | all -> all aforementioned options + Available features to disable: browsers -> collecting browser information | browser-killswitch -> force-closes browers | -> databases -> tcc & lsquarantine databases | filesystem -> walking the filesystem for timestamps | proc-info -> collecting process information via TrueTree and eslogger | slack -> slack data | ul -> unified logging modules | all -> all aforementioned options usage: --disable browsers browser-killswitch databases filesystem proc-info slack --disable all --es-logs -> specify which Endpoint Security events (space-separated) to collect (defaults are: create exec mmap). To disable, see --disable es-logs diff --git a/aftermath.xcodeproj/project.pbxproj b/aftermath.xcodeproj/project.pbxproj index 9fad263..f8040fa 100644 --- a/aftermath.xcodeproj/project.pbxproj +++ b/aftermath.xcodeproj/project.pbxproj @@ -16,6 +16,9 @@ 5E93B0AE2941608D009D2AB5 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E93B0AD2941608D009D2AB5 /* Data.swift */; }; 5E93B0B0294160B6009D2AB5 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E93B0AF294160B6009D2AB5 /* String.swift */; }; 5EA438FF2A7010FF00F3E2B9 /* XProtectBehavioralService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA438FE2A7010FF00F3E2B9 /* XProtectBehavioralService.swift */; }; + 5ECE5DC12ADF2B4A00939BB0 /* BTM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ECE5DC02ADF2B4A00939BB0 /* BTM.swift */; }; + 5ECE5DC42AE0406700939BB0 /* MemoryModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ECE5DC32AE0406700939BB0 /* MemoryModule.swift */; }; + 5ECE5DC62AE040B700939BB0 /* Stat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ECE5DC52AE040B700939BB0 /* Stat.swift */; }; 5EFDDCD72AC6661A00EEF193 /* Brave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFDDCD62AC6661A00EEF193 /* Brave.swift */; }; 70A44403275707A90035F40E /* SystemReconModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A44402275707A90035F40E /* SystemReconModule.swift */; }; 70A44405275A76990035F40E /* LSQuarantine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A44404275A76990035F40E /* LSQuarantine.swift */; }; @@ -91,6 +94,9 @@ 5E93B0AD2941608D009D2AB5 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; 5E93B0AF294160B6009D2AB5 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 5EA438FE2A7010FF00F3E2B9 /* XProtectBehavioralService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XProtectBehavioralService.swift; sourceTree = ""; }; + 5ECE5DC02ADF2B4A00939BB0 /* BTM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTM.swift; sourceTree = ""; }; + 5ECE5DC32AE0406700939BB0 /* MemoryModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryModule.swift; sourceTree = ""; }; + 5ECE5DC52AE040B700939BB0 /* Stat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stat.swift; sourceTree = ""; }; 5EFDDCD62AC6661A00EEF193 /* Brave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Brave.swift; sourceTree = ""; }; 70A44402275707A90035F40E /* SystemReconModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemReconModule.swift; sourceTree = ""; }; 70A44404275A76990035F40E /* LSQuarantine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LSQuarantine.swift; sourceTree = ""; }; @@ -177,6 +183,15 @@ path = endpointSecurity; sourceTree = ""; }; + 5ECE5DC22AE0405500939BB0 /* memory */ = { + isa = PBXGroup; + children = ( + 5ECE5DC32AE0406700939BB0 /* MemoryModule.swift */, + 5ECE5DC52AE040B700939BB0 /* Stat.swift */, + ); + path = memory; + sourceTree = ""; + }; 70A44401275707800035F40E /* systemRecon */ = { isa = PBXGroup; children = ( @@ -266,6 +281,7 @@ A09B239B2848F6050062D592 /* Periodic.swift */, A007834D28947D71008489EA /* Emond.swift */, A007834F28947E80008489EA /* LoginItems.swift */, + 5ECE5DC02ADF2B4A00939BB0 /* BTM.swift */, ); path = persistence; sourceTree = ""; @@ -383,6 +399,7 @@ A374535B2757C1110074B65C /* extensions */, A0E1E3F9275ED4B7008D0DC6 /* filesystem */, A02509F228ADB1930030D6A7 /* helpers */, + 5ECE5DC22AE0405500939BB0 /* memory */, A0E1E3F4275ED2D6008D0DC6 /* network */, A0776CAC27482FF2007D18D8 /* persistence */, A029AB132876A01300649701 /* processes */, @@ -538,6 +555,7 @@ A029AB1C28774CA400649701 /* Tree.swift in Sources */, A007835028947E80008489EA /* LoginItems.swift in Sources */, A0C930D428A4318F0011FB87 /* Timeline.swift in Sources */, + 5ECE5DC12ADF2B4A00939BB0 /* BTM.swift in Sources */, A374535A275735B40074B65C /* LoginHooks.swift in Sources */, 70CF9E3A27611C6100FD884B /* ShellHistoryAndProfiles.swift in Sources */, A0E1E3EB275EC800008D0DC6 /* Firefox.swift in Sources */, @@ -546,6 +564,7 @@ 5E93B0B0294160B6009D2AB5 /* String.swift in Sources */, A0E1E3E9275EC736008D0DC6 /* BrowserModule.swift in Sources */, A02509F428ADB1A80030D6A7 /* CHelpers.swift in Sources */, + 5ECE5DC62AE040B700939BB0 /* Stat.swift in Sources */, 70A44403275707A90035F40E /* SystemReconModule.swift in Sources */, A029AB2B2877F52D00649701 /* launchdXPC.m in Sources */, A0E1E3EF275EC810008D0DC6 /* Safari.swift in Sources */, @@ -574,6 +593,7 @@ A0D6D54927FE52C1002BB3C8 /* SystemExtensions.swift in Sources */, A08342D6284A8247005E437A /* AnalysisModule.swift in Sources */, A029AB192876A29600649701 /* Pids.swift in Sources */, + 5ECE5DC42AE0406700939BB0 /* MemoryModule.swift in Sources */, A08342D8284E48FC005E437A /* LogFiles.swift in Sources */, A0D6D54327F76C58002BB3C8 /* Cron.swift in Sources */, A02509F128AD93DA0030D6A7 /* Storyline.swift in Sources */, diff --git a/aftermath/Command.swift b/aftermath/Command.swift index 862d4d8..c4e21e8 100644 --- a/aftermath/Command.swift +++ b/aftermath/Command.swift @@ -28,8 +28,8 @@ class Command { static var collectDirs: [String] = [] static var unifiedLogsFile: String? = nil static var esLogs: [String] = ["create", "exec", "mmap"] - static let version: String = "2.1.0" - static var disableFeatures: [String:Bool] = ["all": false, "browsers": false, "browser-killswitch": false, "databases": false, "filesystem": false, "proc-info": false, "slack": false] + static let version: String = "2.2.0" + static var disableFeatures: [String:Bool] = ["all": false, "browsers": false, "browser-killswitch": false, "databases": false, "filesystem": false, "proc-info": false, "slack": false, "ul": false] static func main() { setup(with: CommandLine.arguments) @@ -209,6 +209,11 @@ class Command { // FileSystem let fileSysModule = FileSystemModule() fileSysModule.run() + + + // Memory + let memoryModule = MemoryModule() + memoryModule.run() // Artifacts @@ -264,7 +269,7 @@ class Command { print(" usage: --collect-dirs /Users//Downloads /tmp") print("--deep -> performs deep scan and captures metadata from Users entire directory (WARNING: this may be time-consuming)") print("--disable -> disable a set of aftermath features that may collect personal user data") - print(" usage: --disable browsers browser-killswitch databases filesystem proc-info slack") + print(" usage: --disable browsers browser-killswitch databases filesystem proc-info slack ul") print(" --disable all") print("--es-logs -> specify which Endpoint Security events (space-separated) to collect (defaults are: create exec mmap)") print(" usage: --es-logs exec open rename") diff --git a/artifacts/LogFiles.swift b/artifacts/LogFiles.swift index 84a7b3b..445775e 100644 --- a/artifacts/LogFiles.swift +++ b/artifacts/LogFiles.swift @@ -61,8 +61,44 @@ class LogFiles: ArtifactsModule { } } + func collectDiagnosticsReports() { + let diagReportsDir = self.createNewDir(dir: self.logFilesDir, dirname: "diagnostics_reports") + + let files = filemanager.filesInDirRecursive(path: "/Library/Logs/DiagnosticReports") + for file in files { + let filePath = URL(fileURLWithPath: file.relativePath) + if (filemanager.fileExists(atPath: filePath.path)) { + self.copyFileToCase(fileToCopy: filePath, toLocation: diagReportsDir) + } + } + + for user in getBasicUsersOnSystem() { + let files = filemanager.filesInDirRecursive(path: "\(user.homedir)/Library/Logs/DiagnosticReports") + for file in files { + let filePath = URL(fileURLWithPath: file.relativePath) + if (filemanager.fileExists(atPath: filePath.path)) { + self.copyFileToCase(fileToCopy: filePath, toLocation: diagReportsDir, newFileName: "\(user.username)_\(filePath.lastPathComponent)") + } + } + } + } + + func collectCrashReports() { + let crashReportsDir = self.createNewDir(dir: self.logFilesDir, dirname: "crash_reporter") + + let files = filemanager.filesInDirRecursive(path: "/Library/Logs/CrashReporter") + for file in files { + let filePath = URL(fileURLWithPath: file.relativePath) + if (filemanager.fileExists(atPath: filePath.path)) { + self.copyFileToCase(fileToCopy: filePath, toLocation: crashReportsDir) + } + } + } + override func run() { captureLogFiles() captureUserLogs() + collectDiagnosticsReports() + collectCrashReports() } } diff --git a/artifacts/ShellHistoryAndProfiles.swift b/artifacts/ShellHistoryAndProfiles.swift index d230bb0..43a70e5 100644 --- a/artifacts/ShellHistoryAndProfiles.swift +++ b/artifacts/ShellHistoryAndProfiles.swift @@ -21,7 +21,7 @@ class BashProfiles: ArtifactsModule { let userFiles = [ ".bash_history", ".bash_profile", ".bashrc", ".bash_logout", ".zsh_history", ".zshenv", ".zprofile", ".zshrc", ".zlogin", ".zlogout", - ".sh_history" + ".sh_history", ".config/fish/config.fish" ] let globalFiles = ["/etc/profile", "/etc/zshenv", "/etc/zprofile", "/etc/zshrc", "/etc/zlogin", "/etc/zlogout"] @@ -31,7 +31,7 @@ class BashProfiles: ArtifactsModule { for filename in userFiles { let path = URL(fileURLWithPath: "\(user.homedir)/\(filename)") if (filemanager.fileExists(atPath: path.path)) { - let newFileName = "\(user.username)_\(filename)" + let newFileName = "\(user.username)_\(filename.replacingOccurrences(of: "/", with: ""))" self.copyFileToCase(fileToCopy: path, toLocation: self.profilesDir, newFileName: newFileName) } diff --git a/filesystem/browsers/Safari.swift b/filesystem/browsers/Safari.swift index 4ec836b..e9f03c3 100644 --- a/filesystem/browsers/Safari.swift +++ b/filesystem/browsers/Safari.swift @@ -116,8 +116,8 @@ class Safari: BrowserModule { self.addTextToFile(atUrl: safariDownloads, text: "timestamp,url") for user in getBasicUsersOnSystem() { - let downloadsPlist = URL(fileURLWithPath: "\(user.homedir)/Library/Safari/Downloads.plist") - + let downloadsPlist = URL(fileURLWithPath: "\(user.homedir)/Library/Safari/Downloads.plist") + if filemanager.fileExists(atPath: downloadsPlist.path) { let plistDict = Aftermath.getPlistAsDict(atUrl: downloadsPlist) @@ -144,7 +144,6 @@ class Safari: BrowserModule { } } self.addTextToFile(atUrl: safariDownloads, text: "\(timestamp),\(url)") - } } } diff --git a/memory/MemoryModule.swift b/memory/MemoryModule.swift new file mode 100644 index 0000000..b943a50 --- /dev/null +++ b/memory/MemoryModule.swift @@ -0,0 +1,22 @@ +// +// MemoryModule.swift +// aftermath +// +// Created by Stuart Ashenbrenner on 10/18/23. +// + +import Foundation + +class MemoryModule: AftermathModule { + let name = "Memory Module" + let dirName = "Memory" + let description = "A module for collecting memory data" + lazy var moduleDirRoot = self.createNewDirInRoot(dirName: dirName) + + func run() { + self.log("Collecting available memory information") + + let stat = Stat() + stat.run() + } +} diff --git a/memory/Stat.swift b/memory/Stat.swift new file mode 100644 index 0000000..14a2e5e --- /dev/null +++ b/memory/Stat.swift @@ -0,0 +1,92 @@ +// +// Stat.swift +// aftermath +// +// Created by Stuart Ashenbrenner on 10/18/23. +// + +import Foundation + +class Stat: MemoryModule { + + private func scannerModule(inputString: String, stringToFind: String) -> Double? { + let scanner = Scanner(string: inputString) + + if scanner.scanUpTo(stringToFind, into: nil), + scanner.scanString(stringToFind, into: nil) { + var result: Double = 0.0 + + if scanner.scanDouble(&result) { + return result + } + } + return nil + } + + override func run() { + + let writeFile = self.createNewCaseFile(dirUrl: moduleDirRoot, filename: "memory_usage.txt") + + // trim extra characters from output + var trimSet = CharacterSet.whitespacesAndNewlines + trimSet.insert(charactersIn: "\"") + + // create vm_stat shell + let command = "vm_stat" + let vmstatOutput = Aftermath.shell(command) + + // convert bytes to GiB + let byteConverter = 0.00000000093132257 + + // pagesize + var pagesizeOutput = Aftermath.shell("pagesize") + pagesizeOutput = pagesizeOutput.trimmingCharacters(in: trimSet) + let pagesizeDouble = Double(pagesizeOutput) + + // parse vm_stat output + var componentDict = [String:Double]() + let vmLines = vmstatOutput.split(separator: "\n") + for l in vmLines { + let components = l.split(separator: ":") + + if components.count == 2 { + let key = components[0].trimmingCharacters(in: trimSet) + let value = components[1].trimmingCharacters(in: trimSet) + let valueDouble = Double(value) + let updatedValue = valueDouble ?? 0 * pagesizeDouble! + componentDict[key] = updatedValue + } + } + + // app memory + let appMemory = (componentDict["Anonymous pages"] ?? 0.0) - (componentDict["Pages purgeable"] ?? 0.0) + let wired = Double(componentDict["Pages wired down"]!) + let active = Double(componentDict["Pages active"]!) + let inactive = Double(componentDict["Pages inactive"]!) + let spec = Double(componentDict["Pages speculative"]!) + let throttled = Double(componentDict["Pages throttled"]!) + let freeMemory = Double(componentDict["Pages free"]!) + let purgeable = Double(componentDict["Pages purgeable"]!) + let compressed = Double(componentDict["Pages occupied by compressor"]!) + let tradTotal = ((wired + active + inactive + spec + throttled + freeMemory + compressed) * pagesizeDouble!) * byteConverter + let fileBacked = Double(componentDict["File-backed pages"]!) + let physicalTotal = ((appMemory + wired + compressed + fileBacked + purgeable + freeMemory) * pagesizeDouble!) * byteConverter + + // swap usage + let vmSwapUsageOutput = Aftermath.shell("sysctl vm.swapusage") + + if let vmSwapUsage = scannerModule(inputString: vmSwapUsageOutput, stringToFind: "used = ") { + self.addTextToFile(atUrl: writeFile, text: "Swap used: \(vmSwapUsage * 0.0009765625)\n") + } + + // memory pressure + let memoryPressureOutput = Aftermath.shell("memory_pressure") + if let memoryPressure = scannerModule(inputString: memoryPressureOutput, stringToFind: "percentage: ") { + self.addTextToFile(atUrl: writeFile, text: "Memory Pressure: \(100 - memoryPressure)%") + } + + // write out + self.addTextToFile(atUrl: writeFile, text: "\nTraditional Memory:\nWired Memory: \(wired)\nActive Memory: \(active)\nInactive Memory: \(inactive)\nPages Speculative: \(spec)\nPages Throttled: \(throttled)\nPurgeable: \(purgeable)\nCompressed: \(compressed)\nFree Memory: \(freeMemory)\nTotal: \(String(format: "%.2f", tradTotal))GiB\n") + self.addTextToFile(atUrl: writeFile, text: "\nActivity Monitor Memory:\nApp Memory: \(appMemory)\nWired: \(wired)\nCompressed: \(compressed)\nMemory Used: \(appMemory + wired + compressed)\nCached files: \(fileBacked + purgeable)\nTotal Physical: \(String(format: "%.2f", physicalTotal))GiB\n") + } +} diff --git a/persistence/BTM.swift b/persistence/BTM.swift new file mode 100644 index 0000000..e067e4d --- /dev/null +++ b/persistence/BTM.swift @@ -0,0 +1,21 @@ +// +// BTM.swift +// aftermath +// +// Created by Stuart Ashenbrenner on 10/17/23. +// + +import Foundation + +class BTM: PersistenceModule { + + override func run() { + self.log("Dumping btm file") + + let command = "sfltool dumpbtm" + let output = Aftermath.shell(command) + + let btmDumpFile = self.createNewCaseFile(dirUrl: moduleDirRoot, filename: "btm.txt") + self.addTextToFile(atUrl: btmDumpFile, text: output) + } +} diff --git a/persistence/PersistenceModule.swift b/persistence/PersistenceModule.swift index 7fe38a5..ee230cc 100644 --- a/persistence/PersistenceModule.swift +++ b/persistence/PersistenceModule.swift @@ -28,24 +28,34 @@ class PersistenceModule: AftermathModule, AMProto { let hooks = LoginHooks(saveToRawDir: persistenceRawDir) hooks.run() + // capture all cron tabs let cron = Cron(saveToRawDir: persistenceRawDir) cron.run() + // collect overrides file let overrides = Overrides(saveToRawDir: persistenceRawDir) overrides.run() + // write out all system extensions let systemExtensions = SystemExtensions(saveToRawDir: persistenceRawDir) systemExtensions.run() + // collect any periodic scripts let periodicScripts = Periodic(saveToRawDir: persistenceRawDir) periodicScripts.run() + // on older OSs, collect emond let emond = Emond(saveToRawDir: persistenceRawDir) emond.run() + // gather all Login Items let loginItems = LoginItems(saveToRawDir: persistenceRawDir) loginItems.run() + // dump the BTM file + let btmParser = BTM() + btmParser.run() + self.log("Finished gathering persistence mechanisms") } diff --git a/unifiedlogs/UnifiedLogModule.swift b/unifiedlogs/UnifiedLogModule.swift index ce20a71..2ee199d 100644 --- a/unifiedlogs/UnifiedLogModule.swift +++ b/unifiedlogs/UnifiedLogModule.swift @@ -67,25 +67,29 @@ class UnifiedLogModule: AftermathModule, AMProto { } func run() { - self.log("Starting logging unified logs") - self.log("Filtering Unified Log. Hang Tight!") - - // run the external input file of predicates - if let externalLogFile = self.logFile { - if !filemanager.fileExists(atPath: externalLogFile) { - self.log("No external predicate file found at \(externalLogFile)") - } else { - let externalParsedPredicates = parsePredicateFile(path: externalLogFile) - print(externalParsedPredicates) - filterPredicates(predicates: externalParsedPredicates) + if Command.disableFeatures["ul"] == false { + self.log("Starting logging unified logs") + self.log("Filtering Unified Log. Hang Tight!") + + // run the external input file of predicates + if let externalLogFile = self.logFile { + if !filemanager.fileExists(atPath: externalLogFile) { + self.log("No external predicate file found at \(externalLogFile)") + } else { + let externalParsedPredicates = parsePredicateFile(path: externalLogFile) + print(externalParsedPredicates) + filterPredicates(predicates: externalParsedPredicates) + } } + + // run default predicates + filterPredicates(predicates: self.defaultPredicates) + self.log("Unified Log filtering complete.") + + self.log("Finished logging unified logs") + } else { + self.log("Skipping unified logging") } - - // run default predicates - filterPredicates(predicates: self.defaultPredicates) - self.log("Unified Log filtering complete.") - - self.log("Finished logging unified logs") } } From e8a8e7860a58fca1be85516ec036263eb80c833f Mon Sep 17 00:00:00 2001 From: stuartjash Date: Thu, 7 Mar 2024 11:18:07 -0800 Subject: [PATCH 19/23] 2.2.1 --- README.md | 57 ++++++++++++++++++++++++++++++++++- aftermath/Command.swift | 2 +- analysis/DatabaseParser.swift | 1 + libs/launchdXPC/launchdXPC.m | 4 +-- persistence/Overrides.swift | 16 +++++++--- 5 files changed, 71 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e34d265..bc6c6a8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![](https://github.com/jamf/aftermath/blob/main/AftermathLogo.png) -![](https://img.shields.io/badge/release-2.2.0-bright%20green) ![](https://img.shields.io/badge/macOS-12.0%2B-blue) ![](https://img.shields.io/badge/license-MIT-orange) +![](https://img.shields.io/badge/release-2.2.1-bright%20green) ![](https://img.shields.io/badge/macOS-12.0%2B-blue) ![](https://img.shields.io/badge/license-MIT-orange) ## About @@ -66,6 +66,61 @@ tcc: process == "tccd" ### Note Because `eslogger` and `tcpdump` run on additional threads and the goal is to collect as much data from them as possible, they exit when aftermath exits. Because of this, the last line of the eslogger json file or the pcap file generated from tcpdump may be truncated. +### File Collection List +- Artifacts + - Configuration Profiles + - Log Files + - LSQuarantine Database + - Shell History and Profiles (bash, csh, fish, ksh, zsh) + - TCC Database + - XBS Database (XProtect Behabioral Service) +- Filesystem + - Browser Data (Cookies, Downloads, Extensions, History) + - Arc + - Brave + - Chrome + - Edge + - Firefox + - Safari + - File Data + - Walk common directories to get accessed, birth, modified timestamps + - Slack +- Memory + - Calculate data based on current memory usage, swap, etc. +- Network + - Active network connections + - Airport Preferences +- Persistence + - BTM Database + - Cron + - Emond + - Launch Items + - Launch Agents + - Launch Daemons + - Login Hooks + - Login Items + - Overrides + - launchd Overrides + - MDM Overrides + - Periodic Scripts + - System Extensions +- Processes + - Leverage [TrueTree](https://github.com/themittenmac/TrueTree) to create process tree +- System Recon + - Environment Variables + - Install History + - Installed Applications + - Installed Users + - Interfaces + - MRT Version + - Running Applications + - Security Assessment (SIP status, Gatekeeper status, Firewall status, Filevault status, Remote Login, Airdrop status, I/O statistics, Screensharing status, Login History, Network Interface Parameters) + - XProtect Version + - XProtect Remediator (XPR) Version +- Unified Logs + - Default Unified Logs (failed_sudo, login, manual_configuration_profile_install, screensharing, ssh, tcc, xprotect_remediator) + - Additional can be passed in at runtime + ## Releases There is an Aftermath.pkg available under [Releases](https://github.com/jamf/aftermath/releases). This pkg is signed and notarized. It will install the aftermath binary at `/usr/local/bin/`. This would be the ideal way to deploy via MDM. Since this is installed in `bin`, you can then run aftermath like ```bash diff --git a/aftermath/Command.swift b/aftermath/Command.swift index c4e21e8..7e3e24d 100644 --- a/aftermath/Command.swift +++ b/aftermath/Command.swift @@ -28,7 +28,7 @@ class Command { static var collectDirs: [String] = [] static var unifiedLogsFile: String? = nil static var esLogs: [String] = ["create", "exec", "mmap"] - static let version: String = "2.2.0" + static let version: String = "2.2.1" static var disableFeatures: [String:Bool] = ["all": false, "browsers": false, "browser-killswitch": false, "databases": false, "filesystem": false, "proc-info": false, "slack": false, "ul": false] static func main() { diff --git a/analysis/DatabaseParser.swift b/analysis/DatabaseParser.swift index c976f17..8b07906 100644 --- a/analysis/DatabaseParser.swift +++ b/analysis/DatabaseParser.swift @@ -328,6 +328,7 @@ class DatabaseParser: AftermathModule { case contacts_full = "kTCCServiceContactsFull" case contacts_limited = "kTCCServiceContactsLimited" case currentLocation = "kTCCServiceLocation" + case endpointSecurity = "kTCCServiceEndpointSecurityClient" case fileAccess = "kTCCServiceFileProviderDomain" case fileAccess_request = "kTCCServiceFileProviderPresence" case fitness = "kTCCServiceMotion" diff --git a/libs/launchdXPC/launchdXPC.m b/libs/launchdXPC/launchdXPC.m index edfcf13..2c70f00 100644 --- a/libs/launchdXPC/launchdXPC.m +++ b/libs/launchdXPC/launchdXPC.m @@ -1,5 +1,5 @@ // -// launchdXPC.c +// launchdXPC.m // Created by Patrick Wardle // Ported from code by Jonathan Levin // @@ -367,7 +367,7 @@ hit up launchd (via XPC) to get process info //end key line? (line: "}") // remove dictionary, as it's no longer needed - if(YES == [obj hasSuffix:@"}"]) + if(YES == [obj isEqualToString:@"}"]) { //remove [dictionaries removeLastObject]; diff --git a/persistence/Overrides.swift b/persistence/Overrides.swift index 1e0aba7..30e0c3d 100644 --- a/persistence/Overrides.swift +++ b/persistence/Overrides.swift @@ -15,7 +15,7 @@ class Overrides: PersistenceModule { self.saveToRawDir = saveToRawDir } - func collectOverrides(urlLocations: [URL], capturedFile: URL) { + func collectLaunchdOverrides(urlLocations: [URL], capturedFile: URL) { for url in urlLocations { let plistDict = Aftermath.getPlistAsDict(atUrl: url) @@ -25,14 +25,20 @@ class Overrides: PersistenceModule { } } + func collectMdmOverrides(path: String) { + self.copyFileToCase(fileToCopy: URL(fileURLWithPath: path), toLocation: moduleDirRoot) + } + override func run() { - self.log("Collecting overrides...") + self.log("Collecting all overrides...") + // launchd overrides let capturedOverridesFile = self.createNewCaseFile(dirUrl: moduleDirRoot, filename: "overrides.txt") - let overrides = filemanager.filesInDirRecursive(path: "/var/db/launchd.db/com.apple.launchd/") + collectLaunchdOverrides(urlLocations: overrides, capturedFile: capturedOverridesFile) - collectOverrides(urlLocations: overrides, capturedFile: capturedOverridesFile) - + // mdm overrides + let mdmOverridesFile = "/Library/Application Support/com.apple.TCC/MDMOverrides.plist" + collectMdmOverrides(path: mdmOverridesFile) } } From 3dc70ac3024a38298e22f4e30426523139c03405 Mon Sep 17 00:00:00 2001 From: stuartjash Date: Thu, 7 Mar 2024 14:15:59 -0800 Subject: [PATCH 20/23] 2.2.1 --- analysis/DatabaseParser.swift | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/analysis/DatabaseParser.swift b/analysis/DatabaseParser.swift index 8b07906..705e8e0 100644 --- a/analysis/DatabaseParser.swift +++ b/analysis/DatabaseParser.swift @@ -276,9 +276,12 @@ class DatabaseParser: AftermathModule { case unknown = "1" case allowed = "2" case limited = "3" + case addOnly = "4" + case appDataAllowed = "5" } enum TCCAuthReason: String, CaseIterable { + case inherited = "0" case error = "1" case userConsent = "2" case userSet = "3" @@ -292,9 +295,9 @@ class DatabaseParser: AftermathModule { case entitled = "11" case appTypePolicy = "12" } - + /* - Compiled from /System/Library/PrivateFrameworks/TCC.framework/Resources/en.lproj/Localizable.strings and https://rainforest.engineering/2021-02-09-macos-tcc/ + Originally compiled from /System/Library/PrivateFrameworks/TCC.framework/Resources/en.lproj/Localizable.strings and https://rainforest.engineering/2021-02-09-macos-tcc/ */ enum TCCService: String, CaseIterable { // critical @@ -312,6 +315,8 @@ class DatabaseParser: AftermathModule { // file access case adminFiles = "kTCCServiceSystemPolicySysAdminFiles" + case appData = "kTCCServiceSystemPolicyAppData" + case appManagement = "kTCCServiceSystemPolicyAppBundles" case desktopFolder = "kTCCServiceSystemPolicyDesktopFolder" case developerFiles = "kTCCServiceSystemPolicyDeveloperFiles" case documentsFolder = "kTCCServiceSystemPolicyDocumentsFolder" @@ -321,24 +326,25 @@ class DatabaseParser: AftermathModule { // service access case addressBook = "kTCCServiceAddressBook" case appleEvents = "kTCCServiceAppleEvents" + case audioCapture = "kTCCServiceAudioCapture" case availability = "kTCCServiceUserAvailability" - case bluetooth_always = "kTCCServiceBluetoothAlways" + case bluetoothAlways = "kTCCServiceBluetoothAlways" case calendar = "kTCCServiceCalendar" case camera = "kTCCServiceCamera" case contacts_full = "kTCCServiceContactsFull" case contacts_limited = "kTCCServiceContactsLimited" case currentLocation = "kTCCServiceLocation" case endpointSecurity = "kTCCServiceEndpointSecurityClient" - case fileAccess = "kTCCServiceFileProviderDomain" - case fileAccess_request = "kTCCServiceFileProviderPresence" + case icloudDriveAccess = "kTCCServiceFileProviderDomain" + case fileAccessPresence = "kTCCServiceFileProviderPresence" case fitness = "kTCCServiceMotion" - case focus_notifications = "kTCCServiceFocusStatus" + case focusStatus = "kTCCServiceFocusStatus" case gamecenter = "kTCCServiceGameCenterFriends" case homeData = "kTCCServiceWillow" case mediaLibrary = "kTCCServiceMediaLibrary" case microphone = "kTCCServiceMicrophone" case photos = "kTCCServicePhotos" - case photos_add = "kTCCServicePhotosAdd" + case photosAdd = "kTCCServicePhotosAdd" case proto3Right = "kTCCServicePrototype3Rights" case reminders = "kTCCServiceReminders" case removableVolumes = "kTCCServiceSystemPolicyRemovableVolumes" From 32f023e4e8dd209c930d4251ede046ac36edb6b2 Mon Sep 17 00:00:00 2001 From: stuartjash Date: Fri, 8 Mar 2024 05:22:55 -0800 Subject: [PATCH 21/23] added single boot enum --- analysis/DatabaseParser.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analysis/DatabaseParser.swift b/analysis/DatabaseParser.swift index 705e8e0..e322967 100644 --- a/analysis/DatabaseParser.swift +++ b/analysis/DatabaseParser.swift @@ -277,7 +277,7 @@ class DatabaseParser: AftermathModule { case allowed = "2" case limited = "3" case addOnly = "4" - case appDataAllowed = "5" + case singleBootAllowed = "5" // allowed for a unique boot_uuid } enum TCCAuthReason: String, CaseIterable { From 72b39f8e09fe92adde8c6cdebb49dc57c2dda227 Mon Sep 17 00:00:00 2001 From: Jaron Bradley Date: Fri, 8 Mar 2024 14:25:02 -0500 Subject: [PATCH 22/23] Updated TrueTree to be more up to date with the current release --- README.md | 2 - aftermath.xcodeproj/project.pbxproj | 42 ++-- .../xcshareddata/swiftpm/Package.resolved | 3 +- aftermath/Command.swift | 4 - memory/MemoryModule.swift | 22 -- memory/Stat.swift | 92 ------- processes/Network.swift | 219 +++++++++++++++++ processes/Node.swift | 83 +++++++ processes/Pids.swift | 7 +- processes/ProcessModule.swift | 63 ++++- processes/Processes.swift | 206 ++++++++++++++++ processes/Tree.swift | 225 ------------------ 12 files changed, 581 insertions(+), 387 deletions(-) delete mode 100644 memory/MemoryModule.swift delete mode 100644 memory/Stat.swift create mode 100644 processes/Network.swift create mode 100644 processes/Node.swift create mode 100644 processes/Processes.swift delete mode 100644 processes/Tree.swift diff --git a/README.md b/README.md index bc6c6a8..cfe0811 100644 --- a/README.md +++ b/README.md @@ -85,8 +85,6 @@ Because `eslogger` and `tcpdump` run on additional threads and the goal is to co - File Data - Walk common directories to get accessed, birth, modified timestamps - Slack -- Memory - - Calculate data based on current memory usage, swap, etc. - Network - Active network connections - Airport Preferences diff --git a/aftermath.xcodeproj/project.pbxproj b/aftermath.xcodeproj/project.pbxproj index f8040fa..d193841 100644 --- a/aftermath.xcodeproj/project.pbxproj +++ b/aftermath.xcodeproj/project.pbxproj @@ -17,8 +17,6 @@ 5E93B0B0294160B6009D2AB5 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E93B0AF294160B6009D2AB5 /* String.swift */; }; 5EA438FF2A7010FF00F3E2B9 /* XProtectBehavioralService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA438FE2A7010FF00F3E2B9 /* XProtectBehavioralService.swift */; }; 5ECE5DC12ADF2B4A00939BB0 /* BTM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ECE5DC02ADF2B4A00939BB0 /* BTM.swift */; }; - 5ECE5DC42AE0406700939BB0 /* MemoryModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ECE5DC32AE0406700939BB0 /* MemoryModule.swift */; }; - 5ECE5DC62AE040B700939BB0 /* Stat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ECE5DC52AE040B700939BB0 /* Stat.swift */; }; 5EFDDCD72AC6661A00EEF193 /* Brave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFDDCD62AC6661A00EEF193 /* Brave.swift */; }; 70A44403275707A90035F40E /* SystemReconModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A44402275707A90035F40E /* SystemReconModule.swift */; }; 70A44405275A76990035F40E /* LSQuarantine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A44404275A76990035F40E /* LSQuarantine.swift */; }; @@ -32,7 +30,6 @@ A02509F428ADB1A80030D6A7 /* CHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02509F328ADB1A80030D6A7 /* CHelpers.swift */; }; A029AB152876A02800649701 /* ProcessModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A029AB142876A02800649701 /* ProcessModule.swift */; }; A029AB192876A29600649701 /* Pids.swift in Sources */ = {isa = PBXBuildFile; fileRef = A029AB182876A29600649701 /* Pids.swift */; }; - A029AB1C28774CA400649701 /* Tree.swift in Sources */ = {isa = PBXBuildFile; fileRef = A029AB1B28774CA400649701 /* Tree.swift */; }; A029AB2B2877F52D00649701 /* launchdXPC.m in Sources */ = {isa = PBXBuildFile; fileRef = A029AB2A2877F52D00649701 /* launchdXPC.m */; }; A05BF3BD284FF8C0009E197B /* FileSystemModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05BF3BC284FF8C0009E197B /* FileSystemModule.swift */; }; A05BF3BF284FF8CF009E197B /* Slack.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05BF3BE284FF8CF009E197B /* Slack.swift */; }; @@ -66,6 +63,9 @@ A1E433E528B9270800E2B510 /* dummyPlist.plist in Resources */ = {isa = PBXBuildFile; fileRef = A1E433E428B9270800E2B510 /* dummyPlist.plist */; }; A3046F8E27627DAC0069AA21 /* Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3046F8D27627DAC0069AA21 /* Module.swift */; }; A3046F902763AE5E0069AA21 /* CaseFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3046F8F2763AE5E0069AA21 /* CaseFiles.swift */; }; + A31009A42B9B838100068593 /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31009A32B9B838100068593 /* Network.swift */; }; + A31009A62B9B83E300068593 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31009A52B9B83E300068593 /* Node.swift */; }; + A31009A82B9B845E00068593 /* Processes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31009A72B9B845E00068593 /* Processes.swift */; }; A3745358275730870074B65C /* LaunchItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3745357275730870074B65C /* LaunchItems.swift */; }; A374535A275735B40074B65C /* LoginHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3745359275735B40074B65C /* LoginHooks.swift */; }; A374535D2757C1300074B65C /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A374535C2757C1300074B65C /* FileManager.swift */; }; @@ -95,8 +95,6 @@ 5E93B0AF294160B6009D2AB5 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 5EA438FE2A7010FF00F3E2B9 /* XProtectBehavioralService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XProtectBehavioralService.swift; sourceTree = ""; }; 5ECE5DC02ADF2B4A00939BB0 /* BTM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTM.swift; sourceTree = ""; }; - 5ECE5DC32AE0406700939BB0 /* MemoryModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryModule.swift; sourceTree = ""; }; - 5ECE5DC52AE040B700939BB0 /* Stat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stat.swift; sourceTree = ""; }; 5EFDDCD62AC6661A00EEF193 /* Brave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Brave.swift; sourceTree = ""; }; 70A44402275707A90035F40E /* SystemReconModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemReconModule.swift; sourceTree = ""; }; 70A44404275A76990035F40E /* LSQuarantine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LSQuarantine.swift; sourceTree = ""; }; @@ -110,7 +108,6 @@ A02509F328ADB1A80030D6A7 /* CHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CHelpers.swift; sourceTree = ""; }; A029AB142876A02800649701 /* ProcessModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessModule.swift; sourceTree = ""; }; A029AB182876A29600649701 /* Pids.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pids.swift; sourceTree = ""; }; - A029AB1B28774CA400649701 /* Tree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tree.swift; sourceTree = ""; }; A029AB282877F4F400649701 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; A029AB292877F50900649701 /* launchdXPC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = launchdXPC.h; sourceTree = ""; }; A029AB2A2877F52D00649701 /* launchdXPC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = launchdXPC.m; sourceTree = ""; }; @@ -146,6 +143,9 @@ A1E433E428B9270800E2B510 /* dummyPlist.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = dummyPlist.plist; sourceTree = ""; }; A3046F8D27627DAC0069AA21 /* Module.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Module.swift; sourceTree = ""; }; A3046F8F2763AE5E0069AA21 /* CaseFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaseFiles.swift; sourceTree = ""; }; + A31009A32B9B838100068593 /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; + A31009A52B9B83E300068593 /* Node.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = ""; }; + A31009A72B9B845E00068593 /* Processes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Processes.swift; sourceTree = ""; }; A3745357275730870074B65C /* LaunchItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchItems.swift; sourceTree = ""; }; A3745359275735B40074B65C /* LoginHooks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginHooks.swift; sourceTree = ""; }; A374535C2757C1300074B65C /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; @@ -183,15 +183,6 @@ path = endpointSecurity; sourceTree = ""; }; - 5ECE5DC22AE0405500939BB0 /* memory */ = { - isa = PBXGroup; - children = ( - 5ECE5DC32AE0406700939BB0 /* MemoryModule.swift */, - 5ECE5DC52AE040B700939BB0 /* Stat.swift */, - ); - path = memory; - sourceTree = ""; - }; 70A44401275707800035F40E /* systemRecon */ = { isa = PBXGroup; children = ( @@ -230,7 +221,9 @@ children = ( A029AB142876A02800649701 /* ProcessModule.swift */, A029AB182876A29600649701 /* Pids.swift */, - A029AB1B28774CA400649701 /* Tree.swift */, + A31009A32B9B838100068593 /* Network.swift */, + A31009A52B9B83E300068593 /* Node.swift */, + A31009A72B9B845E00068593 /* Processes.swift */, ); path = processes; sourceTree = ""; @@ -399,7 +392,6 @@ A374535B2757C1110074B65C /* extensions */, A0E1E3F9275ED4B7008D0DC6 /* filesystem */, A02509F228ADB1930030D6A7 /* helpers */, - 5ECE5DC22AE0405500939BB0 /* memory */, A0E1E3F4275ED2D6008D0DC6 /* network */, A0776CAC27482FF2007D18D8 /* persistence */, A029AB132876A01300649701 /* processes */, @@ -552,7 +544,6 @@ A3046F902763AE5E0069AA21 /* CaseFiles.swift in Sources */, A029AB152876A02800649701 /* ProcessModule.swift in Sources */, 5E6780F22922E7E800BAF04B /* Edge.swift in Sources */, - A029AB1C28774CA400649701 /* Tree.swift in Sources */, A007835028947E80008489EA /* LoginItems.swift in Sources */, A0C930D428A4318F0011FB87 /* Timeline.swift in Sources */, 5ECE5DC12ADF2B4A00939BB0 /* BTM.swift in Sources */, @@ -564,9 +555,10 @@ 5E93B0B0294160B6009D2AB5 /* String.swift in Sources */, A0E1E3E9275EC736008D0DC6 /* BrowserModule.swift in Sources */, A02509F428ADB1A80030D6A7 /* CHelpers.swift in Sources */, - 5ECE5DC62AE040B700939BB0 /* Stat.swift in Sources */, 70A44403275707A90035F40E /* SystemReconModule.swift in Sources */, A029AB2B2877F52D00649701 /* launchdXPC.m in Sources */, + A31009A42B9B838100068593 /* Network.swift in Sources */, + A31009A82B9B845E00068593 /* Processes.swift in Sources */, A0E1E3EF275EC810008D0DC6 /* Safari.swift in Sources */, A006B5A12882FBA70091FAA1 /* DatabaseParser.swift in Sources */, 70A44405275A76990035F40E /* LSQuarantine.swift in Sources */, @@ -585,6 +577,7 @@ 5E494473293AC914007FFBDD /* URL.swift in Sources */, A007834E28947D71008489EA /* Emond.swift in Sources */, 5E29FD752A2FB0EF008D528F /* ESLogs.swift in Sources */, + A31009A62B9B83E300068593 /* Node.swift in Sources */, A076742F2755798F00ED7066 /* ArtifactsModule.swift in Sources */, A0759135275985170006766F /* TCC.swift in Sources */, A0E1E3F6275ED2E4008D0DC6 /* NetworkModule.swift in Sources */, @@ -593,7 +586,6 @@ A0D6D54927FE52C1002BB3C8 /* SystemExtensions.swift in Sources */, A08342D6284A8247005E437A /* AnalysisModule.swift in Sources */, A029AB192876A29600649701 /* Pids.swift in Sources */, - 5ECE5DC42AE0406700939BB0 /* MemoryModule.swift in Sources */, A08342D8284E48FC005E437A /* LogFiles.swift in Sources */, A0D6D54327F76C58002BB3C8 /* Cron.swift in Sources */, A02509F128AD93DA0030D6A7 /* Storyline.swift in Sources */, @@ -773,7 +765,7 @@ CODE_SIGN_STYLE = Manual; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=macosx*]" = 6PV5YF2UES; + "DEVELOPMENT_TEAM[sdk=macosx*]" = C793NB2B2B; ENABLE_HARDENED_RUNTIME = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -783,7 +775,7 @@ MACH_O_TYPE = mh_execute; NEW_SETTING = ""; ONLY_ACTIVE_ARCH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.crashsecurity.aftermath; + PRODUCT_BUNDLE_IDENTIFIER = com.jamf.aftermath; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_INCLUDE_PATHS = "$(SRCROOT) $(SRCROOT)/libs/ProcLib $(SRCROOT)/libs/launchdXPC"; @@ -802,8 +794,8 @@ CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES; CODE_SIGN_STYLE = Manual; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = 6PV5YF2UES; - "DEVELOPMENT_TEAM[sdk=macosx*]" = 6PV5YF2UES; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = C793NB2B2B; ENABLE_HARDENED_RUNTIME = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -813,7 +805,7 @@ MACH_O_TYPE = mh_execute; NEW_SETTING = ""; ONLY_ACTIVE_ARCH = NO; - PRODUCT_BUNDLE_IDENTIFIER = com.crashsecurity.aftermath; + PRODUCT_BUNDLE_IDENTIFIER = com.jamf.aftermath; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_INCLUDE_PATHS = "$(SRCROOT) $(SRCROOT)/libs/ProcLib $(SRCROOT)/libs/launchdXPC"; diff --git a/aftermath.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/aftermath.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0130adf..d054fbc 100644 --- a/aftermath.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/aftermath.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "d0d4edfdf2bf3cd05b3ba2dec0af1a9c271c93f944cbba8677cc647f74a6b323", "pins" : [ { "identity" : "zipfoundation", @@ -10,5 +11,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/aftermath/Command.swift b/aftermath/Command.swift index 7e3e24d..565e709 100644 --- a/aftermath/Command.swift +++ b/aftermath/Command.swift @@ -211,10 +211,6 @@ class Command { fileSysModule.run() - // Memory - let memoryModule = MemoryModule() - memoryModule.run() - // Artifacts let artifactModule = ArtifactsModule() diff --git a/memory/MemoryModule.swift b/memory/MemoryModule.swift deleted file mode 100644 index b943a50..0000000 --- a/memory/MemoryModule.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// MemoryModule.swift -// aftermath -// -// Created by Stuart Ashenbrenner on 10/18/23. -// - -import Foundation - -class MemoryModule: AftermathModule { - let name = "Memory Module" - let dirName = "Memory" - let description = "A module for collecting memory data" - lazy var moduleDirRoot = self.createNewDirInRoot(dirName: dirName) - - func run() { - self.log("Collecting available memory information") - - let stat = Stat() - stat.run() - } -} diff --git a/memory/Stat.swift b/memory/Stat.swift deleted file mode 100644 index 14a2e5e..0000000 --- a/memory/Stat.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// Stat.swift -// aftermath -// -// Created by Stuart Ashenbrenner on 10/18/23. -// - -import Foundation - -class Stat: MemoryModule { - - private func scannerModule(inputString: String, stringToFind: String) -> Double? { - let scanner = Scanner(string: inputString) - - if scanner.scanUpTo(stringToFind, into: nil), - scanner.scanString(stringToFind, into: nil) { - var result: Double = 0.0 - - if scanner.scanDouble(&result) { - return result - } - } - return nil - } - - override func run() { - - let writeFile = self.createNewCaseFile(dirUrl: moduleDirRoot, filename: "memory_usage.txt") - - // trim extra characters from output - var trimSet = CharacterSet.whitespacesAndNewlines - trimSet.insert(charactersIn: "\"") - - // create vm_stat shell - let command = "vm_stat" - let vmstatOutput = Aftermath.shell(command) - - // convert bytes to GiB - let byteConverter = 0.00000000093132257 - - // pagesize - var pagesizeOutput = Aftermath.shell("pagesize") - pagesizeOutput = pagesizeOutput.trimmingCharacters(in: trimSet) - let pagesizeDouble = Double(pagesizeOutput) - - // parse vm_stat output - var componentDict = [String:Double]() - let vmLines = vmstatOutput.split(separator: "\n") - for l in vmLines { - let components = l.split(separator: ":") - - if components.count == 2 { - let key = components[0].trimmingCharacters(in: trimSet) - let value = components[1].trimmingCharacters(in: trimSet) - let valueDouble = Double(value) - let updatedValue = valueDouble ?? 0 * pagesizeDouble! - componentDict[key] = updatedValue - } - } - - // app memory - let appMemory = (componentDict["Anonymous pages"] ?? 0.0) - (componentDict["Pages purgeable"] ?? 0.0) - let wired = Double(componentDict["Pages wired down"]!) - let active = Double(componentDict["Pages active"]!) - let inactive = Double(componentDict["Pages inactive"]!) - let spec = Double(componentDict["Pages speculative"]!) - let throttled = Double(componentDict["Pages throttled"]!) - let freeMemory = Double(componentDict["Pages free"]!) - let purgeable = Double(componentDict["Pages purgeable"]!) - let compressed = Double(componentDict["Pages occupied by compressor"]!) - let tradTotal = ((wired + active + inactive + spec + throttled + freeMemory + compressed) * pagesizeDouble!) * byteConverter - let fileBacked = Double(componentDict["File-backed pages"]!) - let physicalTotal = ((appMemory + wired + compressed + fileBacked + purgeable + freeMemory) * pagesizeDouble!) * byteConverter - - // swap usage - let vmSwapUsageOutput = Aftermath.shell("sysctl vm.swapusage") - - if let vmSwapUsage = scannerModule(inputString: vmSwapUsageOutput, stringToFind: "used = ") { - self.addTextToFile(atUrl: writeFile, text: "Swap used: \(vmSwapUsage * 0.0009765625)\n") - } - - // memory pressure - let memoryPressureOutput = Aftermath.shell("memory_pressure") - if let memoryPressure = scannerModule(inputString: memoryPressureOutput, stringToFind: "percentage: ") { - self.addTextToFile(atUrl: writeFile, text: "Memory Pressure: \(100 - memoryPressure)%") - } - - // write out - self.addTextToFile(atUrl: writeFile, text: "\nTraditional Memory:\nWired Memory: \(wired)\nActive Memory: \(active)\nInactive Memory: \(inactive)\nPages Speculative: \(spec)\nPages Throttled: \(throttled)\nPurgeable: \(purgeable)\nCompressed: \(compressed)\nFree Memory: \(freeMemory)\nTotal: \(String(format: "%.2f", tradTotal))GiB\n") - self.addTextToFile(atUrl: writeFile, text: "\nActivity Monitor Memory:\nApp Memory: \(appMemory)\nWired: \(wired)\nCompressed: \(compressed)\nMemory Used: \(appMemory + wired + compressed)\nCached files: \(fileBacked + purgeable)\nTotal Physical: \(String(format: "%.2f", physicalTotal))GiB\n") - } -} diff --git a/processes/Network.swift b/processes/Network.swift new file mode 100644 index 0000000..8855169 --- /dev/null +++ b/processes/Network.swift @@ -0,0 +1,219 @@ +// +// Network.swift +// TrueTree +// +// Created by Jaron Bradley on 1/11/23. +// Copyright © 2023 TheMittenMac. All rights reserved. +// + +import Foundation +import ProcLib +// Handy reference -> https://stackoverflow.com/questions/29294491/swift-obtaining-ip-address-from-socket-returns-weird-value + +struct NetworkConnection { + let type: String? + let pid: Int + let family: String + let source: String + let sourcePort: UInt16 + let destination: String + let destinationPort: UInt16 + let status: String +} + +class TTNetworkConnections { + private let PROC_PIDLISTFD_SIZE = Int32(MemoryLayout.stride) + private let PROC_PIDFDSOCKETINFO_SIZE = Int32(MemoryLayout.stride) + var connections = [NetworkConnection]() + let pid: Int32 + + init(pid: Int32) { + self.pid = pid + + // get the size of the number of open files + let size = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil , 0) + + //get list of open file descriptors + let fdInfo = UnsafeMutablePointer.allocate(capacity: Int(size)) + defer { fdInfo.deallocate() } + buildConnections(fdInfo, size) + } + + private func getSocketFamily(socketInfoBuffer: UnsafeMutablePointer) -> String? { + switch socketInfoBuffer.pointee.psi.soi_family { + + case AF_INET: + return "IPv4" + + case AF_INET6: + return "IPv6" + + default: + return nil + } + } + + private func getType(socketInfoBuffer: UnsafeMutablePointer) -> String? { + switch Int(socketInfoBuffer.pointee.psi.soi_kind) { + case SOCKINFO_IN: + return "UDP" + + case SOCKINFO_TCP: + return "TCP" + + default: + return nil + } + } + + private func getLocalPort(socketInfoBuffer: UnsafeMutablePointer, socketType: String) -> UInt16 { + var port = UInt16(0) + + if socketType == "UDP" { + port = UInt16(socketInfoBuffer.pointee.psi.soi_proto.pri_in.insi_lport) + } + + if socketType == "TCP" { + port = UInt16(socketInfoBuffer.pointee.psi.soi_proto.pri_tcp.tcpsi_ini.insi_lport) + } + + return port.byteSwapped + } + + private func getRemotePort(socketInfoBuffer: UnsafeMutablePointer, socketType: String) -> UInt16 { + if socketType == "UDP" { + return 0 + } + + let port = UInt16(socketInfoBuffer.pointee.psi.soi_proto.pri_tcp.tcpsi_ini.insi_fport) + return port.byteSwapped + } + + private func getIP4DestinationAddress(socketInfoBuffer: UnsafeMutablePointer) -> String { + var result = [CChar].init(repeating: 0, count: 16) + inet_ntop(AF_INET, &socketInfoBuffer.pointee.psi.soi_proto.pri_tcp.tcpsi_ini.insi_faddr.ina_46.i46a_addr4, &result, 16) + let ipAddr = String(cString: result) + + return ipAddr + } + + private func getIP6DestinationAddress(socketInfoBuffer: UnsafeMutablePointer) -> String { + var result = [CChar].init(repeating: 0, count: 128) + inet_ntop(AF_INET6, &(socketInfoBuffer.pointee.psi.soi_proto.pri_tcp.tcpsi_ini.insi_faddr.ina_6), &result, 128); + let ipAddr = String(cString: result) + + return ipAddr + } + + private func getIP4SourceAddress(socketInfoBuffer: UnsafeMutablePointer) -> String { + var result = [CChar].init(repeating: 0, count: 16) + inet_ntop(AF_INET, &socketInfoBuffer.pointee.psi.soi_proto.pri_tcp.tcpsi_ini.insi_laddr.ina_46.i46a_addr4, &result, 16) + let ipAddr = String(cString: result) + + return ipAddr + } + + private func getIP6SourceAddress(socketInfoBuffer: UnsafeMutablePointer) -> String { + var result = [CChar].init(repeating: 0, count: 128) + inet_ntop(AF_INET6, &(socketInfoBuffer.pointee.psi.soi_proto.pri_tcp.tcpsi_ini.insi_laddr.ina_6), &result, 128); + let ipAddr = String(cString: result) + + return ipAddr + } + + private func getStatus(socketInfoBuffer: UnsafeMutablePointer) -> String { + var status = "" + switch socketInfoBuffer.pointee.psi.soi_proto.pri_tcp.tcpsi_state { + case TSI_S_CLOSED: + status = "CLOSED" + case TSI_S_LISTEN: + status = "LISTENING" + case TSI_S_SYN_SENT: + status = "SYN SENT (active, have sent syn)" + case TSI_S_SYN_RECEIVED: + status = "SYN RECEIVED (have send and received syn)" + case TSI_S_ESTABLISHED: + status = "ESTABLISHED" + case TSI_S__CLOSE_WAIT: + status = "CLOSE WAIT (received fin, waiting for close) " + case TSI_S_FIN_WAIT_1: + status = "FIN WAIT1 (have closed, sent fin)" + case TSI_S_CLOSING: + status = "CLOSING (closed xchd FIN; await FIN ACK)" + case TSI_S_LAST_ACK: + status = "LAST ACK (had fin and close; await FIN ACK)" + case TSI_S_FIN_WAIT_2: + status = "FIN WAIT2 (have closed, fin is acked)" + case TSI_S_TIME_WAIT: + status = "TIME WAIT (in 2*msl quiet wait after close)" + case TSI_S_RESERVED: + status = "RESERVED" + default: + status = "Unknown" + } + + return status + } + + private func buildConnections(_ fdInfo: UnsafeMutablePointer, _ size: Int32) { + proc_pidinfo(self.pid, PROC_PIDLISTFDS, 0, fdInfo, size) + + // Go through each open file descriptor + for x in 0...Int(size/PROC_PIDLISTFD_SIZE) { + var localPort = UInt16(0) + var destinationPort = UInt16(0) + var destination = "" + var source = "" + + // Skip if file descriptor is not a socket + if PROX_FDTYPE_SOCKET != fdInfo[x].proc_fdtype { continue } + + // Get the socket info, skipping if an error occurs + let socketInfo = UnsafeMutablePointer.allocate(capacity: 1) + defer { socketInfo.deallocate() } + + if PROC_PIDFDSOCKETINFO_SIZE != proc_pidfdinfo(self.pid, fdInfo[x].proc_fd, PROC_PIDFDSOCKETINFO, socketInfo, PROC_PIDFDSOCKETINFO_SIZE) { + continue + } + + // Get IPv4 or IPV6 + guard let family = getSocketFamily(socketInfoBuffer: socketInfo) else { return } + + // Get UDP or TCP + guard let type = getType(socketInfoBuffer: socketInfo) else { return } + + // If this is a UDP connection + if type == "UDP" { + localPort = getLocalPort(socketInfoBuffer: socketInfo, socketType: type) + + } else if type == "TCP" { + // Far more details can be collected from TCP connections + localPort = getLocalPort(socketInfoBuffer: socketInfo, socketType: type) + destinationPort = UInt16(socketInfo.pointee.psi.soi_proto.pri_tcp.tcpsi_ini.insi_fport).byteSwapped + + // If this is a IPv4 address get the local and remote connections + if family == "IPv4" { + destination = getIP4DestinationAddress(socketInfoBuffer: socketInfo) + source = getIP4SourceAddress(socketInfoBuffer: socketInfo) + + } else if family == "IPv6" { + destination = getIP6DestinationAddress(socketInfoBuffer: socketInfo) + source = getIP6SourceAddress(socketInfoBuffer: socketInfo) + } + } + + let status = getStatus(socketInfoBuffer: socketInfo) + + let n = NetworkConnection(type: type, + pid: Int(pid), + family: family, + source: source, + sourcePort: localPort, + destination: destination, + destinationPort: destinationPort, + status: status) + + connections.append(n) + } + } +} diff --git a/processes/Node.swift b/processes/Node.swift new file mode 100644 index 0000000..dbd6f21 --- /dev/null +++ b/processes/Node.swift @@ -0,0 +1,83 @@ +// +// tree.swift +// TrueTree +// +// Created by Jaron Bradley on 11/2/19. +// 2020 TheMittenMac +// + + +import Foundation + + +final class Node { + let pid: Int + let path: String + let timestamp: String + let source: String + let displayString: String + private(set) var children: [Node] + + init(_ pid: Int, path: String, timestamp: String, source: String, displayString: String) { + self.pid = pid + self.path = path + self.timestamp = timestamp + self.source = source + self.displayString = displayString + children = [] + } + + func add(child: Node) { + children.append(child) + } + + func searchPlist(value: String) -> Node? { + if value == self.path { + return self + } + + for child in children { + if let found = child.searchPlist(value: value) { + return found + } + } + return nil + } +} + + +// Extension for printing the tree. Great approach. Currently uses global vars from argmanager +//https://stackoverflow.com/questions/46371513/printing-a-tree-with-indents-swift +extension Node { + func treeLines(_ nodeIndent:String="", _ childIndent:String="") -> [String] { + var text = "" + if path.hasSuffix(".plist") { + text = "\(displayString)" + + } else if displayString.hasPrefix("UDP") || displayString.hasPrefix("TCP") { + text = "\(displayString)" + + } else { + text = "\(displayString) \(String(pid))" + text += " \(timestamp)" + text += " Acquired parent from -> \(source)" + } + + return [ nodeIndent + text ] + + children.enumerated().map{ ($0 < children.count-1, $1) } + .flatMap{ $0 ? $1.treeLines("┣╸","┃ ") : $1.treeLines("┗╸"," ") } + .map{ childIndent + $0 } + + } + + func printTree(toFile: URL) { + let tree = treeLines().joined(separator:"\n") + print(toFile) + + do { + try tree.write(to: toFile, atomically: true, encoding: String.Encoding.utf8) + } catch { + print("Could not write TrueTree output to specified file") + } + } +} diff --git a/processes/Pids.swift b/processes/Pids.swift index 0e31a1b..3fa672c 100644 --- a/processes/Pids.swift +++ b/processes/Pids.swift @@ -1,4 +1,4 @@ -// +/* // Process.swift // aftermath // @@ -61,8 +61,8 @@ class Pids { var responsiblePid: CInt if (pidCheck == -1) { - print("Error getting responsible pid for process " + String(pidOfInterest)) - print("Defaulting to self") + //print("Error getting responsible pid for process " + String(pidOfInterest)) + //print("Defaulting to self") responsiblePid = CInt(pidOfInterest) } else { responsiblePid = pidCheck @@ -100,3 +100,4 @@ class Pids { return pids } } +*/ diff --git a/processes/ProcessModule.swift b/processes/ProcessModule.swift index 5873d58..719c6ac 100644 --- a/processes/ProcessModule.swift +++ b/processes/ProcessModule.swift @@ -17,22 +17,59 @@ class ProcessModule: AftermathModule { lazy var processFile = self.createNewCaseFile(dirUrl: moduleDirRoot, filename: "process_dump.txt") func run() { - if Command.disableFeatures["proc-info"] == false { - self.log("Starting process dump...") - - let saveFile = self.createNewCaseFile(dirUrl: self.moduleDirRoot, filename: "true_tree_output.txt") + self.log("Starting Process Module") + let saveFile = self.createNewCaseFile(dirUrl: self.moduleDirRoot, filename: "true_tree_output.txt") + + let pc = ProcessCollector() + let rootNode = pc.getNodeForPid(1) + guard rootNode != nil else { + print("Could not find the launchd process. Aborting...") + exit(1) + } + + //let nodePidDict = tree.createNodeDictionary() + //let treeRootNode = tree.buildTrueTree(nodePidDict) + + // Create a TrueTree + for proc in pc.processes { - let tree = Tree() - let nodePidDict = tree.createNodeDictionary() - let treeRootNode = tree.buildTrueTree(nodePidDict) + // Create an on the fly node for a plist if one exists + if let plist = proc.submittedByPlist { + // Check if this plist is already in the tree + if let existingPlistNode = rootNode?.searchPlist(value: plist) { + existingPlistNode.add(child: proc.node) + continue + } + + let plistNode = Node(-1, path: plist, timestamp: "00:00:00", source: "launchd_xpc", displayString: plist) + rootNode?.add(child: plistNode) + plistNode.add(child: proc.node) + continue + } - treeRootNode.printTree(saveFile) + // Otherwise add the process as a child to its true parent + let parentNode = pc.getNodeForPid(proc.trueParentPid) + parentNode?.add(child: proc.node) - self.log("Finished gathering process information") - - } else { - self.log("Skipping process collection") + // Create an on the fly node for any network connections this pid has and add them to itself + for x in proc.network { + if let type = x.type { + var displayString = "" + if type == "TCP" { + displayString = "\(type) - \(x.source):\(x.sourcePort) -> \(x.destination):\(x.destinationPort) - \(x.status)" + } else { + displayString = "\(type) - Local Port: \(x.sourcePort)" + } + + let networkNode = Node(-1, path: "none", timestamp: "00:00:00", source: "Network", displayString: displayString) + proc.node.add(child: networkNode) + } + } } - + + rootNode?.printTree(toFile: saveFile) + + self.log("Finished Process Module") } + } diff --git a/processes/Processes.swift b/processes/Processes.swift new file mode 100644 index 0000000..26baedf --- /dev/null +++ b/processes/Processes.swift @@ -0,0 +1,206 @@ +// +// process.swift +// TrueTree +// +// Created by Jaron Bradley on 11/1/19. +// 2020 TheMittenMac +// + +import Foundation +import ProcLib +import LaunchdXPC + +struct ttProcess { + let pid: Int + let ppid: Int + let responsiblePid: Int + let path: String + let submittedByPid: Int? + let submittedByPlist: String? + let timestamp: String + let node: Node + let trueParentPid: Int + let source: String + let network: [NetworkConnection] +} + + +class ProcessCollector { + var processes = [ttProcess]() + let timestampFormat = DateFormatter() + let InfoSize = Int32(MemoryLayout.stride) + let MaxPathLen = Int(4 * MAXPATHLEN) + typealias rpidFunc = @convention(c) (CInt) -> CInt + + init() { + timestampFormat.dateFormat = "yyyy-MM-dd HH:mm:ss" + self.collect() + } + + func collect() { + // Inspired by https://gist.github.com/kainjow/0e7650cc797a52261e0f4ba851477c2f + + // Call proc_listallpids once with nil/0 args to get the current number of pids + let initialNumPids = proc_listallpids(nil, 0) + + // Allocate a buffer of these number of pids. + // Make sure to deallocate it as this class does not manage memory for us. + let buffer = UnsafeMutablePointer.allocate(capacity: Int(initialNumPids)) + defer { + buffer.deallocate() + } + + // Calculate the buffer's total length in bytes + let bufferLength = initialNumPids * Int32(MemoryLayout.size) + + // Call the function again with our inputs now ready + let numPids = proc_listallpids(buffer, bufferLength) + + // Loop through each pid and build a process struct + for i in 0.. 1 { + trueParent = submittedPid + source = "application_services" + } else if responsiblePid != pid { + trueParent = responsiblePid + source = "responsible_pid" + } else { + trueParent = ppid + source = "parent_process" + } + + // Collect a plist if it caused this program to run + var plistNode: String? + if let launchctlPlist = getSubmittedByPlist(UInt(pid)) { + if launchctlPlist.hasSuffix(".plist") { + plistNode = launchctlPlist + source = "launchd_xpc" + } + } + + // Collect network connections + let n = TTNetworkConnections(pid: Int32(pid)) + let networkConnections = n.connections + + // Create the tree node + let node = Node(pid, path: path, timestamp: ts, source: source, displayString: path) + + // Create the process entry + let p = ttProcess(pid: pid, + ppid: ppid, + responsiblePid: responsiblePid, + path: path, + submittedByPid: submittedPid, + submittedByPlist: plistNode ?? nil, + timestamp: ts, + node: node, + trueParentPid: trueParent, + source: source, + network: networkConnections + ) + + // Add the process to the array of captured processes + processes.append(p) + + } + + // Sort the processes by time + processes = processes.sorted { $0.timestamp < $1.timestamp } + } + + func getPPID(_ pidOfInterest:Int) -> Int? { + // Call proc_pidinfo and return nil on error + let pidInfo = UnsafeMutablePointer.allocate(capacity: 1) + guard InfoSize == proc_pidinfo(Int32(pidOfInterest), PROC_PIDTBSDINFO, 0, pidInfo, InfoSize) else { return nil } + defer { pidInfo.deallocate() } + + return Int(pidInfo.pointee.pbi_ppid) + } + + func getResponsiblePid(_ pidOfInterest:Int) -> Int { + // Get responsible pid using private Apple API + let rpidSym:UnsafeMutableRawPointer! = dlsym(UnsafeMutableRawPointer(bitPattern: -1), "responsibility_get_pid_responsible_for_pid") + let responsiblePid = unsafeBitCast(rpidSym, to: rpidFunc.self)(CInt(pidOfInterest)) + + guard responsiblePid != -1 else { + print("Error getting responsible pid for process \(pidOfInterest). Setting to responsible pid to itself") + return pidOfInterest + } + + return Int(responsiblePid) + } + + func getPath(_ pidOfInterest: Int) -> String { + let pathBuffer = UnsafeMutablePointer.allocate(capacity: MaxPathLen) + defer { pathBuffer.deallocate() } + pathBuffer.initialize(repeating: 0, count: MaxPathLen) + + if proc_pidpath(Int32(pidOfInterest), pathBuffer, UInt32(MemoryLayout.stride * MaxPathLen)) == 0 { + return "unknown" + } + + return String(cString: pathBuffer) + } + + func getTimestamp(_ pidOfInterest: Int) -> Date { + // Call proc_pidinfo and return current date on error + let pidInfo = UnsafeMutablePointer.allocate(capacity: 1) + guard InfoSize == proc_pidinfo(Int32(pidOfInterest), PROC_PIDTBSDINFO, 0, pidInfo, InfoSize) else { return Date() } + defer { pidInfo.deallocate() } + + return Date(timeIntervalSince1970: TimeInterval(pidInfo.pointee.pbi_start_tvsec)) + } + + func getNodeForPid(_ pidOfInterest: Int) -> Node? { + for proc in processes { + if proc.pid == pidOfInterest { + return proc.node + } + } + + return nil + } +} + +extension ProcessCollector { + func printTimeline(outputFile:String?) { + for proc in processes { + let text = "\(proc.timestamp) \(proc.path) \(proc.pid)" + if let outputFile = outputFile { + let fileUrl = URL(fileURLWithPath: outputFile) + do { + try text.write(to: fileUrl, atomically: true, encoding: String.Encoding.utf8) + } catch { + print("Could not write TrueTree output to specified file") + } + } + print("\(proc.timestamp) \(proc.path) \(proc.pid)") + } + } +} diff --git a/processes/Tree.swift b/processes/Tree.swift deleted file mode 100644 index 4b7a471..0000000 --- a/processes/Tree.swift +++ /dev/null @@ -1,225 +0,0 @@ -// -// tree.swift -// aftermath -// -// Copyright 2022 JAMF Software, LLC -// -// The following code (with minor modifications) is from TrueTree, written by Jaron Bradley. -// 2020 TheMittenMac -// TrueTree: https://github.com/themittenmac/TrueTree -// Inspired by https://www.journaldev.com/21383/swift-tree-binary-tree-data-structure -// TrueTree License: https://github.com/themittenmac/TrueTree/blob/master/license.md - - -import Foundation -import LaunchdXPC - - -class Node: ProcessModule { - var pid: T - var ppid: UInt32 - weak var parent: Node? - var procPath: String - var responsiblePid: CInt - var timestamp: Date - var submittedByPlist: String? - var submittedByPid: Int? - var launchdProgramPath: String? - var children: [Node] = [] - var source: String? - - init(_ pid: T, ppid: UInt32, procPath: String, responsiblePid: CInt, timestamp: Date) { - self.pid = pid - self.ppid = ppid - self.procPath = procPath - self.responsiblePid = responsiblePid - self.timestamp = timestamp - } - - func printNodeData() -> [String] { - var val: String - - if self.procPath.hasSuffix(".plist") || self.procPath.hasSuffix("Terminated)") { - val = self.procPath - } else { - val = "\(self.procPath) \(self.pid)" - val += " \(self.timestamp)" - - if let source = self.source { - val += " \(source)" - } - } - return [val] + self.children.flatMap{$0.printNodeData()}.map{" "+$0} - } - - func printTree(_ toFile: URL?) { - let text = printNodeData().joined(separator: "\n") - if let toFile = toFile { - - do { - try text.write(to: toFile, atomically: true, encoding: String.Encoding.utf8) - } catch { - print("Could not write TrueTree output to specified file") - } - } else { - let text = printNodeData().joined(separator: "\n") - print(text) - } - } -} - - -class Tree: ProcessModule { - func buildStandardTree(_ nodePidDict:[Int:Node]) -> Node { - // Builds a tree using standard unix pids and ppids - for (_, node) in nodePidDict { - let ppid = Int(node.ppid) - if let parentNode = nodePidDict[ppid] { - parentNode.children.append(node) - } - } - - guard let rootNode = nodePidDict[1] else { - exit(1) - } - - return rootNode - } - - - func buildTrueTree(_ nodePidDict:[Int:Node]) -> Node { - // Empty dictionary to hold plist items - var nodePlistDict = [String:Node]() - - - - // Builds a tree based on the TrueTree concept and returns the root node - for (pid, node) in nodePidDict { - if pid == 1 { - continue - } - - // If a plist was responsible for the creation of a process add the plist as a node entry - // Pids don't matter as we will only reference the procPath - if let submittedByPlist = node.submittedByPlist { - if nodePlistDict[submittedByPlist] == nil { - let plistNode = Node(-1, ppid:1, procPath:submittedByPlist, responsiblePid:-1, timestamp: Date()) - nodePlistDict[submittedByPlist] = plistNode - - // Assign this plist as a child node to launchd - nodePidDict[1]?.children.append(plistNode) - } - } - - var trueParentPid:Int? - var trueParentPlist:String? - var source: String? - trueParentPlist = nil - - // Find the pid (or plist) we should use as the parent - if let submittedByPlist = node.submittedByPlist { - trueParentPlist = submittedByPlist - source = "Aquired parent from -> launchd_xpc" - } else if let submittedByPid = node.submittedByPid { - if nodePidDict.keys.contains(submittedByPid) { - trueParentPid = submittedByPid - source = "Aquired parent from -> Application_Services" - } else { - trueParentPid = Int(node.ppid) - } - - } else if let submittedByPlist = node.submittedByPlist { - trueParentPlist = submittedByPlist - source = "Aquired parent from -> submitted_by_plist" - } else if node.responsiblePid != pid { - trueParentPid = Int(node.responsiblePid) - source = "Aquired parent from -> responsible_pid" - } else { - trueParentPid = Int(node.ppid) - source = "Aquired parent from -> parent_process_id" - } - - node.source = source - - var parentNode: Node? - // Grab the parent of this node and assign this node as a child to it - if let trueParentPid = trueParentPid { - parentNode = nodePidDict[trueParentPid] - } else if let trueParentPlist = trueParentPlist { - parentNode = nodePlistDict[trueParentPlist] - } - - parentNode?.children.append(node) - } - - guard let rootNode = nodePidDict[1] else { - exit(1) - } - - return rootNode - } - - - func createNodeDictionary() -> [Int:Node] { - - let pids = Pids() - self.addTextToFile(atUrl: processFile, text: "TIMESTAMP PID PPID RESP_PID SUBMITTED_PID PROC_PATH ARGS") //\(node.timestamp) \(node.pid) \(node.ppid) \(node.responsiblePid) \(subNode) \(node.procPath) - var nodePidDict = [Int:Node]() - - // Go through each pid and create an initial tree node for it with all of the pid info we can find - for pid in pids.getActivePids() { - // Skip kernel pid as we won't be able to collect info on it - if pid == 0 { - continue - } - - // Create the tree node - let p = UnsafeMutablePointer.allocate(capacity: 1) - guard let ppid = pids.getPPID(pid, pidInfo: p) else { - print("Issue collecting pid information for \(pid). Skipping...") - continue - } - - let responsiblePid = pids.getResponsiblePid(pid) - let path = pids.getPidPath(pid) - let ts = pids.getTimestamp(pid, pidInfo: p) - - defer { p.deallocate() } - - let node = Node(pid as Any, ppid:ppid, procPath:path, responsiblePid: responsiblePid, timestamp: ts) - var subNode: Int = 0 - - let submitted = getSubmittedPid(Int32(pid)) - if submitted != 0 { - node.submittedByPid = Int(submitted) - subNode = Int(submitted) - - } - - if let launchctlPlist = getSubmittedByPlist(UInt(pid)) { - if launchctlPlist.hasSuffix(".plist") { - node.submittedByPlist = launchctlPlist - } - } - - // get the arguments of the process - let processArguments = getProcessArgs(UInt(pid)) - var allArgs: String = "" - - // a dict of dict - which is why the value.value - if processArguments != nil { - for (value) in processArguments ?? [:] { - let singleArg = String(describing: value.value) - allArgs = allArgs + " " + singleArg - } - } - - self.addTextToFile(atUrl: processFile, text: "\(node.timestamp) \(node.pid) \(node.ppid) \(node.responsiblePid) \(subNode) \(node.procPath) \(allArgs)") - - - nodePidDict[pid] = node - } - - return nodePidDict - } -} From c73cdcea84fafea163c9ea3e26a3899fc6453ce3 Mon Sep 17 00:00:00 2001 From: Jaron Bradley Date: Fri, 8 Mar 2024 14:36:52 -0500 Subject: [PATCH 23/23] Updated the version of the zipfoundation package being used --- aftermath.xcodeproj/project.pbxproj | 4 ++-- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aftermath.xcodeproj/project.pbxproj b/aftermath.xcodeproj/project.pbxproj index d193841..8b1683c 100644 --- a/aftermath.xcodeproj/project.pbxproj +++ b/aftermath.xcodeproj/project.pbxproj @@ -850,8 +850,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/weichsel/ZIPFoundation"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.9.9; + kind = exactVersion; + version = 0.9.18; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/aftermath.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/aftermath.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d054fbc..1eba987 100644 --- a/aftermath.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/aftermath.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/weichsel/ZIPFoundation", "state" : { - "revision" : "43ec568034b3731101dbf7670765d671c30f54f3", - "version" : "0.9.16" + "revision" : "b979e8b52c7ae7f3f39fa0182e738e9e7257eb78", + "version" : "0.9.18" } } ],