逻辑漏洞成因
逻辑漏洞是指那些在程序中,因代码逻辑不正确而引发的漏洞;
逻辑漏洞成因:
- 产生于程序设计不够细致,或编码不够严谨的地方;
- 当程序体量较为庞大,分支结构较为复杂的时候,不太常用到的分支结构就往往得不到足够的重视,设计和编码也就难以得到保障,导致很可能存在逻辑漏洞;
- 发生在频繁变更的程序段上;
- 如因需求的频繁变更,或因执行出现的bug等问题,需要对程序进行修改的时候,往往是没有足够的时间重头开始进行设计,也不容易分析全与其他部分之间的关联;
- 因此,在编码过程中,就很可能存在与其他部分出现冲突,或不匹配的情况,很可能会存在逻辑漏洞;
- 合作出现问题,大型的程序或系统往往是由多人、多团队协作完成的;
- 由于不同的人,对不同的功能存在不同的理解,以及各种原因导致的交流不畅,都会影响到各模块之间的协调和整体的安全性;
- 不严谨的边界判别
- 出现情况往往是在
>
与>=
、<
与<=
的误用,或字符串的长度判别情况;- 边界值被算入判别条件或把边界值漏掉,可能导致循环多进行一轮,或者将本不该放过判别条件的值放过判别条件;
- 字符串的长度判别举例,一个长为16个字符的字符串,所占的空间为字符串数组下标为0-15,第16个下标为字符串结束符,因而需要17个空间进行存储;
- 指针与引用的误用
- 指针与引用均是用于间接的使用其他对象,这就导致指针与引用的误用,这种误用往往出现在函数的调用,如在scanf函数中,参数为数据的引用,误将其用为指针的话,就会出现错误;
- 出现情况往往是在
- 递归爆段
- 递归是函数自身调用自身的一种情况,如果这个调用自身的过程过于深入,而一直没有结果返回,则容易发生递归爆栈,这也是逻辑的漏洞,是函数的功能、数据处理不当造成的;
递归暴栈逻辑漏洞
成因
由于系统会在每次调用函数时为函数安排一个栈空间,并在函数结束时回收这个空间;
如果函数迟迟不返回,这个空间就迟迟回收不回来;
而不断的自身调用自身,就导致这个空间不断的被分配,直到这个空间被分配完,就造成了爆栈;
程序分析
对如下程序进行递归分析
1 | /* recursion10 */ |
首先在进入main函数的
mov rbp,rsp
处下一个断点, 查看栈帧情况;其次在第一次调用func函数,和第二次调用func函数的地方下两个断点;程序在main函数开始时的栈顶指针和栈底指针均为
0x7fffffffde70
地址;然后将程序运行到第一次调用函数func,此时栈顶指针为
0x7fffffffde60
,栈低指针为0x7fffffffde70
;接下来进入func函数内部,运行到第 一次递归调用func函数时,查看此时的栈帧情况,栈顶指针为
0x7fffffffde40
地址,栈底指针为0x7fffffffde50
地址;通过对两次递归调用时的栈帧情况的分析,可以发现每次递归调用消耗
0x20
的栈空间,栈帧逐渐向低地址发展;随着递归的不断深入,栈帧会逐渐减小,直到超过临界值,导致爆栈;
将递归深度改为0x100000
,在gdb加载后运行程序,可以看到程序运行发生了递归爆栈错误;
不严谨的边界判别
成因
- 错误使用
>=
与>
、<=
与<
; - 忽视数组下标以0开始;
- 忽视字符串最后结束符;
后果
- 放过不该放过的叛别情况;
- 程序变得难以预料;
- 数据泄露;
程序分析
对以下程序进行分析;
1 | /* lossly_border */ |
分析程序用于一个申请长度为20个字符的字符串中添加进20个字符,然后打印出字符串长度;
运行程序,发现输出的结果长度为22,与预期的长度20不符;
使用gdb调试,找到字符串存储位置,发现字符串的开始地址是参数
rbp-0x20
;查看其中的值,即长度计算将栈上其他数值也计算了进去,此时打印字符串内容也会将这个值打出来;
0x63
即为存储的字符c
,可以看到存储了20个c
字符。但接下来并没有字符串结束符,而是原先栈中数据0xff7f0000
(这是因为是小端存储模式),所以strlen()
函数在判别字符串长度的时候就把0xff
和0x7f
也一同算进去了,造成了字符串长度出现错误;
==
与=
的误用
成因
==
是常见的判别符号,=
由于与==
相近很容易被当做==
使用;
后果
- if(i==1)表示如果i等于1执行;
- 而if(i=1)则是给i赋值为1,1为返回的值;
程序分析
1 | /* equality */ |
运行程序,打印出i=1,即判断条件为真,执行了printf语句,而之前为i的赋值为0,本不应判断为真的,这就是==
与=
的使用不正确导致的漏洞;
函数默认参数问题
成因
- 函数存在默认参数;
- 函数存在缺省参数情况;
- 部分函数部分参数通常不会用到;
- 这些参数在编码中不受重视,却会影响函数的处理方式;
- 如果这些参数在调用时,出现与正常参数格式等不一样的情况,就会产生难以预料的后果,造成逻辑漏洞;
后果
导致函数处理结果与预期不一致,后果难以预料;
漏洞举例
read(int fd, void *buf, size_t count)
read
函数有三个参数;- 第一个参数是一个
int
类型的文件描述符; - 第二个是数据保存的缓冲区首地址;
- 第三个是请求读入的字节数;
- 第一个参数是一个
read
函数参数问题:如果这个
read
函数的第一个参数为0
,read
函数不会报错,也没有文件可以读取,会选择从标准输入流中读入数据,也就是可以输入任意数据,且函数的返回值也没有异常;
对以下程序进行分析;
1 | /* read0 */ |
程序使用只读的方式从文件read0.txt中读入数据,而read0.txt中存储为1234567890
;
运行程序结果如下;
- 对于open函数,打开一个文件时,返回未被使用的最小文件描述符(非负整数),失败返回-1;
- 由于系统中默认已打开三个文件描述符(0:标准输入,1:标准输出 ,2:标准出错)之外,未被使用的最小文件描述符就是3;
- 可以看到,open函数为read0.txt分配的文件描述符为3,成功读入了11个字符,其中有一位为字符串结束符,最后输出了文件内容;
如果这里的文件描述符,误用了初始化后未使用的另一个描述符handle0
,就会变为从标准输入中读入了;
程序如下,其中slen=read(handle0,s,15)
中,将handle误用为参数handle0;
1 | /* read1 */ |
漏洞分析:
linux中会对文件进行重定向;
对应文件描述符0、1、2的是标准输入、标准输出、标准错误,这里对应为标准输入了;
运行程序,程序会在调用
read
函数的时候等输入;输入
abcdef
,看到确实是从标准输入中进行读入并且输出;
malloc
导致的字符串长度判别错误
成因
- 堆中情况较为复杂;
-
malloc
连续分配的地址不一定连续; - 防止无谓的因字符串对齐而浪费存储空间,使用了下一个分配的地址参与了字符串长度的判断;
- 但是当连续的堆,分配不到相连续的存储空间时;
- 导致下一个分配地址会与当前分配地址间隔比较远;
- 进行长度判别就不准确了;
程序分析
对以下程序进行分析
1 | /* length */ |
- 程序定义了一个结构体,由一个字符串指针和一个字符串构成;
- 存储时,采用了连续分配两个存储空间的方式,对字符串指针所指向字符串,和结构体本身字符串进行存储;
- 在运行程序时,首先连续配置了两个结构体,以及它们对应的字符串;
- 然后将第一个字符串和结构体的空间释放掉;
- 接下来就到了会触发漏洞的关键输入部分;
- 程序允许用户自己输入字符串长度和字符串内容;
- 而对于字符串长度的判断,就是通过接下来申请的结构体的地址进行计算;
- 调用scanf函数,堆中为其分配了
0x410
大小; - 即连续分配的两个地址确实不是连续的,中间间隔了第二个结构体和scanf函数的空间,而长度判别在这里就不起作用了;
- 调用scanf函数,堆中为其分配了
程序正常运行结果如下;
使用gdb查看运行情况,主要是堆分配;
下图为两个原始结构体stu1和stu2申请完成后的汇编代码,两次malloc是stu2的两次申请,可以看到malloc申请的空间依次存储,
rax
是malloc的返回值, 第4次申请的起始地址是存放在rbp-0x90
的位置;查看四次分配的空间排布;
四个malloc,分配了2个结构体中的指针和两个指向结构体指针,这四个指针是在系统固定区域存储,指针所占空间是固定的,每个8字节,所以通过
rbp-0x90
前推是rbp-0xa8
;
2次free释放了struct1的两块空间;
被free掉的空间正好是
0x602010
和0x602050
,也即stu1的两块空间;
接着程序调用了scanf函数;
- 第一次调用scanf函数,堆中为其分配了
0x410
大小(默认为scanf函数分配的空间)的空间; - 这个空间大小已经超过了fastbin的范畴,进入了large bin;
- 在large bin开始分配前,系统会回收重组所有的unsorted bin,于是被释放的stu1的堆块空间:
0x602010
到0x602080
之间的地址就被合并成了一块; - 但是仍然并不满足scanf函数的
0x410
空间需要,还要在最后一个堆块,即stu2所占空间后面的空闲空间进行分配;
- 第一次调用scanf函数,堆中为其分配了
接着进行漏洞的利用,这次scanf申请的字符串空间大小为
70
,查看分配的字符串的地址和结构体的地址;- stu3的两块指针为
rbp-0x88
和rbp-0xb0
;
- stu3的两块指针为
查看两指针位置;
- 分配的起始地址是
0x602010
与0x602500
;
- 分配的起始地址是
查看地址中的存储内容,可以看到分别是刚回收的
0x602010
与继续分配的0x602500
(stu3的结构体地址),一块是stu1的堆块,另一个是相隔很远的堆块,即连续分配的两个地址确实不是连续的,中间间隔了第二个结构体和scanf函数的空间,而长度判别在这里就不起作用了(非常长的一块空间);1
2
3
4if (strlen(str)>=(*stu3addr-*string3addr-4)){
printf("string is too long!\n");
exit(1);
}输入过长会直接覆盖掉第二个结构体的堆结构的chunk头 ,导致free过程中的异常;
-
free(stu2->string);
,在这里出现bug;
-
在长度判别不起作用的情况下,当给string分配的堆块为70(分配72),如果输入过长,只要超过80(72+8(占据p_size的8)),则覆盖stu2的size,会直接覆盖掉第二个结构体的堆结构的chunk头,导致stu2的string在free过程中的异常;
程序出错,但并没有显示长度过长;
在长度判别不起作用的情况下,可以看到程序在进行free函数的时候遇到了错误,这就是由malloc没能按顺序分配时所引发的错误;
备注:实验程序下载