欢迎光临散文网 会员登陆 & 注册

二进制安全之栈溢出

2020-04-16 14:11 作者:汇智知了堂  | 我要投稿

本文主要介绍二进制安全的栈溢出内容。

栈基础
内存四区

  • 代码区(.text):这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指令执行。

  • 数据区(.data):用于存储全局变量和静态变量等。

  • 堆区:动态地分配和回收内存,进程可以在堆区动态地请求一定大小的内存,并在用完后归还给堆区。地址由高到低生长

  • 栈区:用于动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行;此外局部变量也存储在栈区。地址由低到高生长

BSS段:(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,属于静态内存分配。

栈的概念

  • 一种数据结构,数据存储方式为先进后出,压栈(push)和出栈(pop)

  • 每个程序都有自己的进程地址空间,进程地址空间中的某一部分就是该程序的栈,用于保存函数调用信息和局部变量

  • 程序的栈是从进程空间的高地址向低地址增长的,数据是从低地址向高地址存放的



函数调用

  • 函数调用经常嵌套,在同一时刻,堆栈中会有多个函数的信息。

栈帧

  • 每个未完成运行的函数占用一个独立的连续区域,称作栈帧。


基本流程

;调用前 push arg3               ;32位esp-4,64位esp-8 push arg2 push arg1 call func               ;1. 压入当前指令的地址,即保存返回地址 2. jmp到调用函数的入口地址 push ebp                ;保存旧栈帧的底部,在func执行完成后在pop ebp mov ebp,esp         ;设置新栈帧的底部 sub esp,xxx         ;设置新栈帧的顶部


    详细流程

    int func_b(int b1,int b2) {  int var_b1,var_b2;  var_b1 = b1+b2;  var_b2 = b1-b2;  return var_b1 * var_b2; } int func_a(int a1,int a2) {  int var_a;  var_a = fuc_b(a1+a2);  return var_a; } int main(int argc,char** argv,char **envp) {  int var_main;  var_main = func_A(4,3);  return 0; }


      参数传递

      x86

      通过栈传参 先压入最后一个参数

      x64

      rdi rsi rdx rcx r8 r9 接收后六个参数 之后的参数通过栈传参

      64位的利用方式

      构造rop链 ROPgadget –binary level3_x64 –only ‘pop|ret’ # Gadgets information 0x00000000004006ac : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x00000000004006ae : pop r13 ; pop r14 ; pop r15 ; ret 0x00000000004006b0 : pop r14 ; pop r15 ; ret             0x00000000004006b2 : pop r15 ; ret 0x00000000004006ab : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x00000000004006af : pop rbp ; pop r14 ; pop r15 ; ret 0x0000000000400550 : pop rbp ; ret 0x00000000004006b3 : pop rdi ; ret 0x00000000004006b1 : pop rsi ; pop r15 ; ret 0x00000000004006ad : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret 0x0000000000400499 : ret 依次找pop rdi,pop rsi..,pop r9 ,这些寄存器里面存放的是参数,可以通过pop覆盖其中的内容

      栈溢出

      栈溢出指的是程序向栈中某个变量写入的字节数超过了这个变量本身申请的字节数,因而导致栈中与之相邻的变量的值被改变。

      栈溢出目的

      • 破坏程序内存结构

      • 执行system(/bin/sh)

      • 执行shellcode

      栈溢出思路

      判断溢出点

      常见的危险函数: 输入:gets scanf vscanf 输出:sprintf 字符串:strcpy strcat bcopy

      判断padding

      计算我们所要操作的地址和所要覆盖的地址的距离 IDA静态分析中常见的三种索引方式 a. 相对于栈基地址的索引,通过查看EBP相对偏移获得 char name[32]; [esp+0h] [ebp-28h] ==> 0×28+0×4 b. 相对于栈顶指针的索引,需要加上ESP到EBP的偏移,然后转换为a方式 c. 直接地址索引,相当于直接给出了地址

      覆写内容

      覆盖函数返回地址 覆盖栈上某个变量的内容,如局部变量和参数

      Ret2text

      返回到某个代码段的地址,如.text:0804863A mov dword ptr [esp], offset command ; "/bin/sh"要求我们控制程序执行程序本身已有的代码

      Ret2shellocde

      跳转到我们在栈中输入的代码,一般在没有开启NX保护的时候使用. ret2shellcode的目标即在栈上写入布局好的shellcode,利用ret_address返回到shellcode处执行代码。

      Ret2syscal

      让程序返回到系统调用,调用syscall或execve执行某个程序,对于静态编译的程序,没有libc,只好通过execve执行shellcode了。 syscall  --->rax syscall 0x3b  ==>execve rax •             --->rdi  path  ==> /bin/sh          rdi •             --->rsi  argv  /                           rsi •             --->rdx env                                        rdx int execve(const char *filename, char *const argv[],char *const envp[]);        execve("/bin/sh",null.null) 等同于system("bin/sh") syscall(32位程序为int80)会根据系统调用号查找syscall_table,execve对应的系统调用号是0x3b。 当我们给syscall的第一个参数即rax中写入0x3b时(32位程序为0xb),就会找到syscall_table[0x3b],即syscall_execve,然后通过execve启动程序。 找syscall和int 80的方法:ROPgadget –binary test –only ‘int/syscall’ 静态编译的程序没有system等函数的链接支持,故一般利用ret2syscall构造栈溢出

      Ret2libc

      如找到system函数在动态链接库libc中的地址,将return的内容覆盖为该地址,跳转执行 leak出libc_addr + call system + 执行system(‘/bin/sh’) 难点:libc动态加载,每次基址都会变化,如何泄露libc的地址? 思路:got —> read_addr() —>libc read_addr – libc_base = offsset (不变) libc_base = read_addr – offset bin/sh的来源 : 程序本身或libc或者写一个/bin/sh到bss段 binsh = libc.search(“/bin/sh”).next()

      其它

      判断是否是否为动态编译 ⚡ ⚙  ~/stack/day_4  ldd ret2text    linux-gate.so.1 =>  (0xf7f36000)    libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d60000)  -->libc的版本,也可以vmmap查看    /lib/ld-linux.so.2 (0xf7f38000) 判断libc的版本 a. 本地直接通过vmmap查看 b. 远程的根据函数后几位的偏移得到        libc-database            link:https://github.com/lieanu/libc-database.git            usage: ./find func_name offset            exemplify: ./find gets 5a0            effection:                    ➜  libc-database git :( master) ./find gets 5a0                    archive-eglibc (id libc6_2.17-93ubuntu4_i386) c: 5a0怎么来的?    .got.plt:0804A010 off_804A010     dd offset gets    pwndbg> x/20gz 0x0804a010 0x804a010 <gets@got.plt>:   0x08048476f7e643e0  0x08048496f7e64ca0 0x804a020 <__gmon_start__@got.plt>: 0x080484b6080484a6  0xf7e65360f7e1d540 0x804a030 <rand@got.plt>:   0x080484f6080484e6  0x0000000000000000 0x804a040 <stdin@@GLIBC_2.0>:   0x00000000f7fb75a0  0x0000000000000000 0x804a050:  0x0000000000000000  0x0000000000000000 0x804a060 <stdout@@GLIBC_2.0>:  0x00000000f7fb7d60  0x0000000000000000 0x804a070:  0x0000000000000000  0x0000000000000000 0x804a080:  0x0000000000000000  0x0000000000000000 0x804a090:  0x0000000000000000  0x0000000000000000 0x804a0a0:  0x0000000000000000  0x0000000000000000 64位程序和32位程序的区别 1. 传参方式        64位:rdi rsi rdx rcx r8 r9        32位:通过栈传参 2. syscall & int 80

      栈空间布局

      // 伪代码 A(int arg_a1,int arg_a2) B(int arg_b1,int arg_b2,int arg_b3) C() ------------------------------------- // B的压栈流程 ---> ESP                                 //指向栈顶,随着压栈不断抬高        buf[128]                    //局部变量        EBP                         //保存旧栈帧的底部,4字节        return                      //这是B的返回地址,即C        arg_b1        arg_b2        arg_b3              -->EBP                               //指向当前栈帧的底部,随着压栈不断抬高,指向旧栈帧

      栈溢出原理

      • 当局部变量buf超过128字节,会向下覆盖EBP,return以及参数的内容。

      • 构造return

      将buf 的 132到136字节的空间输入shellcode的地址 会跳转执行shellcode

      保护机制

      NX

      • 保护原理

      堆栈不可执行保护,bss段也不可执行,windows下为DEP,可通过gcc -z execstack关闭 开启NX后再把return的内容覆盖为一段shellcode,在开启NX的时候,不能执行。

      绕过原理 : 32位

      实现A函数执行的方法,即构建ROP链 return —> fake_addr —> A 将B的参数从arg_b2到arg_b3也覆盖成A的参数


      // 伪代码 A(int arg_a1,int arg_a2) B(int arg_b1,int arg_b2,int arg_b3) C(int arg_c1,int arg_c2) ------------------------------------- // B的压栈流程 ---> ESP          buf[128]        EBP          return //把return的内容覆盖为A的地址        arg_b1 //程序在调用A函数的时候,把下一个栈数据当作A的返回地址,因此需要在再下一条语句的时候开始覆盖参数        arg_b2  arg_a2 //将B的参数用A的参数覆盖掉        arg_b3 arg_a1 -->EBP


      借鉴上面的方法,在调用A之后,再调用C,构建ROP链 这时不能把系统认为的A的返回地址的arg_b1覆盖为C的返回地址,不然会向上覆盖arg_a2和arg_a2,导致A无法正常执行。 这时需要再找一个return语句,程序里面通常含有pop-pop-ret的链 ROPgadget –binary –only ‘pop|ret’ : 自动寻找rop链 Gadgets information ============================================================ 0x00000000004006ac : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x00000000004006ae : pop r13 ; pop r14 ; pop r15 ; ret 0x00000000004006b0 : pop r14 ; pop r15 ; ret //选择这个地址,代码段无NX 0x00000000004006b2 : pop r15 ; ret 0x00000000004006ab : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x00000000004006af : pop rbp ; pop r14 ; pop r15 ; ret 0x0000000000400550 : pop rbp ; ret 0x00000000004006b3 : pop rdi ; ret 0x00000000004006b1 : pop rsi ; pop r15 ; ret 0x00000000004006ad : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret 0x0000000000400499 : ret 将arg_b1覆盖为addr_pop_pop_ret的地址:4006b0 此时将将arg_a1 pop到r14,arg_a2 pop到r15,然后ret 将ret的内容覆盖为C的入口地址,即可! 程序执行如下代码: // 伪代码 A(int arg_a1,int arg_a2) B(int arg_b1,int arg_b2,int arg_b3) C(int arg_c1,int arg_c2) ------------------------------------- // B的压栈流程 ---> ESP          buf[128]        EBP          return //-->fake_addr_A        arg_b1 //-->4006b0  addr_pop_pop_ret        arg_b2  arg_a1 //pop r14        arg_b3 arg_a2 //pop r15        ret // --->fake_addr_C        0 // --->C的返回地址,现在没用了        arg_c1        arg_c2 -->EBP

      完整流程

      使用buf将栈空间覆盖 在B退出的时候ret到A 依次取覆盖之后的A的两个参数,执行A函数 返回到pop_pop_ret的地址 将ret的地址覆盖为C的地址 将C的返回地址置空 写入C的参数 执行C函数

      总结

      A函数的功能通常时”/bin/sh” C函数的功能为system 上述流程执行完则可以达到反弹shell的目的 由于程序不在栈上执行而是在代码段中执行,所有可以绕过NX保护机制。

      Canary(金丝雀)

      • 保护原理

      开启canary后,会在程序的EBP与ESP之间的位置随机插入一段md5值,占4字节或8字节。 canary为一段以 /0 结尾的一串md5值,如123456/0,起截断作用,防止打印。 在程序return之前与内核地址[fs:0x28]异或校验md5值 异或结果为1时报错退出,为0时正常ret。 几种思路 如果能在栈中拿到md5值,在指定位置可以精准覆盖之。 将从内核中取的md5值,设置为自己定义的值,覆盖的时候覆盖自己定义的值。

      gcc开启canary

      参数:-fstack-protector :启用保护,不过只为局部变量中含有数组的插入保护 参数:-fstack-protector-all :为所有函数插入保护 参数:-fstack-protector-strong -fstack-protector-explicit :只对明确有stack-protect 属性的函数启用保护 参数:-fo-stack-protector :禁用保护

      3种利用方法利用

      覆盖canary的最后一个字节 利用栈溢出将”\0″覆盖掉,则可以将canary打印出来。 smash leak stackguard — top

      查看开启的保护机制

      ⚡ > ~/stack/day_1> checksec leak_canary [*] '/root/stack/day_1/leak_canary'    Arch:     i386-32-little    RELRO:    Partial RELRO    Stack:    Canary found    NX:       NX enabled    PIE:      No PIE (0x8048000)

      PIE

      • 保护原理

      让程序能装载在随机的地址,主要是代码段的地址随机化,改变的是高位的基地址。 gdb中使用show proc info 可以显示代码段的基地址 –enabled-default-pie开启 -no-pie关闭

      • 通常与ALSR联合使用

      ALSR

      • 保护原理

      每次加载程序,使其地址空间分布随机化,即使可执行文件开启PIE保护,还需要系统开启ASLR才会真正打乱基址。主要是堆栈和libc的地址随机化。 修改/proc/sys/kernel/randommize_va_space来控制ASLR的开关。

      栈溢出进阶

      pwntools

      # Pwntools环境预设 from pwn import * context.arch = "amd64/i386" #指定系统架构 context.terminal = ["tmux,"splitw","-h"] #指定分屏终端 context.os = "linux"     #context用于预设环境 # 库信息 elf = ELF('./PWNME') # ELF载入当前程序的ELF,以获取符号表,代码段,段地址,plt,got信息 libc = ELF('lib/i386-linux-gnu/libc-2.23.so')  # 载入libc的库,可以通过vmmap查看 /* 首先使用ELF()获取文件的句柄,然后使用这个句柄调用函数,如 >>> e = ELF('/bin/cat') >>> print hex(e.address) # 文件装载的基地址 >>> print hex(e.symbols['write']) # plt中write函数地址 >>> print hex(e.got['write'])  # GOT表中write符号的地址 >>> print hex(e.plt['write']) # PLT表中write符号的地址                     */                                                           # Pwntools通信                     p = process('./pwnme') # 本地 process与程序交互 r = remote('exploitme.example.com',3333)    # 远程                     # 交互 recv() # 接收数据,一直接收 recv(numb=4096,timeout=default) # 指定接收字节数与超时时间                     recvuntil("111") # 接收到111结束,可以裁剪,如.[1:4] recbline() # 接收到换行结束 recvline(n) # 接收到n个换行结束 recvall() # 接收到EOF recvrepeat(timeout=default) #接收到EOF或timeout send(data) # 发送数据 sendline(data) # 发送一行数据,在末尾会加\n sendlineafter(delims,data) #   在程序接收到delims再发送data                   r.send(asm(shellcraft.sh()))  # 信息通信交互                                       r.interactive() # send payload后接收当前的shell                   # 字符串与地址的转换 p64(),p32()  #将字符串转化为ascii字节流 u64(),u32()  #将ascii的字节流解包为字符串地址          

      got & plt

      在IDA中选择view-open subview - segment可以直接查看到got和plt段

      .plt:08048440 ; __unwind { .plt:08048440                 push    ds:dword_804A004 .plt:08048446                 jmp     ds:dword_804A008 ;804A008是got表的地址 .plt:08048446 sub_8048440     endp


      plt段的某个地址存放着指令 jmp got


      .got.plt:0804A00C off_804A00C     dd offset printf        ; DATA XREF: _printf↑r .got.plt:0804A010 off_804A010     dd offset gets          ; DATA XREF: _gets↑r .got.plt:0804A014 off_804A014     dd offset time          ; DATA XREF: _time↑r .got.plt:0804A018 off_804A018     dd offset puts          ; DATA XREF: _puts↑r .got.plt:0804A01C off_804A01C     dd offset system        ; DATA XREF: _system↑r .got.plt:0804A020 off_804A020     dd offset __gmon_start__ .got.plt:0804A020                                         ; DATA XREF: ___gmon_start__↑r .got.plt:0804A024 off_804A024     dd offset srand         ; DATA XREF: _srand↑r .got.plt:0804A028 off_804A028     dd offset __libc_start_main .got.plt:0804A028                                         ; DATA XREF: ___libc_start_main↑r .got.plt:0804A02C off_804A02C     dd offset setvbuf       ; DATA XREF: _setvbuf↑r .got.plt:0804A030 off_804A030     dd offset rand          ; DATA XREF: _rand↑r .got.plt:0804A034 off_804A034     dd offset __isoc99_scanf


      got段中存放着程序中函数的地址,可以避免每次调用某个函数的时候去libc库中寻找。


      函数调用流程

      找到plt表.plt表存放指令 跳转到got表,got表存放地址,不能填在return的位置 找到对应的func_addr 没有的时候跳转到libc中取出函数,并缓存到got表


      plt2leakgot

      plt["write"](1,got("write"),4) 通过plt的write函数leak出got的地址


      libc_csu_init

      • 在所有的64位程序中都含有libc_csu_init函数

      .text:0000000000400650 __libc_csu_init proc near               ; DATA XREF: _start+16↑o .text:0000000000400650 ; __unwind { .text:0000000000400690 .text:0000000000400690 loc_400690:                             ; CODE XREF: __libc_csu_init+54↓j .text:0000000000400690                 mov     rdx, r13 // 4. 利用点,将r13给到了rdx .text:0000000000400693                 mov     rsi, r14 // 5. 控制rsi .text:0000000000400696                 mov     edi, r15d // 6. 控制rdi的低四位 .text:0000000000400699                 call    qword ptr [r12+rbx*8] //7. 给rbx赋0,相当于call [r12] .text:000000000040069D                 add     rbx, 1 .text:00000000004006A1                 cmp     rbx, rbp .text:00000000004006A4                 jnz     short loc_400690 .text:00000000004006A6 .text:00000000004006A6 loc_4006A6:                             ; CODE XREF: __libc_csu_init+36↑j .text:00000000004006A6                 add     rsp, 8 .text:00000000004006AA                 pop     rbx //1. 控制函数从这里执行 .text:00000000004006AB                 pop     rbp .text:00000000004006AC                 pop     r12 //8. 给r12添一个main_addr .text:00000000004006AE                 pop     r13 //2. 通过栈控制r13 .text:00000000004006B0                 pop     r14 .text:00000000004006B2                 pop     r15 .text:00000000004006B4                 retn   //3. ret到 mov  rdx, r13 .text:00000000004006B4 ; } // starts at 400650 .text:00000000004006B4 __libc_csu_init endp //实现通过栈控制rdx


      • 目的

        • 在64位提供三个参数的用法


      • 利用原理

      程序在启动main函数之前,都由glibc的标准c库启动,即由libc_csu_init启动main函数 libc_csu_init可以获得一个有4个参数调用的地方,比如系统调用函数syscall 如syscall—>rax syacall 0x3b ==>execve —>rdi path “/bin/sh” —>rsi argv —>rdx env 通过syscall调用execve,执行execve(“/bin/sh”,null,null),等价于system(“/bin/sh”) syscall(32位程序为int80)会根据系统调用号查找syscall_table,execve对应的系统调用号是0x3b。 当我们给syscall的第一个参数即rax中写入0x3b时(32位程序为0xb),就会找到syscall_table[0x3b],即syscall_execve,然后通过execve启动程序。 找syscall和int 80的方法:ROPgadget –binary test –only ‘int/syscall’


      ret2_dl_runtime_resolve

      解决32位无输出函数的情况,64位使用IOfile的方式。 找对应的plt中的地址 跳转到对应的got表中,got中如果有,则执行对应函数 如果跳转到的got对应位置没有值,got会向后累加一个地址,然后跳转到plt[0],压入两个参数,一个是index,一个是与DYNAMIC有关的参数,称为link_map(动态链接用到的名字,如puts),然后再调用dl_runtime_resolve(link_map_obj,reloc_index) push    cs:qword_602008 .plt:00000000004007A6                 jmp     cs:qword_602010 dl_runtime_resolve实际上是一个解析实际地址的函数,根据函数名称做解析,然后写回到plt的index对应的got 调用完之后,会根据参数调用解析出来的地址,比如解析出来的puts函数,那么会调用puts函数,并且写入puts_got中 结束后,接着运行程序 因此,我们向DYNAMIC中写入puts字符串就可以了


      二进制安全之栈溢出的评论 (共 条)

      分享到微博请遵守国家法律