作者:Flanker
公眾號:Flanker論安全

安卓生態多姿多彩,在AOSP之外各大廠商的binder service也同樣各式各樣。這些自行實現的service通常來說是閉源的,常常成為會被人忽略的提權攻擊面。在這一系列文章中,我會先描述如何定位可能有問題的binder service進行后續研究,以及逆向中一些有意思的發現,隨后會以之前發現的兩個典型的CVE為例,討論這些漏洞是如何產生的,如何發現它們,以及如何進行利用。

尋找潛在的分析目標

在Android N之前,所有的binder service都是在servicemanager中進行注冊的,client通過/dev/binder與service進行通訊。Android N對binder服務引入了domain切分的概念,常規的服務依然使用/dev/binder,而vendor domain則轉換為使用/dev/vndbinder, hardware domain轉換為使用/dev/hwbinder。常規的untrusted_app訪問被限制在了/dev/binder。

通過service list,我們可以查看設備上注冊了多少normal domain的service。AOSP設備一般會有100+,而各大廠商的設備均會達到200以上。其中大部分都是Java服務,雖說Java服務通常也會引入一些常見的邏輯問題,但暫時不屬于本文的討論范圍。目前的范圍內,我們只關注包含有native code,可能存在內存破壞漏洞的組件。 所以第一個問題出現了,如何確定哪些服務是通過native code處理的?根據binder服務的形式,存在如下可能:

  • 該服務直接運行在native process中
  • 該服務運行在JVM process中(例:注冊于system_server中),但存在JNI調用

無論分析哪種形式,我們都需要先確定該服務的host進程。在進程注冊或打開binder服務的時候, debugfs中會留下相應的node entry或ref entry。Android Internals的作者數年前開源的工具bindump即通過遍歷這個信息來獲取服務的進程關系。其工作原理如下:

  • tool process打開目標服務,獲取本進程新增的ref id
  • 遍歷procfs, 通過ref id匹配各進程的node id,匹配到的進程即為該服務host process

這個方法非常有效,不過隨著Android的演進,原始的bindump工具現在遇到了如下問題:

  • debugfs現在需要root權限才能打開,普通進程已經無法打開debugfs
  • binder node現在具有了domain的概念,需要區分不同domain中的node
  • 原始的bindump link到libbinder.so,但每個版本更新后symbol location會發生變化,導致原有的binary在新版本上無法運行,每個版本都會需要在AOSP source tree下重新編譯(如果vendor改動了libbinder問題就更大了)

為了解決問題2和3,我用Java重寫了bindump,將其打包成可以忽略平臺版本問題單獨運行的jar包,相關代碼和precompiled jar已經放在了GitHub上。

在解決了以上問題之后,我們終于可以定位到運行在native process中的服務,并進行后續分析了。

CVE-2018-9143: buffer overflow in visiond service

media.air是一個運行在Samsung設備系統進程/system/bin/visiond中的服務。visiond本身加載了多個動態執行庫,包括libairserviceproxy, libairservice, libair 等, 并以system-uid運行。 相關服務的實現端,例如 BnAIRClient::onTransact, BnEngine::onTransact, BnAIRService::onTransact等存在于libairserviceproxy中。

虛表指針去哪里了?

逆向C++庫的關鍵準備之一是定位相應虛函數指針,并使用IDA腳本通過這些信息進行type reconstruction。但當我們在IDA中打開media.air服務的動態庫時,卻驚訝地發現,在原來應該有vtable表項指針的地方,除了top-offset和virtual-base offset還在,其他的指針大部分神秘地消失了,如下圖所示

hedan vtable1

而同樣大版本的AOSP/Pixel/Nexus鏡像的binary中并沒有出現這樣的問題。誰偷了我的虛表指針?

乍一看可能會覺得三星在故意搞事,像國內廠商一樣做了某種混淆來對抗靜態分析,但實際上并不是。為了理解這種現象,我們先來回憶下虛表項指針的存儲方式。

首先,IDA給我們展示的rel section并不是ELF中實際的內容,而是處理過后的結果。虛表指針項并不直接存儲在.data.rel.ro section,而是linker 重定位之后的結果。它們的原始內容實際上存在于.rela.dyn中,以R_AARCH64_RELATIVE表項的形式存在。在library被加載時,linker會根據表項中的offset,將重定位后的實際地址寫入對應的offset中,也就是vtable真正的地址。 IDA和其他分析工具會模擬linker的功能預先對這些內容進行解析并寫入,但如果IDA解析relocation table失敗,那么這些地址會維持其在ELF中的原始內容,也就是0。

