首页
社区
课程
招聘
[翻译]一个比Master Key更强大的Android Bug
2013-12-5 14:32 15287

[翻译]一个比Master Key更强大的Android Bug

2013-12-5 14:32
15287
原文:Android Bug Superior to Master Key
出处:http://www.saurik.com/id/18
翻译:Hendy So

今年的早些时候,Bluebox Security宣称发现了一个Android应用程序签名方式的漏洞,它使应用程序的原始开发者放到安装包里的内容(比如代码)可以被修改(这可能导致被恶意程序利用)而不需要校验它的签名。

这个bug在2013黑帽大会上被揭开。由于安全社区对它的大量关注,这个bug很快就在安全圈里以及博客和论坛被找到。现在,这个bug已经完全公开了,CyanongenMod,一个开源的Android ROM,也发布了补丁。

这个bug在媒体上被称为Master Key。因为它可以使你很容易地使用开发者的密钥签名自己的代码。这个bug在LA TimesTechCrunch上有完整的介绍。

在我的前一篇文章中,我对这个bug有详细的文档,并且提供了一个方法来利用此漏洞,可能可以在所有的设备上使用,而且不需要在原始应用程序安装包中满足一些特定的条件(比如需要某些文件)。

当大家都把注意力集中在Master Key时,在同样的地方(Hendy注:指与Master Key漏洞相同的地方,即解析校验apk的地方)AOSP发布了一个补丁,显然它可以(与Master Key一样)有类似的利用方法。安卓安全小分队发表了一篇文章详细解析了这个漏洞。H-Online有相应的英文版本

之后就有不少有关这个bug的文章,都在试验这个漏洞技术。结果得出来的结论是这个漏洞比Master Key的破坏力要小一些。因为它需要原始的签名包必须满足一些非常苛刻及不太可能满足的条件。

在本文当中,我会给出利用这个bug的几种不同的技术,并且没有什么限制。实际上,下面讲到的第二个方法甚至比Master Key适用面更广。它可以使任意复杂的安装包从任意已经签名的包中获取签名。最后,我会叙述这个bug的历史,看看它是在什么时候被发现的。

对此技术的具体实现是Cydia Impactor的一部分,它可以用来在有漏洞的设备获取系统访问权限。Cydia Impactor是我写的一个用来管理手机和破解漏洞的一个工具。它现在可以在破解前自动检测这两个漏洞哪个是用以用的。

负的extra数据
在介绍这个新技术之前,我快速地解释一下现在已有的一个方案,以使得可以更清楚地看到它的局限。这里,我假设大家已经对前面讲到的Master Key漏洞很熟悉了,这个漏洞是基于Android对unzip有多份不同的实现——一份是java实现的,用于检验签名,一份是C++实现的用于解压文件。我前一篇文章详细介绍了这个bug及利用方法。

在这个新的漏洞中,我们来看一下文件的本地文件头(Hendy注:本地文件头是指保存到文件本身的,即文件格式的一部分)——包含文件的meta data(元数据),文件如何存储的信息,文件名以及文件的内容(可能被压缩)。另外,有一个可变长度的额外数据字段以支持文件格式的扩展。

           +---------+
           | Header |
           | Name   |
           +---------+
           | Extra   |
           +---------+
           | Data    |
           +---------+

在代码的底层实现中的错误在于zip文件中的很多值是以有符号的16位数字来读取的,而不是用无符号的16位数字。由于数字二进制补码的格式,这会导致大于32767的字段值会变成负数。65535会变成-1。

导致这个问题的原因在于写这段代码的开发者使用了DataInputStream来解析文件。调用它的readShort方法来获取16位数字。这个方法在java中是返回有符号短整型的。而在java语言中,所有整型都是有符号的。下面是读取extra数据长度的代码:

DataInputStream is = new DataInputStream(rafstrm);
int localExtraLenOrWhatever =
    Short.reverseBytes(is.readShort());

(顺便提一下,我通常会对代码版本进行一些修改,比如去掉不相关的逻辑,省略明显的类型转换以及调整代码格式等。我不会改变实际的标识符,那个localExtraLenOrWhatever显然就是Google里面的某位开发者给变量的命名)。

