pythonでサンドボックス検知

BlackHatPython(2nd edition)を読むpython

Black Hat Python, 2nd Edition
Python Programming for Hackers and Pentesters
by Justin Seitz and Tim Arnold
no starch press
April 2021, 216 pp.

本記事は『Black Hat Python, 2nd Edition Python Programming for Hackers and Pentesters』の要点整理及び自分の理解度確認を目的として書いたものです。ここで解説する本書中のソースコードは全てno starch pressのサイトから誰でもフリーでダウンロードできます。自分の理解が及ばず誤りが散見される可能性がありますがご了承ください。

流石に本書に書いてある文章をそのまま訳してつらつら書くわけにはいかない。

 

ここでは誰でもフリーにダウンロードできる本書中のソースコードにおいて、大切な所や少し難しい所の説明を軽く挟んでいくだけ。

はしょった部分も沢山ある。

 

重要な部分は実際に本書を読まなければ知ることができない。

 

本書をすでに購入済みの人がその本片手に参考にするような記事になっている。

 

是非『Black Hat Python, 2nd Edition Python Programming for Hackers and Pentesters』を手に取ってその周辺の重要な説明部分にも目を通すことを勧める。

スポンサーリンク

サンドボックスとは?

サンドボックスは、隔離された領域でプログラムを実行し、問題発生時においてもほかのプログラムに影響を及ぼさないようにする仕組みのことだ。和訳では公園の「砂場」を意味し、外部から仕切られた環境で自由に遊べる状況に由来する。

引用:https://eset-info.canon-its.jp/malware_info/special/detail/201117.html

 

普通の人間がパソコンを操作していれば、マウスの動きや何かしらの入力がある。

しかしサンドボックス内では、大抵マルウェアを自動解析しているため人間的なインタラクションが無い。

 

今回はそれを利用して、我々のトロイの木馬が今サンドボックス内にいるのか?を判断するという内容。

get_last_input()関数

class LASTINPUTINFO(Structure):
    fields_ = [
        ('cbSize', c_uint),
        ('dwTime', c_ulong)
    ]

クラスLASTINPUTINFOは、最新の入力タイムスタンプを保持する。

クラス変数に二つのタプルを要素とするリスト(構造体)。

cbSizeがunsigned int型で構造体のサイズを表す。dwTimeがunsigned long型で、入力の時間・タイミング。

 

def get_last_inut():    
    struct_lastinputinfo = LASTINPUTINFO()
    struct_lastinputinfo.cbSize = sizeof(LASTINPUTINFO)
    
    windll.user32.GetLastInputInfo(byref(struct_lastinputinfo))    
    run_time = windll.kernel32.GetTickCount()

    elapsed = run_time - struct_lastinputinfo.dwTime
    print(f"[*] It's been {elapsed} milliseconds since the last event.")
    return elapsed

この関数は最後の入力からの経過時間(=elapsed)を返す。

 

まずインスタンスstruct_lastinputinfoを作って、そのサイズを指定。

ctypesモジュールのsizeofは、ctypesの型やインスタンスのメモリバッファのサイズをバイト数で返す。
(Cのsizeof演算子と同じ)

 

GetLastInputInfoで、最後の入力イベントの時刻が入る。

引数はLASTINPUTINFO構造体へのポインタ(byrefは指定した引数へのポインタ)。

GetTickCountはシステムを起動した後の経過時間がミリ秒単位で返る。

ゆえに経過時間はあのような引き算になる。

Detectorクラスのメソッド達

初期化メソッドは以下の通り:

def __init__(self):
        self.double_clicks = 0
        self.keystrokes = 0
        self.mouse_clicks = 0

get_key_pressメソッド

def get_key_press(self):
        for i in range(0, 0xff):
            state = win32api.GetAsyncKeyState(i)
            if state & 0x0001:
                if i == 0x1:
                    self.mouse_clicks += 1
                    return time.time()
                elif i > 32 and i < 127:
                    self.keystrokes += 1
        return None

get_key_press()は、インスタンス変数を変化させる動きをする。

返り値は、ある時は(後々計算で使うように)UNIXエポックからの経過秒数を返す(time.time())。

