前言:zip
压缩格式应用广泛,各个平台都有使用,Windows
平台使用来压缩文件,Android
平台使用来作为apk
文件的格式。由于zip
文件格式比较复杂,在解析zip
文件格式时,如果处理不当,可能导致一些有意思的逻辑漏洞,本篇文章将挑选有意思的漏洞进行解析。
一、文件扩展名欺骗漏洞
很早之前,国外安全研究人员爆料Winrar 4.x版本存在文件扩展名欺骗漏洞,黑客可以通过该漏洞诱骗受害者执行恶意程序。该漏洞的主要原理是:Winrar
在文件预览和解压缩显示文件名使用的是不同结构体的字段导致的。
1.1 zip格式文件的结构
在了解漏洞的原理前,先熟悉下zip格式的文件结构。
如果一个压缩包文件里有多个文件,可以认为每个文件都是被单独压缩,然后再拼成一起。
一个 ZIP
文件由三个部分组成:压缩源文件数据区+压缩源文件目录区+压缩源文件目录结束标志,如下图:
1)文件头(压缩源文件目录区)在文件末尾,即图1中的File Header
,记录了索引段的偏移、大小等等。
2)数据段(压缩源文件数据区)在文件开头,即图1中的Local Header
,记录了数据的一些基本信息,可以用来跟File Header
中记录的数据进行比较,保证数据的完整性。
3)Local Header
还包含了文件被压缩之后的存储区,即图1中的Data
区域。
4)图2和图3为Local Header
(图2中的ZIPFILERECORD
)和File Header
(图3中的ZIPDIRENTRY
)的数据对比,两者数据是一致的。
1.2 漏洞产生原因
Winrar
在文件预览的时候使用的是ZIPDIRENTRY
下面的deFileName
字段来显示文件名,解压缩的时候使用的是ZIPFILERECORD
下面的frFileName
字段来显示文件名。如果将deFileName
字段文件扩展名改成jpg
、gif
等图片的文件扩展名,可以欺骗用户运行恶意程序。
Winrar
文件预览示意图:
用户看到的是jpg
图片,打开的确实exe
文件,真坑啊!
Winrar
解压缩文件示意图:
解压缩之后显示的exe
,两处显示的不一样。
二、Android Master Key漏洞
之前,国外安全研究人员爆出第三个Android Master Key漏洞,该漏洞的主要原理是:android
在解析Zip
包时,没有校验ZipEntry
和Header
中的FileNameLength
是否一致。
2.1 zip文件格式的结构
在了解漏洞的原理前,还是先熟悉下zip
格式的文件结构。
如果一个压缩包文件里有多个文件,可以认为每个文件都是被单独压缩,然后再拼成一起。
一个 ZIP
文件由三个部分组成:压缩源文件数据区+压缩源文件目录区+压缩源文件目录结束标志,如图1所示:
1)文件头(压缩源文件目录区)在文件末尾,即图1中的File Header
,记录了索引段的偏移、大小等等。
2)数据段(压缩源文件数据区)在文件开头,即图1中的Local Header
,记录了数据的一些基本信息,可以用来跟File Header
中记录的数据进行比较,保证数据的完整性。
3)Local Header
还包含了文件被压缩之后的存储区,即图1中的Data
区域。
4)图2和图3为Local Header
(图2中的ZIPFILERECORD
)和File Header
(图3中的ZIPDIRENTRY
)的数据对比,两者数据是一致的。
2.2 漏洞产生原因
先来看一下是如何定位到Local Header
中的Data
数据:
off64_t dataOffset = localHdrOffset +
kLFHLen +
get2LE(lfhBuf + kLFHNameLen) +
Data
的偏移是通过Header
的起始偏移+Header
的大小(固定值)+Extra data
的大小+文件名的大小,如下图
回头看一下,java
在获取Data
偏移的处理,在读取Extra data
的长度的时候,它已经预存了文件名在FileHeader
中的长度。
// 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 =
漏洞就在这里产生了,如果Local Header
中的FileNameLength
被设成一个大数,并且FileName
的数据包含原来的数据,File Header
中的FileNameLength
长度不变,那么底层C++
运行和上层Java
运行就是不一样的流程。
C++ Header 64k Name Data
+--------> +----------------------> +---------->
length=64k classes.dex dex\035\A... dex\035\B...
+--------> +---------> +---------->
如上面所示,底层C++
的执行会读取64k的FileName
长度,而Java
层由于是读取File Header
中的数据,FileName
的长度依旧是11,于是Java
层校验签名通过,底层执行会执行恶意代码。