二进制-逻辑漏洞

逻辑漏洞成因

逻辑漏洞是指那些在程序中,因代码逻辑不正确而引发的漏洞;

逻辑漏洞成因:

  • 产生于程序设计不够细致,或编码不够严谨的地方;
    • 当程序体量较为庞大,分支结构较为复杂的时候,不太常用到的分支结构就往往得不到足够的重视,设计和编码也就难以得到保障,导致很可能存在逻辑漏洞;
  • 发生在频繁变更的程序段上;
    • 如因需求的频繁变更,或因执行出现的bug等问题,需要对程序进行修改的时候,往往是没有足够的时间重头开始进行设计,也不容易分析全与其他部分之间的关联;
    • 因此,在编码过程中,就很可能存在与其他部分出现冲突,或不匹配的情况,很可能会存在逻辑漏洞;
  • 合作出现问题,大型的程序或系统往往是由多人、多团队协作完成的;
    • 由于不同的人,对不同的功能存在不同的理解,以及各种原因导致的交流不畅,都会影响到各模块之间的协调和整体的安全性;
  • 不严谨的边界判别
    • 出现情况往往是在>>=<<=的误用,或字符串的长度判别情况;
      • 边界值被算入判别条件或把边界值漏掉,可能导致循环多进行一轮,或者将本不该放过判别条件的值放过判别条件;
      • 字符串的长度判别举例,一个长为16个字符的字符串,所占的空间为字符串数组下标为0-15,第16个下标为字符串结束符,因而需要17个空间进行存储;
    • 指针与引用的误用
      • 指针与引用均是用于间接的使用其他对象,这就导致指针与引用的误用,这种误用往往出现在函数的调用,如在scanf函数中,参数为数据的引用,误将其用为指针的话,就会出现错误;
  • 递归爆段
    • 递归是函数自身调用自身的一种情况,如果这个调用自身的过程过于深入,而一直没有结果返回,则容易发生递归爆栈,这也是逻辑的漏洞,是函数的功能、数据处理不当造成的;

递归暴栈逻辑漏洞

成因

由于系统会在每次调用函数时为函数安排一个栈空间,并在函数结束时回收这个空间;

如果函数迟迟不返回,这个空间就迟迟回收不回来;

而不断的自身调用自身,就导致这个空间不断的被分配,直到这个空间被分配完,就造成了爆栈;

程序分析

对如下程序进行递归分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* recursion10 */
#include <stdio.h>
int func(int i){
if (i>0){
i--;
printf("%d",i);
func(i);
}
return 0;
}
int main(){
int i=0x10;
func(i);
printf("\n");
return 0;
}
  • 首先在进入main函数的mov rbp,rsp处下一个断点, 查看栈帧情况;其次在第一次调用func函数,和第二次调用func函数的地方下两个断点;

  • 程序在main函数开始时的栈顶指针和栈底指针均为0x7fffffffde70地址;

  • 然后将程序运行到第一次调用函数func,此时栈顶指针为0x7fffffffde60,栈低指针为0x7fffffffde70

  • 接下来进入func函数内部,运行到第 一次递归调用func函数时,查看此时的栈帧情况,栈顶指针为0x7fffffffde40地址,栈底指针为0x7fffffffde50地址;

  • 通过对两次递归调用时的栈帧情况的分析,可以发现每次递归调用消耗0x20的栈空间,栈帧逐渐向低地址发展;

  • 随着递归的不断深入,栈帧会逐渐减小,直到超过临界值,导致爆栈;

将递归深度改为0x100000,在gdb加载后运行程序,可以看到程序运行发生了递归爆栈错误;

不严谨的边界判别

成因

  • 错误使用>=><=<
  • 忽视数组下标以0开始;
  • 忽视字符串最后结束符;

后果

  • 放过不该放过的叛别情况;
  • 程序变得难以预料;
  • 数据泄露;

程序分析

对以下程序进行分析;

