ChenYilong / iOS12AdaptationTips

iOS12AdaptationTips
86 stars 2 forks source link

领悟到 NSCoding 是一个坑,Apple 花了10年时间 #1

Open ChenYilong opened 5 years ago

ChenYilong commented 5 years ago

本文主要阐述以下观点,如果你早已知晓,请忽略本条内容:

另外,本文示例代码基于 Objective-C, 对于 Swift 开发者,可以查看新出的 Codable 文档,本文请谨慎浏览。

NSCoding 简介

NSCoding 是为了 NSData 与对象转换而设计的协议。命名也是进行时态,很形象。

任何想要与 NSData 自由转换身份的对象,都需要遵从 NSCoding。当时 Apple 设计 NSCoding 的时候,可能没想到自己埋了很多坑。

坑被发现后,在 iOS6 推出 NSSecureCoding,解决 NSCoder 解码异常问题

关于 NSCoder 里的坑,Apple 官网讲得很清楚: Documentation-Foundation-Archives and Serialization-NSSecureCoding,在此不做赘述。

关于 NSSecureCoding 用法,下面是 AFNetworking 的两个使用场景:

场景一:常规操作

#pragma mark - NSSecureCoding

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (instancetype)initWithCoder:(NSCoder *)decoder {
    self = [self init];
    if (!self) {
        return nil;
    }

    self.acceptableStatusCodes = [decoder decodeObjectOfClass:[NSIndexSet class] forKey:NSStringFromSelector(@selector(acceptableStatusCodes))];
    self.acceptableContentTypes = [decoder decodeObjectOfClass:[NSIndexSet class] forKey:NSStringFromSelector(@selector(acceptableContentTypes))];

    return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
    [coder encodeObject:self.acceptableStatusCodes forKey:NSStringFromSelector(@selector(acceptableStatusCodes))];
    [coder encodeObject:self.acceptableContentTypes forKey:NSStringFromSelector(@selector(acceptableContentTypes))];
}

场景二:正如上文所属,因为有可能不执行init方法,直接执行 -initWithCoder:,需要在这里做完整的初始化流程。

//AFURLSessionManager
#pragma mark - NSSecureCoding

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (instancetype)initWithCoder:(NSCoder *)decoder {
    NSURLSessionConfiguration *configuration = [decoder decodeObjectOfClass:[NSURLSessionConfiguration class] forKey:@"sessionConfiguration"];

    self = [self initWithSessionConfiguration:configuration];
    if (!self) {
        return nil;
    }

    return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
    [coder encodeObject:self.session.configuration forKey:@"sessionConfiguration"];
}

iOS12 全面推行安全归档,应对文件替换攻击。

以往 iOS 防止数据篡改,主要重心放在网络传输策略,但是数据处理部分依然存在被攻击风险。而归档,正是短板所在。而所有对象归档、编解码都会走同一个方法,这对攻击者而言也十分方便。

这次 WWDC Apple 提到了文件替换攻击(iOS object substitution attack),指的是攻击者替换本地归档文件,伪造数据。从而达到数据篡改、攻击目的。

推测进程间通信 xpc ,应该是高危环节。XPC介绍可以参考: apple-documentation-xpc《ObjC 中国 - XPC》

Apple 给出的策略是,为归档的编解码方法添加,类名校验。也就是在编解码前,先检测属性的类是否是指定的类,然后再决定是否编解码。 iOS12 中,Apple 几乎废弃了 NSKeyedArchiver 和 NSKeyedUnarchiver 原有的所有方法,甚至包括init方法,然后全部加入了类名校验。提高了篡改数据的成本。

不过, Jonathan Zdziarski 在 preventing widespread automated attacks in ios -part-2 一文中曾指出, 如果用户 hook 了 NSKeyedArchiver (或者 NSKeyedUnarchiver) 编解码方法后,runtime 拦截数据(应该也有篡改的风险),因为我对逆向了解较少,不确定本次 iOS12 的改动是否能抵御该风险,但从原理上推测,Jonathan Zdziarski 在文中指出的漏洞 iOS12 依然存在。期待后续会有更安全的应对策略。

TODO List

下面是开发者需要及时跟进的项:

附录

相关API变更:

NSCoder:

//  Foundation/NSCoder.h
// Specify what the expected class of the allocated object is. If the coder responds YES to -requiresSecureCoding, then an exception will be thrown if the class to be decoded does not implement NSSecureCoding or is not isKindOfClass: of the argument. If the coder responds NO to -requiresSecureCoding, then the class argument is ignored and no check of the class of the decoded object is performed, exactly as if decodeObjectForKey: had been called.
- (nullable id)decodeObjectOfClass:(Class)aClass forKey:(NSString *)key API_AVAILABLE(macos(10.8), ios(6.0), watchos(2.0), tvos(9.0));

NSKeyedArchiver

//  Foundation/NSKeyedArchiver.h

/**
 Initializes the receiver for encoding an archive, optionally disabling secure coding.

 If \c NSSecureCoding cannot be used, \c requiresSecureCoding may be turned off here; for improved security, however, \c requiresSecureCoding should be left enabled whenever possible. \c requiresSecureCoding ensures that all encoded objects conform to \c NSSecureCoding, preventing the possibility of encoding objects which cannot be decoded later.

 To produce archives whose structure matches those previously encoded using \c +archivedRootDataWithObject, encode the top-level object in your archive for the \c NSKeyedArchiveRootObjectKey.
 */
