Neural Detonator
AI摘要:本文介绍了HTB上挑战Neural Detonator的过程。其中提供了一个Keras模型文件,其中隐藏了恶意代码。通过逆向分析模型中的Lambda层,发现其包含Base64编码的序列化字节码,该代码会提取特定层的权重生成密钥,并解密另一层中存储的加密数据,最终获取Flag:HTB{d33p_l4y3r_d3t0n8}。解题脚本通过静态解析模型权重文件,重现了解密逻辑,无需运行模型即可得到Flag。
1. 题目概览
Neural Detonator是HTB上一个评级为Hard的场景,不过没想到已经是AI and ML Exploitation系列中最难的场景了
我们得到了一个机器学习模型文件 mlcious.keras。题目描述暗示模型中隐藏了一个“意图”,需要我们拿到 flag。
A standalone machine learning file surfaced on Volnaya’s firmware staging server. No docs. No entrypoint. No task. Just quiet intent. It’s waiting for something. So are we.
2. 分析过程
2.1 模型结构分析
mlcious.keras 本质上是一个 ZIP 压缩包(Keras v3 格式)。解压后,我们发现主要包含:
config.json: 模型架构配置。model.weights.h5: 模型的权重文件(HDF5 格式)。metadata.json: 元数据。
查看 config.json,我们注意到一个异常的层定义。在 layers 列表中,有一个名为 activation_adapter 的 Lambda 层,其函数体并非普通的 Python 代码,而是一段 Base64 编码的字符串。
{
"class_name": "Lambda",
"config": {
"name": "activation_adapter",
"function": [
"4wEAAAAAAAAAAAAAAAMAAAAMAAAATwAAAHMWAAAAcgoAAAAgICAgICAgICBAcgkAAADaCnRyYW1wb2xpbmVyMAEAAAQAAABzjQEAAPiAANgJVtAJVpVSlFmUXNcVMtIVMtEVNNQVNNAJVtEJVtQJVtBXWNQJWYBC2AlU0AlUlVKUWZRc1xUy0hUy0RU01BU00AlU0QlU1AlU0FVW1AlXgELdCQuMGNQJGdcJI9IJI6BC0Qkn1AkngELdCQuMGNQJGdcJI9IJI6BC0Qkn1AkngELdCxGMPZgUnXecfKhCr0qqSqlMrEy4Mr86ujq5PLw80SxH0R9I1B9I1x9P0h9P0R9R1B9R0FJU0FNU0FJU1B9V0QtW1AtW0FdY1AtZgETdChCMLZgE0Qod1Aod1won0gonqALRCivUCiuAQ90LEPAAABFOTwPwAAARTk8D8AAAEU5PA/AAABFOTwOtWfAAADhMTwPwAAA4TE8D8AAAOExPA/EAAC5NTwP0AAAuTU8D8AAAEU5PA/EAABFOTwP0AAARTk8D8QAADE5PA/QAAAxOTwOARNgJC4BCjVSVJ5QtoATREiXUEiWgctENKtQNKtANKtgLGIgyiGmMPZgRmEHRCx7UCx7QBB5yCwAAACkHciYBAADaCnRlbnNvcmZsb3dyEwEAAHIgAQAAchoBAAByHAEAAHIwAQAAchAAAAByCwAAAHIJAAAA+gg8bW9kdWxlPnIyAQAAAQAAAHNaAAAA8AMBAQHgADnQADnQADnQADnQADnQADnQADnQADnQADnQADnQADnQADnQADnQADnQADnQADnQADnQADnQADnwBAkBH/AACQEf8AAJAR/wAAkBH/AACQEfcgsAAAA=",
null,
null
],
"function_type": "lambda"
}
}2.2 逆向字节码
这段 Base64 字符串解码后是 Python 的 marshal 序列化数据。通过编写脚本进行反序列化和反汇编 (dis 模块),我们分析了其内部指令。
(注:如果本地 Python 版本与 marshal 数据版本不匹配,可能需要更换 Python 3.10/3.11 环境进行分析)
第一层:Trampoline (引导程序)
反汇编结果显示,该函数包含两个列表推导式(List Comprehension)和一个生成器表达式,用于寻找特定的层并计算密钥。
关键反汇编片段分析:
寻找 Seed 层:
代码遍历变量,检查name属性是否包含特定字符串。10 LOAD_CONST 0 ('seed_dense/kernel') 12 LOAD_FAST 1 (v) 14 LOAD_ATTR 0 (name) 24 CONTAINS_OP 0这表明它在寻找名为
seed_dense/kernel和seed_dense/bias的变量。派生密钥:
可以看到hashlib.sha1和struct.unpack的调用,以及random.Random的初始化。730 LOAD_METHOD 15 (sha1) ... 754 LOAD_METHOD 10 (tobytes) ... 868 LOAD_METHOD 16 (digest) ... 704 LOAD_METHOD 14 (unpack) 726 LOAD_CONST 7 ('<I') ... 928 LOAD_METHOD 17 (Random) 966 LOAD_METHOD 18 (randbytes) 988 LOAD_CONST 9 (32)逻辑推导:
key = random.Random(struct.unpack('<I', sha1(kernel + bias).digest()[:4])[0]).randbytes(32)解密 Payload:
代码加载了一个长列表(加密的 Payload),并与生成的 Key 进行 XOR 运算。42 BINARY_OP 12 (^) ; XOR 操作
还原后的 Trampoline 伪代码:
def trampoline(ns):
# ... imports ...
# 寻找 seed_dense 层
kernel = [v for v in global_vars if 'seed_dense/kernel' in v.name][0]
bias = [v for v in global_vars if 'seed_dense/bias' in v.name][0]
# 计算种子
data = kernel.tobytes() + bias.tobytes()
seed = struct.unpack('<I', hashlib.sha1(data).digest()[:4])[0]
# 生成密钥
key = random.Random(seed).randbytes(32)
# 解密并执行 Payload
exec(marshal.loads(xor_decrypt(enc_payload, key)), ns)第二层:Payload (载荷)
解密出的 Payload 同样是一段字节码。
关键反汇编片段分析:
寻找 Payload 层:
10 LOAD_CONST 0 ('payload_dense/bias') 14 LOAD_ATTR 0 (name)处理数据:
取前 22 个元素,乘以 255,转换为uint8。200 LOAD_CONST 3 (22) ; Slice end index 202 BUILD_SLICE 2 204 BINARY_SUBSCR ; 取 [:22] 214 LOAD_CONST 4 (255.0) 216 BINARY_OP 5 (*) ; 乘以 255 232 LOAD_ATTR 8 (uint8) 246 CALL 2 ; Cast to uint8解密 Flag:
再次进行 XOR 运算。42 BINARY_OP 12 (^)
还原后的 Payload 伪代码:
def payload():
# 寻找 payload_dense 层
bias = [v for v in global_vars if 'payload_dense/bias' in v.name][0]
# 提取密文
cipher = (bias.numpy()[:22] * 255).astype('uint8').tobytes()
# 解密 Flag
# 使用相同的 key
flag = xor_decrypt(cipher, key)
return flag3. 解题脚本
我们需要编写脚本来执行上述逆向过程。由于 mlcious.keras 中的权重存储在 HDF5 文件中,我们需要使用 h5py 库来读取它们。
完整脚本代码 (solve.py)
import zipfile
import h5py
import os
import hashlib
import struct
import random
import numpy as np
def solve():
print("[*] Extracting model.weights.h5 from mlcious.keras...")
# 1. 从 keras 文件中解压权重文件
if not os.path.exists('model.weights.h5'):
with zipfile.ZipFile('mlcious.keras', 'r') as z:
z.extract('model.weights.h5', '.')
try:
print("[*] Reading H5 file...")
with h5py.File('model.weights.h5', 'r') as f:
# 在 H5 文件中,层名称通常映射为 layers/{layer_name}/vars/...
# 通过分析文件结构确认:
# seed_dense -> layers/dense (kernel在vars/0, bias在vars/1)
# payload_dense -> layers/dense_1 (bias在vars/1)
# 2. 获取 seed_dense 的权重用于生成密钥
print("[*] Loading seed_dense weights...")
# 注意:这里的路径 layers/dense/vars/0 是通过查看 h5 文件结构确定的
# 实际题目中可能需要先遍历 h5 文件找到对应层名的路径
w1_dset = f['layers/dense/vars/0'] # kernel
b1_dset = f['layers/dense/vars/1'] # bias
w1 = np.array(w1_dset)
b1 = np.array(b1_dset)
# 3. 派生密钥 (Derive Key)
# 逻辑:sha1(kernel_bytes + bias_bytes) -> 取前4字节做种子 -> 生成32字节密钥
print("[*] Deriving key...")
data = w1.tobytes() + b1.tobytes()
digest = hashlib.sha1(data).digest()
seed = struct.unpack("<I", digest[:4])[0]
print(f" Derived Seed: {hex(seed)}")
rng = random.Random(seed)
key = list(rng.randbytes(32))
print(f" Key: {key}")
# 4. 获取密文 (Payload)
print("[*] Loading payload_dense bias...")
pb_dset = f['layers/dense_1/vars/1'] # bias
payload_bias = np.array(pb_dset)
# 逻辑:取前22个浮点数 * 255 转为 uint8
target = payload_bias[:22]
enc_flag_bytes = (target * 255).astype("uint8")
cipher = list(enc_flag_bytes)
print(f" Cipher: {cipher}")
# 5. 解密 Flag
print("[*] Decrypting flag...")
flag_bytes = bytes(c ^ key[i % 32] for i, c in enumerate(cipher))
try:
flag = flag_bytes.decode('utf-8')
print(f"\n[+] Flag: {flag}")
except UnicodeDecodeError:
print(f"\n[!] Decrypted bytes (raw): {flag_bytes}")
except Exception as e:
print(f"[-] Error: {e}")
import traceback
traceback.print_exc()
finally:
# 清理解压的文件
if os.path.exists('model.weights.h5'):
os.remove('model.weights.h5')
print("[*] Cleaned up extracted files.")
if __name__ == "__main__":
solve()4. 运行结果
运行上述脚本,我们将得到:
[*] Extracting model.weights.h5 from mlcious.keras...
[*] Reading H5 file...
[*] Loading seed_dense weights...
[*] Deriving key...
Derived Seed: 0x2e07104b
Key: [121, 49, 123, 80, 247, 242, 45, 15, 160, 40, 105, 188, 87, 121, 50, 60, 95, 160, 39, 116, 89, 133, 130, 236, 99, 229, 88, 196, 140, 82, 130, 137]
[*] Loading payload_dense bias...
Cipher: [49, 101, 57, 43, 147, 193, 30, 127, 255, 68, 93, 197, 100, 11, 109, 88, 108, 212, 23, 26, 97, 248]
[*] Decrypting flag...
[+] Flag: HTB{d33p_l4y3r_d3t0n8}
[*] Cleaned up extracted files.