以UPX漏洞为例介绍整数溢出(基础篇)
作者:网友投稿 时间:2018-04-02 21:34
我发现Freebuf上没有整数溢出漏洞的基础介绍,所以这篇文章通过分析我刚刚发现的UPX源代码中的整数溢出漏洞,介绍一下C/C++整数溢出漏洞的原理、触发和修复方法。这篇文章暂不涉及如何利用整数溢出达到远程代码执行,UPX的漏洞只是一个拒绝服务漏洞。
0×01 整数溢出原理C/C++中的整数溢出基本原理非常简单,比如unsigned char number = 200 + 200;。最终number的值是400 mod 256=144。这是因为C++对于无符号整数(unsigned char, unsigned int等)溢出的处理是取模,导致的结果是两个整数相加,反而结果更小。C++中有符号整数溢出是未定义行为。下文中所有提到整数溢出,都指的是无符号整数溢出。整数溢出的利用一般都是用它来导致缓冲区溢出,进而利用缓冲区溢出技巧来代码执行、泄露内存或拒绝服务。
我认为对于文件解析一类的程序要特别注意整数溢出问题,因为有很多文件格式,它们的文件头中包含了长度、偏移信息。攻击者通过构造畸形文件可以直接控制这些信息,尝试触发整数溢出或其他缓冲区溢出漏洞。所以在写代码时我们需要关注的点有:第一,将整数运算的结果作为缓冲区长度分配内存;第二,将整数运算的结果作为偏移量读取内存。
对于第一点,比如这段代码:
size_t len = len1 + 0x40; char *buffer = new char[len]; buffer[0x10] = 'a';如果len1是攻击者可控的值,那么这里就存在整数溢出问题。假设是32位程序,攻击者选取len1 = 0xFFFFFFC1,那么len1+40等于1,所以buffer的长度为1。第三行,作者错误地假设了下标0×10一定会在buffer分配的内存区间内,但实际上这里发生了越界写入。
对于第二点,看这段代码:
char buffer[100]; unsigned char offset = getOffset(); // offset攻击者可控 if (40 + offset < sizeof(buffer)) { buffer[offset] = 0; }这里作者错误地假设了如果40 + offset这个index没有越界,则offset这个index也没有越界。但是如果我们取offset=255。则40 + offset = 39,那么我们就将buffer[255]这个越界地址写入了0。
0×02 分析UPX整数溢出漏洞这个漏洞是我最近找出来的UPX开源项目的漏洞。因为最近研究UPX,随手在CVE数据库里搜索了一下有没有UPX的漏洞,结果还真有,CVE-2017-15056。漏洞报告在 https://github.com/upx/upx/issues/128 。这是一个畸形文件导致内存越界读取漏洞,我看了一下修复的commit。从commit来看,修复并不完美,而且正好可以拿来讲整数溢出。
我们重点看commit中PackLinuxElf32::PackLinuxElf32help1函数中添加在250-256行的校验:

file_size是用户输入的ELF文件的大小,e_phoff, e_phnum, e_shoff, e_shnum都是ELF文件头部的字段。这些值我们可以通过构造畸形ELF文件来控制。显然这里作者在避免缓冲区越界读取问题,检测e_shoff + e_shnum * sizeof(Elf32_Shdr)这个偏移量是否依然在ELF文件大小之内。如果不在,就抛出异常,因为我们的缓冲区只有file_size这么大。作者想到了检查整数溢出,但是他的方法是把e_phoff和e_shoff从unsigned int转换成unsigned long。值得注意的是unsigned long的大小是:MSVC下永远是32位整数,gcc和clang下32位ELF就是32位整数,64位ELF就是64位整数。所以如果我们用32位UPX的话,(unsigned long)e_shoff + e_shnum * sizeof(Elf32_Shdr)是可以溢出的,只要e_shoff足够大,让它们的和大于或等于2^32,它的值就可以小于file_size。
往下:

268至272行针对e_type不是ET_DYN (shared object file)情形,268行的len就是之前258行校验的值,按上文说的len是溢出之后的值,它小于file_size,但是e_phoff很大。所以到272行phdri=e_phoff + file_image又发生整数溢出,phdri会小于file_image。所以如果接下来phdri被用于读取Elf32_Phdr结构体的值,那么读到的实际上是缓冲区file_image以外的值。但是随后发现phdri使用之前会检查e_phoff是否为0×40。所以这个缓冲区越界读取是触发不了的。
接下来274行以下针对e_type是ET_DYN情形,类似地,因为我们取e_shoff为一个接近2^32的值,如0xFFFFE000,这样shdri= (Elf32_Shdr *)(e_shoff + file_image);指向的就是file_image内存之前的位置。然后进入elf_find_section_type(Elf32_Shdr::SHT_DYNSYM)函数:




