/// Copyright 2015 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 "SNTFileInfo.h" #import #include #include #include #include #include #import // 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; // Cached properties @property NSBundle *bundleRef; @property NSDictionary *infoDict; @property NSDictionary *quarantineDict; @property NSDictionary *cachedHeaders; @end @implementation SNTFileInfo extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE; - (instancetype)initWithPath:(NSString *)path error:(NSError **)error { self = [super init]; if (self) { NSBundle *bndl; _path = [self resolvePath:path bundle:&bndl]; _bundleRef = bndl; if (_path.length == 0) { 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; } _fileHandle = [NSFileHandle fileHandleForReadingAtPath:_path]; struct stat fileStat; fstat(_fileHandle.fileDescriptor, &fileStat); _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); } } } return self; } - (instancetype)initWithPath:(NSString *)path { return [self initWithPath:path error:NULL]; } #pragma mark Hashing - (void)hashSHA1:(NSString **)sha1 SHA256:(NSString **)sha256 { const int chunkSize = 4096; CC_SHA1_CTX c1; CC_SHA256_CTX c256; if (sha1) CC_SHA1_Init(&c1); if (sha256) CC_SHA256_Init(&c256); for (uint64_t offset = 0; offset < self.fileSize; offset += chunkSize) { @autoreleasepool { int readSize = 0; if (offset + chunkSize > self.fileSize) { readSize = (int)(self.fileSize - offset); } else { readSize = chunkSize; } NSData *chunk = [self safeSubdataWithRange:NSMakeRange(offset, readSize)]; if (!chunk) { if (sha1) CC_SHA1_Final(NULL, &c1); if (sha256) CC_SHA256_Final(NULL, &c256); return; } if (sha1) CC_SHA1_Update(&c1, chunk.bytes, readSize); if (sha256) CC_SHA256_Update(&c256, chunk.bytes, readSize); } } if (sha1) { unsigned char dgst[CC_SHA1_DIGEST_LENGTH]; CC_SHA1_Final(dgst, &c1); NSMutableString *buf = [[NSMutableString alloc] initWithCapacity:CC_SHA1_DIGEST_LENGTH * 2]; for (int i = 0; i < CC_SHA1_DIGEST_LENGTH; ++i) { [buf appendFormat:@"%02x", (unsigned char)dgst[i]]; } *sha1 = [buf copy]; } if (sha256) { unsigned char dgst[CC_SHA256_DIGEST_LENGTH]; CC_SHA256_Final(dgst, &c256); NSMutableString *buf = [[NSMutableString alloc] initWithCapacity:CC_SHA256_DIGEST_LENGTH * 2]; for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; ++i) { [buf appendFormat:@"%02x", (unsigned char)dgst[i]]; } *sha256 = [buf copy]; } } - (NSString *)SHA1 { NSString *sha1; [self hashSHA1:&sha1 SHA256:NULL]; return sha1; } - (NSString *)SHA256 { NSString *sha256; [self hashSHA1:NULL SHA256:&sha256]; return sha256; } #pragma mark File Type Info - (NSArray *)architectures { return [self.machHeaders allKeys]; } - (BOOL)isExecutable { struct mach_header *mach_header = [self firstMachHeader]; if (mach_header && mach_header->filetype == MH_EXECUTE) return YES; return NO; } - (BOOL)isDylib { struct mach_header *mach_header = [self firstMachHeader]; if (mach_header && mach_header->filetype == MH_DYLIB) return YES; return NO; } - (BOOL)isKext { struct mach_header *mach_header = [self firstMachHeader]; if (mach_header && mach_header->filetype == MH_KEXT_BUNDLE) return YES; return NO; } - (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 (strncmp("#!", magic, 2) == 0); } - (BOOL)isXARArchive { const char *magic = (const char *)[[self safeSubdataWithRange:NSMakeRange(0, 4)] bytes]; return (strncmp("xar!", magic, 4) == 0); } - (BOOL)isDMG { NSUInteger last512 = self.fileSize - 512; const char *magic = (const char *)[[self safeSubdataWithRange:NSMakeRange(last512, 4)] bytes]; return (magic && strncmp("koly", magic, 4) == 0); } #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]]; 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 OS X 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 /// /// 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 the MacOS folder and we want any of these /// to count as being part of the bundle. /// /// This method relies on executable bundles being laid out as follows: /// /// @code /// Bundle.app/ /// Contents/ /// MacOS/ /// executable /// @endcode /// /// If @c self.path is the full path to @c executable above, this method would return an /// NSBundle reference for Bundle.app. /// - (NSBundle *)bundle { if (!self.bundleRef) { self.bundleRef = (NSBundle *)[NSNull null]; // Check that the full path is at least 4-levels deep: // e.g: /Calendar.app/Contents/MacOS/Calendar NSArray *pathComponents = [self.path pathComponents]; if ([pathComponents count] < 4) return nil; pathComponents = [pathComponents subarrayWithRange:NSMakeRange(0, [pathComponents count] - 3)]; NSBundle *bndl = [NSBundle bundleWithPath:[NSString pathWithComponents:pathComponents]]; if (bndl && [bndl objectForInfoDictionaryKey:@"CFBundleIdentifier"]) self.bundleRef = bndl; } return self.bundleRef == (NSBundle *)[NSNull null] ? nil : self.bundleRef; } - (NSString *)bundlePath { return [self.bundle bundlePath]; } - (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:@"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]] = 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_MAGIC || fh->magic == FAT_CIGAM)) { 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); range = NSMakeRange(offset, size); NSData *machHeader = [self parseSingleMachHeader:[self safeSubdataWithRange:range]]; if (machHeader) { NSString *key = [self nameForCPUType:cputype]; 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; struct segment_command_64 *lc = (struct segment_command_64 *)[cmdData bytes]; if (lc->cmd == LC_SEGMENT || lc->cmd == LC_SEGMENT_64) { if (strncmp(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; struct section_64 *sect = (struct section_64 *)[sectData bytes]; if (sect && strncmp(sect->sectname, "__info_plist", 12) == 0 && sect->size < 2000000) { NSData *plistData = [self safeSubdataWithRange:NSMakeRange(sect->offset, sect->size)]; 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) { 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 { switch (cpuType) { case CPU_TYPE_X86: return @"i386"; case CPU_TYPE_X86_64: return @"x86-64"; case CPU_TYPE_POWERPC: return @"ppc"; case CPU_TYPE_POWERPC64: return @"ppc64"; default: return @"unknown"; } return nil; } /// /// 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) { NSBundle *bndl = [NSBundle bundleWithPath:path]; if (bundle) *bundle = bndl; return [bndl executablePath]; } else { return path; } } @end