但是什么導致了IDA解析失敗?這是在N后引入的APS2重定位特性,最先應用在chromium上,如下所述:

Packed Relocations
All flavors of lib(mono)chrome.so enable “packed relocations”, or “APS2 relocations” in order to save binary size.
Refer to this source file for an explanation of the format.
To process these relocations:
Pre-M Android: Our custom linker must be used.
M+ Android: The system linker understands the format.
To see if relocations are packed, look for LOOS+# when running: readelf -S libchrome.so
Android P+ supports an even better format known as RELR.
We'll likely switch non-Monochrome apks over to using it once it is implemented in lld.

vtable2

APS2將重定向表以SLEB128的格式壓縮編碼,對于大型binary可以縮小ELF的體積。具體的編碼解碼實現可以在這里找到。在運行時linker解壓這個section,根據大小變化調整前后section的地址,將其恢復為一個正常的ELF進行加載。IDA尚不支持APS2 encoding所以我們會看到大部分重定向信息都丟失了,可以用上述relocation_packer工具將其解碼恢復。

一個好消息: 在APS2引入兩年之后,IDA 7.3終于增加了對其的支持,現在可以看到IDA已經可以正確地恢復虛表項地址了。

IDA Changelog:
File formats:
...
+ ELF: added support for packed android relocations (APS2 format)
...

vtable3

AirService copies in the air

在解決了逆向的這個問題之后,我們回過頭來分析下這個服務的相關結構。media.air中的BnAirServiceProxy提供了兩個接收客戶端傳入的AirClient的初始化函數,其中一個以StrongBinder的形式接受輸入,并返回一個指向BnAir服務的handle供客戶端進程再次調用。當option參數為0時,該函數會創建一個FileSource線程,當option參數為1時其會創建一個CameraSourceThread線程。只有在CameraSourceThread線程中可以觸發本漏洞。

在獲得服務端BnAir服務的handle后,客戶端將可以進一步調用其實現的transaction。libair.so中提供的BnAIR服務實現了一個針對Frame的狀態機,狀態機的關鍵函數包括configure, startenqueueFrame。在按照順序調用之后最終觸發有漏洞的enqueueFrame函數。

android::RefBase *__fastcall android::FrameManager::enqueueFrame(__int64 someptr, __int64 imemory)
{
//...
 v4 = (android::FrameManager::Frame *)operator new(0x38uLL);
 android::FrameManager::Frame::Frame(v4, v5, *(_DWORD *)(v2 + 0x88), *(_DWORD *)(v2 + 140), 17, *(_DWORD *)(v2 + 144));
 v16 = v4;

 android::RefBase::incStrong(v4, &v16);

 (*(void (**)(void))(**(_QWORD **)v3 + 0x20LL))(); //offset and size is retrived

 v6 = (*(__int64 (**)(void))(*(_QWORD *)v16 + 88LL))(); //v6 = Frame->imemory->base();

 v7 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)imemoryheap + 40LL))(imemoryheap); //v7 = imemoryheap->base();

 memcpy(v6, v7 + v15, v14);//memcpy(frame->imemory->base(), imemoryheap->base() + offset, imemoryheap->size());//overflow here
//...
 if ( imemoryheap )
   android::RefBase::decStrong(
     (android::RefBase *)(imemoryheap + *(_QWORD *)(*(_QWORD *)imemoryheap - 24LL)),
     &imemoryheap);
 result = v16;
 if ( v16 )
   result = (android::RefBase *)android::RefBase::decStrong(v16, &v16);

 return result;

}

可以看到,傳入的IMemory在被mmap后并沒有對長度做任何的檢查,直接memcpy進入了Frame的IMemory中,而后者的預定義size是2*1024*1024,即超過2M的映射,即會引發overflow。

