【软件安全】Chapter 4 软件漏洞

该博客围绕软件安全漏洞展开,介绍了溢出漏洞基本概念,包括栈溢出、堆溢出等多种溢出漏洞,还涉及格式化字符串漏洞、整数溢出漏洞等。同时阐述了攻击C++虚函数的方法,以及注入类和权限类等其他类型漏洞,并给出了相应示例和利用方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、溢出漏洞基本概念。

1、漏洞

2、缓冲区溢出漏洞

二、栈溢出漏洞。

1、栈的回顾

2、示例

3、溢出漏洞利用示例

(1)修改返回地址

(2)覆盖临接变量

三、堆溢出漏洞。

1、堆的回顾

2、堆溢出的基本概念

3、示例:覆盖目标堆块的块身数据

4、示例:在任意位置写入任意数据

5、Dword Shoot攻击 

四、其他溢出漏洞。

1、SEH结构溢出

2、单字节溢出

五、格式化字符串漏洞。

1、格式化字符串

2、格式化函数允许可变参数——数据泄露

3、%n格式写入数据——数据写入

4、自定义打印字符串宽度

六、整数溢出漏洞。

1、整数溢出概念与分类

2、示例1

3、示例2

七、攻击C++虚函数。

1、多态

2、示例

八、其他类型漏洞。

1、注入类漏洞

2、权限类漏洞


一、溢出漏洞基本概念。

1、漏洞

又称脆弱性,计算机系统的硬件、软件、协议在系统设计、具体实现、系统配置或安全策略上存在的缺陷。软件漏洞专指软件系统漏洞

2、缓冲区溢出漏洞

(1)缓冲区连续的内存区域,存放程序运行时加载到内存的运行代码和数据

(2)缓冲区溢出漏洞

当向固定大小的缓冲区写入超出容量的数据,多余的数据会越过缓冲区边界覆盖相邻的内存空间

缓冲区大小由用户输入的数据决定。

溢出的数据可能覆盖相邻内存空间的返回地址、函数指针、堆管理结构等数据,从而导致程序运行失败、或转去执行其他程序代码、或执行预先注入到内存缓冲区中的代码。并会以原有程序的身份权限运行

通常包括栈溢出、堆溢出、异常处理SEH结构溢出、单字节溢出等。

(3)缓冲区溢出的根本原因:

缺乏类型安全功能的程序设计语言。部分函数不对数组边界条件和函数指针引用等进行边界检查(如C标准库中与字符串操作相关的函数),因此程序员需要自己进行边界检查。

二、栈溢出漏洞。

1、栈的回顾

存储函数运行时的局部变量、数组
系统自动预留或回收空间,空间连续
向低地址扩展(向内存地址减小方向延伸):栈底为高地址,栈顶为低地址
ESP指向系统栈最上面栈帧的栈顶
EBP指向系统栈最上面栈帧的底部(基质指针寄存器)
写入数据方向:由低地址到高地址/由栈顶向栈底写

注意:数组无论是在堆区还是在栈区,都是从低地址向高地址增长。

2、示例

void why_here(void) {
    printf("why u r here?!\n"); 
    exit(0);
}
void f() {
    int buff[1];
    buff[2] = (int)why_here; 
} //why_here函数名即函数在代码区中的虚拟内存地址,但没有调用!
int main(int argc, char * argv[]) {
    f();
    return 0;
}
//输出结果为 why u r here?

buff[2] = (int) why_here执行后,理论上会通过返回地址返回主函数。数组是特殊类型的指针,尽管在声明数组时,声明的数组长度为1,也就是设计逻辑中buff[2]不存在,但buff[2]对应位置即为原返回地址位置。

buff[2]一行导致返回地址被修改为(int)why_here,即该函数在内存中的虚拟内存地址,错误跳转至why_here函数中并执行。

P.S.    对windows控制台主函数参数的补充说明

windows控制台程序的主函数参数有三:_argc,_argv,_environ

_argc全局变量是传递给程序的命令行参数的数量计数;

_argv是一个指向包含包含程序参数的单字节字符或多字节字符字符串的数组的指针;

因此反汇编后main调用前的参数入栈环节特征鲜明。

3、溢出漏洞利用示例

(1)修改返回地址

若返回地址被无效地址覆盖,则程序运行失败。

若返回地址被恶意程序入口地址覆盖,则源程序将转向去执行恶意程序。

void stack_overflow(char* argument) {
    char local[4];
    for (int i = 0; argument[i];i++)
    local[i] = argument[i];
} //字符串以转义字符\0结尾,意为0

