CTF-All-In-One/doc/3.3.2_integer_overflow.md
2017-08-17 23:31:30 +08:00

8.4 KiB
Raw Blame History

整数溢出

什么是整数溢出

简介

在 C 语言基础的章节中,我们介绍了 C 语言整数的基础知识,下面我们详细介绍整数的安全问题。

由于整数在内存里面保存在一个固定长度的空间内它能存储的最大值和最小值是固定的如果我们尝试去存储一个数而这个数又大于这个固定的最大值时就会导致整数溢出。x86-32 的数据模型是 ILP32即整数Int、长整数Long和指针Pointer都是 32 位。)

整数溢出的危害

如果一个整数用来计算一些敏感数值,如缓冲区大小或数值索引,就会产生潜在的危险。通常情况下,整数溢出并没有改写额外的内存,不会直接导致任意代码执行,但是它会导致栈溢出和堆溢出,而后两者都会导致任意代码执行。由于整数溢出出现之后,很难被立即察觉,比较难用一个有效的方法去判断是否出现或者可能出现整数溢出。

整数溢出

有符号整数上溢出

int i;
i = INT_MAX;  // 2 147 483 647
i++;
printf("i = %d\n", i);  // i = -2 147 483 648

有符号整数下溢出

i = INT_MIN;  // -2 147 483 648
i--;
printf("i = %d\n", i);  // i = 2 147 483 647

无符号数截断

涉及无符号数的计算永远不会溢出,如果计算结果无法用无符号数表示,就会发生截断。

加法截断:

0xffffffff + 0x00000001
= 0x0000000100000000 (long long)
= 0x00000000 (long)

乘法截断:

0x00123456 * 0x00654321
= 0x000007336BF94116 (long long)
= 0x6BF94116 (long)

整型提升和宽度溢出

整型提升是指当计算表达式中包含了不同宽度的操作数时,较小宽度的操作数会被提升到和较大操作数一样的宽度,然后再进行计算。

示例:源码

#include<stdio.h>
void main() {
    int l;  
    short s;
    char c;

    l = 0xabcddcba;
    s = l;
    c = l;

    printf("宽度溢出\n");
    printf("l = 0x%x (%d bits)\n", l, sizeof(l) * 8);
    printf("s = 0x%x (%d bits)\n", s, sizeof(s) * 8);
    printf("c = 0x%x (%d bits)\n", c, sizeof(c) * 8);

    printf("整型提升\n");
    printf("s + c = 0x%x (%d bits)\n", s+c, sizeof(s+c) * 8);
}
$ ./a.out
宽度溢出
l = 0xabcddcba (32 bits)
s = 0xffffdcba (16 bits)
c = 0xffffffba (8 bits)
整型提升
s + c = 0xffffdc74 (32 bits)

使用 gdb 查看反汇编代码:

gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x0000056d <+0>:	lea    ecx,[esp+0x4]
   0x00000571 <+4>:	and    esp,0xfffffff0
   0x00000574 <+7>:	push   DWORD PTR [ecx-0x4]
   0x00000577 <+10>:	push   ebp
   0x00000578 <+11>:	mov    ebp,esp
   0x0000057a <+13>:	push   ebx
   0x0000057b <+14>:	push   ecx
   0x0000057c <+15>:	sub    esp,0x10
   0x0000057f <+18>:	call   0x470 <__x86.get_pc_thunk.bx>
   0x00000584 <+23>:	add    ebx,0x1a7c
   0x0000058a <+29>:	mov    DWORD PTR [ebp-0xc],0xabcddcba
   0x00000591 <+36>:	mov    eax,DWORD PTR [ebp-0xc]
   0x00000594 <+39>:	mov    WORD PTR [ebp-0xe],ax
   0x00000598 <+43>:	mov    eax,DWORD PTR [ebp-0xc]
   0x0000059b <+46>:	mov    BYTE PTR [ebp-0xf],al
   0x0000059e <+49>:	sub    esp,0xc
   0x000005a1 <+52>:	lea    eax,[ebx-0x1940]
   0x000005a7 <+58>:	push   eax
   0x000005a8 <+59>:	call   0x400 <puts@plt>
   0x000005ad <+64>:	add    esp,0x10
   0x000005b0 <+67>:	sub    esp,0x4
   0x000005b3 <+70>:	push   0x20
   0x000005b5 <+72>:	push   DWORD PTR [ebp-0xc]
   0x000005b8 <+75>:	lea    eax,[ebx-0x1933]
   0x000005be <+81>:	push   eax
   0x000005bf <+82>:	call   0x3f0 <printf@plt>
   0x000005c4 <+87>:	add    esp,0x10
   0x000005c7 <+90>:	movsx  eax,WORD PTR [ebp-0xe]
   0x000005cb <+94>:	sub    esp,0x4
   0x000005ce <+97>:	push   0x10
   0x000005d0 <+99>:	push   eax
   0x000005d1 <+100>:	lea    eax,[ebx-0x191f]
   0x000005d7 <+106>:	push   eax
   0x000005d8 <+107>:	call   0x3f0 <printf@plt>
   0x000005dd <+112>:	add    esp,0x10
   0x000005e0 <+115>:	movsx  eax,BYTE PTR [ebp-0xf]
   0x000005e4 <+119>:	sub    esp,0x4
   0x000005e7 <+122>:	push   0x8
   0x000005e9 <+124>:	push   eax
   0x000005ea <+125>:	lea    eax,[ebx-0x190b]
   0x000005f0 <+131>:	push   eax
   0x000005f1 <+132>:	call   0x3f0 <printf@plt>
   0x000005f6 <+137>:	add    esp,0x10
   0x000005f9 <+140>:	sub    esp,0xc
   0x000005fc <+143>:	lea    eax,[ebx-0x18f7]
   0x00000602 <+149>:	push   eax
   0x00000603 <+150>:	call   0x400 <puts@plt>
   0x00000608 <+155>:	add    esp,0x10
   0x0000060b <+158>:	movsx  edx,WORD PTR [ebp-0xe]
   0x0000060f <+162>:	movsx  eax,BYTE PTR [ebp-0xf]
   0x00000613 <+166>:	add    eax,edx
   0x00000615 <+168>:	sub    esp,0x4
   0x00000618 <+171>:	push   0x20
   0x0000061a <+173>:	push   eax
   0x0000061b <+174>:	lea    eax,[ebx-0x18ea]
   0x00000621 <+180>:	push   eax
   0x00000622 <+181>:	call   0x3f0 <printf@plt>
   0x00000627 <+186>:	add    esp,0x10
   0x0000062a <+189>:	nop
   0x0000062b <+190>:	lea    esp,[ebp-0x8]
   0x0000062e <+193>:	pop    ecx
   0x0000062f <+194>:	pop    ebx
   0x00000630 <+195>:	pop    ebp
   0x00000631 <+196>:	lea    esp,[ecx-0x4]
   0x00000634 <+199>:	ret    