整體的觸發步驟如下:

  • media.air發送一個code=1 的transaction以獲取BnAir的handle,以下步驟的調用對象均為該handle
  • 發送一個code=3 的transaction以觸發 android::AIRService::Client::configure(int)。該函數會完成對應對象的參數初始化
  • 發送一個code=4的transaction以創建一個AIRService Client, 并調用android::AIRService::Client::start()啟動
  • 最后一個code=7的transaction最終傳入攻擊者可控內容和長度的IMemory,觸發android::AIRService::Client::enqueueFrame(int, android::sp<android::IMemory> const&)中的溢出
    fpsr 00000000  fpcr 00000000
backtrace:
    #00 pc 000000000001b014  /system/lib64/libc.so (memcpy+332)
    #01 pc 0000000000029b5c  /system/lib64/libairservice.so (_ZN7android12FrameManager12enqueueFrameERKNS_2spINS_7IMemoryEEE+188)
    #02 pc 0000000000030c8c  /system/lib64/libairservice.so (_ZN7android10AIRService6Client12enqueueFrameEiRKNS_2spINS_7IMemoryEEE+72)
    #03 pc 000000000000fbf8  /system/lib64/libair.so (_ZN7android5BnAIR10onTransactEjRKNS_6ParcelEPS1_j+732)
    #04 pc 000000000004a340  /system/lib64/libbinder.so (_ZN7android7BBinder8transactEjRKNS_6ParcelEPS1_j+132)
    #05 pc 00000000000564f0  /system/lib64/libbinder.so (_ZN7android14IPCThreadState14executeCommandEi+1032)
    #06 pc 000000000005602c  /system/lib64/libbinder.so (_ZN7android14IPCThreadState20getAndExecuteCommandEv+156)
    #07 pc 0000000000056744  /system/lib64/libbinder.so (_ZN7android14IPCThreadState14joinThreadPoolEb+128)
    #08 pc 0000000000074b70  /system/lib64/libbinder.so
    #09 pc 00000000000127f0  /system/lib64/libutils.so (_ZN7android6Thread11_threadLoopEPv+336)
    #10 pc 00000000000770f4  /system/lib64/libc.so (_ZL15__pthread_startPv+204)
    #11 pc 000000000001e7d0  /system/lib64/libc.so (__start_thread+16)

如何利用?

這是一個類似于Project Zero之前公布的bitunmap案例的漏洞,兩者的溢出都發生在mmap過的區域。由于mmap分配的內存區域相對較大,位置不同于常規的堆管理器管理區域,其利用方式不同于傳統的堆溢出。讀者應該會回憶到Project Zero是通過特定函數分配thread,然后溢出thread的control structre的方式來實現控制流劫持。同樣地,在我們的目標中,android::AIRService::Client::configure被調用時,它會創建一個新的thread。通過風水Frame對象,我們構造內存空洞并在空洞中創建thread,觸發溢出后劫持thread中的回調指針來最終控制PC。

但這還遠遠沒有結束。雖然該進程是system-uid,但SELinux對其有嚴格的限制,例如no execmem, no executable file loading, 甚至無法向ServiceManager查詢大部分系統服務。即使控制了PC,接下來又該何去何從,例如如何利用提升的權限來安裝惡意應用,如果根本無法lookup PackageManagerService?

這里需要注意的是,雖然SELinux禁止了visiond去lookup service,但實際上并沒有限制調用service自身的transaction,這依賴于service自身的實現,例如ActivityManagerService的相關函數是通過enforceNotIsolated標注來禁止isolated進程調用。所以只要能成功地將PMS的binder handle傳遞給visiond,攻擊者依然可以以visiond的身份調用PMS來安裝惡意應用,相關步驟如下:

  • Attacking app (untrusted_app context) 獲得PMS的StrongBinder handle
  • Attacking app 將handle傳遞給visiond. 任何接收StrongBinder的服務端函數均可,例如BnAirServiceProxy中的第一個transaction
  • Attacking app 觸發上述漏洞獲取PC控制后,payload在內存中搜索上一步傳入的PMS handle
  • Payload通過該handle調用PMS,完成惡意應用安裝

總結

以上即為CVE-2018-9143,一個典型的binder service漏洞的故事。Samsung已經發布了advisory和補丁,并通過firmeware OTA修復了該漏洞。在下一篇文章中,我會介紹CVE-2018-9139,sensorhubservice中的一個堆溢出,以及如何通過fuzzing發現的該漏洞和它的利用(包括一個控制PC的poc)。

本文所描述的相關poc和有漏洞的服務binary均可以在 https://github.com/flankerhqd/binder-cves 中找到。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1051/