栈区从高地址向低地址延伸,但数据写入从低地址向高地址。因此超出的部分可能覆盖返回地址。

(2)覆盖临接变量

覆盖临近变量的值,可以更改程序执行流程。

#define PASSWORD "1234567" //注意字符串结尾符也占1个字节
int verify_password(char * password)
{
     int authenticated; //记录密码对比结果
     char buffer[8];  //1~7&结尾符
     authenticated = strcmp(password, PASSWORD); //strcmp(a,b)对比大小
     strcpy(buffer, password); //password为输入的密码,因此长度未知
     return authenticated;
}
//strcpy(a,b)将b复制给a,因此可能越界

如果输入的密码超过了7个字符,则越界字符的ASCII码会修改authenticated。如果正好改为0,则可改变程序流程。

每个int为4个字节,这4个字节在内存中的存储分为大端存储和小端存储。

大端存储:低权值位在高地址

小端存储:低权值位在低地址(win)

因此,栈区中从上到下排列顺序大致为char buffer[0] -> char buffer[7] ->int的低权值/高字节 -> int的较高权值/较低字节

如果authenticated的返回值为1即0001,那么在内存中,从上至下存储为1、0、0、0。

通过ollydbg也可以看到,1存储为01 00 00 00(从左到右,从低地址到高地址)。

想要通过覆盖,使得authenticated为0,那么有两个条件需要满足:

1)输入一个8位的字符串,使得结尾符0刚好覆盖authenticated的高字节的1,得0000

2)输入的字符串应大于"12345678",确保经strcmp后authenticated值为1,即只有高字节为1;否则若authenticated为-1即1110,无法通过字符串截止符将其变为0

三、堆溢出漏洞。

1、堆的回顾