End of assembler dump.

在整数转换的过程中,有可能导致下面的错误:

  • 损失值:转换为值的大小不能表示的一种类型
  • 损失符号:从有符号类型转换为无符号类型,导致损失符号

漏洞多发函数

我们说过整数溢出要配合上其他类型的缺陷才能有用,下面的两个函数都有一个 size_t 类型的参数,常常被误用而产生整数溢出,接着就可能导致缓冲区溢出漏洞。

#include <string.h>

void *memcpy(void *dest, const void *src, size_t n);

memcpy() 函数将 src 所指向的字符串中以 src 地址开始的前 n 个字节复制到 dest 所指的数组中,并返回 dest

#include <string.h>

char *strncpy(char *dest, const char *src, size_t n);

strncpy() 函数从源 src 所指的内存地址的起始位置开始复制 n 个字节到目标 dest 所指的内存地址的起始位置中。

两个函数中都有一个类型为 size_t 的参数,它是无符号整型的 sizeof 运算符的结果。

typedef unsigned int size_t;

整数溢出示例

现在我们已经知道了整数溢出的原理和主要形式,下面我们先看几个简单示例,然后实际操作利用一个整数溢出漏洞。

示例

示例一,整数转换:

char buf[80];
void vulnerable() {
    int len = read_int_from_network();
    char *p = read_string_from_network();
    if (len > 80) {
        error("length too large: bad dog, no cookie for you!");
        return;
    }
    memcpy(buf, p, len);
}

这个例子的问题在于,如果攻击者给 len 赋于了一个负数,则可以绕过 if 语句的检测,而执行到 memcpy() 的时候,由于第三个参数是 size_t 类型,负数 len 会被转换为一个无符号整型,它可能是一个非常大的正数,从而复制了大量的内容到 buf 中,引发了缓冲区溢出。

示例二,溢出:

void vulnerable() {
    size_t len;
    char *buf;

    len = read_int_from_network();
    buf = malloc(len + 5);
    read(fd, buf, len);
    ...
}

这个例子看似避开了缓冲区溢出的问题,但是如果 len 过大,len+5 有可能发生溢出。比如说,在 x86-32 上,如果 len = 0xFFFFFFFF,则 len+5 = 0x00000004,这时 malloc() 只分配了 4 字节的内存区域,然后在里面写入大量的数据,缓冲区溢出也就发生了。

示例三,截断:

void main(int argc, char *argv[]) {
    unsigned short int total;
    total = strlen(argv[1]) + strlen(argv[2]) + 1;
    char *buf = (char *)malloc(total);
    strcpy(buf, argv[1]);
    strcat(buf, argv[2]);
    ...
}

这个例子接受两个字符串类型的参数并计算它们的总长度,程序分配足够的内存来存储拼接后的字符串。首先将第一个字符串参数复制到缓冲区中,然后将第二个参数连接到尾部。如果攻击者提供的两个字符串总长度无法用 total 表示,则会发生截断,从而导致后面的缓冲区溢出。

实战

看了上面的示例,我们来真正利用一个整数溢出漏洞。

CTF 中的整数溢出