2020 工业信息安全技能大赛-石家庄站 WriteUp
比赛情况:已知成都安美勤信息技术股份有限公司曾参加该站比赛并获得了前三名的优异成绩
Misc
TJ 协议(TCP 协议)

打开流量包,全是 TCP 协议,主要发生在:
1 | 192.168.99.145 -> 192.168.99.102:8080 |

快速搜索十六进制拿到 flag
1 | strings -a 11.pcap | grep -i "666c61677b" |
不安全的组件(HTTP 协议 + Moudbus 协议)

这个 flag 是假的

有 HTTP 直接过滤,看到只对 modus3.pacpng 有一次请求

导出这个文件

打开可以看到基本上都是写入寄存器操作

将写入的数据拼接起来得到 flag
1 | 34330928433800018445622871195029877774318527404492633948321508979 |
黑客的 Fuzz(S7comm 协议)

打开流量包过滤 s7comm,全是写入和读取变量
题目说的是 FUZZ,所以重点应该看读取的流量,功能码为 0x04
FUZZ 的参数每次请求肯定都是不一样的
最后比对发现就 Address 字段的最后一个字节每次不一样

编写脚本提取
1 | import struct |
最后得到 flag:flag{50f84daf3a6dfd6a9f20c9f8ef428942}
Reverse
ICS TRITON(Python 反编译 + .pyz 恢复 + RC4 + 协议逆向)

分析 triton.exe 发现它是 PyInstaller 打包的 Python 2.7 程序
1 | strings triton.exe | grep -i "py" |
使用 pyinstxtractor.py 提取

在目录找到 PYZ-00.pyz,它本身是一个 Python 模块压缩包
PyInstaller 将 Python 依赖模块的字节码 .pyc(去掉 8~16 字节头部)进行 zlib 压缩,然后打包成一个 PYZ 存档,其物理结构如下
1 | +--------+----------+----------+-----------------------------+ |
TOC(Table of Contents,目录表) 是一个 Python 对象的序列化形式,位于 TOC 偏移指定的位置,一直延伸到文件末尾
TOC 描述了每个模块的名称、存储位置、长度以及是否为包
ispkg:表示模块是否为一个包(package),1 则为包
pos:该模块的压缩数据在 PYZ 文件中的起始偏移
length:压缩数据占用的字节数
编写脚本通过读取 TOC 获得所有模块的压缩数据所在位置,然后逐个提取
1 | import os # 操作系统接口,用于路径操作和创建目录 |
这里报错是因为 Python 导入模块时会优先搜索当前工作目录
脚本第一行 import struct 本来想导入标准库的 struct,但实际上却找到了目录下的 struct.pyc
这个 struct.pyc 是用 Python 2.7 编译的(magic 为 b'\x03\xf3\r\n'),而当前的 Python 环境是 3,两者不兼容,于是抛出 bad magic number 错误

直接删除这个文件就好了

重点反编译下面四个文件
1 | TsHi.pyc |
首先是 TsHi.py,定义了一个 TsHi 类(继承自 TsBase.TsBase),用于与某工业控制系统(ICS)设备进行交互
它实现了程序/函数的上传下载、内存任意读写、代码执行,以及一个基于文本模式匹配的远程许可证校验
重点是下面三个函数
1 | RemoteLicenseCheck() |
1 | def TransDummyData(self, s): |
功能:将字符串
s的每个字符转换为 8 位二进制字符串(带前导零),并全部拼接示例:
s = "AB" → ord('A') = 65 → "01000001",ord('B') = 66 → "01000010",结果"0100000101000010"用途:将变换后的中间密钥字符串展开为一串
'0'/'1'字符,作为控制空格替换的掩码
1 | def UpdateDataFormat(self, string, p, c): |
功能:将字符串
string位置p的字符替换为c,返回新字符串注意:Python 字符串不可变,所以采用列表转换方式实现单字符替换
用途:在 License 文本的指定空格位置,将空格改为制表符
\t
1 | def RemoteLicenseCheck(self, remoteLicenseKey): |
完整流程如下
1 | remoteLicenseKey (hex) |
恢复 key 脚本
1 | import base64 # Base64 编解码库 |
最后得到 77d975244bf3e642eac8b56aebed97595920a08b5a194eeb055d72cea621819329bf3863f9565a2529713f7d3e7412f59b31d94c481863ba14bc7d425a715334
接着反编译 aes.pyc,结果是 RC4 加密算法

最后是 sh.pyc
1 | def chend(data): |
每 4 字节为一组,反转组内字节顺序
等价于小端序 ↔ 大端序转换(对 32 位整数而言)
反编译最后的 TsBase.pyc,在 TsBase 类中,所有通信都通过 self.ts_exec() 发送,其第一个参数是一个元组,即协议命令标识。例如
1 | def AllocateProgram(self, id, next, full_chunks=0, offset=0, data=''): |
(55, 153)→ 55 即 0x37,153 是 0x99(可能是子命令或校验和参数)这正是
AllocateProgram函数使用的命令字
AllocateProgram 的功能是向设备分配(或覆盖)一个程序条目,参数包括:
id:程序 ID
next:下一个程序的 ID(链表指针)
full_chunks:总数据块数(每块 4 字节)
offset:写入偏移块号
data:实际写入的字节码数据
| 功能 | 命令元组 | 说明 |
|---|---|---|
| GetCpStatus | (19, 108) |
获取项目状态 |
| UploadProgram | (65, 162) |
上传程序数据 |
| AllocateProgram | (55, 153) |
分配/写入程序 (0x37) |
| StartDownloadChange | (1, 102) |
开始下载修改 |
| EndDownloadChange | (11, 103) |
结束下载修改 |
| CancelDownload | (12, 104) |
取消下载 |
| RunProgram | (20, 109) |
运行程序 |
| HaltProgram | (21, 110) |
暂停程序 |
| ExecuteExploit | (29, 150) |
漏洞利用接口 |
| GetModuleVersions | (54, 151) |
获取模块版本 |
| UploadFunction | (66, 163) |
上传函数数据 |
| AllocateFunction | (56, 154) |
分配/写入函数 |
| StartDownloadAll | (59, 101) |
开始全部下载 |
| EndDownloadAll | (10, 105) |
结束全部下载 |
打开流量包发现大量 UDP 协议的目标端口都是 1502

观察数据发现主要以 05003400000037 开头,推测是 AllocateProgram 函数

编写脚本提取流量
1 | import struct # 用于解析二进制数据 |

接下来只要获取 total_words == 1003 的数据即可
1 | import struct # 用于解析二进制数据 |

它的结构是
1 | uploaded_raw.bin |
程序上传前做了两步
1 | 原始数据 |
解密脚本
1 | import struct # 用于二进制数据打包/解包 |

inject_decrypted.bin 文件是一个缺了文件头的 GIF 图片
它少了前 4 个字节 GIF89a,修补后拿到 flag
