作者:evilpan
原文鏈接:https://evilpan.com/2020/07/11/android-ipc-tips/
最近在分析一個運行Android系統的IoT平台,其中包含設備管控和日誌服務(Agent)、升級服務(FOTA)、自定義桌面(Launcher)、端上IDS以及前台的圖形界面應用等多個前後台進程。在對其中某個功能進行逆向時發現調用鏈路跨越了多個應用,因此本文就做個簡單記錄。
前言
熟悉安卓開發的同學應該都知道構建IPC的流程,但從逆向工程的角度分析的卻比較少見。 說到安卓跨進程通信/調用,就不得不提到AIDL和Binder,在逆向一個東西之前,首先需要了解它,因此本文也會先對其工作流程和工作原理進行介紹。
AIDL 101
AIDL是Google定義的一個接口定義語言,即Android Interface Definition Language。兩個進程(稱為客戶端和服務端)共享同一份AIDL文件,並在其基礎上實現透明的遠程調用。
從開發者的角度如何使用AIDL呢?下面參考Android的官方文檔以一個實例進行說明。我們的目標是構建一個遠程服務FooService,並且提供幾個簡單的遠程調用,首先創建AIDL文件IFooService.aidl
:
package com.evilpan; interface IFooService { void sayHi(); int add(int lhs, int rhs); }
AIDL作為一種接口語言,其主要目的一方面是簡化創建IPC所需要的IPC代碼處理,另一方面也是為了在多語言下進行兼容和適配。使用Android內置的SDK開發工具可將其轉換為目標語言,本文以Java為例,命令如下:
aidl --lang=java com/evilpan/IFooService.aidl -o .
生成的文件為IFooService.java
,文件的內容後面再介紹,其大致結構如下:
public interface IFooService extends android.os.IInterface { /** Default implementation for IFooService. */ public static class Default implements com.evilpan.IFooService { // ... } /** Local-side IPC implementation stub class. */ public static abstract class Stub extends android.os.Binder implements com.evilpan.IFooService { // ... } public void sayHi() throws android.os.RemoteException; public int add(int lhs, int rhs) throws android.os.RemoteException; }
在這個文件的基礎上,服務端和客戶端分別構造遠程通信的代碼。
Server
服務端要做兩件事:
- 實現AIDL生成的的接口
- 創建對應的Service並暴露給調用者
實現接口主要是實現AIDL中的Stub類,如下:
package com.evilpan.server; import android.os.RemoteException; import android.util.Log; import com.evilpan.IFooService; public class IFooServiceImpl extends IFooService.Stub { public static String TAG = "pan_IFooServiceImpl"; @Override public void sayHi() throws RemoteException { Log.i(TAG, "Hi from server"); } @Override public int add(int lhs, int rhs) throws RemoteException { Log.i(TAG, "add from server"); return lhs + rhs; } }
客戶端調用接口需要經過Service,因此我們還要創建對應的服務:
package com.evilpan.server; import android.app.Service; import android.content.Intent; import android.os.IBinder; import android.util.Log; public class FooService extends Service { public static String TAG = "pan_FooService"; private IBinder mBinder; public FooService() { Log.i(TAG, "Service init"); mBinder = new IFooServiceImpl(); } @Override public IBinder onBind(Intent intent) { Log.i(TAG, "return IBinder Object"); return mBinder; } }
注意這個服務需要在AndroidManifest.xml
中導出:
<service android:name=".FooService" android:enabled="true" android:exported="true"/>
這裡的服務與常規服務不同,不需要通過startService
之類的操作去進行啟動,而是讓客戶端去綁定並啟動,因此也稱為Bound Service。客戶端綁定成功后拿到的IBinder
對象(遠程對象)就相當於上面onBind
中返回的對象,客戶端中操作本地對象可以實現遠程調用的效果。
Client
客戶端在正常調用遠程方法之前也需要做兩件事:
- 實現ServiceConnection接口
- bindService
ServiceConnection接口主要是連接遠程服務成功的異步回調,示例如下:
private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { Log.i(TAG, "onServiceConnected"); mService = IFooService.Stub.asInterface(service); Log.i(TAG, "sayHi"); try { mService.sayHi(); Log.i(TAG, "add"); mService.add(3 , 4); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { Log.i(TAG, "onServiceDisconnected"); }
連接成功時會獲得一個IBinder
對象,就是前面說的IFooService.Stub
實現。我們可以直接通過asInterface將其轉換為IFooService
對象。
bindService
方法用來將Activity綁定到目標Service上,第一個參數為目標Service的Intent,第二個參數為上面的ServiceConnection實例。
@Override protected void onStart() { super.onStart(); Log.i(TAG, "onStart"); Intent intent = new Intent(); String pName = "com.evilpan.server"; intent.setClassName(pName, pName + ".FooService"); boolean ret = bindService(intent, mConnection, Context.BIND_AUTO_CREATE); Log.i(TAG, "bindService: " + ret); }
注意這裡的包名指定的是服務端的包名,並且類名是服務類而不是AIDL中的接口類。綁定成功后啟動客戶端進程,可看到ADB日誌如下所示:
07-11 06:01:25.767 8492 8492 I pan_Client: onCreate 07-11 06:01:25.768 8492 8492 I pan_Client: onStart 07-11 06:01:25.769 8492 8492 I pan_Client: bindService: true 07-11 06:01:25.770 8451 8451 I pan_FooService: Service init 07-11 06:01:25.770 8451 8451 I pan_FooService: return IBinder Object 07-11 06:01:25.785 8492 8492 I pan_Client: onServiceConnected 07-11 06:01:25.785 8492 8492 I pan_Client: sayHi 07-11 06:01:25.785 8451 8463 I pan_IFooServiceImpl: Hi from server 07-11 06:01:25.786 8492 8492 I pan_Client: add 07-11 06:01:25.786 8451 8508 I pan_IFooServiceImpl: add from server
Server和Client示例文件可見附件。
其他
前面我們簡單介紹了AIDL的使用,實際上AIDL支持豐富的數據類型,除了int、long、float、String這些常見類型外,還支持在進程間傳遞對象
(Parcelable),以及傳遞函數
。在AIDL中定義對象如下:
package com.evilpan; parcelable Person { int age; String name; }
也可以在AIDL中只聲明parcelable對象,並在Java文件中自己定義。
而函數也可以看做是一個類型進行傳遞,例如:
package com.evilpan; oneway interface IRemoteServiceCallback { void onAsyncResult(String result); }
可以把IRemoteServiceCallback
當做一個類型,在其他的AIDL中使用:
package com.evilpan; import com.evilpan.IRemoteServiceCallback; interface IRemoteService { void registerCallback(IRemoteServiceCallback cb); }
這種模式可以讓服務端去調用客戶端實現的函數,通常用來返回一些異步的事件或者響應。
Binder
通過上面的介紹我們知道AIDL實際上只是對boundService接口的一個抽象,而boundService的核心是有一個跨進程的IBinder接口(即上面onBind返回的對象)。實現這個接口有三種方式:
通常實現IPC用得更多的是Messenger,因為其接受的信息是在同一個線程中處理的;直接使用AIDL可能需要多線程的能力從而導致複雜性增加,因此不適合大部分應用。
但不管是AIDL還是Messenger,其本質都是使用了Binder。那麼什麼是Binder?簡單來說Binder是Android系統中的進程間通信(IPC)框架。我們都知道Android是基於Linux內核構建的,而Linux中已經有了許多進程間通信的方法,如:
- 管道(半雙工/全雙工)
- 消息隊列
- 信號量
- 共享存儲
- socket
- …
理論上Binder可以基於上面的這些機制實現一套IPC的功能,但實際上Binder自己構建了新的進程間通信方法,這意味着其功能必須要侵入到Linux內核中。為滿足商業公司需求而提交patch到Linux upstream,所受到的阻力可想而知,為什麼Google仍然堅持呢?Brian Swetland在Linux郵件組中指出,現有的Linux IPC機制無法滿足以下兩個需求:
- 通過內核將數據直接到目標地址空間的環形緩衝區,從而減少拷貝開銷。
- 對可在進程間共享和傳遞的遠程代理對象的生命周期管理。
因此目前Binder在內核中實現為獨立的驅動,即/dev/binder
(後續還進行了細分,如hwbinder、vndbinder)。
除了Binder之外,Android還在Linux的基礎上增加了一些其他驅動,比如Ashmem
、Low Memory Killer
等,在內核的drivers/[staging]/android
目錄中。
從驅動的層面看,Binder的使用也很簡單:使用open(2)
系統調用打開/dev/binder
,然後使用ioctl(2)
系統調用進行數據傳輸。以前面的AIDL IPC為例,其底層的實現如下圖所示:
逆向分析
上面介紹了那麼多,但本文不是Binder Internal的文章,不要忘記了我們的目的是逆向。從上面Binder IPC的流程中可以看到一個很重要的特點,即Binder使用transact
發送數據,並且在(另一個進程的)onTransact
回調中接收數據。
大部分逆向工程的工作都是類似的,尋找一種經過編譯器處理特定文件后的的模式,並在此基礎上構建還原出原始的操作。比如,對於C語言的逆向是通過調用約定以及函數入口/出口對棧的分配/釋放來判斷函數的調用,對於C++則是通過對vtable的查找/偏移來判斷虛函數的調用。
對於我們一開始的目標而言,就是需要分析出系統中存在的進程間調用,更準確地說是需要確定某個進程中函數的交叉引用(xref)。以AIDL為例,.aidl
文件是不包含在release后的apk文件中的,不過我們還是可以通過生成文件的特徵判斷這是一個AIDL服務。從生成的代碼上來看,主要有這些特點:
- 服務端和客戶端生成的接口文件是相同的
- 生成的主類拓展
android.os.IInterface
,包含AIDL中所定義的函數聲明 - 主類中包含了自身的3個實現,分別是默認實現
Default
、本地實現Stub
以及遠程代理實現Proxy
一般而言,本地的實現(Stub)需要服務端繼承並實現對應方法,Stub同時也拓展Binder類,並在onTransact
方法中根據code來選擇不同的函數進行處理。比如對於前面的例子,有:
public static abstract class Stub extends android.os.Binder implements com.evilpan.IFooService { public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) { // ... switch (code) { //... case TRANSACTION_sayHi: this.sayHi(); reply.writeNoException(); return true; case TRANSACTION_add: int _arg0 = data.readInt(); int _arg1 = data.readInt(); int _result = this.add(_arg0, _arg1); reply.writeNoException(); reply.writeInt(_result); return true; // ... } } }
Proxy即Client端的實現則通過指定transact的code來調用對應遠程代碼,如下:
private static class Proxy implements com.evilpan.IFooService { private android.os.IBinder mRemote; // ... public void sayHi() throws android.os.RemoteException { //... boolean _status = mRemote.transact(Stub.TRANSACTION_sayHi, _data, _reply, 0); //... } }
除了生成代碼的特徵,通常遠程調用都會用到 Bound Service,因此在服務端的AndroidManifest.xml
文件中必然會有導出的服務聲明,這也可以作為分析的一個輔助驗證。
示例
假設我們正在逆向分析上面編譯好的APK,在找到某個關鍵函數(比如add)后Find Usage
發現沒有任何交叉引用,但實際上這個函數是被調用了的。那麼這就有幾種可能,比如這個函數是通過反射調用的,或者這個函數是在native代碼中調用的。……當然這裡實際上是父類中進行多態調用的,本質是Binder喚起的遠程調用。
跨進程交叉引用的一個前提是需要知道是在哪個進程調用的。如果有權限在Server中進行調試或者代碼注入,我們就可以在觸發調用或者綁定時使用Binder.getCallingUid()
函數獲取調用者的UID,從而獲取Client的包名。
單純靜態分析的話可以把系統中所有相關的進程pull下來,分別反編譯后使用grep進行搜索。因為遠程調用的接口是共享的,所以即便使用了proguard等混淆也不會影響到接口函數。
小結
本文主要是記錄下最近遇到的一個Android智能設備的逆向,與以往單個APK不同,這類智能設備中通常以系統為整體,其中包含了多個業務部門內置或者安裝的應用,在分析時發現許多應用間跳轉和通信的場景。由於NDA的原因沒有詳細介紹,因此使用了我自己創建的Client/Server作為示例進行說明,但其中的方法都是類似的,即先從正向了解IPC的運行方式,然後通過代碼特徵去鑒別不同應用間的跳轉。對於複雜的系統而言,先理清思路比頭鐵逆向也更為重要。
參考資料
- BINDER TRANSACTIONS IN THE BOWELS OF THE LINUX KERNEL
- BINDER – ANALYSIS AND EXPLOITATION OF CVE-2020-0041
- Android Binder Framework
本文由 Seebug Paper 發佈,如需轉載請註明來源。本文地址:https://paper.seebug.org/1364/
转载请注明:IMGIT » Android 進程間通信與逆向分析