diff --git a/Certificates/Certificates.p12 b/Certificates/Certificates.p12 new file mode 100644 index 0000000..a19c57b Binary files /dev/null and b/Certificates/Certificates.p12 differ diff --git a/Certificates/Lessica.cer b/Certificates/Lessica.cer new file mode 100644 index 0000000..534658b Binary files /dev/null and b/Certificates/Lessica.cer differ diff --git a/Reveil/DataModels/Presets/SecurityPresets.swift b/Reveil/DataModels/Presets/SecurityPresets.swift index b5a21f5..53f5f08 100644 --- a/Reveil/DataModels/Presets/SecurityPresets.swift +++ b/Reveil/DataModels/Presets/SecurityPresets.swift @@ -51,11 +51,21 @@ struct SecurityPresets: Codable { } if let bundleProvisioningProfilePath = bundle.path(forResource: "embedded", ofType: "mobileprovision"), - let profileHashValue = IntegrityChecker.getMobileProvisionProfileHashValue(path: bundleProvisioningProfilePath) + let profileHashValue = IntegrityChecker.calculateHashValue(path: bundleProvisioningProfilePath) { secureMobileProvisioningProfileHashes.removeAll(keepingCapacity: true) secureMobileProvisioningProfileHashes.insert(profileHashValue) } + + var updatedHashes: Dictionary = secureResourceHashes + for secureResourceName in secureResourceHashes.keys { + let resourcePath = bundle.path(forResource: secureResourceName, ofType: nil) + if let resourcePath, let resourceHashValue = IntegrityChecker.calculateHashValue(path: resourcePath) + { + updatedHashes.updateValue(resourceHashValue, forKey: secureResourceName) + } + } + secureResourceHashes = updatedHashes } var secureStandaloneLibraries: Set = [ @@ -84,11 +94,21 @@ struct SecurityPresets: Codable { ] var secureMobileProvisioningProfileHashes: Set = [ - "0675eb798917a1d44f11424be328c72a58812ba03900f1c58569af867353f438", + "", ] var secureMainExecutableMachOHashes: Set = [ - "502a2f7f57fcd163c4a8ccaa09b1d8831e53d214c1a8e4ef49904c7615324fdc", + "", + ] + + var secureResourceHashes: Dictionary = [ + "library_stub.zip": "", + "rsc-001-country-mapping.json": "", + "rsc-002-ios-versions.json": "", + "rsc-003-iphone-models.json": "", + "rsc-004-carriers.json": "", + "rsc-005-ipad-models.json": "", + "rsc-006-ipod-models.json": "", ] var insecureEnvironmentVariables: Set = [ diff --git a/Reveil/Resources/Settings.bundle/en.lproj/Root.strings b/Reveil/Resources/Settings.bundle/en.lproj/Root.strings index b24a0f1..c4b4b57 100644 Binary files a/Reveil/Resources/Settings.bundle/en.lproj/Root.strings and b/Reveil/Resources/Settings.bundle/en.lproj/Root.strings differ diff --git a/Reveil/Resources/Settings.bundle/zh_Hans.lproj/Root.strings b/Reveil/Resources/Settings.bundle/zh_Hans.lproj/Root.strings index 7385c70..230d776 100644 Binary files a/Reveil/Resources/Settings.bundle/zh_Hans.lproj/Root.strings and b/Reveil/Resources/Settings.bundle/zh_Hans.lproj/Root.strings differ diff --git a/Reveil/SecuritySuite/FileIntegrityCheck.swift b/Reveil/SecuritySuite/FileIntegrityCheck.swift index c1ddee1..d17880d 100644 --- a/Reveil/SecuritySuite/FileIntegrityCheck.swift +++ b/Reveil/SecuritySuite/FileIntegrityCheck.swift @@ -14,6 +14,7 @@ enum FileIntegrityCheck: Codable { // Compare current hash value(SHA256 hex string) of `embedded.mobileprovision` with a specified hash value. // Use command `"shasum -a 256 /path/to/embedded.mobileprovision"` to get SHA256 value on your macOS. case mobileProvision(String) + case commonResource(String, String) // Compare current hash value(SHA256 hex string) of executable file with a specified (Image Name, Hash Value). // Only work on dynamic library and arm64. @@ -24,11 +25,13 @@ extension FileIntegrityCheck: Explainable { var description: String { switch self { case let .bundleID(exceptedBundleID): - "The expected bundle identify was \(exceptedBundleID)" + "The expected bundle identify was \(exceptedBundleID)." case let .mobileProvision(expectedSha256Value): - "The expected hash value of Mobile Provision file was \(expectedSha256Value)" + "The expected hash value of Mobile Provision file was \(expectedSha256Value)." + case let .commonResource(resourceName, expectedSha256Value): + "The expected hash value of the resource named \(resourceName) was \(expectedSha256Value)." case let .machO(imageName, expectedSha256Value): - "The expected hash value of \"__TEXT.__text\" data of \(imageName) Mach-O file was \(expectedSha256Value)" + "The expected hash value of \"__TEXT.__text\" data of \(imageName) Mach-O file was \(expectedSha256Value)." } } } diff --git a/Reveil/SecuritySuite/IntegrityChecker.swift b/Reveil/SecuritySuite/IntegrityChecker.swift index 2107bab..4e29d09 100644 --- a/Reveil/SecuritySuite/IntegrityChecker.swift +++ b/Reveil/SecuritySuite/IntegrityChecker.swift @@ -29,6 +29,12 @@ final class IntegrityChecker { result = true hitChecks.append(check) } + case let .commonResource(resourceName, expectedSha256Value): + let resourcePath = Bundle(for: Self.self).path(forResource: resourceName, ofType: nil) + if let resourcePath, calculateHashValue(path: resourcePath) != expectedSha256Value { + result = true + hitChecks.append(check) + } case let .machO(imageName, expectedSha256Value): if !checkMachO(imageName, with: expectedSha256Value.lowercased()) { result = true @@ -58,13 +64,13 @@ final class IntegrityChecker { static func checkMobileProvision(_ expectedSha256Values: Set) -> Bool { guard let path = Bundle(for: Self.self).path(forResource: "embedded", ofType: "mobileprovision"), - let hashValue = getMobileProvisionProfileHashValue(path: path) + let hashValue = calculateHashValue(path: path) else { return false } return expectedSha256Values.contains(hashValue) } - static func getMobileProvisionProfileHashValue(path: String) -> String? { + static func calculateHashValue(path: String) -> String? { guard FileManager.default.fileExists(atPath: path) else { return nil } diff --git a/Reveil/ViewModels/Security.swift b/Reveil/ViewModels/Security.swift index f43bfb6..d429650 100644 --- a/Reveil/ViewModels/Security.swift +++ b/Reveil/ViewModels/Security.swift @@ -361,6 +361,33 @@ final class Security: ObservableObject, StaticEntryProvider, Explainable { IntegrityChecker.checkMobileProvision(SecurityPresets.default.secureMobileProvisioningProfileHashes) } + func checkResourceHashes() -> Bool { + let resHashes = SecurityPresets.default.secureResourceHashes + for resName in resHashes.keys { + if let resHash = resHashes[resName] { + let resTampered = IntegrityChecker.amITampered([.commonResource(resName, resHash)]).result + if resTampered { + return false + } + } + } + return true + } + + func getModifiedResourceNames() -> [String] { + var modifiedNames = [String]() + let resHashes = SecurityPresets.default.secureResourceHashes + for resName in resHashes.keys { + if let resHash = resHashes[resName] { + let resTampered = IntegrityChecker.amITampered([.commonResource(resName, resHash)]).result + if resTampered { + modifiedNames.append(resName) + } + } + } + return modifiedNames + } + func checkMachOHash() -> Bool { #if DEBUG return IntegrityChecker.checkMachO(currentExecutablePath, with: SecurityPresets.default.secureMainExecutableMachOHashes) diff --git a/Reveil/ViewModels/SecurityCheck.swift b/Reveil/ViewModels/SecurityCheck.swift index 5ee5fc7..74d0836 100644 --- a/Reveil/ViewModels/SecurityCheck.swift +++ b/Reveil/ViewModels/SecurityCheck.swift @@ -45,6 +45,7 @@ enum SecurityCheck: CaseIterable, Codable, Equatable, Hashable, RawRepresentable case identifiedBundleIdentifier(Status) case identifiedMobileProvisioningProfile(Status) + case identifiedResources(Status) case identifiedMachO(Status) case identifiedEntitlements(Status) @@ -100,6 +101,8 @@ enum SecurityCheck: CaseIterable, Codable, Equatable, Hashable, RawRepresentable self = .identifiedBundleIdentifier(status) case "identifiedMobileProvisioningProfile": self = .identifiedMobileProvisioningProfile(status) + case "identifiedResources": + self = .identifiedResources(status) case "identifiedMachO": self = .identifiedMachO(status) case "identifiedEntitlements": @@ -173,6 +176,8 @@ enum SecurityCheck: CaseIterable, Codable, Equatable, Hashable, RawRepresentable status.prefix + "identifiedBundleIdentifier" case let .identifiedMobileProvisioningProfile(status): status.prefix + "identifiedMobileProvisioningProfile" + case let .identifiedResources(status): + status.prefix + "identifiedResources" case let .identifiedMachO(status): status.prefix + "identifiedMachO" case let .identifiedEntitlements(status): @@ -232,6 +237,7 @@ enum SecurityCheck: CaseIterable, Codable, Equatable, Hashable, RawRepresentable .noSuspiciousEnvironmentVariables(.unchanged), .identifiedBundleIdentifier(.unchanged), .identifiedMobileProvisioningProfile(.unchanged), + .identifiedResources(.unchanged), .identifiedMachO(.unchanged), .identifiedEntitlements(.unchanged), .expectedCodeSigningStatus(.unchanged), @@ -269,6 +275,7 @@ enum SecurityCheck: CaseIterable, Codable, Equatable, Hashable, RawRepresentable case let .noSuspiciousEnvironmentVariables(status): fallthrough case let .identifiedBundleIdentifier(status): fallthrough case let .identifiedMobileProvisioningProfile(status): fallthrough + case let .identifiedResources(status): fallthrough case let .identifiedMachO(status): fallthrough case let .identifiedEntitlements(status): fallthrough case let .expectedCodeSigningStatus(status): fallthrough @@ -308,6 +315,7 @@ enum SecurityCheck: CaseIterable, Codable, Equatable, Hashable, RawRepresentable case let .noSuspiciousEnvironmentVariables(status): fallthrough case let .identifiedBundleIdentifier(status): fallthrough case let .identifiedMobileProvisioningProfile(status): fallthrough + case let .identifiedResources(status): fallthrough case let .identifiedMachO(status): fallthrough case let .identifiedEntitlements(status): fallthrough case let .expectedCodeSigningStatus(status): fallthrough @@ -347,6 +355,7 @@ enum SecurityCheck: CaseIterable, Codable, Equatable, Hashable, RawRepresentable case let .noSuspiciousEnvironmentVariables(status): fallthrough case let .identifiedBundleIdentifier(status): fallthrough case let .identifiedMobileProvisioningProfile(status): fallthrough + case let .identifiedResources(status): fallthrough case let .identifiedMachO(status): fallthrough case let .identifiedEntitlements(status): fallthrough case let .expectedCodeSigningStatus(status): fallthrough @@ -419,6 +428,10 @@ enum SecurityCheck: CaseIterable, Codable, Equatable, Hashable, RawRepresentable status == .failed ? NSLocalizedString("TAMPERED_MOBILE_PROVISIONING_PROFILE", comment: "Mobile provisioning profile was tampered") : NSLocalizedString("ORIGINAL_MOBILE_PROVISIONING_PROFILE", comment: "Mobile provisioning profile is trusted") + case let .identifiedResources(status): + status == .failed ? + NSLocalizedString("TAMPERED_RESOURCES", comment: "Some of the embedded resources were tampered") : + NSLocalizedString("ORIGINAL_RESOURCES", comment: "Embedded resources are original") case let .identifiedMachO(status): status == .failed ? NSLocalizedString("TAMPERED_MACH_O", comment: "Main executable was tampered") : @@ -548,6 +561,8 @@ enum SecurityCheck: CaseIterable, Codable, Equatable, Hashable, RawRepresentable .staticIntegrity case .identifiedMobileProvisioningProfile: .staticIntegrity + case .identifiedResources: + .staticIntegrity case .identifiedMachO: .staticIntegrity case .identifiedEntitlements: @@ -616,6 +631,8 @@ enum SecurityCheck: CaseIterable, Codable, Equatable, Hashable, RawRepresentable .identifiedBundleIdentifier(Security.shared.checkMainBundleIdentifier() ? .passed : .failed) case .identifiedMobileProvisioningProfile: .identifiedMobileProvisioningProfile(Security.shared.checkMobileProvisioningProfileHash() ? .passed : .failed) + case .identifiedResources: + .identifiedResources(Security.shared.checkResourceHashes() ? .passed : .failed) case .identifiedMachO: .identifiedMachO(Security.shared.checkMachOHash() ? .passed : .failed) case .identifiedEntitlements: @@ -726,6 +743,12 @@ enum SecurityCheck: CaseIterable, Codable, Equatable, Hashable, RawRepresentable BasicEntry(customLabel: $0, allowedToCopy: true) } } + case let .identifiedResources(status): + if status == .failed { + entryChildren = Security.shared.getModifiedResourceNames().map { + BasicEntry(customLabel: $0, allowedToCopy: true) + } + } case let .identifiedEntitlements(status): if status == .failed { entryChildren = Security.shared.getUnknownEntitlementKeys().map { diff --git a/Reveil/en.lproj/Localizable.strings b/Reveil/en.lproj/Localizable.strings index 587573b..602d24a 100644 --- a/Reveil/en.lproj/Localizable.strings +++ b/Reveil/en.lproj/Localizable.strings @@ -751,6 +751,9 @@ /* Mobile provisioning profile is trusted */ "ORIGINAL_MOBILE_PROVISIONING_PROFILE" = "Mobile provisioning profile is trusted"; +/* Embedded resources are original */ +"ORIGINAL_RESOURCES" = "Embedded resources are original"; + /* Others */ "OTHERS" = "Others"; @@ -916,6 +919,9 @@ /* Mobile provisioning profile was tampered */ "TAMPERED_MOBILE_PROVISIONING_PROFILE" = "Mobile provisioning profile was tampered"; +/* Some of the embedded resources were tampered */ +"TAMPERED_RESOURCES" = "Some of the embedded resources were tampered"; + /* TB Frequency */ "TB_FREQ" = "Time-Based Frequency"; diff --git a/Reveil/zh-Hans.lproj/Localizable.strings b/Reveil/zh-Hans.lproj/Localizable.strings index 7918587..665739a 100644 --- a/Reveil/zh-Hans.lproj/Localizable.strings +++ b/Reveil/zh-Hans.lproj/Localizable.strings @@ -751,6 +751,9 @@ /* Mobile provisioning profile is trusted */ "ORIGINAL_MOBILE_PROVISIONING_PROFILE" = "已验证应用程序分发配置文件"; +/* Embedded resources are original */ +"ORIGINAL_RESOURCES" = "已验证内建资源均为初始状态"; + /* Others */ "OTHERS" = "其他"; @@ -916,6 +919,9 @@ /* Mobile provisioning profile was tampered */ "TAMPERED_MOBILE_PROVISIONING_PROFILE" = "应用程序分发配置文件被篡改"; +/* Some of the embedded resources were tampered */ +"TAMPERED_RESOURCES" = "部分内建资源被篡改"; + /* TB Frequency */ "TB_FREQ" = "时基频率";