- (instancetype)initRequiringSecureCoding:(BOOL)requiresSecureCoding API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));

/**
 Returns an \c NSData object containing the encoded form of the object graph whose root object is given, optionally disabling secure coding.

 If \c NSSecureCoding cannot be used, \c requiresSecureCoding may be turned off here; for improved security, however, \c requiresSecureCoding should be left enabled whenever possible. \c requiresSecureCoding ensures that all encoded objects conform to \c NSSecureCoding, preventing the possibility of encoding objects which cannot be decoded later.

 If the object graph cannot be encoded, returns \c nil and sets the \c error out parameter.
 */
+ (nullable NSData *)archivedDataWithRootObject:(id)object requiringSecureCoding:(BOOL)requiresSecureCoding error:(NSError **)error API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));

/// Initialize the archiver with empty data, ready for writing.
- (instancetype)init API_DEPRECATED("Use -initRequiringSecureCoding: instead", macosx(10.12,10.14), ios(10.0,12.0), watchos(3.0,5.0), tvos(10.0,12.0));
- (instancetype)initForWritingWithMutableData:(NSMutableData *)data API_DEPRECATED("Use -initRequiringSecureCoding: instead", macosx(10.2,10.14), ios(2.0,12.0), watchos(2.0,5.0), tvos(9.0,12.0));

+ (NSData *)archivedDataWithRootObject:(id)rootObject API_DEPRECATED("Use +archivedDataWithRootObject:requiringSecureCoding:error: instead", macosx(10.2,10.14), ios(2.0,12.0), watchos(2.0,5.0), tvos(9.0,12.0));
+ (BOOL)archiveRootObject:(id)rootObject toFile:(NSString *)path API_DEPRECATED("Use +archivedDataWithRootObject:requiringSecureCoding:error: instead", macosx(10.2,10.14), ios(2.0,12.0), watchos(2.0,5.0), tvos(9.0,12.0));
 //
@property (readwrite) NSDecodingFailurePolicy decodingFailurePolicy API_AVAILABLE(macos(10.11), ios(9.0), watchos(2.0), tvos(9.0));

NSKeyedUnarchiver


/**
 Initializes the receiver for decoding an archive previously encoded by \c NSKeyedUnarchiver.

 Enables \c requiresSecureCoding by default. If \c NSSecureCoding cannot be used, \c requiresSecureCoding may be turned off manually; for improved security, \c requiresSecureCoding should be left enabled whenever possible.

 Sets the unarchiver's \c decodingFailurePolicy to \c NSDecodingFailurePolicySetErrorAndReturn.

 Returns \c nil if the given data is not valid, and sets the \c error out parameter.
 */
- (nullable instancetype)initForReadingFromData:(NSData *)data error:(NSError **)error API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));

/**
 Decodes the root object of the given class from the given archive, previously encoded by \c NSKeyedArchiver.

 Enables \c requiresSecureCoding and sets the \c decodingFailurePolicy to \c NSDecodingFailurePolicySetErrorAndReturn.

 Returns \c nil if the given data is not valid or cannot be decoded, and sets the \c error out parameter.
 */
+ (nullable id)unarchivedObjectOfClass:(Class)cls fromData:(NSData *)data error:(NSError **)error API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0)) NS_REFINED_FOR_SWIFT;

/**
 Decodes the root object of one of the given classes from the given archive, previously encoded by \c NSKeyedArchiver.

 Enables \c requiresSecureCoding and sets the \c decodingFailurePolicy to \c NSDecodingFailurePolicySetErrorAndReturn.

 Returns \c nil if the given data is not valid or cannot be decoded, and sets the \c error out parameter.
 */
+ (nullable id)unarchivedObjectOfClasses:(NSSet<Class> *)classes fromData:(NSData *)data error:(NSError **)error API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0)) NS_REFINED_FOR_SWIFT;

- (instancetype)init API_DEPRECATED("Use -initForReadingFromData:error: instead", macosx(10.2,10.14), ios(2.0,12.0), watchos(2.0,5.0), tvos(9.0,12.0));
- (instancetype)initForReadingWithData:(NSData *)data API_DEPRECATED("Use -initForReadingFromData:error: instead", macosx(10.2,10.14), ios(2.0,12.0), watchos(2.0,5.0), tvos(9.0,12.0));

+ (nullable id)unarchiveObjectWithData:(NSData *)data API_DEPRECATED("Use +unarchivedObjectOfClass:fromData:error: instead", macosx(10.2,10.14), ios(2.0,12.0), watchos(2.0,5.0), tvos(9.0,12.0));
+ (nullable id)unarchiveTopLevelObjectWithData:(NSData *)data error:(NSError **)error API_DEPRECATED("Use +unarchivedObjectOfClass:fromData:error: instead", macosx(10.2,10.14), ios(2.0,12.0), watchos(2.0,5.0), tvos(9.0,12.0)) NS_SWIFT_UNAVAILABLE("Use 'unarchiveTopLevelObjectWithData(_:) throws' instead");
+ (nullable id)unarchiveObjectWithFile:(NSString *)path API_DEPRECATED("Use +unarchivedObjectOfClass:fromData:error: instead", macosx(10.2,10.14), ios(2.0,12.0), watchos(2.0,5.0), tvos(9.0,12.0));

参考文献

Danie1s commented 5 years ago

牛逼,我刚看了WWDC2018的Data You Can Trust,主要内容基本被你总结出来了

ripperhe commented 5 years ago

mark

yscMichael commented 2 years ago

mark