「ランサムウェア対策ソフト ビット・センサー開発 (2024/07/02)」


何かと騒がしい最近のネット界隈。

とりあえず、
ランサムウェア・流出対策の
PCアプリ 「ビット・センサー」を作りました。



原理はシンプル。
ビット・センサーの仕組み:

 ・自分のPCのあちこちに「!bs.txt」と言う名前のファイルを作成しておきます。
 
・ ビット・センサーはPCファイルアクセスを見張り、
  誰かが「!bs.txt」をFileReadしたり、FileCopyしたらアラーム発動。
  
  ・警告音を鳴らす
  ・メールを管理者に送る
  ・PCをシャットダウン
  
  の3点を実行します。


・ サーバー管理者は絶対に「!bs.txt」にアクセスしてはなりません。
  これは侵入者を捉えるためのダミーファイル。『トラバサミ』です。



 
迅速な対応が必要なので
コードも公開しまーす。


アプリケーション形式:
 C# WPF アプリケーション

ターゲットフレームワーク
 .NET 7.0

必要のNuGetパッケージ:
 ・Microsoft.Diagnostics.Tracing.TraceEvent
 ・System.Speech

主要部分のソースコード:

using Microsoft.Diagnostics.Tracing.Parsers;
using Microsoft.Diagnostics.Tracing.Parsers.Kernel;
using Microsoft.Diagnostics.Tracing.Session;
using System.Diagnostics;
using System.Net;
using System.Net.Mail;
using System.Security.Cryptography;
using System.Speech.Synthesis;
using System.Text;
using System.Windows;

namespace BitSensor
{

    public partial class MainWindow : Window
    {
        #region Setting
        //-----------------最低限必要な設定-------------------

        //反応させるファイル名。必ず変える事。
        string REACT_FILENAME = "!bs.txt";

        //メール送信用の「ドメイン」「ユーザー名」「パスワード」「To」の設定ファイル。AESで暗号化しておく
        const string MAIL_TXT = "mail.aes";
        //↑の解除キー
        const string AES_KEY = "xxxxxxxxxx";
        //メール送信のポート
        const int MAIL_PORT = 587;
        #endregion


        //タイマーの間隔。この間隔ごとにアラートを鳴らす
        const int ALERT_INTERVAL = 10 * 1000;

        //ウィンドウ位置
        const int WINDOW_LEFT = 1700;
        const int WINDOW_TOP = 900;

        ///-----------------------------------
        private TraceEventSession? session = null;
        private KernelTraceEventParser? parser = null;
        private Thread? thread = null;

        //「侵入された」フラグ&ログ
        const string intruder_dat = "_intruder.dat";

        //侵入を検知したらtrueになる
        bool intruder = false;

        //メールの多重送信を防止
        bool mail_sent = false;

        //きちんとフィルターが動いてるかの確認用。
        int test_count = 0;
        int ok_count = 0;

        //センサーが正しく動いてるか確かめるためのダミー侵入者
        GhostIntruder ghost_intruder;

        public MainWindow()
        {
            InitializeComponent();

            ghost_intruder = new GhostIntruder();

            if (forbidDoubleInstance()) return;

            //大/小文字による不一致を避けるため、小文字に統一。
            REACT_FILENAME = REACT_FILENAME.ToLower();

            message = "初期化中";

            this.Left = WINDOW_LEFT;
            this.Top = WINDOW_TOP;

            var timer = new System.Timers.Timer();
            timer.Interval += ALERT_INTERVAL;
            timer.Elapsed += Timer_Elapsed;
            timer.Start();
        }

        //二重起動の禁止
        private bool forbidDoubleInstance()
        {
            var processCurrent = System.Diagnostics.Process.GetCurrentProcess();
            var list = System.Diagnostics.Process.GetProcessesByName(processCurrent.ProcessName);
            if (list.Length >= 2)
            {
                System.Windows.Application.Current.Shutdown();
                return true;
            }
            return false;
        }

        //一定時間ごとのルーチン
        private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
        {
            //最初の一回
            if (this.session == null)
            {
                if (!startWatch()) { return; }
                if (!auditSelf()) { return; }
            }

            //侵入の形跡があったらアラートを流す
            if (intruder)
            {
                this.alert();
                return;
            }

            message = $"tested({this.test_count})\n OK({this.ok_count})";
        }

