在解包之前对带入介质做检疫
目标
编写一个检疫器:在解压导入介质的压缩包之前,只读取列表,判定规模、危险条目、压缩比和格式不一致;再用提取过滤器逐条筛选,解压到隔离目录,并撰写附有依据哈希的导入判定书。
为什么重要
进入隔离网络的东西,大多是人带来的介质,里面装着一个压缩包。为了看里面而解压的那一行命令,本身就已经是在触碰文件系统。绝对路径、上级路径逃逸、指向外部的链接、特殊文件、压缩炸弹,都是在解压的瞬间发生作用。 tar 头部写着名称、大小、权限、条目类型和链接目标,所以不解压也能把这五项全部看到。因此要改变顺序:先读取,做出判定,附带条件地解压。 判定书上必须附带依据。哪个归档的哪个哈希,用哪份报告读取,该报告得出了什么原因,这些必须连起来,合作方才知道该改什么,审查人员也才能再次作出同样的判定。 评分器不会相信你写的文字。它会亲自打开归档,自己计算应当得出的值,并按运行约定重新运行你的检疫器,连其输出也一并核对。
步骤
- 用生成脚本创建两个介质,并在受理登记簿 /root/media/intake.tsv 中写入序列号、接收时间、提交者角色、外层哈希、大小。
- 创建 /root/media/inspect.py,不解压 MEDIA-2609-017,只读取列表,把条目数、总解压大小、最大条目写入 /root/media/report/MEDIA-2609-017.json。
- 在 inspect.py 中加入 danger 分类。设置 absolute、traversal、escaping_link、special、exec_bit 五栏,没有对应项的栏也保留为空列表。
- 在 inspect.py 中加入 ratio 和 ratio_verdict。总解压大小除以压缩包大小所得的值超过 100,则为 bomb。
- 在 inspect.py 中加入 mismatch。把扩展名所说的格式与开头字节所说的格式不同的条目,用 name、declared、actual 写出。
- 在 inspect.py 中加入
--extract-to,用 data 过滤器逐条判定,解压到 /root/media/quarantine/MEDIA-2609-017,并在报告中写入 extracted 和 rejected。 - 用 /root/media/decide.py 把报告转换成导入判定书 /root/media/report/verdict-MEDIA-2609-017.json。写入 media、archive_sha256、report_sha256、decision、reasons、accepted_entries、rejected_entries。
- 把同一个检疫器和同样的判定规则用于 MEDIA-2609-018,生成 /root/media/report/MEDIA-2609-018.json 和 /root/media/report/verdict-MEDIA-2609-018.json,并写出把两个介质并排放置的 /root/media/report/compare.tsv。
参考
- 运行约定:
python3 /root/media/inspect.py --archive <매체> --out <보고서> [--extract-to <디렉터리>]。判定规则是python3 /root/media/decide.py --report <보고서> --out <판정서>。评分器会按这个约定亲自重新运行。 - 介质是
/root/media/in/MEDIA-2609-017.tar.gz和/root/media/in/MEDIA-2609-018.tar.gz,隔离目录是/root/media/quarantine/<일련번호>。 - 判定规则(本实验的假设):只要有 absolute、traversal、escaping_link、special、bomb 中的任何一项,就是 reject;原因只有 exec_bit 或 mismatch,就是 quarantine;什么都没有,就是 accept。reasons 是把原因名称排序后的列表。
- 列表用
tar -tvf读取,Python 用tarfile.open(...).getmembers()读取。逐条判定由tarfile.data_filter(member, dest)给出。 - 开头字节用
od -An -tx1 -N8或 Python 查看。因为有混入 NUL 的条目,用grep、head无法正确查看。 - 常见错误:只对
extractall()设置过滤器,遇到第一个拒绝就停止;重新生成报告后,判定书的哈希原封不动;把 danger 中的空栏整个去掉。 - 本实验是隔离网络,所以没有
zip、unzip、xz、gpg。只使用tar和gzip。
打开介质之前先写受理登记簿
用生成脚本创建两个介质,并在 /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 是否会不同。