UNIX時間とは、UTC(=協定世界時)の1970年1月1日00:00:00秒(=「UNIXエポック:UNIX Epoch」と呼ばれる)からの経過秒数のことである。

引用:https://www.atmarkit.co.jp/fdotnet/dotnettips/980unixtime/unixtime.html

GetAsyncKeyState関数は、関数呼び出し時にキーが押されているかどうか、また前回のGetAsyncKeyState関数呼び出し以降にキーが押されたかどうかを判定する。

引数は仮想キーコード。

キーコード0x01はマウスの左ボタン。32から127は一般的なキー(詳しくはここ)。

 

関数が成功すると、前回のGetAsyncKeyState関数呼び出し以降にキーが押されたかどうか、およびキーが現在押されているかどうかを示す値が返る。

最上位ビット(0x8000)がセットされたときは現在そのキーが押されていることを示し、最下位ビット(0x0001)がセットされたときは前回のGetAsyncKeyState関数呼び出し以降にそのキーが押されたことを示す。

detectorメソッド

長いから、detectorメソッドの前半・後半を分けて示す。

detectorメソッドの前半部分)

def detector(self):
        previous_timestamp = None
        first_double_click = None
        double_click_threshold = 0.35
        
        max_double_clicks = 10
        max_keystrokes = random.randint(10,25)
        max_mouse_clicks = random.randint(5,25)
        max_input_threshold = 30000

        last_input = get_last_input()
        if last_input >= max_input_threshold:
            sys.exit(0)        

はじめで定義してる各種変数は頭の片隅に入れておく。

ちなみにrandom.randint(a, b)a以上b以下の整数をランダムに返す(すなわち端っこの数bも入る)。

 

last_inputhget_last_inut関数の返り値、つまり経過時間。

だから、その経過時間が不自然に長かったらサンドボックス内にいるということ。そして何事もなかったかのようにsys.exit(0)で正常終了。

detectorメソッドの後半部分)

detection_complete = False
while not detection_complete:
    keypress_time = self.get_key_press()
    if keypress_time is not None and previous_timestamp is not None:
        elapsed = keypress_time - previous_timestamp
        
        if elapsed <= double_click_threshold:
            self.mouse_clicks -= 2
            self.double_clicks += 1
            if first_double_click is None:
                first_double_click = time.time()
            else:
                if self.double_clicks >= max_double_clicks:
                    if (keypress_time - first_double_click <= (max_double_clicks*double_click_threshold)):
                          sys.exit(0)
        if (self.keystrokes >= max_keystrokes and 
            self.double_clicks >= max_double_clicks and 
            self.mouse_clicks >= max_mouse_clicks):
            detection_complete = True
            
        previous_timestamp = keypress_time
    elif keypress_time is not None:
        previous_timestamp = keypress_time    

whileループが終わるのは、すなわち「detection_complete = True」となるのは、インスタンス変数self.keystrokesself.double_clicksself.mouse_clicksの値たちがそれぞれmax_mouse_clicksmax_keystrokesmax_double_clicks以上になった時。

 

まずキーの入力またはマウスのクリックがあったらタイムスタンプを取得(keypress_time)。

経過時間(elapsed)は前の入力時間との差。

 

previous_timestampNone、つまりその時の入力が一番最初の入力だったらprevious_timestampに今のタイミングでのkeypress_timeを代入する。

(それ以降のprevious_timestampはその都度更新されていく。)

 

double_click_thresholdはダブルクリックの際のクリックする間隔。

だからelapseddouble_click_threshold以下だったら、普通のクリックではなくダブルクリックだから、インスタンス変数self.mouse_clicksself.double_clicksを調整。

それで、first_double_clickはUNIXエポックからの経過秒数。

ここがポイント👇

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

ダブルクリックが多すぎる。

これはホワイトワッカーがこっちのテクニックを見越してダブルクリックしまくってる可能性がある。

へっ😏!!
裏の裏をかいてこっちもその対策だ!

何事もなかったかのように退散する。
結局、メソッドdetector()は、sys.exit(0)するかしないかの判断をしてくれるということ。

タイトルとURLをコピーしました