初探 ICS Security - Modbus

Before all

今天心血來潮研究了一下工業控制和Modbusㄉ東西,最後在github上寫了個小專案,歡迎來玩玩 ><
https://github.com/William957-web/modbus_weather_station_lab

References:
https://www.csimn.com/CSI_pages/Modbus101.html

https://pymodbus.readthedocs.io/en/latest/source/client.html

P.S. TryHackMe上面的 Attacking ICS Plant系列推薦去打打看

Note

工控名詞

所謂的工控,就是工業控制(?)
好像很明確了
看幾個常見名詞

  1. Operational Technology, OT
  2. Industrial Control System, ICS
  3. Programmable Logic Controller, PLC
  4. Supervisor Control And Data Acqusition, SCADA
  5. Distributed Control System, DCS
  6. Human Machine Interaction, HMI

簡單來說,OT就是操作技術的大圈圈,而ICS就是今天的主角工業控制
工業控制裡面大致上又可以分為 PLC (邏輯控制的程式)、SCADA(可監控和控制的系統)、DCS (分散式的控制系統)以及 HMI(人機互動的部分)

SCADA v.s. DCS
相較之下,通常DCS更注重於控制,而SCADA則注重監測
而SCADA也較為靈活,但並沒有絕對的優缺比較,而兩者本質上是出於相同目的的。

詳細更多內容請參考:https://blog.digiinfr.com/dcs%E5%92%8Cscada%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F/

最後,DCS, SCADA會用到PLC,本身也會被HMI去做實體化,而PLC就是接收SENSOR跟呼叫ACTUATORS,所以它們之間的關係圖大概長這樣:
image

P.S. 在正式部署前的測試環境叫做testbed

modbus protocol

工業控制有很多種 protocol,modbus就是常用的其中一種!
這些protocol的目的是在工控環境下傳遞正確的資訊,讓工廠有正確的操作。

再來是 modbus 的簡介:
本體是基於RTU over RS-485(實體層),而遠端呼叫時modbus是透過tcp傳送的,也是後續lab採用的方法(容易被打的地方)。
modbus協議定義了主從(master/slave)的關係,必須有個master以及一個以上的slave去接收/回顯訊息。

每個 modbus 的封包都必須包含一個 function code 以及要呼叫的變量(?)以及值

Function Code Table

Function Code Register Type
1 Read Coil
2 Read Discrete Input
3 Read Holding Registers
4 Read Input Registers
5 Write Single Coil
6 Write Single Holding Register
15 Write Multiple Coils
16 Write Multiple Holding Registers

變量分為這三種:

  • Discrete Input : 1 bit, read only
  • Coil : 1 bit, read/write
  • Input Registers : 16 bits, read only
  • Holding Registers : 16 bits, read/write

傳遞時封包列起來會像這樣:
(Read Holding Registers)
image
(Write Single Holding Register)
image

會看到有個 Func 的選項,就是丟出去的 function code,modbus段就是丟出去的參數們

Python Implementation

以我做的 lab 為範例

安裝 python modbus 庫,就包含server端跟client端ㄌ

1
pip3 install pymodbus==2.5.2

p.s. 以前版本限制沒那麼多,我是用這個

Client

attack/recon.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env python3

import sys
import time
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
from pymodbus.exceptions import ConnectionException

ip = sys.argv[1]
client = ModbusClient(ip, port=502)
client.connect()
while True:
rr = client.read_holding_registers(1, 16)
print(rr.registers)
time.sleep(1)

利用ModbusClient建立連線,以read_holding_registers去閱讀Holding Register,就是func code 3的呼叫
attack/set_register.py

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3

import sys
import time
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
from pymodbus.exceptions import ConnectionException

ip = sys.argv[1]
registry = int(sys.argv[2])
value = int(sys.argv[3])
client = ModbusClient(ip, port=502)
client.connect()
client.write_register(registry, value)

跟剛剛很像,以write_register去寫入register

Server

station.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from pymodbus.server.sync import StartTcpServer
from pymodbus.datastore import ModbusSequentialDataBlock
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
from pymodbus.device import ModbusDeviceIdentification
import random
import threading
import time

online = 1

def update_registers(context):
while True:
slave_id = 0x00
address = 0x01

temperature = random.randint(20, 30)
pressure = random.randint(1000, 1100)
humidity = random.randint(30, 50)
context[slave_id].setValues(3, address, [temperature])
context[slave_id].setValues(3, address + 1, [pressure])
context[slave_id].setValues(3, address + 2, [humidity])
context[slave_id].setValues(3, address + 15, [online])

time.sleep(5)

# Data storage
store = ModbusSlaveContext(
hr=ModbusSequentialDataBlock(0, [0]*100)
)
context = ModbusServerContext(slaves=store, single=True)

# Machine Configurations
identity = ModbusDeviceIdentification()
identity.VendorName = 'pymodbus'
identity.ProductCode = 'PM'
identity.VendorUrl = 'http://github.com/riptideio/pymodbus/'
identity.ProductName = 'pymodbus Server'
identity.ModelName = 'pymodbus Server'
identity.MajorMinorRevision = '1.0'

# Multi Threadings
update_thread = threading.Thread(target=update_registers, args=(context,))
update_thread.daemon = True
update_thread.start()

# Modbus TCP Server
StartTcpServer(context, identity=identity, address=("0.0.0.0", 502))

這段程式碼建立了一百個初始值為0的data block

1
2
3
store = ModbusSlaveContext(
hr=ModbusSequentialDataBlock(0, [0]*100)
)

接著以context = ModbusServerContext(slaves=store, single=True)
建立一個 modbus server(slave資料就是剛剛的datablock)
函數update_registers裡面的setValues則是去改 register 值。

大致上就是這些啦~

After all

工控其實挺有趣的,之前在HTB也有打過抽換惡意PLC彈rev shell的題目,之後可以多碰碰
但要先處理資格考…🐳