TT Lab
开始
学习 学习路径 课程

闭网现场 — 国防领域

在解包之前对带入介质做检疫

在 TT Lab 中继续学习

目标

编写一个检疫器:在解压导入介质的压缩包之前,只读取列表,判定规模、危险条目、压缩比和格式不一致;再用提取过滤器逐条筛选,解压到隔离目录,并撰写附有依据哈希的导入判定书。

为什么重要

进入隔离网络的东西,大多是人带来的介质,里面装着一个压缩包。为了看里面而解压的那一行命令,本身就已经是在触碰文件系统。绝对路径、上级路径逃逸、指向外部的链接、特殊文件、压缩炸弹,都是在解压的瞬间发生作用。 tar 头部写着名称、大小、权限、条目类型和链接目标,所以不解压也能把这五项全部看到。因此要改变顺序:先读取,做出判定,附带条件地解压。 判定书上必须附带依据。哪个归档的哪个哈希,用哪份报告读取,该报告得出了什么原因,这些必须连起来,合作方才知道该改什么,审查人员也才能再次作出同样的判定。 评分器不会相信你写的文字。它会亲自打开归档,自己计算应当得出的值,并按运行约定重新运行你的检疫器,连其输出也一并核对。

步骤

  1. 用生成脚本创建两个介质,并在受理登记簿 /root/media/intake.tsv 中写入序列号、接收时间、提交者角色、外层哈希、大小。
  2. 创建 /root/media/inspect.py,不解压 MEDIA-2609-017,只读取列表,把条目数、总解压大小、最大条目写入 /root/media/report/MEDIA-2609-017.json。
  3. 在 inspect.py 中加入 danger 分类。设置 absolute、traversal、escaping_link、special、exec_bit 五栏,没有对应项的栏也保留为空列表。
  4. 在 inspect.py 中加入 ratio 和 ratio_verdict。总解压大小除以压缩包大小所得的值超过 100,则为 bomb。
  5. 在 inspect.py 中加入 mismatch。把扩展名所说的格式与开头字节所说的格式不同的条目,用 name、declared、actual 写出。
  6. 在 inspect.py 中加入 --extract-to,用 data 过滤器逐条判定,解压到 /root/media/quarantine/MEDIA-2609-017,并在报告中写入 extracted 和 rejected。
  7. 用 /root/media/decide.py 把报告转换成导入判定书 /root/media/report/verdict-MEDIA-2609-017.json。写入 media、archive_sha256、report_sha256、decision、reasons、accepted_entries、rejected_entries。
  8. 把同一个检疫器和同样的判定规则用于 MEDIA-2609-018,生成 /root/media/report/MEDIA-2609-018.json 和 /root/media/report/verdict-MEDIA-2609-018.json,并写出把两个介质并排放置的 /root/media/report/compare.tsv。

参考

打开介质之前先写受理登记簿

用生成脚本创建两个介质,并在 /root/media/intake.tsv 中写入 1 行表头和 2 行介质。各栏是 serial、received_at、submitter_role、sha256、bytes,用制表符分隔。

没有从外部获取的样本,所以用 python3 亲自创建。种子和 mtime 是固定的,运行多少次都会得到同样的介质。哈希不解压归档,直接对文件本身计算——这个值是日后追问“这是不是当时收到的那个文件”的唯一依据。提交者不写人名,而写角色。

不解压,只读取列表来测量规模

让 /root/media/inspect.py 不解压 MEDIA-2609-017,只读取头部,把 archive_sha256、compressed_bytes、entries、total_uncompressed_bytes、largest_entry_bytes、largest_entry_name 写入 /root/media/report/MEDIA-2609-017.json。

tarfile.open(路径, 'r:gz').getmembers() 会原样返回头部列表。每个条目的 size 是头部中写的值,所以不需要读取内容。archive_sha256 是归档文件本身的哈希,compressed_bytes 是该文件的大小。

把危险条目分成五类

让 inspect.py 在报告中加入 danger。在 absolute、traversal、escaping_link、special、exec_bit 五栏中写入条目名称列表,没有对应条目的栏也保留为空列表。

名称以斜杠开头的就是绝对路径。上级路径逃逸不是看字符串里有没有 '..',而是按层级计算深度来判定。链接请把 issym 和 islnk 分开看——符号链接以它自己所在位置为基准,硬链接以归档根为基准。特殊文件是 ischr、isblk、isfifo。

用压缩比识别炸弹

让 inspect.py 在报告中加入 ratio 和 ratio_verdict。ratio 是总解压大小除以压缩包大小,并在小数点后第二位四舍五入所得的值,超过 100 则 ratio_verdict 为 bomb,否则为 ok。

两个值报告里都已经有了。不要解压后再测量——那就变成了为了判定炸弹而去解压炸弹。把阈值提取成常量,下一个人就可以按所在组织的标准修改。

核对扩展名与实际格式

让 inspect.py 在报告中加入 mismatch。对于普通文件中,扩展名所说的格式与开头字节所说的格式不同的条目,用 name、declared、actual 写出。

extractfile 不会解压到磁盘,只是打开一个流。开头的 512 个字节就足够了。gzip 以 1f 8b 开头,ELF 以 7f 45 4c 46 开头。开头出现 NUL 的话就不是文本。不知道扩展名的条目不要判定,直接跳过——把不了解的情况写成错误,清单就没法用了。

用提取过滤器逐条筛选后解压

在 inspect.py 中加入 --extract-to,用 data 过滤器逐条判定,并解压到 /root/media/quarantine/MEDIA-2609-017。在报告中写入 extracted 数量和 rejected 列表(name、reason)。

如果只对 extractall 设置 filter='data',遇到第一个拒绝就会抛出异常而停止。对每个条目亲自调用 tarfile.data_filter(member, dest),接住 FilterError,被拒绝的条目和原因就能一次性汇总。reason 请直接使用异常类的名称。只把没有被拒绝的条目收集起来,最后一次性解压。

撰写附有依据哈希的导入判定书

用 /root/media/decide.py 读取报告,生成 /root/media/report/verdict-MEDIA-2609-017.json。写入 media、archive_sha256、report_sha256、decision、reasons、accepted_entries、rejected_entries。

判定规则写在说明的“参考”一节中。reasons 是把 danger 中非空的栏名称,再加上 bomb 和 mismatch 之后排序得到的列表。report_sha256 是 sha256sum /root/media/report/MEDIA-2609-017.json 的值——重新生成报告的话,这个值也要重新填写。这一步不是把被拦下的条目修改一下就让它通过。

看第二个介质的判定是否会不同

把同样的检疫器和同样的判定规则用于 MEDIA-2609-018,生成报告和判定书,并写出把两个介质并排放置的 /root/media/report/compare.tsv。表头是 media、entries、ratio、ratio_verdict、danger、mismatch、rejected、decision。

不要重新编写,直接使用前面创建的两个脚本。danger 一栏是五类条目数量的总和。如果流程对值没有反应,总是给出同样的答案,那就不是检疫,而是盖章——请看两行的 decision 是否会不同。