重叠的内容
要计算文件中压缩数据的起始位置,代码将文件头的位置加上文件名的长度以及extra区域的长度。如果extra区域的长度是负数(并不会被检测),那么加法计算(现在变成了减法)的结果就是要读取的数据的位置。

由于偏移量是负数,这会导致偏移回到文件头里面,从而导致与其它数据的冲突。安卓安全小分队文章中提到的攻击方法实际上非常巧妙并且基于一个非常幸运的巧合:在大部分Android安装包里都有的关键文件classes.dex刚好以dex开头(Hendy注:指文件内容以dex三个ASCII字符开头,参看安卓安全小分队的文章)。

这种攻击方法将extra字段的长度设为65533,当java读取后会变成-3。这会使得文件数据(Hendy注:压缩数据)的起始位置从文件名(Hendy注:classes.dex)往回移动三个字符,这正好仍被判断为一个合法的dex文件,因为classes.dex最后三位“dex”正是dex文件的开始三个字节。如果这个文件是stored形式(即未压缩。这是zip文件里文件存在的一种方式,对于已经压缩的文件,通常就是stored方式——Hendy注),那么它就是合法的。

修改后的文件就可以在文件名后面放长度为65533字节的内容了。只要原始文件的长度不大于64K,那么它就可以填充在文件头和修改的数据之间。在下图中,我给出了从C++的视角(正确的情况)和Java视角看到的重叠了的情况:

  C++  Header    Name     64k Extra    Data
      +------>+--------->+--------->+-------->
      size=10|classes.dex\035\A* ...dex\035\B*
      +------>+--------->
 Java  Header    Name <-+
                     /+-------->
           (-3) Extra    Data 


重大的局限
可以看出,这种破解方法有着诸多要求,这使得对于随便一个文件很难用到。在Sophos(一个有一款非常流行的反病毒软件的安全公司)写的一篇叫剖析另一个Android漏洞——中国研究人员声称发现新的绕过代码校验的漏洞(Anatomy of another Android hole – Chinese researchers claim new code verification bypass),他们给出了详细的限制。

因此,extra字段漏洞并不如master key bug通用。特别是,所有能被这种方式攻击的APK在解压的时候必须以classes.dex开头并且大小小于65536字节。这使得应用范围大为缩小,甚至可以说小得可怜:比如,从我个人的android手机里提取出来的96个apk文件当中,有75个是不能被这个漏洞攻击的。

一周以前,Android Police在一篇名为Second “Master Key” Style APK Exploit Is Reveald Just Two Days After Original Goes Public, Already Patched By Google的文章有类似的描述。

幸运的是,这种攻击方式有一些局限性。首先,不像Master Key漏洞,这种方式只能替换classes.dex这个文件,并且只有当原始的classes.dex小于64K时。其次,构造修改的apk的过程需要更加精细,并且需要你对文件结构有完全的理解。

