一覧OP-15-2 · 封じ込め・復旧・報告

バックアップからの復旧と業務再開

最初に観測した実行より前のb1と、後のb2を比較します。最初の観測時刻を侵入開始そのものと決めつけず、復旧の判断に必要な追加検査と監視の欠落を確認します。

説明

バックアップのハッシュが一致しても、その内容が侵害前の正常な状態とは限りません。復旧候補の時点、内容の検査、業務データ、再発を観測する仕組みをそれぞれ確認します。

最初に観測した実行より前のb1と、後のb2を比較します。最初の観測時刻を侵入開始そのものと決めつけず、復旧の判断に必要な追加検査と監視の欠落を確認します。

1バックアップ候補を比較する

hash_okは保管後の同一性確認です。malware_checkは別の内容検査です。

from oplab import * db=connect(15) show(db, 'SELECTバックアップ候補を比較する * FROM backups ORDER BY t')

どちらもハッシュは一致していますが、b2の検査にはflaggedがあります。同一性と安全性は別の条件です。

2観測した事案の時刻を確認する

first observed executionは取得資料内の最初の観測であり、真の侵入開始時刻とは限りません。

show(db, 'SELECT観測した事案の時刻を確認する * FROM incident ORDER BY t')

実行の観測は20、封じ込めは30です。それ以前の資料が欠けていれば、侵入はもっと前だった可能性が残ります。

3観測前の候補を絞る

ハッシュ照合済みで、最初の観測より前の候補を出します。

show(db, 'SELECT観測前の候補を絞る b.id,b.t,b.malware_check FROM backups b WHERE b.hash_ok=1 AND b.t<(SELECT MIN(t) FROM incident WHERE action="first observed execution")')

b1が候補です。時刻だけでは正常な復旧点と確定せず、起動設定やファイル内容などを検査します。

4選んだバックアップを別のデータベースへ復旧する

b1のデータベースファイルを読み込み、別の空のSQLiteデータベースへbackup APIでコピーします。復旧先の検査は、このコピーに対して行います。

import sqlite3 backup=sqlite3.connect(":memory:") backup.deserialize((ROOT/"OP-15/backup-b1.sqlite").read_bytes()) restored=sqlite3.connect(":memory:") backup.backup選んだバックアップを別のデータベースへ復旧する(restored) print(restored.execute("PRAGMA integrity_check").fetchone()[0])

okはSQLiteの構造整合性検査に合格した結果です。正常な業務データか、侵害を含まないかは別の検査になります。

5復旧先の業務データを確認する

ordersの件数と金額を読み、b1の業務確認値である2件・合計300と照合します。

print(restored.execute("SELECT id,amount FROM orders ORDER BY id").fetchall()) print(restored.execute("SELECT COUNT(*),SUM(amount)復旧先の業務データを確認する FROM orders").fetchone()) backup.close() restored.close()

コピー先で2行と合計300を確認できました。ファイルが開けたことに加え、業務で使う値まで確認して復旧を評価します。

6復旧候補の検査結果を読む

バックアップIDを合わせ、どの候補に対する確認かを明示します。

show(db, 'SELECT復旧候補の検査結果を読む check_name,result FROM checks WHERE backup="b1" ORDER BY id')

起動設定、業務クエリー、センサーの確認があります。各検査が対象にした範囲を越えて、全て安全と広げないようにします。

7復旧後に観測できるログを調べる

監視のenabledと最終到着時刻を確認します。

show(db, 'SELECT復旧後に観測できるログを調べる * FROM monitoring ORDER BY source')

プロセスは有効ですが通信ログは無効です。センサーのheartbeatがあっても、全ログ種別を取得できているとは限りません。

8監視の未充足を抽出する

時刻40時点で、無効または5秒を超えて更新されていない種類を探します。

show(db, 'SELECT監視の未充足を抽出する source,enabled,40-last_seen AS age FROM monitoring WHERE enabled=0 OR last_seen<35')

networkが残ります。復旧したサービスの業務確認に加え、再発を確認できる観測条件をそろえて判断します。

まとめ

networkが残ります。復旧したサービスの業務確認に加え、再発を確認できる観測条件をそろえて判断します。

表示された証拠IDと検索条件を残すと、調査結果を元の記録までたどれます。

操作履歴と解説を振り返れます。

Python 3.15 / SQLite・固定の調査記録

1
from oplab import * db=connect(15) show(db, 'SELECT * FROM backups ORDER BY t')
id | t | hash_ok | malware_check | orders
b1 | 10 | 1 | not_tested | 2
b2 | 25 | 1 | flagged | 3
2
show(db, 'SELECT * FROM incident ORDER BY t')
id | t | action
i1 | 20 | first observed execution
i2 | 30 | contained
3
show(db, 'SELECT b.id,b.t,b.malware_check FROM backups b WHERE b.hash_ok=1 AND b.t<(SELECT MIN(t) FROM incident WHERE action="first observed execution")')
id | t | malware_check
b1 | 10 | not_tested
4
import sqlite3 backup=sqlite3.connect(":memory:") backup.deserialize((ROOT/"OP-15/backup-b1.sqlite").read_bytes()) restored=sqlite3.connect(":memory:") backup.backup(restored) print(restored.execute("PRAGMA integrity_check").fetchone()[0])
ok
5
print(restored.execute("SELECT id,amount FROM orders ORDER BY id").fetchall()) print(restored.execute("SELECT COUNT(*),SUM(amount) FROM orders").fetchone()) backup.close() restored.close()
[(1, 100), (2, 200)]
(2, 300)
6
show(db, 'SELECT check_name,result FROM checks WHERE backup="b1" ORDER BY id')
check_name | result
startup paths | clean
business query | ok
sensor heartbeat | ok
7
show(db, 'SELECT * FROM monitoring ORDER BY source')
source | enabled | last_seen
network | 0 | 20
process | 1 | 40
8
show(db, 'SELECT source,enabled,40-last_seen AS age FROM monitoring WHERE enabled=0 OR last_seen<35')
source | enabled | age
network | 0 | 20

演習完了