前言:zip
压缩格式应用广泛,各个平台都有使用,Windows
平台使用来压缩文件,Android
平台使用来作为apk
文件的格式。由于zip
文件格式比较复杂,在解析zip
文件格式时,如果处理不当,可能导致一些有意思的逻辑漏洞,本篇文章将挑选有意思的漏洞进行解析。
很早之前,国外安全研究人员爆料Winrar 4.x版本存在文件扩展名欺骗漏洞,黑客可以通过该漏洞诱骗受害者执行恶意程序。该漏洞的主要原理是:Winrar
在文件预览和解压缩显示文件名使用的是不同结构体的字段导致的。
在了解漏洞的原理前,先熟悉下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
)的数据对比,两者数据是一致的。
Winrar
在文件预览的时候使用的是ZIPDIRENTRY
下面的deFileName
字段来显示文件名,解压缩的时候使用的是ZIPFILERECORD
下面的frFileName
字段来显示文件名。如果将deFileName
字段文件扩展名改成jpg
、gif
等图片的文件扩展名,可以欺骗用户运行恶意程序。
Winrar
文件预览示意图:
用户看到的是jpg
图片,打开的确实exe
文件,真坑啊!
Winrar
解压缩文件示意图:
解压缩之后显示的exe
,两处显示的不一样。
之前,国外安全研究人员爆出第三个Android Master Key漏洞,该漏洞的主要原理是:android
在解析Zip
包时,没有校验ZipEntry
和Header
中的FileNameLength
是否一致。
在了解漏洞的原理前,还是先熟悉下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
)的数据对比,两者数据是一致的。
先来看一下是如何定位到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
层校验签名通过,底层执行会执行恶意代码。