声明

本次教学节选于星梦安全团队在Bilibili上发表的视频url为
https://www.bilibili.com/video/BV1A54y1j7ZP
都为作者手打,如有错误请指出
如有侵权请联系本文作者
部分引用于知乎作者“关于编程哪些事

大家好 这里是很久都没有更新的W1cky 由于最近某些不可抗力原因 是在没时间更新 那么想着逞着这一天假期给你们说一下 整型溢出吧~

0x01 定义

首先 我们先看一下在CTFWiki上 整型溢出的定义;


在 C 语言中,整数的基本数据类型分为短整型 (short),整型 (int),长整型 (long),这三个数据类型还分为有符号和无符号,每种数据类型都有各自的大小范围,(因为数据类型的大小范围是编译器决定的,所以之后所述都默认是 64 位下使用 gcc-5.4)........
当程序中的数据超过其数据类型的范围,则会造成溢出,整数类型的溢出被称为整数溢出。

其实 我们在上一次的pwn2-sctf-2016 -- 题目解析中就学习过一些整型溢出的知识了 关于这个 我们再回顾一下

是一个看似普普通通的程序 但是 他对gets函数做出了限制 当gets大于32时 退出 只有gets小于32的时候 才可以继续执行 那么现在该怎么办呢?


其实 在上方我们可以大概知道整型溢出是什么意思 但是 今天我们会更加升入的了解 ----- 什么是整型溢出

一些基础

如果我们要学习整型溢出 我们首先需要学习补码,反码等知识 但是如果我们想要学习他们 我们必须首先
学习 机器数和真值的概念

机器数

一般指用二进制表达的数字 但是 一个不同的点就在于机器数是带符号的在计算机用机器数的最高位存放符号,正数为0,负数为1 而机器数往往表示的不是一个数字的真值

真值

真值很容易理解 就是除掉符号位的值


将带符号位的机器数对应的真正数值称为机器数的真值

例:0000 0001的真值 = +000 0001 = +1,1000 0001的真值 = –000 0001 = –1

真值

原码

举例都是在1字节长度的情况下

原码就是符号位加上真值的绝对值即用第一位表示符号,其余位表示值

那么
-1 = 100 0001
1 = 000 0001
原码是人脑最容易理解和计算的表示方式

反码

正数的反码就是它本身
负数的反码就是符号位不变 其他都取反 1=0 0=1
[+1] = [0000 0001]原= [0000 0001]反
[-1] = [1000 0001]原= [1111 1110]反

补码

补码的表示方法是:

正数的补码就是其本身;

负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后+1。(也即在反码的基础上+1)

[+1] = [0000 0001]原= [0000 0001]反= [0000 0001]补

[-1] = [1000 0001]原= [1111 1110]反= [1111 1111]补

对于负数,补码表示方式也是人脑无法直观看出其数值的。通常也需要转换成原码再计算其数值

有符号数和无符号数

了解完上方的知识后 我们对有符号数和无符号数就应该有一些了解了

对于有符号数 他的第一位永远是符号数 也就是说 无符号数是可以使用第一个数字的 也就是符号数的符号位置
那么 无符号数的范围就是 0 - 0x11111111 也就是255
但是 虽然符号数的少一位 但是它可以用来表示负数;

他们的范围就是
符号数:
-128 ############# 0 ############### 127
无符号数
0################################# 225
他们的大小都是225 但是一个在正数的方面比较大 一个虽然缩水 但是可以表达负数

16进制

1字节的情况下 无符号数最多是=1111 1111 也就是8个 在2(双字节)的情况下 就是 1111 1111 1111 1111(16)
那么 0b1111 1111 = 0xff 0b1111 1111 1111 1111 = 0xffff

0x02 整型溢出的原理


因为计算机底层指令是不区分有符号和无符号的,数据都是以二进制形式存在 (编译器的层面才对有符号和无符号进行区分,产生不同的汇编指令)

short int a;