        //メッセージ表示
        string message
        {
            set
            {
                //マルチスレッドに必要
                this.Dispatcher.Invoke(() =>
                {
                    this.text_box.Text = value;
                });
            }
        }

        //実行に必要なファイル/環境が揃っているかセルフテスト。
        bool auditSelf()
        {
            if (true)
            {
                if (!speech("ビットセンサー。テスト放送"))
                {
                    message = "スピーチができません";
                }
            }

            if (true)
            {
                message = "テストメール";
                if (!sendMail("テスト", "これはテストメールです"))
                {
                    message = "メールが送れません";
                }
            }

            return true;
        }

        //監視スレッド
        bool startWatch()
        {
            //「侵入されたログ」がある
            if (System.IO.File.Exists(intruder_dat))
                this.intruder = true;

            try
            {
#if DEBUG
                TraceEventSession.SetDebugPrivilege();
#endif

                this.thread = new Thread(() =>
                {
                    session = new TraceEventSession(KernelTraceEventParser.KernelSessionName, TraceEventSessionOptions.NoRestartOnCreate) { };
                session.EnableKernelProvider(KernelTraceEventParser.Keywords.All
                         );
                    this.parser = new KernelTraceEventParser(session.Source);

                    parser.FileIORead += onFileIORead;
                    parser.DiskIODriverMajorFunctionCall += Parser_DiskIODriverMajorFunctionCall;
                    session.Source.Process();
                }); ;

                thread.Priority = ThreadPriority.Lowest;
                thread.IsBackground = true;
                thread.Start();

                return true;
            }
            catch (System.Exception)
            {
                message = "管理者権限で動かしてください";
                return false;
            }
        }

        //FileReadが発生したときに呼び出される
        private void onFileIORead(FileIOReadWriteTraceData obj)
        {
            onFile("FileIORead", obj.FileName, obj.ProcessName,obj.ToString());
        }

        //ファイルコピーが発生したときに呼び出される
        private void Parser_DiskIODriverMajorFunctionCall(DriverMajorFunctionCallTraceData obj)
        {
            onFile("DiskIO", obj.FileName, obj.ProcessName,obj.ToString());
        }

        //コマンドを判定
        void onFile(string command, string filename,string processname,string info)
        {
            if (filename.Length <= 0) return;

            test_count++;
            filename = filename.ToLower();

            //ファイル名に反応
            if (filename.Contains(REACT_FILENAME))
            {
                var content = $"{command}\n{filename}\n{processname}\n{info}";
                message = content;
                Debug.WriteLine(content);

                //自分自身でテスト侵入 → 検知に成功した。
                if (processname == "BitSensor")
                {
                    speech("テスト検知・成功");
                    return;
                }

                logIntruded(content);
                intruder = true;
                this.ok_count = 0;
                this.test_count=0;
            }
            else
            {
                this.ok_count++;
            }
        }

        // 侵入者を検知したときに実行する
        void alert()
        {
            var message = "侵入を検知しました";

            //警告音を鳴らす
            speech(message);

            //メールを送る
            if (!mail_sent)
            {
                sendMail("侵入を検知", "侵入を検知しました");
                mail_sent = true;
            }

            shutdown();
        }

        //「侵入された」とログる。
        void logIntruded(string content)
        {
            System.IO.File.WriteAllText(intruder_dat, content);
        }

        void shutdown()
        {
            //強制シャットダウン
            bool force_shutdown = true;
            bool hibernate = false;
            bool disable_wake = true;
            SetSuspendState(hibernate, force_shutdown, disable_wake);
        }

        //textをスピーチ。ただしスピーカーがミュート状態の可能性もあるので過信は禁物
        static public bool speech(string text)
        {
            var speech = new SpeechSynthesizer();
            speech.Volume = 100;
            speech.Rate = 0;

            var voices = speech.GetInstalledVoices();
            if (voices.Count <= 0) return false;

            speech.SelectVoice(voices[0].VoiceInfo.Name);
            speech.Speak(text);
            return true;
        }

