mirror of
https://github.com/google/santa.git
synced 2026-01-14 16:58:02 -05:00
* Initial structure for ES wrappers, enriched types, logging * Basic working ES and logging functionality * Add in oneTBB and thread-safe-lru deps * Added a bunch of enriched types * Auto-mute self when establishing ES client * Basic auth, tamper client. Syslog of all events. Basic compiler tracking. * Update copyright header blobs, convert some tabs to spaces * Auth result cache. Fix getting translocation path. * Added remaining cache methods * Add AuthResultCache to Recorder client. Cache now operates on es_file_t. * Hooked up SNTPrefixTree * Fix CompilerController for RENAME. Fix AllowList logging missing path. * Block loading Santa kext * Added device manager client * Properly log DiskAppear events * Fix build to adopt new adhoc build * Handle clearing cache on UNMOUNT events * Ignore other ES clients if configured * Remove SNTAllowlistInfo. Rename AllowList to Allowlist. Minor cleanup. * Recorder now logs asynchronously. Enricher now returns shared_ptrs. * Added File writer. Added timestamps to BasicStream serializer. * Skip calling stat in SNTFileInfo when path given by ES. * Fix build issue * Address draft PR feedback * santactl integrated, XPC works, fix file writer bug * Integrate syncservice. Start observing some config changes. * Add metrics service wrapper * Add metrics config observers and metrics interval reset. * Start better dependency control. Add Null logger support. * Added more deps * Added more deps * Fix issue where metric service wasn't starting * Add missing variant include * Fix missing parent proc name * Added googletest and new unit test macro * Started expanding AuthResultCacheTest * Properly mock EndpointSecurityAPI * Finished AuthResultCacheTest * bazelrc now builds all C++ as C++17. Added LoggerTest. * Add FileTest. Abstract some File constants to Logger. * Added Empty serializer test * Started work on BasicStringTest. Fixed some BasicString serialization bugs. * Added Unlink BasicString serialization test * Added some more tests. Commonized some test code * Finished BasicStringTest. Converted to XCTest. * Standardize esapi variable naming * Bubble up gTest expect failures to XCTest failures * AuthResultCacheTest now uses XCTest. Added common TestUtils.h * EmptyTest now uses XCTest. * FileTest now uses XCTest * LoggerTest now uses XCTest. Removed santa_unit_gtest bazel macro. * Added ClientTest * Add basic Enricher tests * Add MessageTest. Make more TestUtils. * Rename metrics to Metrics * Add MetricsTest. * Apply template pattern to Serializer * Add SNTDecisionCacheTest. * Add SNTCachedDecisionTest. * Testing with coveralls debug mode * Allow manual CI runs * Remove unused property * Started work on SNTEndpointSecurityClientTest. * WIP SNTEndpointSecurityClientTest, fix test run issue * Added more base ES client tests * Add more base ES client tests * Base ES client tests done. Added serializer utils/tests. Expanded basic string tests. * Add utils test to test suite * Add copy ctor. Add test output to bazel coverage. * Single thread bazel coverage * Updaload coverage file * Updaload coverage file * Old gen cov test * Restructure message handlers to enable better testability * Added enable tests for all ES clients * Made a single MockEndpointSecurityAPI class to share everywhere * Added most of SNTCompilerControllerTest * Cleanup SNTCompilerControllerTest * Started expanding Auth client test * Finished up the Authorizer tests * Move to using enum class for notify/auth instead of bool * WIP for tamper resistance test. ASAN issues. * Add OCMock patch to fix test issue on ARM Macs * Changed patches directory name to external_patches * Update WORKSPACE path * Finished up Tamper Resistance tests * Finished up Recorder tests. * Move SNTExecutionControllerTest to ObjC++ * Initial work to port SNTExecutionControllerTest * Finished porting SNTExecutionControllerTest. * Added SNTExecutionControllerTest to list of unit tests * Ported SNTEndpointSecurityDeviceManager. * Test cleanup, use MockESAPI expectation helpers * Verify SNTEndpointSecurityDeviceManager expectations differently * Test cleanup, omit gTest param list where unused * Log message cleanup * Rename SNTApplicationTest to santad_test.mm * Finished porting santad_test, formerly SNTApplicationTest * Fix SNTEndpointSecurityDeviceManager issues * Pulled in missed fixes. Updated tests. * Renamed lowercase filenames to match rest of codebase * Fix non-static dispatch_once_t, and noisy watching compiler log message * WIP Started process of removing components no longer used * WIP Continued process of removing components no longer used * BUILD file cleanup. Proto warning. Removed unused global * Rename SNTEventProvider to SNTEndpointSecurityEventHandler * Rename SNTEndpointSecurityEventHandler protocol * Remove EnableSysxCache option. Remove --quick flag used during dev. * Ran testing/fix.sh * Addmissing param to fix.sh that was omitting .mm files. * clang-format * Fix linter: find cmd missing .mm ext, git grep exclude patch files. * Use MakeESProcess default params in tests * Move variables to camelCase in objc classes * More case changes * Sanitize strings * Change dispatch queue priorities and standardize daemon queue naming * Exclude patch files in markdown check * Ensure string log messages end with newline * Fix BasicStringTest * Disable clang-format in code producing different results in local/remote versions * Moved to using date ranges in copyright notices as per current guidelines * Update Source/common/SNTConfigurator.h Suggestion adding whitespace in comment to fix clang-format mangling Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com> * Removed santa_panic macro used in one place * Updated comment about ES cachability * Pin oneTBB to specific commit * Address outstanding WORKSPACE 'canonical reproducible form' messages * Use string append instead of ostringstream due to benchmark results * Remove use of freind classes in EnrichedTypes.h * Added SNTKVOManager, removed observers from SNTConfigurator. * Fixed SNTEndpointSecurityRecorderTest class name * Reduce usage of the auto keyword * Each SNTKVOManager instance now adds its own observer * Replaced more auto keywords with real types. * Remove leftover code coverage debugging from ci.yml * Updated comment * Memoize SNTFileInfo sha256. Reduce some cache sizes. * Fix issue checking for translocated paths * Use more performant NSURL creation method * Fix lint issue * Address PR feedback * Use an array literal for kvo objects * Fix some clang tidy and import issues * Replace third party LRU cache with SantaCache for now * Fix clang tidy issues * Address PR feedback * Fix comment typo Co-authored-by: Pete Markowsky <pmarkowsky@users.noreply.github.com> * Added todo for when we adopt macOS 13 Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com> Co-authored-by: Pete Markowsky <pmarkowsky@users.noreply.github.com>
782 lines
26 KiB
Objective-C
782 lines
26 KiB
Objective-C
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
|
///
|
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
/// you may not use this file except in compliance with the License.
|
|
/// You may obtain a copy of the License at
|
|
///
|
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
///
|
|
/// Unless required by applicable law or agreed to in writing, software
|
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
/// See the License for the specific language governing permissions and
|
|
/// limitations under the License.
|
|
|
|
#import "Source/common/SNTFileInfo.h"
|
|
|
|
#import <CommonCrypto/CommonDigest.h>
|
|
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
|
#import <fmdb/FMDB.h>
|
|
|
|
#include <mach-o/arch.h>
|
|
#include <mach-o/loader.h>
|
|
#include <mach-o/swap.h>
|
|
#include <pwd.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/xattr.h>
|
|
|
|
#import "Source/common/SNTLogging.h"
|
|
|
|
// Simple class to hold the data of a mach_header and the offset within the file
|
|
// in which that header was found.
|
|
@interface MachHeaderWithOffset : NSObject
|
|
@property NSData *data;
|
|
@property uint32_t offset;
|
|
- (instancetype)initWithData:(NSData *)data offset:(uint32_t)offset;
|
|
@end
|
|
@implementation MachHeaderWithOffset
|
|
- (instancetype)initWithData:(NSData *)data offset:(uint32_t)offset {
|
|
self = [super init];
|
|
if (self) {
|
|
_data = data;
|
|
_offset = offset;
|
|
}
|
|
return self;
|
|
}
|
|
@end
|
|
|
|
@interface SNTFileInfo ()
|
|
@property NSString *path;
|
|
@property NSFileHandle *fileHandle;
|
|
@property NSUInteger fileSize;
|
|
@property NSString *fileOwnerHomeDir;
|
|
@property NSString *sha256Storage;
|
|
|
|
// Cached properties
|
|
@property NSBundle *bundleRef;
|
|
@property NSDictionary *infoDict;
|
|
@property NSDictionary *quarantineDict;
|
|
@property NSDictionary *cachedHeaders;
|
|
@property MOLCodesignChecker *cachedCodesignChecker;
|
|
@property(nonatomic) NSError *codesignCheckerError;
|
|
@end
|
|
|
|
@implementation SNTFileInfo
|
|
|
|
extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
|
|
|
- (instancetype)initWithResolvedPath:(NSString *)path error:(NSError **)error {
|
|
struct stat fileStat;
|
|
if (path.length) {
|
|
lstat(path.UTF8String, &fileStat);
|
|
}
|
|
return [self initWithResolvedPath:path stat:&fileStat error:error];
|
|
}
|
|
|
|
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile error:(NSError **)error {
|
|
return [self initWithResolvedPath:@(esFile->path.data) stat:&esFile->stat error:error];
|
|
}
|
|
|
|
- (instancetype)initWithResolvedPath:(NSString *)path
|
|
stat:(const struct stat *)fileStat
|
|
error:(NSError **)error {
|
|
if (!fileStat) {
|
|
// This is a programming error. Bail.
|
|
LOGE(@"NULL stat buffer unsupported");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
self = [super init];
|
|
if (self) {
|
|
_path = path;
|
|
if (!_path.length) {
|
|
if (error) {
|
|
NSString *errStr = @"Unable to use empty path";
|
|
*error = [NSError errorWithDomain:@"com.google.santa.fileinfo"
|
|
code:270
|
|
userInfo:@{NSLocalizedDescriptionKey : errStr}];
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
if (!((S_IFMT & fileStat->st_mode) == S_IFREG)) {
|
|
if (error) {
|
|
NSString *errStr = [NSString stringWithFormat:@"Non regular file: %s", strerror(errno)];
|
|
*error = [NSError errorWithDomain:@"com.google.santa.fileinfo"
|
|
code:290
|
|
userInfo:@{NSLocalizedDescriptionKey : errStr}];
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
_fileSize = fileStat->st_size;
|
|
|
|
if (_fileSize == 0) return nil;
|
|
|
|
if (fileStat->st_uid != 0) {
|
|
struct passwd *pwd = getpwuid(fileStat->st_uid);
|
|
if (pwd) {
|
|
_fileOwnerHomeDir = @(pwd->pw_dir);
|
|
}
|
|
}
|
|
|
|
int fd = open([_path UTF8String], O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0) {
|
|
if (error) {
|
|
NSString *errStr = [NSString stringWithFormat:@"Unable to open file: %s", strerror(errno)];
|
|
*error = [NSError errorWithDomain:@"com.google.santa.fileinfo"
|
|
code:280
|
|
userInfo:@{NSLocalizedDescriptionKey : errStr}];
|
|
}
|
|
return nil;
|
|
}
|
|
_fileHandle = [[NSFileHandle alloc] initWithFileDescriptor:fd closeOnDealloc:YES];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithPath:(NSString *)path error:(NSError **)error {
|
|
NSBundle *bndl;
|
|
NSString *resolvedPath = [self resolvePath:path bundle:&bndl];
|
|
if (!resolvedPath.length) {
|
|
if (error) {
|
|
NSString *errStr = @"Unable to resolve empty path";
|
|
if (path) errStr = [@"Unable to resolve path: " stringByAppendingString:path];
|
|
*error = [NSError errorWithDomain:@"com.google.santa.fileinfo"
|
|
code:260
|
|
userInfo:@{NSLocalizedDescriptionKey : errStr}];
|
|
}
|
|
return nil;
|
|
}
|
|
self = [self initWithResolvedPath:resolvedPath error:error];
|
|
if (self && bndl) _bundleRef = bndl;
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithPath:(NSString *)path {
|
|
return [self initWithPath:path error:NULL];
|
|
}
|
|
|
|
#pragma mark Hashing
|
|
|
|
- (void)hashSHA1:(NSString **)sha1 SHA256:(NSString **)sha256 {
|
|
const int MAX_CHUNK_SIZE = 256 * 1024; // 256 KB
|
|
const size_t chunkSize = _fileSize > MAX_CHUNK_SIZE ? MAX_CHUNK_SIZE : _fileSize;
|
|
char *chunk = malloc(chunkSize);
|
|
|
|
@try {
|
|
CC_SHA1_CTX c1;
|
|
CC_SHA256_CTX c256;
|
|
|
|
if (sha1) CC_SHA1_Init(&c1);
|
|
if (sha256) CC_SHA256_Init(&c256);
|
|
|
|
int fd = self.fileHandle.fileDescriptor;
|
|
|
|
fcntl(fd, F_RDAHEAD, 1);
|
|
struct radvisory radv;
|
|
radv.ra_offset = 0;
|
|
const int MAX_ADVISORY_READ = 10 * 1024 * 1024;
|
|
radv.ra_count = (int)_fileSize < MAX_ADVISORY_READ ? (int)_fileSize : MAX_ADVISORY_READ;
|
|
fcntl(fd, F_RDADVISE, &radv);
|
|
ssize_t bytesRead;
|
|
|
|
for (uint64_t offset = 0; offset < _fileSize;) {
|
|
bytesRead = pread(fd, chunk, chunkSize, offset);
|
|
if (bytesRead > 0) {
|
|
if (sha1) CC_SHA1_Update(&c1, chunk, (CC_LONG)bytesRead);
|
|
if (sha256) CC_SHA256_Update(&c256, chunk, (CC_LONG)bytesRead);
|
|
offset += bytesRead;
|
|
} else if (bytesRead == -1 && errno == EINTR) {
|
|
continue;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// We turn off Read Ahead that we turned on
|
|
fcntl(fd, F_RDAHEAD, 0);
|
|
if (sha1) {
|
|
unsigned char digest[CC_SHA1_DIGEST_LENGTH];
|
|
CC_SHA1_Final(digest, &c1);
|
|
NSString *const SHA1FormatString =
|
|
@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x";
|
|
*sha1 = [[NSString alloc]
|
|
initWithFormat:SHA1FormatString, digest[0], digest[1], digest[2], digest[3], digest[4],
|
|
digest[5], digest[6], digest[7], digest[8], digest[9], digest[10],
|
|
digest[11], digest[12], digest[13], digest[14], digest[15], digest[16],
|
|
digest[17], digest[18], digest[19]];
|
|
}
|
|
if (sha256) {
|
|
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
|
|
CC_SHA256_Final(digest, &c256);
|
|
NSString *const SHA256FormatString =
|
|
@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
|
|
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x";
|
|
|
|
*sha256 = [[NSString alloc]
|
|
initWithFormat:SHA256FormatString, digest[0], digest[1], digest[2], digest[3], digest[4],
|
|
digest[5], digest[6], digest[7], digest[8], digest[9], digest[10],
|
|
digest[11], digest[12], digest[13], digest[14], digest[15], digest[16],
|
|
digest[17], digest[18], digest[19], digest[20], digest[21], digest[22],
|
|
digest[23], digest[24], digest[25], digest[26], digest[27], digest[28],
|
|
digest[29], digest[30], digest[31]];
|
|
}
|
|
} @finally {
|
|
free(chunk);
|
|
}
|
|
}
|
|
|
|
- (NSString *)SHA1 {
|
|
NSString *sha1;
|
|
[self hashSHA1:&sha1 SHA256:NULL];
|
|
return sha1;
|
|
}
|
|
|
|
- (NSString *)SHA256 {
|
|
// Memoize the value
|
|
if (!self.sha256Storage) {
|
|
NSString *sha256;
|
|
[self hashSHA1:NULL SHA256:&sha256];
|
|
self.sha256Storage = sha256;
|
|
}
|
|
return self.sha256Storage;
|
|
}
|
|
|
|
#pragma mark File Type Info
|
|
|
|
- (NSArray *)architectures {
|
|
return [self.machHeaders allKeys];
|
|
}
|
|
|
|
- (uint32_t)machFileType {
|
|
struct mach_header *mach_header = [self firstMachHeader];
|
|
if (mach_header) return mach_header->filetype;
|
|
return -1;
|
|
}
|
|
|
|
- (BOOL)isExecutable {
|
|
return [self machFileType] == MH_EXECUTE;
|
|
}
|
|
|
|
- (BOOL)isDylib {
|
|
return [self machFileType] == MH_DYLIB;
|
|
}
|
|
|
|
- (BOOL)isBundle {
|
|
return [self machFileType] == MH_BUNDLE;
|
|
}
|
|
|
|
- (BOOL)isKext {
|
|
return [self machFileType] == MH_KEXT_BUNDLE;
|
|
}
|
|
|
|
- (BOOL)isMachO {
|
|
return (self.machHeaders.count > 0);
|
|
}
|
|
|
|
- (BOOL)isFat {
|
|
return (self.machHeaders.count > 1);
|
|
}
|
|
|
|
- (BOOL)isScript {
|
|
const char *magic = (const char *)[[self safeSubdataWithRange:NSMakeRange(0, 2)] bytes];
|
|
return (magic && memcmp("#!", magic, 2) == 0);
|
|
}
|
|
|
|
- (BOOL)isXARArchive {
|
|
const char *magic = (const char *)[[self safeSubdataWithRange:NSMakeRange(0, 4)] bytes];
|
|
return (magic && memcmp("xar!", magic, 4) == 0);
|
|
}
|
|
|
|
- (BOOL)isDMG {
|
|
if (self.fileSize < 512) return NO;
|
|
NSUInteger last512 = self.fileSize - 512;
|
|
const char *magic = (const char *)[[self safeSubdataWithRange:NSMakeRange(last512, 4)] bytes];
|
|
return (magic && memcmp("koly", magic, 4) == 0);
|
|
}
|
|
|
|
- (NSString *)humanReadableFileType {
|
|
if ([self isExecutable]) return @"Executable";
|
|
if ([self isDylib]) return @"Dynamic Library";
|
|
if ([self isBundle]) return @"Bundle/Plugin";
|
|
if ([self isKext]) return @"Kernel Extension";
|
|
if ([self isScript]) return @"Script";
|
|
if ([self isXARArchive]) return @"XAR Archive";
|
|
if ([self isDMG]) return @"Disk Image";
|
|
return @"Unknown";
|
|
}
|
|
|
|
#pragma mark Page Zero
|
|
|
|
- (BOOL)isMissingPageZero {
|
|
// This method only checks i386 arch because the kernel enforces this for other archs
|
|
// See bsd/kern/mach_loader.c, search for enforce_hard_pagezero.
|
|
MachHeaderWithOffset *x86Header =
|
|
self.machHeaders[[self nameForCPUType:CPU_TYPE_X86 cpuSubType:CPU_SUBTYPE_I386_ALL]];
|
|
if (!x86Header) return NO;
|
|
|
|
struct mach_header *mh = (struct mach_header *)[x86Header.data bytes];
|
|
if (mh->filetype != MH_EXECUTE) return NO;
|
|
|
|
NSRange range =
|
|
NSMakeRange(x86Header.offset + sizeof(struct mach_header), sizeof(struct segment_command));
|
|
NSData *lcData = [self safeSubdataWithRange:range];
|
|
if (!lcData) return NO;
|
|
|
|
// This code assumes the __PAGEZERO is always the first load-command in the file.
|
|
// Given that the macOS ABI says "the static linker creates a __PAGEZERO segment
|
|
// as the first segment of an executable file." this should be OK.
|
|
struct load_command *lc = (struct load_command *)[lcData bytes];
|
|
if (lc->cmd == LC_SEGMENT) {
|
|
struct segment_command *segment = (struct segment_command *)lc;
|
|
if (segment->vmaddr == 0 && segment->vmsize != 0 && segment->initprot == 0 &&
|
|
segment->maxprot == 0 && strcmp("__PAGEZERO", segment->segname) == 0) {
|
|
return NO;
|
|
}
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
#pragma mark Bundle Information
|
|
|
|
///
|
|
/// Directories with a "Contents/Info.plist" entry can be mistaken as a bundle. To be considered an
|
|
/// ancestor, the bundle must have a valid extension.
|
|
///
|
|
- (NSSet *)allowedAncestorExtensions {
|
|
static NSSet *set;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
set = [NSSet setWithArray:@[
|
|
@"app",
|
|
@"bundle",
|
|
@"framework",
|
|
@"kext",
|
|
@"xctest",
|
|
@"xpc",
|
|
]];
|
|
});
|
|
return set;
|
|
}
|
|
|
|
///
|
|
/// Try and determine the bundle that the represented executable is contained within, if any.
|
|
///
|
|
/// Rationale: An NSBundle has a method executablePath for discovering the main binary within a
|
|
/// bundle but provides no way to get an NSBundle object when only the executablePath is known.
|
|
/// Also a bundle can contain multiple binaries within its subdirectories and we want any of these
|
|
/// to count as being part of the bundle.
|
|
///
|
|
/// This method walks up the path until a bundle is found, if any.
|
|
///
|
|
/// @param ancestor YES this will return the highest NSBundle, with a valid extension, found in the
|
|
/// tree. NO will return the the lowest NSBundle, without validating the extension.
|
|
///
|
|
- (NSBundle *)findBundleWithAncestor:(BOOL)ancestor {
|
|
NSBundle *bundle;
|
|
NSMutableArray *pathComponents = [[self.path pathComponents] mutableCopy];
|
|
|
|
// Ignore the root path "/", for some reason this is considered a bundle.
|
|
while (pathComponents.count > 1) {
|
|
NSBundle *bndl = [NSBundle bundleWithPath:[NSString pathWithComponents:pathComponents]];
|
|
if ([bndl objectForInfoDictionaryKey:@"CFBundleIdentifier"]) {
|
|
if ((!ancestor && bndl.bundlePath.pathExtension.length) ||
|
|
[[self allowedAncestorExtensions] containsObject:bndl.bundlePath.pathExtension]) {
|
|
bundle = bndl;
|
|
}
|
|
if (!ancestor) break;
|
|
}
|
|
[pathComponents removeLastObject];
|
|
}
|
|
return bundle;
|
|
}
|
|
|
|
- (NSBundle *)bundle {
|
|
if (!self.bundleRef) {
|
|
self.bundleRef =
|
|
[self findBundleWithAncestor:self.useAncestorBundle] ?: (NSBundle *)[NSNull null];
|
|
}
|
|
return self.bundleRef == (NSBundle *)[NSNull null] ? nil : self.bundleRef;
|
|
}
|
|
|
|
- (NSString *)bundlePath {
|
|
return [self.bundle bundlePath];
|
|
}
|
|
|
|
- (void)setUseAncestorBundle:(BOOL)useAncestorBundle {
|
|
if (self.useAncestorBundle != useAncestorBundle) {
|
|
self.bundleRef = nil;
|
|
self.infoDict = nil;
|
|
}
|
|
_useAncestorBundle = useAncestorBundle;
|
|
}
|
|
|
|
- (NSDictionary *)infoPlist {
|
|
if (!self.infoDict) {
|
|
NSDictionary *d = [self embeddedPlist];
|
|
if (d) {
|
|
self.infoDict = d;
|
|
return self.infoDict;
|
|
}
|
|
|
|
d = self.bundle.infoDictionary;
|
|
if (d) {
|
|
self.infoDict = d;
|
|
return self.infoDict;
|
|
}
|
|
|
|
self.infoDict = (NSDictionary *)[NSNull null];
|
|
}
|
|
return self.infoDict == (NSDictionary *)[NSNull null] ? nil : self.infoDict;
|
|
}
|
|
|
|
- (NSString *)bundleIdentifier {
|
|
return [[self.infoPlist objectForKey:@"CFBundleIdentifier"] description];
|
|
}
|
|
|
|
- (NSString *)bundleName {
|
|
return [[self.infoPlist objectForKey:@"CFBundleDisplayName"] description]
|
|
?: [[self.infoPlist objectForKey:@"CFBundleName"] description];
|
|
}
|
|
|
|
- (NSString *)bundleVersion {
|
|
return [[self.infoPlist objectForKey:@"CFBundleVersion"] description];
|
|
}
|
|
|
|
- (NSString *)bundleShortVersionString {
|
|
return [[self.infoPlist objectForKey:@"CFBundleShortVersionString"] description];
|
|
}
|
|
|
|
#pragma mark Quarantine Data
|
|
|
|
- (NSString *)quarantineDataURL {
|
|
NSURL *dataURL = [self quarantineData][@"LSQuarantineDataURL"];
|
|
if (dataURL == (NSURL *)[NSNull null]) dataURL = nil;
|
|
return [dataURL absoluteString];
|
|
}
|
|
|
|
- (NSString *)quarantineRefererURL {
|
|
NSURL *originURL = [self quarantineData][@"LSQuarantineOriginURL"];
|
|
if (originURL == (NSURL *)[NSNull null]) originURL = nil;
|
|
return [originURL absoluteString];
|
|
}
|
|
|
|
- (NSString *)quarantineAgentBundleID {
|
|
NSString *agentBundle = [self quarantineData][@"LSQuarantineAgentBundleIdentifier"];
|
|
if (agentBundle == (NSString *)[NSNull null]) agentBundle = nil;
|
|
return agentBundle;
|
|
}
|
|
|
|
- (NSDate *)quarantineTimestamp {
|
|
NSDate *timeStamp = [self quarantineData][@"LSQuarantineTimeStamp"];
|
|
return timeStamp;
|
|
}
|
|
|
|
#pragma mark Internal Methods
|
|
|
|
- (NSDictionary *)machHeaders {
|
|
if (self.cachedHeaders) return self.cachedHeaders;
|
|
|
|
// Sanity check file length
|
|
if (self.fileSize < sizeof(struct mach_header)) {
|
|
self.cachedHeaders = [NSDictionary dictionary];
|
|
return self.cachedHeaders;
|
|
}
|
|
|
|
NSMutableDictionary *machHeaders = [NSMutableDictionary dictionary];
|
|
|
|
NSData *machHeader =
|
|
[self parseSingleMachHeader:[self safeSubdataWithRange:NSMakeRange(0, 4096)]];
|
|
if (machHeader) {
|
|
struct mach_header *mh = (struct mach_header *)[machHeader bytes];
|
|
MachHeaderWithOffset *mhwo = [[MachHeaderWithOffset alloc] initWithData:machHeader offset:0];
|
|
machHeaders[[self nameForCPUType:mh->cputype cpuSubType:mh->cpusubtype]] = mhwo;
|
|
} else {
|
|
NSRange range = NSMakeRange(0, sizeof(struct fat_header));
|
|
NSData *fatHeader = [self safeSubdataWithRange:range];
|
|
struct fat_header *fh = (struct fat_header *)[fatHeader bytes];
|
|
|
|
if (fatHeader && (fh->magic == FAT_CIGAM || fh->magic == FAT_MAGIC)) {
|
|
int nfat_arch = OSSwapBigToHostInt32(fh->nfat_arch);
|
|
range = NSMakeRange(sizeof(struct fat_header), sizeof(struct fat_arch) * nfat_arch);
|
|
NSMutableData *fatArchs = [[self safeSubdataWithRange:range] mutableCopy];
|
|
if (fatArchs) {
|
|
struct fat_arch *fat_arch = (struct fat_arch *)[fatArchs mutableBytes];
|
|
for (int i = 0; i < nfat_arch; ++i) {
|
|
int offset = OSSwapBigToHostInt32(fat_arch[i].offset);
|
|
int size = OSSwapBigToHostInt32(fat_arch[i].size);
|
|
int cputype = OSSwapBigToHostInt(fat_arch[i].cputype);
|
|
int cpusubtype = OSSwapBigToHostInt(fat_arch[i].cpusubtype);
|
|
|
|
range = NSMakeRange(offset, size);
|
|
NSData *machHeader = [self parseSingleMachHeader:[self safeSubdataWithRange:range]];
|
|
if (machHeader) {
|
|
NSString *key = [self nameForCPUType:cputype cpuSubType:cpusubtype];
|
|
MachHeaderWithOffset *mhwo = [[MachHeaderWithOffset alloc] initWithData:machHeader
|
|
offset:offset];
|
|
machHeaders[key] = mhwo;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.cachedHeaders = [machHeaders copy];
|
|
return self.cachedHeaders;
|
|
}
|
|
|
|
- (NSData *)parseSingleMachHeader:(NSData *)inputData {
|
|
if (inputData.length < sizeof(struct mach_header)) return nil;
|
|
struct mach_header *mh = (struct mach_header *)[inputData bytes];
|
|
|
|
if (mh->magic == MH_CIGAM || mh->magic == MH_CIGAM_64) {
|
|
NSMutableData *mutableInput = [inputData mutableCopy];
|
|
mh = (struct mach_header *)[mutableInput mutableBytes];
|
|
swap_mach_header(mh, NXHostByteOrder());
|
|
}
|
|
|
|
if (mh->magic == MH_MAGIC || mh->magic == MH_MAGIC_64) {
|
|
return [NSData dataWithBytes:mh length:sizeof(struct mach_header)];
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
///
|
|
/// Locate an embedded plist in the file
|
|
///
|
|
- (NSDictionary *)embeddedPlist {
|
|
// Look for an embedded Info.plist if there is one.
|
|
// This could (and used to) use CFBundleCopyInfoDictionaryForURL but that uses mmap to read
|
|
// the file and so can cause SIGBUS if the file is deleted/truncated while it's working.
|
|
MachHeaderWithOffset *mhwo = [[self.machHeaders allValues] firstObject];
|
|
if (!mhwo) return nil;
|
|
|
|
struct mach_header *mh = (struct mach_header *)mhwo.data.bytes;
|
|
if (mh->filetype != MH_EXECUTE) return self.infoDict;
|
|
BOOL is64 = (mh->magic == MH_MAGIC_64 || mh->magic == MH_CIGAM_64);
|
|
uint32_t ncmds = mh->ncmds;
|
|
uint32_t nsects = 0;
|
|
uint64_t offset = mhwo.offset;
|
|
|
|
uint32_t sz_header = is64 ? sizeof(struct mach_header_64) : sizeof(struct mach_header);
|
|
uint32_t sz_segment = is64 ? sizeof(struct segment_command_64) : sizeof(struct segment_command);
|
|
uint32_t sz_section = is64 ? sizeof(struct section_64) : sizeof(struct section);
|
|
|
|
offset += sz_header;
|
|
|
|
// Loop through the load commands looking for the segment named __TEXT
|
|
for (uint32_t i = 0; i < ncmds; ++i) {
|
|
NSData *cmdData = [self safeSubdataWithRange:NSMakeRange(offset, sz_segment)];
|
|
if (!cmdData) return nil;
|
|
|
|
if (is64) {
|
|
struct segment_command_64 *lc = (struct segment_command_64 *)[cmdData bytes];
|
|
if (lc->cmd == LC_SEGMENT_64 && memcmp(lc->segname, "__TEXT", 6) == 0) {
|
|
nsects = lc->nsects;
|
|
offset += sz_segment;
|
|
break;
|
|
}
|
|
offset += lc->cmdsize;
|
|
} else {
|
|
struct segment_command *lc = (struct segment_command *)[cmdData bytes];
|
|
if (lc->cmd == LC_SEGMENT && memcmp(lc->segname, "__TEXT", 6) == 0) {
|
|
nsects = lc->nsects;
|
|
offset += sz_segment;
|
|
break;
|
|
}
|
|
offset += lc->cmdsize;
|
|
}
|
|
}
|
|
|
|
// Loop through the sections in the __TEXT segment looking for an __info_plist section.
|
|
for (uint32_t i = 0; i < nsects; ++i) {
|
|
NSData *sectData = [self safeSubdataWithRange:NSMakeRange(offset, sz_section)];
|
|
if (!sectData) return nil;
|
|
uint64_t sectoffset, sectsize = 0;
|
|
BOOL found = NO;
|
|
if (is64) {
|
|
struct section_64 *sect = (struct section_64 *)[sectData bytes];
|
|
if (sect && memcmp(sect->sectname, "__info_plist", 12) == 0 && sect->size < 2000000) {
|
|
sectoffset = sect->offset;
|
|
sectsize = sect->size;
|
|
found = YES;
|
|
}
|
|
} else {
|
|
struct section *sect = (struct section *)[sectData bytes];
|
|
if (sect && memcmp(sect->sectname, "__info_plist", 12) == 0 && sect->size < 2000000) {
|
|
sectoffset = sect->offset;
|
|
sectsize = sect->size;
|
|
found = YES;
|
|
}
|
|
}
|
|
|
|
if (found) {
|
|
NSData *plistData =
|
|
[self safeSubdataWithRange:NSMakeRange(mhwo.offset + sectoffset, sectsize)];
|
|
if (!plistData) return nil;
|
|
NSDictionary *plist;
|
|
plist = [NSPropertyListSerialization propertyListWithData:plistData
|
|
options:NSPropertyListImmutable
|
|
format:NULL
|
|
error:NULL];
|
|
if (plist) return plist;
|
|
}
|
|
offset += sz_section;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
///
|
|
/// Return the first mach_header in this file.
|
|
///
|
|
- (struct mach_header *)firstMachHeader {
|
|
return (struct mach_header *)([[[[self.machHeaders allValues] firstObject] data] bytes]);
|
|
}
|
|
|
|
///
|
|
/// Extract a range of the file as an NSData, handling any exceptions.
|
|
/// Returns nil if the requested range is outside of the range of the file.
|
|
///
|
|
- (NSData *)safeSubdataWithRange:(NSRange)range {
|
|
@try {
|
|
if ((range.location + range.length) > self.fileSize) return nil;
|
|
[self.fileHandle seekToFileOffset:range.location];
|
|
NSData *d = [self.fileHandle readDataOfLength:range.length];
|
|
if (d.length != range.length) return nil;
|
|
return d;
|
|
} @catch (NSException *e) {
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
///
|
|
/// Retrieve quarantine data for a file and caches the dictionary
|
|
/// This method attempts to handle fetching the quarantine data even if the running user
|
|
/// is not the one who downloaded the file.
|
|
///
|
|
- (NSDictionary *)quarantineData {
|
|
if (!self.quarantineDict && self.fileOwnerHomeDir && NSURLQuarantinePropertiesKey) {
|
|
self.quarantineDict = (NSDictionary *)[NSNull null];
|
|
|
|
NSURL *url = [NSURL fileURLWithPath:self.path];
|
|
NSDictionary *d = [url resourceValuesForKeys:@[ NSURLQuarantinePropertiesKey ] error:NULL];
|
|
|
|
if (d[NSURLQuarantinePropertiesKey]) {
|
|
d = d[NSURLQuarantinePropertiesKey];
|
|
|
|
if (d[@"LSQuarantineIsOwnedByCurrentUser"]) {
|
|
self.quarantineDict = d;
|
|
} else if (d[@"LSQuarantineEventIdentifier"]) {
|
|
NSMutableDictionary *quarantineDict = [d mutableCopy];
|
|
|
|
// If self.path is on a quarantine disk image, LSQuarantineDiskImageURL will point to the
|
|
// disk image and self.fileOwnerHomeDir will be incorrect (probably root).
|
|
NSString *fileOwnerHomeDir = self.fileOwnerHomeDir;
|
|
if (d[@"LSQuarantineDiskImageURL"]) {
|
|
struct stat fileStat;
|
|
stat([d[@"LSQuarantineDiskImageURL"] fileSystemRepresentation], &fileStat);
|
|
if (fileStat.st_uid != 0) {
|
|
struct passwd *pwd = getpwuid(fileStat.st_uid);
|
|
if (pwd) {
|
|
fileOwnerHomeDir = @(pwd->pw_dir);
|
|
}
|
|
}
|
|
}
|
|
|
|
NSURL *dbPath = [NSURL fileURLWithPathComponents:@[
|
|
fileOwnerHomeDir, @"Library", @"Preferences",
|
|
@"com.apple.LaunchServices.QuarantineEventsV2"
|
|
]];
|
|
FMDatabase *db = [FMDatabase databaseWithPath:[dbPath absoluteString]];
|
|
db.logsErrors = NO;
|
|
if ([db open]) {
|
|
FMResultSet *rs = [db executeQuery:@"SELECT * FROM LSQuarantineEvent "
|
|
@"WHERE LSQuarantineEventIdentifier=?",
|
|
d[@"LSQuarantineEventIdentifier"]];
|
|
if ([rs next]) {
|
|
NSString *agentBundleID = [rs stringForColumn:@"LSQuarantineAgentBundleIdentifier"];
|
|
NSString *dataURLString = [rs stringForColumn:@"LSQuarantineDataURLString"];
|
|
NSString *originURLString = [rs stringForColumn:@"LSQuarantineOriginURLString"];
|
|
double timeStamp = [rs doubleForColumn:@"LSQuarantineTimeStamp"];
|
|
|
|
quarantineDict[@"LSQuarantineAgentBundleIdentifier"] = agentBundleID;
|
|
quarantineDict[@"LSQuarantineDataURL"] = [NSURL URLWithString:dataURLString];
|
|
quarantineDict[@"LSQuarantineOriginURL"] = [NSURL URLWithString:originURLString];
|
|
quarantineDict[@"LSQuarantineTimestamp"] =
|
|
[NSDate dateWithTimeIntervalSinceReferenceDate:timeStamp];
|
|
|
|
self.quarantineDict = quarantineDict;
|
|
}
|
|
[rs close];
|
|
[db close];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return (self.quarantineDict == (NSDictionary *)[NSNull null]) ? nil : self.quarantineDict;
|
|
}
|
|
|
|
///
|
|
/// Return a human-readable string for a cpu_type_t.
|
|
///
|
|
- (NSString *)nameForCPUType:(cpu_type_t)cpuType cpuSubType:(cpu_subtype_t)cpuSubType {
|
|
const NXArchInfo *archInfo = NXGetArchInfoFromCpuType(cpuType, cpuSubType);
|
|
NSString *arch;
|
|
if (archInfo && archInfo->name) {
|
|
arch = @(archInfo->name);
|
|
} else {
|
|
arch = [NSString stringWithFormat:@"%i:%i", cpuType, cpuSubType];
|
|
}
|
|
return arch;
|
|
}
|
|
|
|
///
|
|
/// Resolves a given path:
|
|
/// + Follows symlinks
|
|
/// + Converts relative paths to absolute
|
|
/// + If path is a directory, checks to see if that directory is a bundle and if so
|
|
/// returns the path to that bundles CFBundleExecutable and stores a reference to the
|
|
/// bundle in the bundle out-param.
|
|
///
|
|
- (NSString *)resolvePath:(NSString *)path bundle:(NSBundle **)bundle {
|
|
// Convert to absolute, standardized path
|
|
path = [path stringByResolvingSymlinksInPath];
|
|
if (![path isAbsolutePath]) {
|
|
NSString *cwd = [[NSFileManager defaultManager] currentDirectoryPath];
|
|
path = [cwd stringByAppendingPathComponent:path];
|
|
}
|
|
path = [path stringByStandardizingPath];
|
|
|
|
// Determine if file exists.
|
|
// If path is actually a directory, check to see if it's a bundle and has a CFBundleExecutable.
|
|
BOOL directory;
|
|
if (![[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&directory]) {
|
|
return nil;
|
|
} else if (directory && ![path isEqualToString:@"/"]) {
|
|
NSBundle *bndl = [NSBundle bundleWithPath:path];
|
|
if (bundle) *bundle = bndl;
|
|
return [bndl executablePath];
|
|
} else {
|
|
return path;
|
|
}
|
|
}
|
|
|
|
///
|
|
/// Cache and return a MOLCodeSignChecker for the given file. If there was an error creating the
|
|
/// code sign checker it will be returned in the passed-in error parameter.
|
|
///
|
|
- (MOLCodesignChecker *)codesignCheckerWithError:(NSError **)error {
|
|
if (!self.cachedCodesignChecker && !self.codesignCheckerError) {
|
|
NSError *e;
|
|
self.cachedCodesignChecker = [[MOLCodesignChecker alloc] initWithBinaryPath:self.path error:&e];
|
|
self.codesignCheckerError = e;
|
|
}
|
|
if (error) *error = self.codesignCheckerError;
|
|
return self.cachedCodesignChecker;
|
|
}
|
|
|
|
@end
|