a = a + 1;
//对应的汇编
movzx  eax, word ptr [rbp - 0x1c]
add    eax, 1
mov    word ptr [rbp - 0x1c], ax

unsigned short int b;

b = b + 1;
# assembly code
add    word ptr [rbp - 0x1a], 1

上溢出

由于在计算机底层不画分有符号数和无符号数 所以 整型溢出只对符号数有效 我的理解是 在符号数中 会有一位在正数总为0 所以我们就可以利用它把一个正数换成一个负数
安装ctfwiki中说 就是:


0x7fff (0b0111111111111111)表示的是 32767,但是 0x8000 (0b1000000000000000)表示的是 -32768,用数学表达式来表示就是在有符号短整型中 32767+1 == -32768

下溢出

下溢出则会上溢出相反 这个时候只有无符号数可以溢出 因为他们无法表示负数
于有符号来说 0 - 1 == -1 也就是 0 + 1 000 0000 0000 0001 没问题,但是对于无符号来说就成了 0 - 1 == 65535
0 - 1 = 11111 1111 1111 1111

0x03 举例

下面参考CTFwiki

在我见过的整数溢出的漏洞中,我认为可以总结为两种情况。

未限制范围

这种情况很好理解,比如有一个固定大小的桶,往里面倒水,如果你没有限制倒入多少水,那么水则会从桶中溢出来。

简单的写一个示例:

$ cat test.c
#include<stddef.h>
int main(void)
{
    int len;
    int data_len;
    int header_len;
    char *buf;

    header_len = 0x10;
    scanf("%uld", &data_len);

    len = data_len+header_len
    buf = malloc(len);
    read(0, buf, data_len);
    return 0;
}
$ gcc test.c
$ ./a.out
-1
asdfasfasdfasdfafasfasfasdfasdf
# gdb a.out
► 0x40066d <main+71>    call   malloc@plt <0x400500>
        size: 0xf

只申请 0x20 大小的堆,但是却能输入 0xffffffff 长度的数据,从整型溢出到堆溢出

错误的类型转换

即使正确的对变量进行约束,也仍然有可能出现整数溢出漏洞,我认为可以概括为错误的类型转换,如果继续细分下去,可以分为:

  1. 范围大的变量赋值给范围小的变量
$ cat test2.c
void check(int n)
{
    if (!n)
        printf("vuln");
    else
        printf("OK");
}

int main(void)
{
    long int a;

    scanf("%ld", &a);
    if (a == 0)
        printf("Bad");
    else
        check(a);
    return 0;
}
$ gcc test2.c
$ ./a.out
4294967296
vuln

上述代码就是一个范围大的变量 (长整型 a),传入 check 函数后变为范围小的变量 (整型变量 n),造成整数溢出的例子。

已经长整型的占有 8 byte 的内存空间,而整型只有 4 byte 的内存空间,所以当 long -> int,将会造成截断,只把长整型的低 4byte 的值传给整型变量。

在上述例子中,就是把 long: 0x100000000 -> int: 0x00000000

但是当范围更小的变量就能完全的把值传递给范围更大的变量,而不会造成数据丢失。

  1. 只做了单边限制

这种情况只针对有符号类型

$ cat test3.c
int main(void)
{
    int len, l;
    char buf[11];

    scanf("%d", &len);
    if (len < 10) {
        l = read(0, buf, len);
        *(buf+l) = 0;
        puts(buf);
    } else
        printf("Please len < 10");        
}
$ gcc test3.c
$ ./a.out
-1
aaaaaaaaaaaa
aaaaaaaaaaaa

从表面上看,我们对变量 len 进行了限制,但是仔细思考可以发现,len 是有符号整型,所以 len 的长度可以为负数,但是在 read 函数中,第三个参数的类型是 size_t,该类型相当于 unsigned long int,属于无符号长整型

最后修改:2021 年 05 月 14 日 11 : 21 PM
如果觉得我的文章对你有用,请随意赞赏