Applications of AI in InfoSec — Writeup
| 模块 ID | 难度 | 预计时长 | 章节数 | 奖励 |
|---|---|---|---|---|
| 292 | Easy · Tier II | 8 小时 | 25(含 4 个交互式评估 + 1 个技能评估) | 20 Cubes |
模块链接: academy.hackthebox.com/module/details/292
目录
| # | 章节 | 类型 | 题目 |
|---|---|---|---|
| 1 | Introduction | Theory | — |
| 2 | Environment Setup | Interactive | Q1 ✅ |
| 3 | JupyterLab | Interactive | — |
| 4 | Python Libraries for AI | Theory | — |
| 5 | Datasets | Theory | — |
| 6 | Data Preprocessing | Theory | — |
| 7 | Data Transformation | Theory | — |
| 8 | Metrics for Evaluating a Model | Theory | — |
| 9 | Spam Classification | Theory | — |
| 10 | The Spam Dataset | Interactive | — |
| 11 | Preprocessing the Spam Dataset | Interactive | — |
| 12 | Feature Extraction | Interactive | — |
| 13 | Training and Evaluation (Spam Detection) | Interactive | — |
| 14 | Model Evaluation (Spam Detection) | Interactive | Q1 ✅ |
| 15 | Network Anomaly Detection | Interactive | — |
| 16 | Preprocessing and Splitting the Dataset | Interactive | — |
| 17 | Training and Evaluation (Network Anomaly Detection) | Interactive | — |
| 18 | Model Evaluation (Network Anomaly Detection) | Interactive | Q1 ✅ |
| 19 | Malware Classification | Theory | — |
| 20 | The Malware Dataset | Interactive | — |
| 21 | Preprocessing the Malware Dataset | Interactive | — |
| 22 | The Model | Interactive | — |
| 23 | Training and Evaluation (Malware Image Classification) | Interactive | — |
| 24 | Model Evaluation (Malware Image Classification) | Interactive | Q1 ✅ |
| 25 | Skills Assessment | Interactive | Q1 ✅ |
1. Introduction
知识要点
- 本模块构建三个完整的 AI 项目:垃圾短信分类器(NLP + Naive Bayes)、网络异常检测(表格数据 + Random Forest)、恶意软件图像分类(CNN + ResNet50 迁移学习)
- 全部代码以 Python 代码块形式提供,在 Jupyter Notebook 中按顺序执行
- 运行环境:Playground VM(
http://<TARGET_IP>:8888)或本地环境(建议 4GB+ RAM、4 核 CPU) - 模块评估方式:训练好的模型上传到 Playground VM 的评估端口,通过性能阈值后返回 flag
理解与洞察
本模块的价值不在于单个算法,而在于走完 ML 项目的完整闭环——从原始数据到可交付模型。三个项目正好覆盖了 scikit-learn(传统 ML)和 PyTorch(深度学习)两套工具链,以及文本 / 表格 / 图像三种数据类型。
一个关键认识:这个模块不考理论推导,考的是能不能跑通端到端流程。代码可以直接复制粘贴,但如果预处理流程和训练时不一致,模型上传后就会挂——这恰恰是真实 ML 工程中最常见的 bug 来源。
实践收获
建立了"数据 → 预处理 → 特征工程 → 模型训练 → 评估 → 部署"的完整心智模型,后续遇到新的 ML 任务时知道每一步该做什么。
2. Environment Setup
知识要点
- Miniconda:Anaconda 的精简版,提供
conda包管理器,可创建隔离的 Python 虚拟环境 - 安装方式:Windows 用 Scoop、macOS 用 Homebrew、Linux 下载安装脚本
- 创建虚拟环境:
conda create -n ai python=3.11→conda activate ai - 核心依赖安装:
conda install numpy scipy pandas scikit-learn matplotlib seaborn nltk+conda install pytorch torchvision - 频道配置:
conda config --add channels conda-forge等,channel_priority strict确保包版本一致性 conda config --set auto_activate_base false可禁止每次打开终端自动激活 base 环境
理解与洞察
用 Miniconda 而不是系统 Python 或 pip 的根本原因:ML 项目的依赖树极其复杂(PyTorch 需要匹配 CUDA 版本、scikit-learn 和 numpy 有 ABI 耦合),pip 经常在安装深度学习库时出现版本冲突。conda 能从源码级别管理二进制兼容性,是 ML 领域的事实标准。
易踩的坑
conda init之后必须重启终端才能生效,否则conda activate报错- PyTorch 安装时要注意指定 CUDA 版本(
pytorch-cuda=12.4),装错了 GPU 不会报错但训练会回退到 CPU,速度差 10 倍以上 - Playground VM 可以用但性能有限,本地跑训练效率高得多
练习题解
Q1: If you choose to use the Playground VM, you can start it here and familiarize yourself with the environment. We recommend keeping the VM running as you work through the module and follow along with the code snippets. Type DONE to continue.
答案: DONE
3. JupyterLab
知识要点
- JupyterLab:基于 Web 的交互式开发环境,数据科学和 ML 的标准工具
- 三种单元格类型:Code cells(执行 Python/R/Julia 代码)、Markdown cells(格式化文档)、Raw cells(原始文本)
- 有状态环境(Stateful):一个 cell 定义的变量、函数、导入在后续所有 cell 中持续可用,直到内核重启
- 执行代码:
Shift + Enter(执行并跳下一个)、Ctrl + Enter(执行不跳) - 重启内核:Kernel → Restart Kernel(清除所有变量但保留输出)或 Restart & Clear All Outputs
- 安装:
conda install -y jupyter jupyterlab notebook ipykernel
理解与洞察
Jupyter 的有状态特性是把双刃剑。优势是可以边写边看结果,逐步构建数据管线;风险是如果乱序执行 cell,变量状态会和代码顺序不一致。调试时的第一反应应该是 Restart & Run All,从头跑一遍确认状态一致。
实践收获
掌握了 Jupyter 作为 ML 实验环境的工作模式——用 notebook 做快速迭代和可视化探索,确认方案可行后导出为 .py 脚本做自动化和版本控制。
4. Python Libraries for AI
知识要点
- Scikit-learn:基于 NumPy/SciPy/Matplotlib 的传统 ML 库
- 数据预处理:
StandardScaler(标准化)、MinMaxScaler(归一化)、OneHotEncoder(分类编码)、SimpleImputer(缺失值填充) - 统一 API:
model.fit(X_train, y_train)训练 →model.predict(X_test)预测 - 模型评估:
train_test_split(数据拆分)、cross_val_score(交叉验证)、accuracy_score/f1_score(指标计算)
- 数据预处理:
- PyTorch:Facebook 开发的深度学习框架
- Tensor:类似 NumPy ndarray,但支持梯度追踪和 GPU 加速
- 动态计算图:前向传播时即时构建计算图,调试比静态图(TensorFlow 1.x)更直观
- 模型构建:
nn.Sequential(简单堆叠)或继承nn.Module(自定义前向传播) - 训练循环五步:
zero_grad()→ 前向传播 → 算 loss →backward()→step() - 数据加载:
Dataset+DataLoader实现批量迭代、shuffle、多进程并行 - 模型持久化:
torch.save(model.state_dict())保存参数 /torch.jit.script()保存完整模型
理解与洞察
scikit-learn 和 PyTorch 的本质区别不是"一个简单一个复杂",而是抽象级别不同。scikit-learn 封装了训练循环(fit() 一行搞定),适合结构化数据和经典算法;PyTorch 暴露训练循环的每一步,适合需要自定义网络结构、损失函数或训练策略的深度学习任务。选哪个的判断标准:如果 scikit-learn 有现成的算法能解决你的问题,就用 scikit-learn;需要 CNN/RNN/Transformer 时才上 PyTorch。
易混淆的概念
PyTorch Tensor 和 NumPy ndarray 看起来很像,但 Tensor 附带梯度追踪(requires_grad=True)且能在 GPU 上运算。两者之间互转需要 .numpy() / torch.from_numpy(),且 GPU 上的 Tensor 必须先 .cpu() 才能转 NumPy。
5. Datasets
知识要点
- 数据集四大类型:表格数据(CSV/数据库)、图像数据(像素数组)、文本数据(自然语言)、时间序列(带时间戳的序列)
- 高质量数据集的七个属性:相关性、完整性、一致性、准确性、代表性、平衡性、规模
- 示例数据集
demo_dataset.csv包含网络日志:source_ip、destination_port、protocol、bytes_transferred、threat_level - 数据探索三板斧:
df.head()(看样本)、df.info()(看类型和缺失)、df.isnull().sum()(统计缺失值)
理解与洞察
模型的上限是由数据决定的,不是算法。一个在脏数据上训练的复杂模型,不如在干净数据上训练的简单模型。拿到数据后的第一件事永远是看数据而非写模型——df.info() 中如果数字列出现 object 类型,几乎可以确定里面混了非数字字符串,需要清洗。
实践收获
建立了数据质量意识:数据集的"好"不是越大越好,而是要平衡(正负样本比例接近)和代表性(覆盖真实场景)。如果 99% 都是正常流量,模型全部预测"正常"就能拿 99% 准确率,但它什么也没学到——这就是 Section 8 中 Accuracy 指标可能骗人的数据根源。
6. Data Preprocessing
知识要点
- 数据预处理四大任务:清洗(处理缺失值/异常值)、变换(编码/缩放)、整合(合并多源数据)、格式化(类型转换/重塑)
- 无效值检测方法:正则校验 IP 格式、范围校验端口(0-65535)/ 字节数(≥0)/ 威胁等级(0-2)
- 处理无效数据两种策略:
- 丢弃:
data.drop(invalid.index, errors='ignore'),适合数据量大、坏数据少的情况 - 填充(Imputation):
SimpleImputer(strategy='median')用中位数填充数值列、strategy='most_frequent'填充分类列;KNNImputer基于邻居关系推断
- 丢弃:
- 统一无效值表示:先用
df.replace()把各种占位符(MISSING_IP、STRING_PORT、?)替换为NaN,再用pd.to_numeric(errors='coerce')转换
理解与洞察
这一节教的不只是 API 调用,而是一种排查思路——先用验证函数定位坏数据在哪,再根据数据量决定怎么处理。demo 数据集 100 条里有 23 条是坏的,丢完只剩 77 条——这种情况必须填充而不能丢弃。
关键技巧:先把所有形形色色的无效值统一替换成 NaN,再一次性用 SimpleImputer 处理,比逐个修复高效得多。
实践收获
掌握了"定位坏数据 → 决定策略 → 统一表示 → 批量处理"的数据清洗流程,这在任何 ML 项目中都是第一步。
7. Data Transformation
知识要点
- 编码分类特征:
OneHotEncoder:每个类别生成一个二元列(protocol_TCP=1/0),不引入顺序关系LabelEncoder:整数编码(TCP=0, UDP=1),会引入虚假排序,仅适用于有序分类
- 处理偏斜数据:对分布不均的数值特征做
np.log1p()对数变换,压缩极端值使分布更均匀;log1p而不是log因为log(0)未定义 - 数据拆分(训练/验证/测试 = 60%/20%/20%):
- 第一次
train_test_split(test_size=0.2)→ 80% 训练 + 20% 测试 - 第二次对 80% 做
train_test_split(test_size=0.25)→ 0.8×0.25=0.2 → 60% 训练 + 20% 验证
- 第一次
理解与洞察
One-Hot vs LabelEncoder 的陷阱是这节最重要的概念:LabelEncoder 把 TCP=0、UDP=1、HTTP=2,模型可能误以为 HTTP > UDP > TCP。规则很简单——无序分类变量永远用 One-Hot。
三段拆分中第二次的 test_size=0.25 容易让人困惑——它是对剩余 80% 的比例而非对整体的比例。记住 0.8 × 0.25 = 0.2 就不会搞混。
实践收获
掌握了 ML 数据管线的中间环节:清洗后的数据 → 编码分类变量 → 处理偏斜分布 → 拆分三段。每一步都有明确的"为什么":编码是因为算法只认数字,log 变换是因为极端值会主导模型,拆分是因为需要独立的验证/测试集来防止过拟合。
8. Metrics for Evaluating a Model
知识要点
- Accuracy =
(TP + TN) / 全部,整体正确率;类别不平衡时具有误导性 - Precision =
TP / (TP + FP),预测为正中真正为正的比例;高 Precision = 少误报 - Recall =
TP / (TP + FN),实际为正中被正确识别的比例;高 Recall = 少漏报 - F1-Score =
2 × Precision × Recall / (Precision + Recall),两者的调和平均 - 其他指标:Specificity(识别负样本的能力)、AUC-ROC(不同阈值下的综合能力)、Confusion Matrix(各类别预测明细)
理解与洞察
Accuracy 会骗人是这节最重要的一课。数据集 99% 是正常流量时,模型全部预测"正常"就能拿 99% Accuracy,但一条攻击都没抓到。这就是为什么安全领域几乎不单看 Accuracy。
Precision 和 Recall 本质上是一个跷跷板——提高阈值减少误报(Precision 升),但更多真正的攻击也会被漏掉(Recall 降)。F1-Score 提供的是两者的平衡点。
实践收获
建立了根据业务场景选指标的能力:入侵检测偏 Recall(宁可误报不能漏报);垃圾邮件过滤偏 Precision(不能把重要邮件扔进垃圾箱)。没有放之四海皆准的"最好指标",取决于错误的代价。
9. Spam Classification
知识要点
- 贝叶斯定理:
P(A|B) = P(B|A) × P(A) / P(B)——在观测到证据 B 后,更新对事件 A 的信念 - 应用到垃圾邮件检测:
P(Spam|Features) = P(Features|Spam) × P(Spam) / P(Features) - Naive Bayes 的"朴素"假设:特征之间条件独立,即
P(F1,F2|Spam) = P(F1|Spam) × P(F2|Spam) - 分类决策:分别计算 P(Spam|Features) 和 P(Not Spam|Features),取后验概率更大的类别
理解与洞察
"朴素"假设在现实中几乎总是不成立的——"free" 和 "prize" 在垃圾短信里高度相关,不是独立的。但 Naive Bayes 依然表现很好,原因是分类只需要比较两个概率的大小关系,不需要精确估计概率绝对值。条件独立假设虽然让概率估计不准,但不影响大小排序。
本节的计算示例很好地展示了贝叶斯更新的威力:先验 P(Spam)=0.3(30%),看到特征后后验 P(Spam|F1,F2)≈0.588(59%)——观测证据把信念从 30% 拉到了近 60%。
实践收获
理解了 Naive Bayes 在文本分类中的完整推理链——从先验概率到似然计算到后验比较,以及为什么一个"错误"的假设仍然能产生有效的分类器。
10. The Spam Dataset
知识要点
- SMS Spam Collection:5574 条短信,标注为 ham(正常)或 spam(垃圾),来自 UCI 机器学习仓库
- 数据格式:TSV(制表符分隔),加载时必须指定
sep="\t"、header=None、names=["label", "message"] - 数据检查三步:
df.head()(看解析是否正确)、df.isnull().sum()(检查缺失值)、df.duplicated().sum()(检查重复) - 去重:
df.drop_duplicates()去除 403 条重复项,剩余 5169 条
理解与洞察
这个数据集是 TSV 不是 CSV——不注意 sep="\t" 的话整行文本会被当成一个字段,后面所有步骤都会出错但不一定报错,只是结果一塌糊涂。
重复条目必须去掉:如果同一条短信出现多次,它可能同时出现在训练集和测试集中,导致测试分数虚高(模型"见过"这条数据而非真正学会分类)。
实践收获
养成了加载数据后立即 head() + info() + duplicated() 的习惯,在进入任何建模步骤之前先确认数据的完整性和正确性。
11. Preprocessing the Spam Dataset
知识要点
NLP 预处理标准流水线(依次执行):
- 转小写:
str.lower()— "Free" 和 "free" 合并为同一特征 - 去标点和数字:正则
[^a-z\s$!]— 保留$(暗示金额)和!(暗示紧迫感),这两个符号对垃圾短信有强区分力 - 分词:
word_tokenize()— 比split(" ")更准确,能正确处理缩写(如 don't → do, n't) - 去停用词:移除 the、is、and 等高频低信息量词,使用
nltk.corpus.stopwords - 词干提取:
PorterStemmer将 running/runs/ran → run,大幅减少词汇量 - 重新拼接:
" ".join(tokens)恢复为字符串,供 CountVectorizer 消费
理解与洞察
预处理的顺序不能乱——必须先分词再去停用词再做词干提取。如果先词干提取再去停用词,某些停用词的词干形式可能不在停用词列表里,导致漏删。
保留 $ 和 ! 是个值得注意的设计决策:大多数 NLP 教程会无脑去掉所有标点,但在垃圾短信分类这个特定场景下,这两个符号携带了关键的区分信息。预处理不是机械流程,需要结合领域知识做判断。
易踩的坑
训练和预测时必须用完全相同的预处理函数。Pipeline 只封装了 CountVectorizer 之后的步骤,前面的文本清洗(小写/正则/分词/去停用词/词干提取)需要手动保证一致性。任何差异都会导致词汇表对不上,预测结果无意义。
12. Feature Extraction
知识要点
- 词袋模型(Bag of Words):建立词汇表,每条消息变成一个向量,元素值 = 该词在该消息中出现的次数
- CountVectorizer 实现词袋模型,关键参数:
min_df=1:词至少在 1 个文档中出现才保留(实战可调高到 5 去除极罕见词)max_df=0.9:出现在 90%+ 文档中的词被排除(太普遍,相当于另一层停用词过滤)ngram_range=(1, 2):同时提取 unigram 和 bigram,捕获局部词序
- 输出:稀疏矩阵 X(行=文档数,列=词汇量),大部分元素为 0
- 标签转换:
y = df["label"].apply(lambda x: 1 if x == "spam" else 0)将 ham/spam 转为 0/1
理解与洞察
ngram_range=(1, 2) 是提升效果的关键参数。仅用 unigram 时 "free" 单独出现不一定是垃圾邮件("feel free to ask"),但 bigram "free prize" 几乎一定是。Bigram 补回了词袋模型丢失的局部词序信息。
输出矩阵非常稀疏——5000+ 条消息 × 几万个词汇,但每条消息平均只包含十几个词,99%+ 的元素是 0。scikit-learn 用 scipy.sparse 格式存储,内存效率远高于稠密矩阵。
实践收获
掌握了文本 → 数值特征的完整转换链:原始文本 → 预处理(Section 11)→ CountVectorizer → 可供 ML 模型消费的特征矩阵。这个模式适用于所有基于词袋的文本分类任务。
13. Training and Evaluation (Spam Detection)
知识要点
- Pipeline:将
CountVectorizer+MultinomialNB链式组合为一个统一的估计器pipeline.fit(X, y)自动先向量化再训练pipeline.predict(new_text)自动先向量化再预测joblib.dump(pipeline)保存完整流水线,加载后直接可用
- GridSearchCV:遍历超参数组合,用交叉验证选最优配置
- 搜索
alpha(拉普拉斯平滑因子)在 [0.01, 0.1, 0.15, 0.2, 0.25, 0.5, 0.75, 1.0] 中的最优值 - 评估指标:
scoring="f1",5-fold 交叉验证
- 搜索
- 模型评估:对新消息必须先手动执行与训练时完全相同的预处理,再调用
pipeline.predict() - 模型持久化:
joblib.dump()保存 /joblib.load()恢复,保存的是完整 Pipeline
理解与洞察
alpha 参数的直觉:MultinomialNB 的 alpha 是拉普拉斯平滑因子。如果 alpha=0,遇到训练集里没见过的新词时概率直接归零,导致整条消息的概率计算失效(一个零乘以任何数都是零)。alpha 越大越"保守"(概率分布越均匀),越小越"激进"(更信任训练数据)。
Pipeline 的局限性:Pipeline 只封装了 CountVectorizer 之后的步骤,前面的文本清洗(小写/正则/分词/去停用词/词干提取)不在 Pipeline 里。预测新消息时必须手动调用相同的 preprocess_message() 函数。
实践收获
掌握了 scikit-learn 的两个核心工程模式——Pipeline(保证训练和预测流程一致)和 GridSearchCV(自动化超参数搜索),这两个工具在任何 scikit-learn 项目中都会反复使用。
14. Model Evaluation (Spam Detection)
练习题解
Q1: What is the flag you get from submitting a good model for evaluation?
解题思路:
整体流程:下载 SMS Spam Collection 数据集 → 文本预处理(小写/去标点/分词/去停用词/词干提取)→ CountVectorizer 特征提取 → GridSearchCV 训练 MultinomialNB → 保存并上传。
完整训练代码:
import requests, zipfile, io, os, re, json
import pandas as pd
import numpy as np
import nltk
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from nltk.stem import PorterStemmer
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.model_selection import GridSearchCV
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline
import joblib
nltk.download("punkt", quiet=True)
nltk.download("punkt_tab", quiet=True)
nltk.download("stopwords", quiet=True)
# 1. 下载数据集
url = "https://archive.ics.uci.edu/static/public/228/sms+spam+collection.zip"
response = requests.get(url, verify=False)
with zipfile.ZipFile(io.BytesIO(response.content)) as z:
z.extractall("sms_spam_collection")
# 2. 加载数据
df = pd.read_csv(
"sms_spam_collection/SMSSpamCollection",
sep="\t", header=None, names=["label", "message"],
)
df = df.drop_duplicates()
# 3. 文本预处理
df["message"] = df["message"].str.lower()
df["message"] = df["message"].apply(lambda x: re.sub(r"[^a-z\s$!]", "", x))
df["message"] = df["message"].apply(word_tokenize)
stop_words = set(stopwords.words("english"))
df["message"] = df["message"].apply(lambda x: [w for w in x if w not in stop_words])
stemmer = PorterStemmer()
df["message"] = df["message"].apply(lambda x: [stemmer.stem(w) for w in x])
df["message"] = df["message"].apply(lambda x: " ".join(x))
# 4. 特征提取 + 训练
vectorizer = CountVectorizer(min_df=1, max_df=0.9, ngram_range=(1, 2))
y = df["label"].apply(lambda x: 1 if x == "spam" else 0)
pipeline = Pipeline([
("vectorizer", vectorizer),
("classifier", MultinomialNB())
])
param_grid = {"classifier__alpha": [0.01, 0.1, 0.15, 0.2, 0.25, 0.5, 0.75, 1.0]}
grid_search = GridSearchCV(pipeline, param_grid, cv=5, scoring="f1")
grid_search.fit(df["message"], y)
best_model = grid_search.best_estimator_
# 5. 保存模型
joblib.dump(best_model, "spam_detection_model.joblib")
上传模型:
curl -F "model=@spam_detection_model.joblib" http://<TARGET_IP>:8000/api/upload
答案: HTB{sp4m_cla55if13r_3v4lu4t0r}
15. Network Anomaly Detection
知识要点
- Random Forest:集成学习算法,构建多棵决策树并聚合结果(分类=多数投票,回归=取平均)
- 三个核心机制:
- Bootstrap 采样:有放回抽样创建多个训练子集,每棵树看到的数据不同
- 随机特征选择:每次分裂只考虑特征子集,降低树之间的相关性
- 投票聚合:单棵树可能不准,但多棵树投票后准确率大幅提升
- NSL-KDD 数据集:网络入侵检测的标准 benchmark,改进自 KDD Cup 1999(消除了冗余记录和类别不平衡)
- 数据包含 41 个特征(网络连接的统计属性)和攻击类型标签
理解与洞察
Random Forest 是这个任务的理想选择:网络流量数据有 40+ 个特征且维度很高,Random Forest 天然擅长处理高维数据,不需要特征缩放(基于分裂阈值而非距离计算),对异常值鲁棒,训练速度远快于深度学习,而且默认参数就能给出很好的效果。
NSL-KDD 之于网络入侵检测,就像 MNIST 之于图像分类——学术界的标准 benchmark。它修复了原始 KDD 数据集的两个致命问题:冗余记录(导致模型偏向频繁模式)和类别严重不平衡。
实践收获
理解了为什么"集成"比"单个"强:每棵树只看部分数据和部分特征,单独看可能不准,但 100 棵树投票后噪声被平均掉,准确率大幅提升。这个思想不局限于 Random Forest,是整个集成学习领域的基础。
16. Preprocessing and Splitting the Dataset
知识要点
- 二分类目标:
attack_flag— normal → 0,任何攻击 → 1 - 多分类目标:
attack_map— 将几十种具体攻击名映射为 5 类:- 0 = Normal,1 = DoS(neptune, smurf 等),2 = Probe(nmap, portsweep 等)
- 3 = Privilege Escalation(buffer_overflow, rootkit 等),4 = Access(guess_passwd, ftp_write 等)
- 分类变量编码:
pd.get_dummies(df[['protocol_type', 'service']])One-Hot 编码 - 数值特征:34 个统计指标(duration, src_bytes, dst_bytes, serror_rate 等)直接使用
- 数据拆分:80/20 分出测试集 → 从训练集中再 70/30 分出验证集
理解与洞察
二分类 vs 多分类的取舍:二分类(normal/attack)简单但信息量少——只知道"有攻击"但不知道什么类型。多分类(5 类)能区分攻击类型,对实际安全响应更有价值(DoS 需要限流,Probe 需要监控,Privilege Escalation 需要立即隔离)。本模块的评估端口要求的是多分类模型。
Random Forest 不需要特征缩放(它基于分裂阈值而非距离计算),所以 34 个数值特征可以直接用,不需要像 SVM/KNN 那样先做 StandardScaler。
易踩的坑
random_state=1337必须和教程一致,否则数据拆分不同,最终模型效果和预期可能有差异- 攻击名称列表(dos_attacks, probe_attacks 等)中有些拼写不直观(如
loadmdoule而非loadmodule),直接从教程复制,不要手打
17. Training and Evaluation (Network Anomaly Detection)
知识要点
- 训练:
RandomForestClassifier(random_state=1337)默认参数即可 - 评估指标:
accuracy_score、precision_score、recall_score、f1_score(多分类使用average='weighted') - 可视化:
confusion_matrix+seaborn.heatmap绘制混淆矩阵;classification_report输出各类别明细 - 分两轮评估:先在验证集上调参/确认方向,最终在测试集上报告性能
- 模型保存:
joblib.dump(rf_model, 'network_anomaly_detection_model.joblib')
理解与洞察
Random Forest 默认参数 + 无特征工程就达到了 99.5% F1,而 Spam Detection 的 Naive Bayes 精心调参才到 93%。这说明算法和数据的匹配度比调参更重要——Random Forest 天然适合高维表格数据,而文本分类有更多噪声需要对抗。
average='weighted' 的含义:多分类时 F1 有 macro(各类别等权平均)和 weighted(按样本数加权)两种。Privilege 类只有几十条样本,如果用 macro,它的低 F1 会严重拖低整体分数;weighted 按样本数加权更公平。
混淆矩阵怎么读:对角线 = 正确分类数量,非对角线 = 错误分类。如果 Probe 行的 Access 列有数字,说明有些 Probe 攻击被误判为 Access,可以针对性地改进。
实践收获
体验了从训练到评估到可视化的完整闭环,掌握了用混淆矩阵和分类报告定位模型弱点的方法——这比单看一个 F1 数字要有价值得多。
18. Model Evaluation (Network Anomaly Detection)
练习题解
Q1: What is the flag you get from submitting a good model for evaluation?
解题思路:
使用 NSL-KDD 数据集,将流量分为 5 类(Normal/DoS/Probe/Privilege/Access),采用 Random Forest。
完整训练代码:
import requests, zipfile, io
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, f1_score
import joblib
# 1. 下载数据集
url = "https://academy.hackthebox.com/storage/modules/292/KDD_dataset.zip"
response = requests.get(url, verify=False)
z = zipfile.ZipFile(io.BytesIO(response.content))
z.extractall('.')
# 2. 加载数据
columns = [
'duration', 'protocol_type', 'service', 'flag', 'src_bytes', 'dst_bytes',
'land', 'wrong_fragment', 'urgent', 'hot', 'num_failed_logins', 'logged_in',
'num_compromised', 'root_shell', 'su_attempted', 'num_root', 'num_file_creations',
'num_shells', 'num_access_files', 'num_outbound_cmds', 'is_host_login', 'is_guest_login',
'count', 'srv_count', 'serror_rate', 'srv_serror_rate', 'rerror_rate', 'srv_rerror_rate',
'same_srv_rate', 'diff_srv_rate', 'srv_diff_host_rate', 'dst_host_count', 'dst_host_srv_count',
'dst_host_same_srv_rate', 'dst_host_diff_srv_rate', 'dst_host_same_src_port_rate',
'dst_host_srv_diff_host_rate', 'dst_host_serror_rate', 'dst_host_srv_serror_rate',
'dst_host_rerror_rate', 'dst_host_srv_rerror_rate', 'attack', 'level'
]
df = pd.read_csv('KDD+.txt', names=columns)
# 3. 创建多分类目标
dos = ['apache2','back','land','neptune','mailbomb','pod','processtable','smurf','teardrop','udpstorm','worm']
probe = ['ipsweep','mscan','nmap','portsweep','saint','satan']
priv = ['buffer_overflow','loadmdoule','perl','ps','rootkit','sqlattack','xterm']
access = ['ftp_write','guess_passwd','http_tunnel','imap','multihop','named','phf',
'sendmail','snmpgetattack','snmpguess','spy','warezclient','warezmaster','xclock','xsnoop']
def map_attack(a):
if a in dos: return 1
elif a in probe: return 2
elif a in priv: return 3
elif a in access: return 4
else: return 0
df['attack_map'] = df['attack'].apply(map_attack)
# 4. 编码分类变量 + 选取数值特征
encoded = pd.get_dummies(df[['protocol_type', 'service']])
numeric_features = [
'duration','src_bytes','dst_bytes','wrong_fragment','urgent','hot',
'num_failed_logins','num_compromised','root_shell','su_attempted',
'num_root','num_file_creations','num_shells','num_access_files',
'num_outbound_cmds','count','srv_count','serror_rate','srv_serror_rate',
'rerror_rate','srv_rerror_rate','same_srv_rate','diff_srv_rate',
'srv_diff_host_rate','dst_host_count','dst_host_srv_count',
'dst_host_same_srv_rate','dst_host_diff_srv_rate',
'dst_host_same_src_port_rate','dst_host_srv_diff_host_rate',
'dst_host_serror_rate','dst_host_srv_serror_rate',
'dst_host_rerror_rate','dst_host_srv_rerror_rate'
]
train_set = encoded.join(df[numeric_features])
multi_y = df['attack_map']
# 5. 数据拆分
train_X, test_X, train_y, test_y = train_test_split(train_set, multi_y, test_size=0.2, random_state=1337)
multi_train_X, _, multi_train_y, _ = train_test_split(train_X, train_y, test_size=0.3, random_state=1337)
# 6. 训练 + 保存
rf_model = RandomForestClassifier(random_state=1337)
rf_model.fit(multi_train_X, multi_train_y)
joblib.dump(rf_model, 'network_anomaly_detection_model.joblib')
上传模型:
curl -F "model=@network_anomaly_detection_model.joblib" http://<TARGET_IP>:8001/api/upload
答案: HTB{n3tw0rk_tr4ff1c_4n0m4ly_d3t3ct0r}
19. Malware Classification
知识要点
- 恶意软件家族:按行为、传播方式、技术特征分类的恶意软件类别(如 Emotet、WannaCry),可在 malpedia 查看详细信息
- 传统分类方法:静态分析(反汇编/反编译)+ 动态分析(沙箱运行观察行为)+ 逆向工程,耗时且需要专业技能
- 恶意软件图像分类:将二进制文件的每个字节(0-255)映射为灰度像素值,生成可视化图像;同一家族的恶意软件因共享代码结构,图像纹理具有相似性
- 使用 CNN 对这些图像进行分类,将恶意软件家族识别问题转化为图像分类问题
理解与洞察
"把二进制画成图片"初看反直觉,但想清楚就很自然——二进制文件本质就是一串 0-255 的字节序列,每个字节映射为一个灰度像素就成了图像。关键 insight 是:同一家族的恶意软件因为共享代码段、打包方式和数据结构,生成的图像在视觉上呈现出相似的纹理模式——这就是 CNN 能分类的基础。
用图像分类的两个实际优势:(1)CNN 在图像分类上已经非常成熟(ResNet/VGG/EfficientNet),可以直接迁移使用;(2)操作图片不会感染你的机器,比直接分析恶意二进制文件安全得多。
实践收获
理解了如何通过数据表示的转换(二进制 → 图像)将一个难题(恶意软件分类)转化为已有成熟方案的问题(图像分类)。这种"把问题映射到已知领域"的思路在 ML 工程中非常常见。
20. The Malware Dataset
知识要点
- malimg 数据集:9339 张恶意软件灰度 PNG 图像,覆盖 25 个恶意软件家族
- 目录结构:每个家族一个文件夹,文件夹名 = 家族名(如
Adialer.C、Allaple.A、Rbot!gen等) - 图像来源:PE 文件(Windows 可执行文件)的每个字节值(0-255)直接映射为灰度像素亮度(0=黑,255=白)
- 图像尺寸不一致(因为不同二进制文件长度不同),后续需要统一 Resize
- 下载方式:
wget kaggle.com/.../malimg-original -O malimg.zip && unzip malimg.zip
理解与洞察
数据集的目录组织方式(每个家族一个文件夹)正好匹配 PyTorch ImageFolder 的预期格式——不需要手动写标注文件或 CSV 映射表,ImageFolder 自动把文件夹名作为标签。这是 PyTorch 图像分类项目中最常用的数据组织方式。
数据分布不均衡:平均每类 374 张,但有些类别不到 100 张,有些超过 1000 张。这种不平衡可能导致模型对小类别识别较差,实战中可以用 oversampling / class weights 缓解。
实践收获
掌握了恶意软件字节图(byteplot)的概念——每个像素就是二进制文件中一个字节的值。不同家族的恶意软件因代码结构和打包方式不同,字节图纹理差异明显,这为 CNN 分类提供了视觉基础。
21. Preprocessing the Malware Dataset
知识要点
- 数据拆分:使用
split-folders库按 80/20 比例拆分训练集和测试集(splitfolders.ratio(ratio=(0.8, 0, 0.2))) - 图像预处理(
transforms.Compose):Resize((75, 75)):统一所有图像尺寸(原始大小因二进制长度不同而不一)ToTensor():将 PIL Image 转为 PyTorch Tensor,像素值从 [0,255] 缩放到 [0,1]Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]):ImageNet 预训练模型要求的标准化参数
- ImageFolder:从目录结构自动加载数据集(文件夹名=类名),自动生成标签
- DataLoader:封装批量迭代,关键参数
batch_size(每批样本数)、shuffle(是否打乱)、num_workers(并行加载进程数)
理解与洞察
Normalize 的 mean/std 值为什么不是自己数据集的统计值? 因为我们用的 ResNet50 是在 ImageNet 上预训练的,它的卷积核权重是基于 ImageNet 的分布学习的。输入数据必须用同样的标准化参数,否则预训练权重的响应会偏移,特征提取效果大打折扣。
Resize 到 75×75 的权衡:更大(如 224×224,ResNet 的原始输入尺寸)保留更多纹理细节,但训练更慢、内存更大;75×75 是教程为了训练速度做的折中,牺牲了一些精度。
batch_size 的选择逻辑:太小(32)每个 batch 梯度噪声大、训练慢;太大(2048)可能超出 GPU 显存。512 是在大部分 GPU 上能跑且效率不错的选择。
实践收获
掌握了 PyTorch 图像数据管线的标准三件套:transforms(预处理)→ ImageFolder(数据集)→ DataLoader(迭代器)。这个模式适用于所有 PyTorch 图像分类项目。
22. The Model
知识要点
- 基于 ResNet50 的迁移学习:加载 ImageNet 预训练权重(
weights='DEFAULT'),50 层深、约 2300 万参数 - 冻结策略:
requires_grad = False冻结所有预训练层,只训练替换后的最后一层 - 自定义全连接层:
Linear(2048, 1000) → ReLU → Linear(1000, n_classes)- 2048 = ResNet50 倒数第二层的输出维度
- 1000 = 可调的隐藏层大小
- n_classes = 25(动态从
len(train_dataset.classes)获取)
- 模型定义继承
nn.Module,需实现__init__()和forward()方法
理解与洞察
迁移学习为什么能用:ResNet50 在 ImageNet 上学到的低层特征(边缘、纹理、形状)是通用的,对恶意软件字节图同样适用。我们只需要替换最后一层来适配 25 个类别,不需要从头训练 2300 万个参数。
冻结 vs 不冻结的权衡:冻结后只训练最后的全连接层(约 200 万参数 vs 全量 2300 万),训练速度快 10 倍以上。代价是底层特征提取器无法针对恶意软件图像的特殊纹理微调。实测冻结仍能达到 ~89% 测试准确率,PoC 够用;如果追求更高精度可以逐步解冻更多层。
n_classes 动态获取的好处:用 len(train_dataset.classes) 而非硬编码 25,增删恶意软件家族后代码无需修改。
实践收获
掌握了 PyTorch 中迁移学习的标准模式:加载预训练模型 → 冻结层 → 替换最后一层 → 训练。这个模式适用于绝大多数图像分类任务,只需要改最后一层的输出维度。
23. Training and Evaluation (Malware Image Classification)
知识要点
- 训练循环五步模板(每个 batch 重复):
optimizer.zero_grad()— 清除上一步梯度(PyTorch 默认累加梯度)outputs = model(inputs)— 前向传播loss = criterion(outputs, labels)— 计算 CrossEntropyLossloss.backward()— 反向传播,计算每个参数的梯度optimizer.step()— Adam 优化器用梯度更新参数
- 评估模式:
model.eval()+torch.no_grad()关闭梯度计算和 BatchNorm/Dropout 的训练行为 - 模型保存:
torch.jit.script(model)序列化为 TorchScript(.pth),包含模型结构+参数,可独立加载 - 训练参数:10 epoch、batch_size=512、Adam 优化器(默认学习率)、CrossEntropyLoss
- 实测效果:训练准确率 ~96%,测试准确率 ~89%
理解与洞察
model.eval() vs model.train() 不只是语义标记:eval 模式下 BatchNorm 使用全局统计量而非 batch 统计量,Dropout 停止随机置零。忘了切到 eval 模式会导致每次推理结果不一致。
为什么用 jit.script 而非 state_dict:torch.save(model.state_dict()) 只保存参数权重,加载时需要先实例化同样结构的模型类。jit.script 把结构+参数一起序列化,评估端口不需要你的 MalwareClassifier 类定义就能加载——这对模型交付至关重要。
保存前必须 .to("cpu"):GPU 上训练的模型内部引用了 GPU 设备,在纯 CPU 环境加载会报错。
实践收获
- 掌握了完整的 PyTorch 训练→评估→保存闭环
- 体验了 GPU 加速的实际效果:CPU 每 epoch ~210 秒 vs MPS ~19 秒(11 倍加速),CUDA GPU 可能更快
- 理解了 scikit-learn(
fit()一行)和 PyTorch(手写训练循环)两种范式的差异和各自适用场景
24. Model Evaluation (Malware Image Classification)
练习题解
Q1: What is the flag you get from submitting a good model for evaluation?
解题思路:
使用 malimg 数据集(25 个恶意软件家族的字节图),基于预训练 ResNet50 进行迁移学习。
前置准备:
pip3 install torch torchvision split-folders
wget https://www.kaggle.com/api/v1/datasets/download/ikrambenabd/malimg-original -O malimg.zip
unzip malimg.zip
完整训练代码:
import os, time
import torch
import torch.nn as nn
import torchvision.models as models
from torchvision import transforms
from torch.utils.data import DataLoader
from torchvision.datasets import ImageFolder
import splitfolders
# 自动检测 GPU:CUDA > MPS (Apple Silicon) > CPU
if torch.cuda.is_available():
device = torch.device("cuda")
elif torch.backends.mps.is_available():
device = torch.device("mps")
else:
device = torch.device("cpu")
print(f"Using device: {device}")
# 1. 分割数据集(80% train / 20% test)
splitfolders.ratio(
input="./malimg_paper_dataset_imgs/",
output="./newdata/",
ratio=(0.8, 0, 0.2)
)
# 2. 数据加载与预处理
transform = transforms.Compose([
transforms.Resize((75, 75)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
train_dataset = ImageFolder(root="./newdata/train", transform=transform)
test_dataset = ImageFolder(root="./newdata/test", transform=transform)
train_loader = DataLoader(train_dataset, batch_size=512, shuffle=True, num_workers=0)
test_loader = DataLoader(test_dataset, batch_size=1024, shuffle=False, num_workers=0)
n_classes = len(train_dataset.classes)
# 3. 模型定义(ResNet50 迁移学习,冻结除最后一层外所有权重)
class MalwareClassifier(nn.Module):
def __init__(self, n_classes):
super().__init__()
self.resnet = models.resnet50(weights='DEFAULT')
for param in self.resnet.parameters():
param.requires_grad = False
num_features = self.resnet.fc.in_features
self.resnet.fc = nn.Sequential(
nn.Linear(num_features, 1000),
nn.ReLU(),
nn.Linear(1000, n_classes)
)
def forward(self, x):
return self.resnet(x)
model = MalwareClassifier(n_classes).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())
# 4. 训练
for epoch in range(10):
model.train()
running_loss, n_total, n_correct = 0, 0, 0
t0 = time.time()
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
_, predicted = outputs.max(1)
n_total += labels.size(0)
n_correct += predicted.eq(labels).sum().item()
running_loss += loss.item()
acc = 100 * n_correct / n_total
print(f"Epoch {epoch+1}/10: Acc={acc:.2f}% Loss={running_loss/len(train_loader):.4f} ({time.time()-t0:.1f}s)")
# 5. 评估
model.eval()
n_correct, n_total = 0, 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
_, predicted = torch.max(output, 1)
n_total += target.size(0)
n_correct += (predicted == target).sum().item()
print(f"Test accuracy: {100*n_correct/n_total:.2f}%")
# 6. 保存(需要移回 CPU 再 jit.script)
model_cpu = model.to("cpu")
model_scripted = torch.jit.script(model_cpu)
model_scripted.save("malware_classifier.pth")
上传模型:
curl -F "model=@malware_classifier.pth" http://<TARGET_IP>:8002/api/upload
答案: HTB{9569648083a8106ba057bbbe2d00d8ec}
25. Skills Assessment
练习题解
Q1: What is the flag you get from submitting a good model for evaluation?
解题思路:
IMDB 影评情感分析:判断影评正面(1)或负面(0)。数据集为 JSON 格式,25000 条影评。使用 TF-IDF + LinearSVC 比 Naive Bayes 效果更好。
完整训练代码:
import os, re, json
import pandas as pd
import nltk
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from nltk.stem import PorterStemmer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.svm import LinearSVC
from sklearn.pipeline import Pipeline
import joblib
import requests, zipfile, io
nltk.download("punkt", quiet=True)
nltk.download("punkt_tab", quiet=True)
nltk.download("stopwords", quiet=True)
# 1. 下载数据集
url = "https://academy.hackthebox.com/storage/modules/292/skills_assessment_data.zip"
response = requests.get(url, verify=False)
with zipfile.ZipFile(io.BytesIO(response.content)) as z:
z.extractall(".")
# 2. 加载数据
with open("train.json") as f:
train_data = json.load(f)
df = pd.DataFrame(train_data)
df = df.drop_duplicates(subset=['text'])
# 3. 预处理
stop_words = set(stopwords.words("english"))
stemmer = PorterStemmer()
def preprocess(text):
text = str(text).lower()
text = re.sub(r"<[^>]+>", " ", text) # 移除 HTML 标签
text = re.sub(r"[^a-z\s$!]", "", text) # 保留字母、空格、$ 和 !
tokens = word_tokenize(text)
tokens = [w for w in tokens if w not in stop_words]
tokens = [stemmer.stem(w) for w in tokens]
return " ".join(tokens)
df['processed'] = df['text'].apply(preprocess)
y = df['label'].astype(int)
# 4. 训练 TF-IDF + LinearSVC
pipeline = Pipeline([
("vectorizer", TfidfVectorizer(
min_df=2, max_df=0.9,
ngram_range=(1, 2),
max_features=80000,
sublinear_tf=True
)),
("classifier", LinearSVC(C=1.0, max_iter=10000))
])
pipeline.fit(df['processed'], y)
# 5. 保存
joblib.dump(pipeline, "skills_assessment.joblib")
上传模型:
curl -F "model=@skills_assessment.joblib" http://<TARGET_IP>:5000/api/upload
答案: HTB{s3nt1m3nt_4n4lys1s_d4t4}
答案速查
| 章节 | 题号 | 答案 |
|---|---|---|
| 2 - Environment Setup | Q1 | DONE |
| 14 - Model Evaluation (Spam Detection) | Q1 | HTB{sp4m_cla55if13r_3v4lu4t0r} |
| 18 - Model Evaluation (Network Anomaly Detection) | Q1 | HTB{n3tw0rk_tr4ff1c_4n0m4ly_d3t3ct0r} |
| 24 - Model Evaluation (Malware Image Classification) | Q1 | HTB{9569648083a8106ba057bbbe2d00d8ec} |
| 25 - Skills Assessment | Q1 | HTB{s3nt1m3nt_4n4lys1s_d4t4} |