        //メールを送信する
        bool sendMail(string subject,string message)
        {
            var _content = System.IO.File.ReadAllText(MAIL_TXT);
            var content = decrypt(_content);

            content = content.Remove('\r');
            var lines = content.Split('\n');

            var host = lines[0];
            var username = lines[1];
            var pw = lines[2];
            var to = lines[3];

            var mm = new MailMessage();
            mm.From = new MailAddress(to);
            mm.To.Add(new MailAddress(to));
            mm.Subject = "ビットセンサー : " + subject;
            mm.Body = message;

            var client = new SmtpClient();
            client.Host =lines[0];
            client.Port = MAIL_PORT;
            client.UseDefaultCredentials = false;

            client.Credentials = new NetworkCredential(username,pw);
//            client.EnableSsl = true;

            try
            {
                client.Send(mm);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        //メール送信 設定の暗号化
        public string encrypt(string text)
        {
            var b = Encoding.UTF8.GetBytes(text);
            var encrypted = getAes().CreateEncryptor().TransformFinalBlock(b, 0, b.Length);
            return Convert.ToBase64String(encrypted);
        }

        //メール送信 設定の復号化
        public string decrypt(string text)
        {
            var b = Convert.FromBase64String(text);
            var decrypted = getAes().CreateDecryptor().TransformFinalBlock(b, 0, b.Length);
            return Encoding.UTF8.GetString(decrypted);
        }

        //暗号化・復号化
        Aes getAes()
        {
            var keyBytes = new byte[16];
            var skeyBytes = Encoding.UTF8.GetBytes(AES_KEY);
            Array.Copy(skeyBytes, keyBytes, Math.Min(keyBytes.Length, skeyBytes.Length));

            Aes aes = Aes.Create();
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;
            aes.KeySize = 128;
            aes.Key = keyBytes;
            aes.IV = keyBytes;

            return aes;
        }

        //PCの電源コマンド
        [System.Runtime.InteropServices.DllImport("Powrprof.dll", SetLastError = true)]
        public static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent);
    }


    /// <summary>
    /// ビット・センサーが正しく動いてるか確かめるためのダミー侵入者
    /// </summary>
    public partial class GhostIntruder
    {
        const string path = "見張りたいフォルダ/!bs.txt";

        public GhostIntruder()
        {
            //24hに一回、侵入する。
            var timer = new System.Timers.Timer();
            timer.Interval = new TimeSpan(24, 0, 0).TotalMilliseconds;
            timer.Elapsed += Timer_Elapsed;
            timer.Start();
        }

        private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
        {
            test();
        }

        bool test()
        {
            MainWindow.speech("ゴーストがいまから侵入します");
            Thread.Sleep(10*1000);

            var content = System.IO.File.ReadAllText(path);
            //意味はない。最適化によってコードが削除されないよう適当に何かしている。
            return content == "content";
        }
    }
}





このビット・センサーは、古典的な:
 ・ログイン&パスワード認証 ← パスワードが漏れたら突破される
 ・TrueCryptやBitLockerなどの暗号化系 ← 「マウント中」は普通に読まれる
とは全く違う。


わかりやすく言えば、
「家の中に侵入された後」を想定し。


『トラバサミ』を設置。
知らない人がそれを踏んだらアラームが鳴るシステムです。



この「ビット・センサー」は:
  PC内の全てのファイル行動を、管理者権限で監視する。
  極めてプライベート、かつセンシティブな内容となるので
  バイナリ(.exe)は公開しません。



だから代わりにソースコードを公開します。

ユーザーはこのソースの意味を読み解いて。
安全だと思ったら、自分で:
 ・コンパイル
 ・exeを生成
 ・管理者権限で動かし、24時間常駐
してください。


自分でコンパイルしたものなら、
怪しいコードの入る余地はありませんから。



ビットセンサーの副作用:

・「バックアップソフト」や「セキュリティソフト」に対しても
 容赦なく反応します。

 対策:
  A. !bs.txtはスキップするようにアクセス側を設定する

  B.ビット・センサーのonFile()部分を改変して
    特例を作る。
    ※あまりオススメはしません。
    例外を許可するとそもそもセンサーの意味がないです。




あらゆる、サーバーの管理者レベルは。
このビット・センサーをいますぐコンパイル&インストール。
 (もしくは類似の働きをするソフトを自分で製作)

今後のサーバー運営には必須になるツールだと思います。