1
2
3
4
5
6
7
8
9
10
11
12
13
/* lossly_border */
#include <stdio.h>
#include <string.h>
int main(){
char s[20]="";
int i=0;
char c='c';
for (i=0;i<20;i++)
s[i]=c;
int slen = strlen(s);
printf("%d\n",slen);

}
  • 分析程序用于一个申请长度为20个字符的字符串中添加进20个字符,然后打印出字符串长度;

  • 运行程序,发现输出的结果长度为22,与预期的长度20不符;

    image-20210303223556667
  • 使用gdb调试,找到字符串存储位置,发现字符串的开始地址是参数rbp-0x20

  • 查看其中的值,即长度计算将栈上其他数值也计算了进去,此时打印字符串内容也会将这个值打出来;

  • 0x63即为存储的字符c,可以看到存储了20个c字符。但接下来并没有字符串结束符,而是原先栈中数据0xff7f0000(这是因为是小端存储模式),所以strlen()函数在判别字符串长度的时候就把0xff0x7f也一同算进去了,造成了字符串长度出现错误;

===的误用

成因

==是常见的判别符号,=由于与==相近很容易被当做==使用;

后果

  • if(i==1)表示如果i等于1执行;
  • 而if(i=1)则是给i赋值为1,1为返回的值;

程序分析

1
2
3
4
5
6
7
/* equality */
#include <stdio.h>
int main(){
int i=0;
if (i=1) printf("i=1\n");
return 0;
}

运行程序,打印出i=1,即判断条件为真,执行了printf语句,而之前为i的赋值为0,本不应判断为真的,这就是===的使用不正确导致的漏洞;

函数默认参数问题

成因

  • 函数存在默认参数;
  • 函数存在缺省参数情况;
  • 部分函数部分参数通常不会用到;
  • 这些参数在编码中不受重视,却会影响函数的处理方式;
  • 如果这些参数在调用时,出现与正常参数格式等不一样的情况,就会产生难以预料的后果,造成逻辑漏洞;

后果

导致函数处理结果与预期不一致,后果难以预料;

漏洞举例

read(int fd, void *buf, size_t count)

  • read函数有三个参数;

    • 第一个参数是一个int类型的文件描述符;
    • 第二个是数据保存的缓冲区首地址;
    • 第三个是请求读入的字节数;
  • read函数参数问题:

    如果这个read函数的第一个参数为0read函数不会报错,也没有文件可以读取,会选择从标准输入流中读入数据,也就是可以输入任意数据,且函数的返回值也没有异常;

对以下程序进行分析;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* read0 */
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int handle=0;
int handle0=0;
int slen=0;
char s[20]="";
handle = open("read0.txt",O_RDONLY);
printf("%d\n",handle);

slen = read(handle,s,15);

printf("%d\n",slen);
printf("%s\n",s);

}

程序使用只读的方式从文件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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* read1 */
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int handle=0;
int handle0=0;
int slen=0;
char s[20]="";
handle = open("read0.txt",O_RDONLY);
printf("%d\n",handle);

slen = read(handle0,s,15);

printf("%d\n",slen);
printf("%s\n",s);

}

漏洞分析:

  • linux中会对文件进行重定向;

  • 对应文件描述符0、1、2的是标准输入、标准输出、标准错误,这里对应为标准输入了;

  • 运行程序,程序会在调用read函数的时候等输入;

  • 输入abcdef,看到确实是从标准输入中进行读入并且输出;

malloc导致的字符串长度判别错误

成因

  • 堆中情况较为复杂;
  • malloc连续分配的地址不一定连续;
  • 防止无谓的因字符串对齐而浪费存储空间,使用了下一个分配的地址参与了字符串长度的判断;
  • 但是当连续的堆,分配不到相连续的存储空间时;
  • 导致下一个分配地址会与当前分配地址间隔比较远;
  • 进行长度判别就不准确了;

程序分析

对以下程序进行分析

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/* length */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct stu{
char *string;
char name[20];
};


