最新消息:

Android 進程間通信與逆向分析

SEEBUG IMGIT 25浏览 0评论

作者: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

服務端要做兩件事:

  1. 實現AIDL生成的的接口
  2. 創建對應的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

客戶端在正常調用遠程方法之前也需要做兩件事:

  1. 實現ServiceConnection接口
  2. 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返回的對象)。實現這個接口有三種方式:

  1. 拓展Binder類來實現接口
  2. 使用Messenger來創建服務的接口,實際上底層也是基於AIDL實現的
  3. 直接使用AIDL

通常實現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機制無法滿足以下兩個需求:

  1. 通過內核將數據直接到目標地址空間的環形緩衝區,從而減少拷貝開銷。
  2. 對可在進程間共享和傳遞的遠程代理對象的生命周期管理。

因此目前Binder在內核中實現為獨立的驅動,即/dev/binder(後續還進行了細分,如hwbinder、vndbinder)。

除了Binder之外,Android還在Linux的基礎上增加了一些其他驅動,比如AshmemLow Memory Killer等,在內核的drivers/[staging]/android目錄中。

從驅動的層面看,Binder的使用也很簡單:使用open(2)系統調用打開/dev/binder,然後使用ioctl(2)系統調用進行數據傳輸。以前面的AIDL IPC為例,其底層的實現如下圖所示:

Android 進程間通信與逆向分析

逆向分析

上面介紹了那麼多,但本文不是Binder Internal的文章,不要忘記了我們的目的是逆向。從上面Binder IPC的流程中可以看到一個很重要的特點,即Binder使用transact發送數據,並且在(另一個進程的)onTransact回調中接收數據。

大部分逆向工程的工作都是類似的,尋找一種經過編譯器處理特定文件后的的模式,並在此基礎上構建還原出原始的操作。比如,對於C語言的逆向是通過調用約定以及函數入口/出口對棧的分配/釋放來判斷函數的調用,對於C++則是通過對vtable的查找/偏移來判斷虛函數的調用。

對於我們一開始的目標而言,就是需要分析出系統中存在的進程間調用,更準確地說是需要確定某個進程中函數的交叉引用(xref)。以AIDL為例,.aidl文件是不包含在release后的apk文件中的,不過我們還是可以通過生成文件的特徵判斷這是一個AIDL服務。從生成的代碼上來看,主要有這些特點:

  1. 服務端和客戶端生成的接口文件是相同的
  2. 生成的主類拓展android.os.IInterface,包含AIDL中所定義的函數聲明
  3. 主類中包含了自身的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喚起的遠程調用。

Android 進程間通信與逆向分析

跨進程交叉引用的一個前提是需要知道是在哪個進程調用的。如果有權限在Server中進行調試或者代碼注入,我們就可以在觸發調用或者綁定時使用Binder.getCallingUid()函數獲取調用者的UID,從而獲取Client的包名。

單純靜態分析的話可以把系統中所有相關的進程pull下來,分別反編譯后使用grep進行搜索。因為遠程調用的接口是共享的,所以即便使用了proguard等混淆也不會影響到接口函數。

小結

本文主要是記錄下最近遇到的一個Android智能設備的逆向,與以往單個APK不同,這類智能設備中通常以系統為整體,其中包含了多個業務部門內置或者安裝的應用,在分析時發現許多應用間跳轉和通信的場景。由於NDA的原因沒有詳細介紹,因此使用了我自己創建的Client/Server作為示例進行說明,但其中的方法都是類似的,即先從正向了解IPC的運行方式,然後通過代碼特徵去鑒別不同應用間的跳轉。對於複雜的系統而言,先理清思路比頭鐵逆向也更為重要。

參考資料


Android 進程間通信與逆向分析
本文由 Seebug Paper 發佈,如需轉載請註明來源。本文地址:https://paper.seebug.org/1364/

转载请注明:IMGIT » Android 進程間通信與逆向分析

发表我的评论
取消评论

*

code

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址