diff --git a/README.md b/README.md index 90b6367..cfe0811 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.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,59 @@ 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 +- 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 @@ -84,14 +137,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 | 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 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.xcodeproj/project.pbxproj b/aftermath.xcodeproj/project.pbxproj index ce40c6c..8b1683c 100644 --- a/aftermath.xcodeproj/project.pbxproj +++ b/aftermath.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ 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 */; }; 70CF9E3A27611C6100FD884B /* ShellHistoryAndProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70CF9E3927611C6100FD884B /* ShellHistoryAndProfiles.swift */; }; @@ -28,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 */; }; @@ -62,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 */; }; @@ -90,6 +94,8 @@ 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 = ""; }; 70CF9E3927611C6100FD884B /* ShellHistoryAndProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellHistoryAndProfiles.swift; sourceTree = ""; }; @@ -102,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 = ""; }; @@ -138,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 = ""; }; @@ -213,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 = ""; @@ -264,6 +274,7 @@ A09B239B2848F6050062D592 /* Periodic.swift */, A007834D28947D71008489EA /* Emond.swift */, A007834F28947E80008489EA /* LoginItems.swift */, + 5ECE5DC02ADF2B4A00939BB0 /* BTM.swift */, ); path = persistence; sourceTree = ""; @@ -290,6 +301,7 @@ A0E1E3EE275EC810008D0DC6 /* Safari.swift */, 5E6780F12922E7E800BAF04B /* Edge.swift */, 5E4BC8FF29D75A8E0004DAA6 /* Arc.swift */, + 5EFDDCD62AC6661A00EEF193 /* Brave.swift */, ); path = browsers; sourceTree = ""; @@ -520,6 +532,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 */, @@ -531,9 +544,9 @@ 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 */, A374535A275735B40074B65C /* LoginHooks.swift in Sources */, 70CF9E3A27611C6100FD884B /* ShellHistoryAndProfiles.swift in Sources */, A0E1E3EB275EC800008D0DC6 /* Firefox.swift in Sources */, @@ -544,6 +557,8 @@ A02509F428ADB1A80030D6A7 /* CHelpers.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 */, @@ -562,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 */, @@ -749,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)", @@ -759,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"; @@ -778,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)", @@ -789,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"; @@ -834,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 0130adf..1eba987 100644 --- a/aftermath.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/aftermath.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,14 +1,15 @@ { + "originHash" : "d0d4edfdf2bf3cd05b3ba2dec0af1a9c271c93f944cbba8677cc647f74a6b323", "pins" : [ { "identity" : "zipfoundation", "kind" : "remoteSourceControl", "location" : "https://github.com/weichsel/ZIPFoundation", "state" : { - "revision" : "43ec568034b3731101dbf7670765d671c30f54f3", - "version" : "0.9.16" + "revision" : "b979e8b52c7ae7f3f39fa0182e738e9e7257eb78", + "version" : "0.9.18" } } ], - "version" : 2 + "version" : 3 } diff --git a/aftermath/Command.swift b/aftermath/Command.swift index 397997a..565e709 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.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() { 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) @@ -138,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) @@ -155,78 +172,56 @@ class Command { mainModule.addTextToFile(atUrl: CaseFiles.metadataFile, text: "file,birth,modified,accessed,permissions,uid,gid,xattr,downloadedFrom") - // Start logging Endpoint Security data + // 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 @@ -269,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 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") 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/DatabaseParser.swift b/analysis/DatabaseParser.swift index c976f17..e322967 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 singleBootAllowed = "5" // allowed for a unique boot_uuid } 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,23 +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 fileAccess = "kTCCServiceFileProviderDomain" - case fileAccess_request = "kTCCServiceFileProviderPresence" + case endpointSecurity = "kTCCServiceEndpointSecurityClient" + 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" 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/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/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/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/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/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") + } + } } 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 6758023..15de98b 100644 --- a/filesystem/browsers/BrowserModule.swift +++ b/filesystem/browsers/BrowserModule.swift @@ -22,13 +22,16 @@ 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...") - // 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 @@ -50,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) } } - } } 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/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/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/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/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) } } diff --git a/persistence/PersistenceModule.swift b/persistence/PersistenceModule.swift index 79d6fe1..ee230cc 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 @@ -25,22 +28,35 @@ 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/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 7f92082..719c6ac 100644 --- a/processes/ProcessModule.swift +++ b/processes/ProcessModule.swift @@ -17,13 +17,59 @@ class ProcessModule: AftermathModule { lazy var processFile = self.createNewCaseFile(dirUrl: moduleDirRoot, filename: "process_dump.txt") func run() { - + self.log("Starting Process Module") let saveFile = self.createNewCaseFile(dirUrl: self.moduleDirRoot, filename: "true_tree_output.txt") - let tree = Tree() - let nodePidDict = tree.createNodeDictionary() - let treeRootNode = tree.buildTrueTree(nodePidDict) + 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 { + + // 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 + } + + // Otherwise add the process as a child to its true parent + let parentNode = pc.getNodeForPid(proc.trueParentPid) + parentNode?.add(child: proc.node) + + // 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) + } + } + } - treeRootNode.printTree(saveFile) + 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 - } -} 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..2ee199d 100644 --- a/unifiedlogs/UnifiedLogModule.swift +++ b/unifiedlogs/UnifiedLogModule.swift @@ -67,21 +67,29 @@ class UnifiedLogModule: AftermathModule, AMProto { } func run() { - 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.") + } }