int main(){
char *string1 = (char*) malloc(50);
struct stu *stu1 = (struct stu *)malloc(sizeof(struct stu));
stu1->string = string1;
strcpy(string1,"AA");
strcpy(stu1->name,"AAAAAAA");

char *string2 = (char*) malloc(50);
struct stu *stu2 = (struct stu *)malloc(sizeof(struct stu));
stu2->string = string2;

strcpy(string2,"BB");
strcpy(stu2->name,"BBBBBBB");

free(stu1->string);
free(stu1);

int i = 0;
scanf("%d",&i);
char *string3 = (char*) malloc(i);
struct stu *stu3 = (struct stu *)malloc(sizeof(struct stu));
stu3->string = string3;

unsigned int *string3addr = (unsigned int*) &stu3->string;
unsigned int *stu3addr = (unsigned int*) &stu3;
printf("%x %x\n",*stu3addr,*string3addr);
char str[100];
scanf("%s",str);
printf("%d",*stu3addr-*string3addr-4);
if (strlen(str)>=(*stu3addr-*string3addr-4)){
printf("string is too long!\n");
exit(1);
}
strcpy(string3,str);
strcpy(stu3->name,"CCCCCCC");

free(stu2->string);
free(stu2);
free(stu3->string);
free(stu3);
return 0;
}
  • 程序定义了一个结构体,由一个字符串指针和一个字符串构成;
    • 存储时,采用了连续分配两个存储空间的方式,对字符串指针所指向字符串,和结构体本身字符串进行存储;
  • 在运行程序时,首先连续配置了两个结构体,以及它们对应的字符串;
  • 然后将第一个字符串和结构体的空间释放掉;
  • 接下来就到了会触发漏洞的关键输入部分;
  • 程序允许用户自己输入字符串长度和字符串内容;
  • 而对于字符串长度的判断,就是通过接下来申请的结构体的地址进行计算;
    • 调用scanf函数,堆中为其分配了0x410大小;
    • 即连续分配的两个地址确实不是连续的,中间间隔了第二个结构体和scanf函数的空间,而长度判别在这里就不起作用了;

程序正常运行结果如下;

使用gdb查看运行情况,主要是堆分配;

  • 下图为两个原始结构体stu1和stu2申请完成后的汇编代码,两次malloc是stu2的两次申请,可以看到malloc申请的空间依次存储,rax是malloc的返回值, 第4次申请的起始地址是存放在rbp-0x90的位置;

  • 查看四次分配的空间排布;

    • 四个malloc,分配了2个结构体中的指针和两个指向结构体指针,这四个指针是在系统固定区域存储,指针所占空间是固定的,每个8字节,所以通过rbp-0x90前推是rbp-0xa8

  • 2次free释放了struct1的两块空间;

    • 被free掉的空间正好是0x6020100x602050,也即stu1的两块空间;

  • 接着程序调用了scanf函数;

    • 第一次调用scanf函数,堆中为其分配了0x410大小(默认为scanf函数分配的空间)的空间;
    • 这个空间大小已经超过了fastbin的范畴,进入了large bin;
    • 在large bin开始分配前,系统会回收重组所有的unsorted bin,于是被释放的stu1的堆块空间:0x6020100x602080之间的地址就被合并成了一块;
    • 但是仍然并不满足scanf函数的0x410空间需要,还要在最后一个堆块,即stu2所占空间后面的空闲空间进行分配;
  • 接着进行漏洞的利用,这次scanf申请的字符串空间大小为70,查看分配的字符串的地址和结构体的地址;

    • stu3的两块指针为rbp-0x88rbp-0xb0
  • 查看两指针位置;

    • 分配的起始地址是0x6020100x602500
  • 查看地址中的存储内容,可以看到分别是刚回收的0x602010与继续分配的0x602500(stu3的结构体地址),一块是stu1的堆块,另一个是相隔很远的堆块,即连续分配的两个地址确实不是连续的,中间间隔了第二个结构体和scanf函数的空间,而长度判别在这里就不起作用了(非常长的一块空间);

    1
    2
    3
    4
    if (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没能按顺序分配时所引发的错误;

备注:实验程序下载