2019 工业信息安全技能大赛-哈尔滨站 WriteUp
📝 赛事概况
参赛规模:比赛共吸引了来自全国的 24 支队伍参加
比赛项目:大赛设有 PLC 漏洞挖掘和 SCADA 漏洞挖掘两个比赛项目
🏛️ 组织架构
指导单位:国家工业信息安全发展研究中心、黑龙江省工业和信息化厅
主办单位:黑龙江省电子技术研究所、哈尔滨工程大学
🏆 比赛结果
- 第一名:冀信安队
Misc
异常的 S7 数据(S7comm 协议)
大概看了一下,全是 0x05 功能码

绝大多数写入数据都以 ffff 开头

右键作为过滤器应用

WireShark 生成的语法是 s7comm.resp.data == ff:ff:96:e7:8f:51:c4:d0:3a:2b
我们更改为 s7comm.resp.data[0:2] != ff:ff,筛选出数据包

所以 flag 是 flag{FFAD28A0CE69DB34751F}
神奇的数据(S7 符号访问服务)

查看文本文件是一大堆 ASCII 码(十进制),用 | 间隔

搜索 flag 66|6c|61|67|,有两段结果

分析第一段是请求包
以太网帧
| 字段 | 值 |
|---|---|
| 目标 MAC | 08:ac:7d:01:e1:83 |
| 源 MAC | 00:05:b7:da:72:57 |
| EtherType | 0x0800 (IPv4) |
IPv4 首部
| 字段 | 值 |
|---|---|
| 版本 / IHL | 4 / 5 (20 字节) |
| DSCP / ECN | 0x00 |
| 总长度 | 91 (0x005b) |
| 标识 | 0x987e |
| 标志 / 片偏移 | DF (不分片), 0 |
| TTL | 64 |
| 协议 | 6 (TCP) |
| 校验和 | 0x10a1 |
| 源 IP | 100.100.100.21 |
| 目的 IP | 100.100.100.160 |
TCP 首部
| 字段 | 值 |
|---|---|
| 源端口 | 57632 (0xe120) |
| 目的端口 | 102 (ISO-TSAP) |
| 序列号 | 0xbdcc6ad5 |
| 确认号 | 0x86f7846e |
| 数据偏移 | 32 字节 (8) |
| 标志 | ACK + PSH (0x18) |
| 窗口 | 501 (0x01f5) |
| 校验和 | 0x5744 |
| 紧急指针 | 0 |
| 选项 | NOP, NOP, Timestamp (TSval=0x7d8a8100, TSecr=0x09ad3958) |
| 负载长度 | 39 字节 |
TPKT (RFC1006)
| 字段 | 值 |
|---|---|
| 版本 | 3 |
| 保留 | 0 |
| 长度 | 39 (0x0027) |
COTP (ISO 8073)
| 字段 | 值 |
|---|---|
| 头长度 | 2 字节 |
| PDU 类型 | DT (0x0f) |
| EOT / TPDU-NR | 0x80 (EOT, NR=0) |
S7 符号访问服务(请求)
| 字段 | 值 |
|---|---|
| 固定头 | 01 00 01 00 |
| 应用构造标签 | 0x61 (Application 1) |
| 长度 | 26 (0x1a) |
| SEQUENCE 长度 | 24 (0x18) |
| 协议版本 (INTEGER) | 3 |
| Context[0] 长度 | 19 (0x13) |
| Context[0] 内容长度 | 17 (0x11) |
| 符号 ID (INTEGER) | 0x666c6167 (“flag”) |
| Context[1] 长度 | 9 |
| ↳ Context[0] (参数1) | 80 01 09 (原始, 值 0x09) |
| ↳ Context[1] (参数2) | 80 00 (原始, 空) |
很明显,目的端口 102 → 典型的西门子 PLC ISO-TSAP 端口
接下来再来看响应包
以太网帧 (802.1Q)
| 字段 | 值 |
|---|---|
| 目标 MAC | 00:05:b7:da:72:57 |
| 源 MAC | 08:ac:7d:01:e1:83 |
| 802.1Q TPID | 0x8100 |
| 优先级 / CFI / VLAN ID | 0 / 0 / 1 |
| EtherType | 0x0800 (IPv4) |
IPv4 首部
| 字段 | 值 |
|---|---|
| 版本 / IHL | 4 / 5 |
| 总长度 | 145 (0x0091) |
| 标识 | 0x15f4 |
| 标志 / 片偏移 | DF, 0 |
| TTL | 64 |
| 协议 | 6 (TCP) |
| 校验和 | 0x92f5 |
| 源 IP | 100.100.100.160 |
| 目的 IP | 100.100.100.21 |
TCP 首部
| 字段 | 值 |
|---|---|
| 源端口 | 102 |
| 目的端口 | 57632 |
| 序列号 | 0x86f7846e |
| 确认号 | 0xbdcc6afc (确认增加 39) |
| 数据偏移 | 32 字节 |
| 标志 | ACK + PSH (0x18) |
| 窗口 | 15000 (0x3a98) |
| 校验和 | 0x56e2 |
| 紧急指针 | 0 |
| 选项 | NOP, NOP, Timestamp (TSval=0x09ad3958, TSecr=0x7d8a8100) |
| 负载长度 | 93 字节 |
TPKT (RFC1006)
| 字段 | 值 |
|---|---|
| 版本 | 3 |
| 保留 | 0 |
| 长度 | 93 (0x005d) |
COTP (ISO 8073)
| 字段 | 值 |
|---|---|
| 头长度 | 2 字节 |
| PDU 类型 | DT |
| EOT / TPDU-NR | 0x80 |
S7 符号访问服务(响应)
| 字段 | 值 |
|---|---|
| 固定头 | 01 00 01 00 |
| 应用构造标签 | 0x61 |
| 长度 | 80 (0x50) |
| SEQUENCE 长度 | 78 (0x4e) |
| 协议版本 | 3 |
| Context[0] 长度 | 73 (0x49) |
| Context[1] 长度 | 71 (0x47) |
| Invoke ID (符号 ID) | 0x666c6167 (“flag”) |
| 变量列表容器 | Context[1] (长度 63) |
| 返回状态 | 0x00 (成功) |
变量列表(VisibleString)
| 序号 | 变量符号名 |
|---|---|
| 1 | PL1012{ROT |
| 2 | PL1012CTRL |
| 3 | PL1012LD0 |
| 4 | PL1012MEAS |
| 5 | PL1012RC |
看上面的变量列表结构,删除掉 PL1012,flag 就出来了
1 | flag{ROTCTRLLD0MEASRC} |
智能变电站设备异常诊断(SCD 文件)

010 打开发现文件头被颠倒了

改回来后打开就是一个 dcs.dcs 文件

初步查看文件类型
1 | file dcs.dcs |
可以看到文件不是普通 XML
1 | dcs.dcs: data |
但文件头附近有明显标识
1 | $KEMOV-SCD-FILE$ |
继续查看十六进制
1 | od -Ax -tx1z -N 128 dcs.dcs |
对应了 $KEMOV-SCD-FILE$
同时后面出现了类似
1 | 20 31 20 31 20 30 20 6b 20 76 20 2e 20 73 20 63 20 64 |
这种结构解出来是
1 | 110kv.scd |
说明这个文件本质上保存的是一个变电站 SCD 配置信息,只是被厂商工具二进制化了
观察文件里的 ASCII 字符串会发现它不是正常连续保存的
1 | 20 49 20 4c 20 31 20 31 20 30 20 31 |
这不是普通 ASCII,而是类似
1 | 0x20 + ASCII 字符 |
所以
1 | 20 49 -> I |
解出来是
1 | IL1101 |
中文部分则基本可以按 UTF-16BE 理解
也就是说,文件里的大量字段是 1 字节长度 + 2 字节字符序列
编写脚本来恢复
1 | from pathlib import Path |
脚本首先能恢复出原始 SCD 文件名
1 | [+] header: $KEMOV-SCD-FILE$ |
接着筛选 110kV 线路智能终端,可以看到
1 | [+] 110kV line intelligent terminals: |
题目说问题可能来源于线路智能终端,所以重点检查
1 | IL1101 |
然后分析 IED_GOCB 记录,即 GOOSE Control Block 配置,异常输出为
1 | [+] duplicated IED_GOCB: |
这里的异常点非常明确:
1 | IL1101 / RPIT / LLN0 / gocb5 |
被重复配置了 10 次
在 IEC 61850/SCD 配置里,同一个 IED、同一个逻辑节点下的 GOOSE 控制块名称应该唯一。重复的 gocb5 会导致控制块引用冲突、GOOSE 配置异常,从而引发运行异常
题目问的是 “分析存在问题根源”
真正的问题是 “变电站基础配置文件 SCD 存在错误”
所以 flag 是 flag{scdisbad}
黑客的大意(信息收集)

识别出是 PNG 图片,修改后缀再打开

GitHub 上搜索到源码,里面就有邮箱

窃取数据的黑客(MMS 协议)

查看会话可以看到一直在跟 192.168.51.34:102 通信
所以 192.168.51.34 就是服务器

按照长度排列,第 3096 包没有 info 信息有点可疑(3022、3059 也一样)

展开后可以看到 flag.7z 以及 flag.txt

按照最先发包顺序,从 3022 之后看攻击者 IP 192.168.51.33 请求操作
在 3066 包中可以看到访问了 flag.7z

编写脚本去响应中找到这个压缩文件
1 | import struct |

解压后拿到 flag 值

flag 在哪(文件附件隐写 + 像素隐写)

使用 foremost 分离出一个 PNG 图片和压缩包
图片发现存在残缺的条形码图文

使用 PS 工具将其修复完整,发现熊猫的颜色颠倒过来,因此对图片进行反相处理

扫描可得到字符串:This_n0t_fl4g
使用该字符串解压刚才的压缩包,得到 3.jpg 文件,使用 binwalk 查看依旧存在 ZIP 文件,于是继续分离出来
最终得到两个一模一样的图片,使用 Stegsolve 工具进行图片对比

发现出现很多像素点,确定是像素隐写,保存该图片为:solved.bmp,发现和原图对比数据杂乱因此尝试使用像素隐写解密工具进行解密
GitHub 搜索尝试几个工具后,最终发现此工具可以成功解密:https://github.com/HFO4/HideByPixel

md5 解密拿到 flag:flag{this_is_fl4g_icsc-^_^}
协议分析又来了(LSIS-XGT 工控协议)

统计会话中的主要端口是 2004

过滤 tcp.port == 2004,发现明显特征

过滤器直接搜 tcp contains "LSIS-XGT"

使用 tshark 提取上面这些数据出来
1 | tshark -r .\capture.pcapng -Y "tcp.port == 2004 && tcp.len > 0" -T fields -e tcp.payload > xgt_payloads.txt |
编写脚本解析
1 | import re |

总结出规律地址
1 | %PB100 |
修改脚本,按照这些顺序拼接数据得到
1 | # 1. 新增字典,用于存储符合条件的数据块(地址 -> 响应数据) |
文件开头是 b2 a5 6f ff fc ff ff ff fb ff ff ff 00 00 ff ff,对其异或 0xff 得到 4d 5a 90 00 ...
4d 5a 就是 Windows EXE 的 MZ 文件头
1 | raw = open("raw_pb.bin", "rb").read() |
从字符串中识别出它是一个 .NET / WPF 程序

IDA 反编译发现一堆数值

这种格式在 WPF 里非常像 Margin="62,62,0,0"
1 | strings -a flag.exe | grep -E "[0-9]+,[0-9]+,0,0" |
提取出来类似于 UI 布局

编写脚本绘图拿到 flag
1 | import re |

Reverse
简单的 APK 分析(签到)

解包 APK 得到 multiprotocol.tesla.scada 文件
反编译该文件通过对比正版软件,很容易得到 flag(为混淆前的类名)

Tesla 工业 APP 分析(DEX 脱壳 + so 层逆向 + 控制流平坦化混淆 + SM4 + Brainfuck + 摩斯密码 + 变种 Base91 + Base85)

解压附件中的 APK

在字体库中发现某个字体文件异常过大

检查发现原本是 APK 文件

修改后缀后再次解压缩

jadx 反编译 classes.dex
这段代码是一个 Android 加固脱壳的代理 Application,通过反射替换系统 Application 为真实的目标 Application 并调用其生命周期,同时加载 native 库执行初始化
在 onCreate() 方法中,代码大量使用 ReflectUtils 对 ActivityThread 和 LoadedApk 这些系统核心类进行反射操作:
将
LoadedApk中的mApplication设为null,并从系统的mAllApplications列表中移除旧的Application对象强行将
ApplicationInfo中的className修改为"com.targetapk.MyApplication"
在系统感知之前,用真实的业务 Application 替换掉这个临时的代理入口
代码通过反射调用 LoadedApk.makeApplication 动态创建了一个新的 Application 实例,并将其赋值给系统的 mInitialApplication,最后直接调用了 app.onCreate()
这一步证明了该代理类的存在意义——它并不自己承担业务逻辑,而是充当 “启动器”,负责找到、构造并启动被加固保护的原始 Application,确保原始 App 的代码能够正常跑起来

010 分析中 DEX 头里有两个关键字段:data_size 和 data_off

所以 DEX 的数据区结束位置是 data_end = data_off + data_size = 0x938 + 0x1948 = 0x2280
也就是说,壳 dex 的正常数据到 0x2280 就结束了,但这个 classes.dex 的总长度是 0xf83f
壳 dex 尾部 4 字节是 payload 长度:00 00 d5 bb

然后我们算一下:
1 | 总长度 - DEX 正常结束位置 - 4 字节长度字段 |
刚好等于最后 4 字节表示的数值,所以可以确认 00 00 d5 bb 就是前面那段附加 payload 的长度
然后对 payload 每个字节异或 0xff,就能还原 zip,开头会变成 PK 03 04
1 | from pathlib import Path |
解压缩后拿到真正的 DEX

public static String O00o00O00o00O00o00O00o00O00o00O00o00O00o00O00o00OO(String str) 中虽然传入 String str
但是完全没用,最终进入 native 的永远是 Your Flag!

进入 O00o00O00o00O00o00O00o00O00o00O00o00O00o00O00o00OO 内部

其中
1 | static int[] = { |
这个就是
1 | 85^0 |
这个类里面有三个函数,大概率是 encode(),decode(),check()
所以推测为 Base85 编码,Java 层如下
1 | Your Flag! |
接下来去逆向 so 层

找到了这个 JNI 函数,分析 sub_2E74 函数

这几百行 while 混淆就是典型的控制流平坦化混淆

扔给 AI 处理后得到关键逻辑
1 | sub_2E74(JNIEnv *env, char *input) |
继续分析 sub_1228 函数,同样也被混淆了
这里 0x20 = 32,对应着 SM4 的 32 轮加密,32 个轮密钥 rk[0~31]
1 | if ( v2 < 0x20 ) |
A3B1BAC6 是 SM4 固定参数
1 | v47[0] = |
标准 SM4:
1 | FK0 = A3B1BAC6 |

但是这个函数里面是没有 Key 的,回头看调用时传的参数 sub_1228(v34,v37);
a2 才是 16 字节密钥,可以在开头的定义变量中按 X 看引用的代码段

所以回到 sub_2E74 看 v37 在哪里初始化

IDA 没有识别到,那么密钥在哪呢?
回到隐藏 APK,里面有一个资源文件 r/D.aac,它内容是一串十六进制
1 | D4E8E5A0EBE5F9A0E9F3A0D9EFF5F2E5DFF6E5F2F9DFF3EDE1F2F4ACC3E9F0E8E5F2F4E5F8F4A0E3EFEEF3E9F3F4F3A0EFE6A0F4E8F2E5E5A0F0E1F2F4F3ACC3EFEDE5A0EFEEA1 |
这段每个字节减去 0x80,就会得到明文
1 | The key is Youre_very_smart,Ciphertext consists of three parts,Come on! |
其中 Youre_very_smart 就是 Key,接下来去找密文
在最外层的 APK 中,functions_fr.properties 文件的其中一个值很奇怪
1 | hide=....................../-//.-......................-.-/.-.... ../.-............./-//.-////////////-.-/.-///////////.-..... ..../-//.-........-.-/.-................/.-........./-//.-// //////-.-/.-/.-........./-//.-////////-.-/.-/////////.-..... ../-//.-......-.-/.-......../.-........./-//.-////////-.-/.- /////////.-........./-//.-////////-.-/.-///////////.-....... ....../-//.-............-.-/.-......................../.-... ....../-//.-........-.-/.-../.-............./-//.-////////// //-.-/.-/////.-.........../-//.-..........-.-/.-......../.-. ............/-//.-////////////-.-/.-/////////////////////.-. ....../-//.-//////-.-/.-///.../.-.........../-//.-.......... -.-/.-............../.-............./-//.-////////////-.-/.- ///////.-............./-//.-............-.-/.-.............. /./////////.-. |
这段看起来像摩尔斯,其实是 Brainfuck 变体,替换规则
1 | / -> ! |
然后按照 Ook 规则转 Brainfuck 得到 |Sk[GT@+[lFc5+,L%PL
第二段密文在最开始的 APK 中,这段看起来像 Brainfuck,其实是 Morse,替换规则:
1 | ! -> - |
替换后变成 ...--/-.--/.-/.-.-.-/-.-./-.-.-./...--/--/.-.-./--..--/--../....,解码得到 3ya.c;3m+,zh

第三段在 /android/support/v7/internal/view/menu/ActionMenuItemView 中,这一段是真 Brainfuck 变体,但是 ! 和 ? 被交换了,替换规则:
1 | ! -> ? |
得到 jR1"KTb?:

但是题目没有告诉顺序,所以需要枚举 6 种排列
继续回到 IDA 中,在字符串窗口发现了一串字符

AI 分析出这个 sub_DA4 函数是变种 Base91

1 | from itertools import permutations |