Pau Olive(bug#8219321的验证原型开发者)在Twritter上说:很难找到一个包含有classes.dex并且使用系统签名的apk(在我前一篇文章发表之前,大家都把注意力集中到了classes.dex上面),现在并成了要找到那些小于64KB的classes.dex。后来,他提到,他在Motorola手机上面的确找到了一个

然而,这还不是唯一的难点,有一个其他人都没有提到的问题是,由于目标文件的大小保存在本地的文件头里,原始文件与替换后的文件大小必须是一样的。这使得构造有效数据(payload,指攻击代码)本身成为一种限制(难点),比如字节对齐,内部文件结构等。

尽管有这些限制,这个bug还是受到了技续地青睐,因为它的发现要迟得多,而bug#8219321已经被修复了。但bug#9695860还没有修复。Android Police和Sophos都提到对这个bug的修复极有可能会推迟。

有一些不好的消息,由于(这个漏洞的)修复非常新,还不可能推广到所有的设备,包括Nexus设备以及三星S4、HTC One的Google Play版本。

当然,Google最近宣称,对漏洞报告的响应,已经从之前的60天接受到现在的7天。尽管Google这次响应确实非常快,这两个漏洞都修复了,它的效率值得表扬。但它的修复并不能应用到所有的android手机。剩下的我们只能看Mountain View(Hendy注:Google总部所在地)依靠它与众多的手机厂商的协议来推送固件更新来修复“extra field”及“master key”漏洞。因为这两个漏洞都与Android平台核心的应用校验有关。

更加极端的偏移
我这里首先介绍的第一个新技术是有关将此漏洞应用到其它文件上,而不仅仅是classes.dex。方法是使用一个更大的负数的偏移量,将文件内容改到本地文件头之前。并没有要求本地文件头之前不能留空白。

          C++  Header    Name      32k Extra     Data
              +------>+--------->+----------->+-------->
dex\035\A* ...size=10|classes.dex PADDING ... dex\035\B*
              +------>+--------->
         Java  Header    Name
<-------------------------------+
+-------->      (-32k) Extra
   Data 


不再需要有前缀重叠的要求(Hendy注:指“dex”三个字符),代价仅仅是压缩我们的大小到32K。这样一来,这个bug就变得很容易使用了。特别是,它允许我们使用在我前一篇有关Master Key的文章中讲到的AndroidManifest.xml文件,这是一个很小的,并且在每个包中都存在的文件,这样就可以使用Dalvik调试技术了。

即便如此,我之前描述的使用调试器来攻击方式仅适用于从设备外部通过usb线来攻击。在设备上的其它应用程序是不能访问调试器的。”scary”恶意软件的风险包括替换其它文件,特别是代码(classes.dex或JNI库)

无论如休,这种技术仍然比Master Key局限更大:虽然我们现在可以替换任意文件,我们仍然只能替换之前已经存在的文件,并且我们现在有着文件大小不能超过32K的限制,还有我们替换的文件要与原始文件的大小保持一致。

中心目录
不过,我们可以做得更好。看一下Google发布的原始补丁,读着可能注意到有两个地方读取extra长度字段。这两个地方都犯了同样的错误,即使用了16位的有符号整数。当人们都关注其中一个地方时,另一个其实更加有趣。

        为了测试第二个错误,我们需要将我们的注意力转到zip文件格式的另一个部分——中心目录。这是个索引区,zip文件解析器可以用来快速找到zip文件中保存的文件。它的位置在文件的尾部。每个中心目录入口都指向本地文件区。
       
       +-> +---------+   ^ ^
       |   | Detail      |   |   |
       |   | Local       |  -/   |
       |   | Name      |       |
       |   | Comment|       |
       |   | Extra       |       |
       |   +------------+      |
       |   +------------+      /
       |   | Detail      |     /
       |   | Local       |   -/
       |   | Name      |
       |   | Comment|
       |   | Extra       |
       |   +------------+
       |       ...
       |   +------------+
       |   | Magic       | magic
       |   | Number   |    ^
       +-- | Start      |     | find
           | Comment |     | scan
           +------------+     |  up
               EOF	 


        顺便提一句,zip文件解析器找到这个文件头(Hendy注:中心目录结构体)的方法是使用紧跟在中心目录后面的中心目录结束记录(end of central directory,EOCD)。中心目录结束记录中有一个字段指向了索引区的开始。尽管这个必须通过从文件尾部向前扫描来找到,因为在文件的末尾保存有一个变长的文件描述(comment)。
        这非常有趣,因为Google还使用了zip文件来保存OTA固件升级,使了用一个整个文件的签名方案来保护他们,然后把签名保存在文件尾部的comment部分。为了避免扫描文件,Google将签名的大小保存在了接下来的部分。
然而,由于Google使用了现成的zip实现来访问zip文件,我们可以伪造签名的长度(将它声明得很大),然后将一个小的zip文件保存到后面,以允许它拿到一个签名的固件升级并且以这些新的bug相似的方式替换它里面的内容。这就是Motorola Droid的root方式。详细信息建议参看这篇文章。

索引区同样保存有额外数据。如果我们看一下Android里的C++ zip文件库,我们可以看到为了跳转到下一个字段,它使用了当前入口的偏移加上目录入口的长度以及三个变长字段:name、comment及extra。它们都是无符号值。

unsigned int fileNameLen, extraLen, commentLen, hash;

fileNameLen = get2LE(ptr + kCDENameLen);
extraLen = get2LE(ptr + kCDEExtraLen);
commentLen = get2LE(ptr + kCDECommentLen);

hash = computeHash(ptr + kCDELen, fileNameLen);
addToHash(ptr + kCDELen, fileNameLen, hash);

ptr += kCDELen + fileNameLen + extraLen + commentLen;

压缩(Clamped)Extra数据
接下来我们看一下java代码,我们再次看到了它使用了readShort读取16位的有符号的值并保存到一个有符号的整型中。然而,不像前面解析本地文件头的代码,它在解析到中心目录负的extra数据长度时,它会简单地认为它是非法地并且跳过它。

nameLength = it.readShort();
int extraLength = it.readShort();
int commentLength = it.readShort();

byte[] nameBytes = new byte[nameLength];
Streams.readFully(in, nameBytes, 0, nameBytes.length);
name = new String(nameBytes, 0,
    nameBytes.length, Charsets.UTF_8);

if (commentLength > 0) {
    byte[] commentBytes = new byte[commentLength];
    Streams.readFully(in, commentBytes, 0, commentLength);
    comment = new String(commentBytes, 0,
        commentBytes.length, Charsets.UTF_8);
}

if (extraLength > 0) {
    extra = new byte[extraLength];
    Streams.readFully(in, extra, 0, extraLength);
}

这意味着我们不再需要担心往文件前面移动这种不好利用和行为:如果你给extra长度字段一个非常大的值,C++代码会接受它,而java代码会把它压缩为0。这使得我们有机会让java和c解析器用两种不同的方式来解析文件。

唯一的限制是索引表中入口的数量保存在索引区的尾部,并且两种解析器都会在处理文件时读取它:两种实现都使用了一个for循环来读取那么多的文件。因此,C++和Java版的中心目录必须有相同数量的入口。

简单的技术
为了演示对文件这样的修改,我打算使用ASCII形式的图表。图表中的方框表示中心目录里的入口。Name字段假设保存在方框里面。箭头表示由于extra数据长度字段引起的移动。

左边是C++逻辑的正确解析,右边是Java视角看到的情况。每个入口都有一个斜杠、反斜杠或X,用来表示谁可以看到这个文件。反斜杠表示C++,正斜杠表示Java,X表示两者都可以看到。

我们看的第一个技巧非常简单:将列表中的最后一个入口分成两条独立的记录,一个给C++,另一个给Java。我们通过把extra数据长度字段设为一个非常大的值,Java将会忽略它,然后Java会解析对于C++来说是extra数据的中心目录。

       C++     +-+     Java
           +-- |X| --+
           |   +-+   | (0)
           |   +-+ <-+
      >32k |   |/|
           |   +-+
           |   PAD
           +-> +-+
               |\|
               +-+
               EOF 


这个技巧可以扩展至多个入口:因为64K是一个很大的空间(一个中心目录入口只有46字节加入文件名长度),我们接下来只需要对每条链条继续正常处理,就可将两个完全不同的zip文件合并在一起,而只共享其中一个文件。

       C++     +-+     Java
           +-- |X| --+
           |   +-+   | (0)
           |   +-+ <-+
           |   |/| --+
           |   +-+   | 0
           |   +-+ <-+
      >32k |   |/| --+
           |   +-+   | 0
           |   +-+ <-+
           |   |/|
           |   +-+
           |   PAD
           +-> +-+
           +-- |\|
         0 |   +-+
           +-> +-+
           +-- |\|
         0 |   +-+
           +-> +-+
               |\|
               +-+
               EOF 


高级交叉
为了完整性起见,我们来看一下如果我们有非常多的文件,并且原始zip文件中这些文件名的长度总和加上每个文件的46字节的头部总和大于C++解析器能够跳过的64K的extra数据长度。在这种情况下我们该怎么办。解决方案是将索引表交叉。

一种处理方法是重新同步共享的文件。只要Java看到的记录比C++看到的记录长,一个32K(32K是会被java压缩为0的边界)的偏移就会使Java使用跳过比32K小一点点(通过文件名的差异)的extra数据。

       C++     +-+     Java
           +-- |X| --+
           |   +-+   | (0)
           |   +-+ <-+
           |   |/| 
       32k |   | | <- long name
           |   | | --+
           |   +-+   |
           |   PAD   |
           +-> +-+   | 31.9k
           +-- |\| <-|-- short name
           |   +-+   |
           +-> +-+ <-+
               |X|
               ...  


这种方式仍然不好用,因为它对每个zip文件中的文件名长度有严格的限制。如果我们将文件每4个分成一组(2个给java,2个给c++),就可以通过使java看到的文件远离被他们分隔出来的C++跳过的区域来解决这个问题。

       C++     +-+     Java
           +-- |X| --+
           |   +-+   | (0)
           |   +-+ <-+
           |   |/| --+
           |   +-+   |
           |   PAD   |
           |   PAD   | 25k
       50k |   PAD   |
           |   PAD   |
           |   +-+ <-+
           |   |/| --+
           |   +-+   |
           |   PAD   |
           |   PAD   |
           +-> +-+   |
           +-- |\|   | 25k
         0 |   +-+   |
           +-> +-+   |
           +-- |\|   |
         0 |   +-+   |
           +-> +-+ <-+
               |X|
               ...


当我花了很多的时间来画这些ASCII图表时,我意识到(并不是立即就意识到的)通过使用可变长度的comment字段,我们也可以使用一对记录达到前一个例子相同的效果:comment与extra数据长度分隔出来的空间。

       C++     +-+     Java
           +-- |X| --+
           |   +-+   | 0 (0)
           |   +-+ <-+
           |   |/| --+
     0 50k |   +-+   |
           |   PAD   |
           |   PAD   |
           |   PAD   | 25k 25k
           +-> +-+   |
           +-- |\|   |
       0 0 |   +-+   |
           +-> +-+ <-+
               |X|
               ... 


使用comment字段也可以使我们的交叉形式简单得多,它不需要重新同步。我们使用第一条记录来产生两个中心目录,然后在两个列表中交替进行,使每条记录都跳过相应的另一种实现看到的相应的记录。

       C++     +-+     Java
           +-- |X| --+
           |   +-+   |
           |   PAD   |
     50k 0 |   PAD   | 24.975k 24.975k
           |   PAD   |
           |   PAD   |
           |   +-+ <-+
           |   |/| --+
           |   +-+   |
           +-> +-+   | size of
           +-- |\| <-|-- next
           |   +-+   |
   size of |   +-+ <-+
    next --|-> |/| --+
           |   +-+   |
           +-> +-+   | size of
           +-- |\| <-|-- next
           |   +-+   |
   size of |   +-+ <-+
    next --|-> | | --+
           |   ...   | 


跳过入口
现在我们还有两个限制——我们必须在两个列表之间共享第一个文件,同时我们在两个版本的中心目录里必须有相同数量的文件。当然,除此之外,Java版本的索引表必须与原始文件完全一致(因为它们是签名的)。

尝试使用基于前一个bug(Hendy注:Master Key)的技巧可能还是有一定吸引力的。但除了那个bug看起来比较低级以外,在很多设备上那个bug已经被修复了,现在这个bug还是可以利用的。我们现在不能使用相同的文件名来解决这个问题——zip解析器会拒绝它们。

谢天谢地,现在有一种可以加到zip文件中的入口,它不需要被签名校验,因为它们没有内容可以签名——它们就是目录入口(它同样具有不同的实现处理不相同的特性)。

Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
    final JarEntry je = entries.nextElement();
    if (je.isDirectory()) continue;

    final String name = je.getName();
    if (name.startsWith("META-INF/"))
        continue;

    final Certificate[] localCerts =
        loadCertificates(jarFile, je, readBuffer);

创建一个目录非常容易——所需要做的只是在文件名的后面加一个斜杠。同样的代码会跳过META-INF目录下的文件,这些文件是用作签名校验系统的一部分。因为这个目录级别很高,我还没有试过使用META-INF目录来隐藏文件。

public boolean isDirectory() {
    return name.charAt(name.length() - 1) == '/';
}

历史沿革
当我在Android 2.3上测试我对这个漏洞破解实现时,我发现它用不了——老版本上正确处理了文件的签名,但现在这种处理方式被改了。出于好奇,我看了一下libcore的修改历史,可以确定这个bug是Google在2010的时候引入的。

Android的一个设计思想是他们如何得到一些标准库。他们没有使用GNU Classpath的实现,而选择了Apach Harmony(就我理解,很大程序上是因为license的原因)。Apache Harmony在2011年已经停止更新了——现在Google自己为Android维护着一份独立演化的版本。

结果就是,Harmony的ZipFile的原始实现是用C写的。通过JNI,C代码创建了每个ZipEntry对象。在两层代码之间传递数据显然需要大量的缓冲区拷贝,这在Android手机平台上会占用太多的内存。因此,Google用Java重写了ZipFile。

最早的ZipFile的Java实现并没有此bug,它小心地处理了符号的问题。实际上,写这个代码的人已经写了他们自己的实现来特殊处理读取无符号的小头(little-endian)值。它提供了readShortLE(返回int)和readIntLE(返回long)。

然而,他们认为对于中心目录头的每个字段都调用会比较慢(这个读操作即使在需要从zip文件中解出一个小文件时也需要进行),因此他们修改了读取头的逻辑,改为一次读取然后在解析的时候进行位操作(这是正确的)。

/*
* We're seeing performance issues when we call
* readShortLE and readIntLE, so we're going to
* read the entire header at once and then parse
* the results out without using any function calls.
* Uglier, but should be much faster.
*
* Note that some lines look a bit different, because
* the corresponding fields or locals are long and
* so we need to do & 0xffffffffl to avoid problems
翻译:我们看到了调用readShortLE和readIntLE的性能问题。因此我们将一次读取整个头部,然后在解析时不需要进行任何函数调用。不够优雅,但应该快得多。
注意有些行看起来会有些不同,因为对应的字段或变量是long型,因此我们需要执行“& 0xffffffffl”操作以避免由有符号扩展引起的问题。

* induced by sign extension.
*/
nameLen = (hdrBuf[28] & 0xff) |
         ((hdrBuf[29] & 0xff) << 8);
int extraLen = (hdrBuf[30] & 0xff) |
              ((hdrBuf[31] & 0xff) << 8);
int commentLen = (hdrBuf[32] & 0xff) |
                ((hdrBuf[33] & 0xff) << 8);

在2009年Harmony还没有关闭之前,Google向Harmony提交了最原始代码,这个项目后来就完全由Google来主持了,目的就是为了Android。我们因此可以在Harmony的bug tracker上找到作为HARMONY-6346问题附件的补丁,以及讨论。

一年之后,在2010年晚些时候,在项目过度(到Google)后,我们可以在Google的代码仓库里找到引入这个bug的补丁。这个补丁是为了修改Android bug#3181430的,显然这会导致设置了CRC的高位时有符号扩展的bug(我从一个相关的测试用例的评论中找到了这个描述)。

这个代码在使用int/long时非常危险。我不能只将字段改为int,因为它们似乎使用-1L来表示“未设置(unset)”,然而它仍然接受整个int取值范围的值(包括-1)。我们不得不查看zip的规范以确认是否是这样的。但目前来看,我们只能去避免使用有符号扩展。

我们现在知道,这个补丁引入了更多的有符号扩展问题。我也确信上面关于在为什么代码中从一开始就使用int和long这个问题上的理解是有误的——int字段实际上是用来保存16位数字(对于long,是32位),因此,上面说的“整个int取值范围”是没有用到的。

修复这个Bug
上面这些bug对于普通的用户来说有多大的威胁还是个问号——大部分用户得益于使用大的应用分发渠道,比如Google Play商店,(我们认为)它可以扫描这些漏洞以确保它们不是恶意软件。

然而,在几天前Symantec发表的一篇文章中,说到在几个常用的市场上已经发现了一个中国开发者发布的利用了Master Key漏洞的恶意软件。他们提供了应用的截屏以及代码都开了些什么的详细信息。

我们在第三方应用市场上已经发现了4个由同一个攻击者制作的受感染的Android应用。这些app是一个非常流行的新闻app、一个街机游戏,一个纸牌游戏以及一个博彩app。所有这些app都是给中文用户设计的。

因此可以理解有些人可能更倾向于使用本地的修复来保护。除此之外,我认为像这样详细地给出漏洞原理而不提供可能的解决方案(比如给用户一个hotfix)是不负责任的。最后,我想给出这些问题是如何修复的,以及更进一步事情是如何工作的。

在我的前一篇文章中,我建议用户使用第三方的产品层面的修复。如果没有的话,我提供了一个可以修复第二种bug的补丁。然而,除了之前的警告之外,在研究这里的第二篇文章时,我发现ReKay(我重点推荐的)并没有字修复bug#9695860(它的网站并没有明确地说明这一点)。

现在我将提供一个可以完全修改这两个bug的工具。我现在可以告诉那些希望修复的用户在Backport(这个名字是我放修复补丁的项目名字,意思是从Android的后续版本中“反向移植”的)就可以找到补丁。我本文的真正的目标是为了解释这个补丁是如何工作的。

在我发表这篇文章的几分钟后,Collin Muliner(他是ReKey的作者之一)联系了我,他刚发布了ReKey的新版本(在我发表这篇文章之前)。但我要说的是,它还是不能保护#9695860 bug。但我可以想像,在今后将会有可以完全修复这个bug的更新。假设真是这样的话,我将会再次推荐重点ReKey给那些希望保护自己免受此bug危害的用户(所以,用户们应该给他们一些时间)。

Substrate扩展
要为现有设备修复这个bug,我们将使用Substrate——我为iOS以及Android提供的代码修改平台。开发者如果想要了解更多有关Substrate的东西,可以浏览这个网站,那里有完整的API文档。不过一些模板文件是非常简单的。

        public class Hook {
    public static void initialize() {
               ...
           }
}

第一个我们需要修改的部分是读取本地文件头的代码。它在一个叫getInputStream的方法中。它的参数是ZipEntry,返回值是用来读取内容的InputStream。我们的目标是检查要读取的字段,然后调用原始的实现以读取文件。

需要注意的是,我们的做法不会替换整个方法,因为可能有一些其它的基于hook的方法已经修复了这个bug——使用替换还不是hook补丁的方式会破坏那些可能依赖于这个类的行为。因此避免使用这种hook很重要。

为了访问文件头数据,我们使用ZipFile的mRaf字段,它是个RandomAccessFile对象。RandomAccessFile是一个导出了一些像readShort等非常有用的方法的类。对mRaf的访问必须同步,因为调用者通常会先seek到它们感兴趣的位置,但扫描文件会改变它的状态。我们会检查两个修补的字段。

要替换方法的内容,我们使用Substrate的MS.hookMethod方法。它允许我们重定向任何方法的实现。这个API使用一个叫invoked的functor,在我们提供的代码中,我们使用invoke来调用原始的实现。参数都被封装在一个数组里面。

在实际校验过程中,我们会拒绝那些符号位是1的导致解析错误的字段的zip文件(因为之前版本的Android解析这种包会失败,这是正确的。后面移除这样的逻辑是个损失。新的zip文件在老的设备上会失败可以明显说明这个问题)。因为我们读取的值是小头(little-endian)的,我们必须使用0x0080而不是0x8000来检查符号位。

final Field raf = ZipFile.class.
   getDeclaredField("mRaf");
raf.setAccessible(true);

final Field local = ZipEntry.class.
   getDeclaredField("mLocalHeaderRelOffset");
local.setAccessible(true);

Method getInputStream = ZipFile.class.
   getDeclaredMethod("getInputStream", ZipEntry.class);

MS.hookMethod(ZipFile.class, getInputStream,
   new MS.MethodAlteration<ZipFile, InputStream>() {
      public InputStream invoked(
         ZipFile thiz, Object... args
      )
         throws Throwable
      {
         ZipEntry entry = (ZipEntry) args[0];

         RandomAccessFile raf =
            (RandomAccessFile) raf.get(thiz);
         synchronized (raf) {
            raf.seek(local.getLong(entry));

            raf.skipBytes(6);
            if ((raf.readShort() & 0x0080) != 0)
               throw new ZipException();

            raf.skipBytes(20);
            if ((raf.readShort() & 0x0080) != 0)
               throw new ZipException();
         }

         return invoke(thiz, args);
      }
   }
);

我们第二个要hook的类是ZipEntry,它的构造方法会读取中心目录,将文件指针移动到下一个头部的起始位置。这个方法有两个参数,一个byte数组是用来保存头部内容的(一个老的优化做法),还有一个InputStream指向它的位置。

由于这个类的作用是保存头部信息,因此它的临时不可用是没有关系的。我们因此可以在它执行完后来校验文件是否完全。这使我们可以使用头部内容的缓冲区而不会弄乱传给我们的InputStream的位置。

恰巧传给这个方法的InputStream实例是BufferedInputStream,它可以保证标记和重置(mark and reset)的实现,这可以用来往前扫描校验,然后返回头部的最顶端。这样做可以使我们在调用这个方法前进行校验。

不过,这样做依赖于InputStream的底层实现。尽管它恰好是对的,但这并不是一个非常明智的设想。因为其它人可能会修改这个类的行为,类的实现(后面)也可能会改变。但是我们可以确认的是类型签名(Hendy注:指java的类型签名,类似Ljava/lang/String;这样的)是不会变的。

我们要校验的字段在数据块中,因此我使用了for循环来使代码比较小。需要特别注意的是,代码中没有校验time和date字段,这些字段总是溢出的。不过,它们仅仅是数据,并不会影响文件的读取和校验。

Constructor init = ZipEntry.class.
   getDeclaredConstructor(byte[].class, InputStream.class);

MS.hookMethod(ZipEntry.class, init,
   new MS.MethodAlteration<ZipEntry, Void>() {
      public Void invoked(ZipEntry thiz, Object... args)
         throws Throwable
      {
         byte[] header = (byte[]) args[0];

         invoke(thiz, args);

         DataInputStream in = new DataInputStream(
            new ByteArrayInputStream(header));

         in.skip(8);
         for (int i = 0; i != 2; ++i)
            if ((in.readShort() & 0x0080) != 0)
               throw new ZipException();

         in.skip(16);
         for (int i = 0; i != 3; ++i)
            if ((in.readShort() & 0x0080) != 0)
               throw new ZipException();

         return null;
      }
   }
);

这个Substrate扩展的完整源码可以从它的git仓库克隆:git://git.saurik.com/backport.git,或者也可以使用我正在使用的Gitweb在线仓库阅读。(这里有Hook.java的直链)。用户如果只想安装apk可以从Cydia Gallery(在Substrate里面)中得到。

完整的绕过签名描述
现在我们有了能力在一个zip文件中创建两个中心目录。两个中心目录必须以同样的文件开头,但这个文件可以是一个目录入口。两个zip文件必须有相同数量的文件,但小的那个可以使用目录入口来匹配大的那个。

除此之外,两个中心目录是完全独立的,可以有完全不相关的文件名,可以指向zip文件中完全不相关的如前所述的本地数据区入口。这意味着我们可以合并任意复杂内容的zip文件到一起。

我想现在已经很清楚了,这是一个比现在广泛知道的Master Key漏洞强大得多的漏洞。比较而言,前者仅允许我们替换原始已签名的zip中已经存在的内容,比如,我们不能在一个不存在classes.dex的zip文件中添加一个classes.dex。

不过Master Key bug也有好处,它更容易实现(在我上一篇文章中已经演示了,可以使用zip和sed),并且支持Android 2.3。因此在将来的一段时间里它都可以继续使用。然而,因为应用里面通常会包含大量的资源文件,要制作真正地偷签名的伪装包就需要这种新的,更加强大的漏洞。

[培训]内核驱动高级班,冲击BAT一流互联网大厂工 作,每周日13:00-18:00直播授课

收藏
点赞1
打赏
分享
最新回复 (8)
雪    币: 341
活跃值: (133)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
地狱怪客 2 2013-12-5 16:54
2
0
我是一楼啊。。。。
雪    币: 185
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
whnet 2013-12-5 16:58
3
0
这哥们是跟android master key 瞌上了
雪    币: 269
活跃值: (25)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
ReturnsMe 2 2013-12-5 18:47
4
0
被楼主骗进来了。。。翻译还是支持
雪    币: 12299
活跃值: (3390)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
xJJuno 2013-12-5 19:34
5
0
貌似腾讯翻译过?
雪    币: 435
活跃值: (172)
能力值: ( LV13,RANK:280 )
在线值:
发帖
回帖
粉丝
火翼[CCG] 6 2013-12-6 08:19
6
0
百度上也有
http://safe.baidu.com/
雪    币: 213
活跃值: (147)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
jasonzhou 2013-12-6 16:47
7
0
百度上分析的应该是最早的,现在还有人在翻译,要么是百度推广的有问题,要么是……
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Arcko 2013-12-7 15:19
8
0
路过,学习saurik大神
雪    币: 205
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
nightbaby 2013-12-11 11:54
9
0
虽然看不懂,但是还是感谢lz的分享
游客
登录 | 注册 方可回帖
返回