程序运行时动态分配的内存
需要程序员用专有函数进行申请(new)
空间不连续(用链表来存储的空闲内存地址)
向高地址扩展(向内存地址增大方向延伸)
占有态的堆块被使用它的程序索引,堆表只索引所有空闲态堆块
堆表主要有两种:空闲双向链表(空表),快速单向链表(快表
堆块的相关操作主要有:分配,释放,合并
详细内容见:堆栈基础笔记

2、堆溢出的基本概念

发生在堆中的缓冲区溢出。

实现难度更大,但威胁更大,已成为缓冲区溢出攻击的主要方式之一。

覆盖数据,也可在任意位置写入任意数据

3、示例:覆盖目标堆块的块身数据

(1)申请堆块buf1(低地址),buf2(高地址);FILENAME字符串用于存储文件名myoutfile

#define FILENAME "myoutfile"
char* buf1 = (char*)malloc(20);
char* buf2 = (char*)malloc(20); //堆向高地址延伸

(2)计算得两堆块之间的地址距离diff,用buf2保存写入目标文件名myoutfile

long diff = (long)buf2-(long)buf1;
strcpy(buf2,FILENAME);

(3)输出当前状态后,输入要写入目标文件myoutfile的字符串,复制到buf1中存储。

注意!往buf1复制时没有边界检查!可能越界!越界后,溢出部分写入相邻的buf2中。

printf("buf1 存储地址:%p\n",buf1);
printf("buf2 存储地址:%p,存储内容为文件名:%s\n",buf2,buf2);
printf("两个地址之间的距离:%d 个字节 \n",diff);
if(argc<2) { //输入的字符串少于2个,因此只有输入内容
    printf("请输入要写入文件%s 的字符串:\n",buf2);
    gets(bufchar);
    strcpy(buf1,bufchar);
} else {  //输入了2个字符串,argv[1]中为想存入buf1的字符串
    strcpy(buf1,argv[1]);
}

(4)打开buf2:fopen用于打开一个文件,并返回一个与该文件相关联的文件指针。buf2为一个字符串,包含被打开的文件名字(如果文件不在当前工作目录下还包括路径)。a为一个模式字符串,指定文件该如何被打开:在这里以追加模式打开,若文件已存在,则需要写入的内容将会写入到已有内容后,不会覆盖,如果不存在,则会1创建该文件。

fd=fopen(buf2,"a");

(5)如果没有发生溢出,打开的文件会是原来buf2中保存的myoutfile,但是示例的目的是覆盖buf2,因此fopen打开的是特定的输出文件。或者说,在溢出后尝试打开buf2时,打开的不是buf2原来存储的myoutfile而是新创建的a。

例如,假设diff=72(字节),而输入的字符串为72字节填充数据+hostility,那么buf2内容就变为了hostility。在尝试打开时会创建新文件。

(6)将buf1中字符串写入打开的文件后关闭。

fprintf(fd,"%s\n\n",buf1);
fclose(fd);

完整示例代码

#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<memory.h>
#define FILENAME "myoutfile"
int main(int argc,char *argv[])
{
    FILE *fd;
    long diff;
    char bufchar[100];
    char* buf1 = (char*)malloc(20);
    char* buf2 = (char*)malloc(20);
    diff = (long)buf2-(long)buf1;
    strcpy(buf2,FILENAME);
    printf("----信息显示----\n");
    printf("buf1 存储地址:%p\n",buf1);
    printf("buf2 存储地址:%p,存储内容为文件名:%s\n",buf2,buf2);
    printf("两个地址之间的距离:%d 个字节 \n",diff);
    printf("----信息显示----\n\n");
    if(argc<2) {
        printf("请输入要写入文件%s 的字符串:\n",buf2);
        gets(bufchar);
        strcpy(buf1,bufchar);
    }
    else {
        strcpy(buf1,argv[1]);
    }
    printf("----信息显示----\n");
    printf("buf1 存储内容:%s \n",buf1);
    printf("buf2 存储内容:%s \n",buf2);
    printf("----信息显示----\n");
    printf("将%s\n 写入文件 %s 中\n\n",buf1,buf2);
    fd=fopen(buf2,"a");
    if(fd==NULL) { //创建&打开均失败,一般不会,不用管
        fprintf(stderr,"%s 打开错误\n",buf2);
        if(diff<=strlen(bufchar)) {
            printf("提示:buf1 内存溢出!\n");
        }
        getchar();
        exit(1);
    }
    fprintf(fd,"%s\n\n",buf1);
    fclose(fd);
    if(diff<=strlen(bufchar)) {
        printf("提示:buf1 已溢出,溢出部分覆盖 buf2 中的 myoutfile\n");
    }
    getchar();
    return 0;
}

4、示例:在任意位置写入任意数据

从链表中卸下一个节点流程如下图

5、Dword Shoot攻击 

如果堆溢出覆写了一个空闲堆块的flink和blink,通过构造一个地址和一个数据,当该空闲堆块从链表中卸下时,就能获得一次向内存构造的任意地址写入一个任意数据的机会,例如通过写入恶意代码入口地址劫持进程,运行植入的恶意代码。

四、其他溢出漏洞。

1、SEH结构溢出

(1)SEH:异常处理结构体

(2)SEH异常处理

  1. 线程初始化时,会自动向栈中安装一个SEH,作为默认的异常处理。
  2. 异常发生时程序中断,首先从TEB的0字节偏移处取出距离栈顶最近的SEH,使用异常处理函数句柄指向的代码来处理异常。当最近的异常处理函数运行失败时,将顺着SEH链表依次尝试其他的异常处理函数。
  3. 如果程序安装的所有异常处理函数都不能处理这个异常,系统会调用默认的系统处理程序。

(3)SHE攻击

可利用缓冲区溢出覆盖SHE链表的入口地址、异常处理函数或链表指针,例如将异常处理函数改为恶意程序的入口地址。

2、单字节溢出

(1)指程序中的缓冲区仅能溢出一个字节。

(2)当溢出的字节与栈帧指针紧挨时,即要求其为函数中首个变量,可以利用。

void single_func(char *src) {
    char buf[256];
    int i;
    for(i = 0;i <= 256;i++)
         buf[i] = src[i]; //拷贝 257 个字节到 256 个字节的缓冲区
}

五、格式化字符串漏洞。

1、格式化字符串

(1)告诉程序如何输出,如printf("my name is: %s" , "bing")中的第一个参数。

(2)printf函数:printf("format" , 输出表列 )

(3)format结构:%[标志][最小输出宽度][.精度][长度]类型

%d整型%u十进制
%ld长整型%c字符
%o八进制%s字符串
%x十六进制%f小数

注意:%s用于输出字符串,但是通过%s读到的是字符串的首地址。

2、格式化函数允许可变参数——数据泄露

(1)原因

C语言的格式化函数允许可变参数,并根据传入的格式化字符串获取可变参数的类型与个数,依据格式化符号进行参数输出

当参数个数不足即少于格式化符号时,函数会自动取出格式化字符串后的多个栈中的内容作为参数,并根据格式化符号输出。(自作主张取数&变类型

(2)示例1:Debug

int main(void) {
    int a=1,b=2,c=3;
    char buf[]="test";
    printf("%s %d %d %d %x\n",buf,a,b,c);
    return 0;
}
//输出 test 1 2 3 ***

 printf函数到入栈的参数位置取参数,当没有给出%x的参数时,自动将栈区参数的下一个地址作为参数输入。结合函数调用方法可知,***为参数c后面的高地址中存储的数据

(3)示例2:Debug&Release

int main(int argc, char *argv[]) {
     char str[200];
     fgets(str,200,stdin);
     printf(str);
     return 0;
}

Debug模式:方便调试,编译格式工整

Release模式:性能优先,空间占用较少,可能省略或简化部分栈帧切换

Release模式下,输入AAAA%x%x%x%x,得到AAAA18FE84BB40603041414141(0x41即A)

注意这里的%不是\%即没有进行转译,因此在printf一行中作为格式化字符串要求数据十六进制输出。

可见Release模式下,没有严格按照栈帧切换流程来。printf函数执行时,先通过最上面一格的Str Addr找到AAAA所在位置,读取参数输出。然后当读取到4个%x时,printf函数就“将格式化字符串后面的多个栈中的内容取出作为参数”,并以16进制输出。换句话说,由于printf只有一个参数str,因此会把str后面的内存地址里的数据取出,以16进制打印。最后一个%x打印结果为41414141,即将AAAA转为16进制输出结果。如果将%x改为%s,那么就是去地址41414141处取出数据,变成格式化字符串进行输出。那么如果将AAAA变成别的内容,就可以到别的地址去读取任意数据,从而实现读取任意地址数据的功能。

3、%n格式写入数据——数据写入

(1)格式化符号%n

不向printf传递格式化信息,而是将格式化函数输出字符串的长度写入函数参数指定的位置。

print("Jamsa%n", &first_count)即向整型变量first_count写入5。

(2)示例:Release模式

int formatstring (int argc, char *argv[]) {
    char buffer[100];
    springf(buffer, argv[1]);
} //springf作用是将格式化的数据写入某个字符串缓冲区

若将aaaabbbbcc%n作为命令行参数输入,数值10就会写入地址0x61616161(aaaa)的内存单元。

原因:Release模式下,没有紧挨buffer的局部变量。读到%n时,会在堆栈中取下一个参数,并将其作为整数指针使用,而调用sprintf时没有传入下一个参数,因此buffer中前四个字节被当作参数,已输出字符串的长度10被写入0x61616161处。

因此通过这种格式化字符串的利用方式,可以实现向任意内存写入任意数值。

4、自定义打印字符串宽度

int num=66666666;
printf("Before:num = %d\n", num); //输出Before:num = 66666666
printf("%d%n\n", num, &num);      //输出666666,并将66666666的长度8写入num
printf("After:num = %d\n", num);  //输出After:num = 8

通过%n格式化符号和自定义打印字符串宽度,可以写入某内存地址任意数据。

int num=66666666;
printf("Before:num = %d\n", num); //输出Before:num = 66666666
printf("%d100d%n\n", num, &num);  //输出94个空格+666666
//输出后将%n前的字符串长度100写入num
//数值右侧用0补齐不足的位数
printf("After:num = %d\n", num);  //输出After:num = 8

覆盖地址方法:
如果想将地址0x8048000写入num,只需将其对应的十进制134512640作为格式符控制宽度。

六、整数溢出漏洞。

1、整数溢出概念与分类

计算结果大于整数所表示的范围时发生整数溢出

存储溢出使用另外的数据类型来存储int
运算溢出对整型变量进行计算时没有考虑其边界范围,运算后的数值范围超出存储空间
符号问题一般的长度变量使用unsigned int,但如果忽略了符号,安全检查判断时可能出问题

2、示例1

unsigned int size = len + 1; //注意这里是无符号整数
char *buffer = (char*)malloc(size);
if(!buffer)
return NULL;
memcpy(buffer,data,len);
buffer[len]=’\0’;

如果将0xFFFFFFFF作为len输出,那么计算size时出现整数溢出,malloc成功分配大小为0的内存块,但是在后面的mencpy时发生堆溢出。

3、示例2

void func() {
    ShellExecute(NULL,"open","notepad",NULL,NULL,SW_SHOW); //打开记事本
}
void func1() {
    ShellExecute(NULL,"open","calc",NULL,NULL,SW_SHOW); //打开计算器
}
int main() {
    void (*fuc_ptr)() = func; //用fuc_ptr存储函数func的地址
    char info[MAX_INFO]; char info1[30000]; char info2[30000];
    //可知fuc_ptr和 info在内存中相差MAX_INFO个字节的空间,即fuc_ptr在info后面存储
    freopen("input.txt","r",stdin); //确定读入字符串的来源:input.txt
    cin.getline(info1,30000,' '); //读入字符串info1
    cin.getline(info2,30000,' '); //读入字符串info2
    short len1 = strlen(info1);   //info1长度,注意用 short 存储的!!!
    short len2 = strlen(info2);   //info2长度,注意用 short 存储的!!!
    short all_len = len1 + len2;  //字符串合并后总长度,注意用 short 存储的!!!
    //若len1+len2结果超出all_len范围,会变为一个负数,下面的all_len<MAX_INFO成立
    if(all_len<MAX_INFO) {    
        strcpy(info,info1); //先复制
        strcat(info,info2); //再拼接
        //将info1与info2都写入info中,info可能存不下!溢出的部分会覆盖func_ptr
    }
    fuc_ptr(); //可能发生溢出,正好导致func_ptr被写为func1地址
    return 0;
}

七、攻击C++虚函数。

1、多态

(1)类的成员函数声明时,若用virtual修饰,则为虚函数

(2)虚函数的入口统一保存在虚表(Vtable)中

(3)使用虚函数:虚表指针->虚表->函数入口

(4)虚表指针保存在对象的内存空间中,接下面是其他成员变量

因此如果虚表中存储的虚表指针被篡改,调用虚函数是就会执行篡改后的地址的shellcode,发动虚函数攻击。

2、示例

攻击方法:

  1. 修改虚表地址:将对象overflow的虚表地址修改为shellcode倒数第四个字节开始地址
  2. 修改虚函数指针:修改shellcode最后4位(虚表)来指向overflow.buf内存地址,即让虚函数指针指向保存shellcode的overflow.buf区域
char shellcode[] = //包含了待植入内存的恶意代码
    "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
    ……
    //注意:计算得overflow.buf的地址为0x00428ba4
    "\xA4\x8B\x42\x00"; 
class Failwest {
public:
 char buf[200];
 virtual void test(void) {
     cout<<"Class Vtable::test()"<<endl;}
};
Failwest overflow, *p;
void main(void) {
    char *p_vtable;//指向虚表指针
    p_vtable = overflow.buf - 4; //虚表指针在对象overflow的成员变量buf[200]前
    int len = strlen(shellcode);
    __asm int 3; //人为增加一个断点,调试的时候就会停在这里
    //计算得overflow.buf的倒数第四个字节的开始地址为0x00428c54
    p_vtable[0] = 0x54;
    p_vtable[1] = 0x8c;
    p_vtable[2] = 0x42;
    p_vtable[3] = 0x00;
    //shellcode为恶意代码,存入了overflow.buf处
    strcpy(overflow.buf, shellcode); 
    p = &overflow; 
    p->test(); //希望在调用虚函数test时,转去执行恶意代码
}

 

八、其他类型漏洞。

1、注入类漏洞

共同特点:来自外部的输入数据被当作代码或非预期的指令、数据被执行。

包括二进制代码注入(外部代码获得执行机会)和脚本注入(脚本数据被执行)。

(1)SQL注入

SQL 注入是将 Web 页面的原 URL、表单域或数据包输入的参数,修改拼接成 SQL 语句,传递给 Web 服务器,进而传给数据库服务器以执行数据库命令。

SELECT * FROM Articles WHERE Keywords LIKE  '%hack' ; DROP TABLE Articles;--%'
//先执行 SELECT * FROM Articles WHERE Keywords LIKE  '%hack'
//再执行 DROP TABLE Articles;--%

(2)操作系统命令注入

大多数 Web 服务器都能够使用内置的 API 与服务器的操作系统进行几乎任何必需的交互,如果应用程序向操作系统命令程序传送用户提交的输入,而且没有对输入进行过滤和检测,就可能遭受命令注入攻击。

(3)Web脚本语言注入

web 脚本解释语言支持动态执行在运行时生成的代码,攻击可能来源于:

合并用户提交数据后的代码蕴含设定的非正常业务逻辑,通过代码执行来实施特定攻击;

  1. 根据用户提交的数据指定的代码文件的动态包含。

(4)SOAP注入

SOAP用在Web服务中,通过浏览器访问的Web应用程序常常使用 SOAP 在后端应用程序组件之间进行通信。如果用户提交的数据中包含XML元素,并被直接插入到 SOAP 消息中,攻击者就能够破坏消息的结构,进而破坏应用程序的逻辑或造成其他不利影响。

2、权限类漏洞

(1)水平越权

同级权限用户或同一角色不同用户之间可以越权访问、修改或删除。

(2)垂直越权

向上越权:低权限用户可以做高权限用户才能做的事。

向下越权:高级别用户可以访问低权限用户的信息。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值