たれながし.info

とあるITエンジニアの備忘録

PythonのctypesでRemote DLL Injectionしてみる


はじめに

「DLL Injection」にはいくつかの方法があって、Windows APIの「CreateRemoteThread」を使う方法は結構メジャーらしい。この方法を「Remote DLL Injection」とか「CreateRemoteThreadを使ったDLL Injection」と呼ぶみたいです。

「Remote DLL Injection」を実施するには、「CreateRemoteThread」を含むいくつかのWindows APIの実行が必要です。Windows APIを実行するには、C言語C++を使うことが一般的ですが(私がそれらの言語に慣れておらず)面倒なので、Pythonのctypesを使って実施してみます。

ctypesについて

ctypesは、C言語 と互換性のあるデータ型の提供と、動的リンク/共有ライブラリ内の関数呼び出しを可能とするPythonのライブラリになります。
docs.python.org

嵌ったポイント

ctypesを使うにあたり、変数の型の扱いで嵌りました。

ctypesで外部関数を実行する場合、argtypesで引数の型、restypeで戻り値の型を指定できます。
これを適切に行わないと外部関数が正しく動かない場合がありました。

以下は、型の指定を行わないで、Windows API「VirtualAllocEx/WriteProcessMemory」を実行した例です。
VirtualAllocExで割り当てたメモリ領域に、WriteProcessMemoryで書きこむ動作になります。
Pythonのエラーは発生しませんでしたが、WriteProcessMemoryの実行が998(ERROR_NOACCESS)で失敗しました。

<プログラム>
mem_address = kernel32.VirtualAllocEx(handle, 0, size, MEM_COMMIT, PAGE_READWRITE)

ret = kernel32.WriteProcessMemory(handle, mem_address, dll_pass.encode(), c_size_t(len(dll_pass)), byref(c_size_t(0)))
print("WriteProcessMemory(0以外なら成功): {}".format(ret))
print("GetLastError:{}\n".format(kernel32.GetLastError()))

<結果>
WriteProcessMemory(0以外なら成功): 0
GetLastError:998


ctypesで外部関数を実行した場合、デフォルトの動作では、戻り値はC言語のint型(32bit)として扱うそうです。
VirtualAllocExの戻り値は割り当てられたメモリのアドレスとなり、64bit環境だと64bitになるので、C言語のint型(32bit)に収まらず、引数として渡されたWriteProcessMemoryの実行がエラーになったと思われます。

次に、VirtualAllocExの戻り値をLPVOID型(任意の型へのポインタ)に指定してWriteProcessMemoryを実行すると、引数に関するPythonのエラーが発生します。
メモリアドレスを示す第2引数の型の不一致により、OverflowErrorが発生したという内容と思われます。

<プログラム>
kernel32.VirtualAllocEx.restype = LPVOID #追加
mem_address = kernel32.VirtualAllocEx(handle, 0, size, MEM_COMMIT, PAGE_READWRITE)

ret = kernel32.WriteProcessMemory(handle, mem_address, dll_pass.encode(), c_size_t(len(dll_pass)), byref(c_size_t(0)))
print("WriteProcessMemory(0以外なら成功): {}".format(ret))
print("GetLastError:{}\n".format(kernel32.GetLastError()))

<結果>
ctypes.ArgumentError: argument 2: <class 'OverflowError'>: int too long to convert


最後に、WriteProcessMemoryの引数の型も指定すると、WriteProcessMemoryの実行が無事成功しました。

<プログラム>
kernel32.VirtualAllocEx.restype = LPVOID
mem_address = kernel32.VirtualAllocEx(handle, 0, size, MEM_COMMIT, PAGE_READWRITE)

kernel32.WriteProcessMemory.argtypes = [HANDLE, LPVOID, LPCVOID, c_size_t, POINTER(c_size_t)] #追加
ret = kernel32.WriteProcessMemory(handle, mem_address, dll_pass.encode(), c_size_t(len(dll_pass)), byref(c_size_t(0)))
print("WriteProcessMemory(0以外なら成功): {}".format(ret))
print("GetLastError:{}\n".format(kernel32.GetLastError()))

<結果>
WriteProcessMemory(0以外なら成功): 1
GetLastError:0


ということで、ctypesで外部関数を実行する場合、argtypesで引数の型、restypeで戻り値の型を指定した方が良いようです。
厳密にどんな時に指定が必要かはよく分かっていないのですが、下記場合は最低でも型指定した方が良いと認識しました(全ての外部関数で指定した方が無難なのかも)。

  • restypeの指定:外部関数の戻り値がC言語のint型として扱われると困る場合
  • argtypesの指定:型指定で受け取った他外部関数の戻り値を引数に使用して外部関数を呼び出す場合

事前準備

環境

Windows 11 Enterprise 22H2(64bit)
Python 3.10.0

DLLの作成

リモートプロセスに読み込ませるDLLを準備します。以前作成したものを使用しました。
tarenagashi.hatenablog.jp

Remote DLL Injectionの実施

コードの作成

notepad.exeを起動して、自作のDLL(c:\testdll.dll)を注入するコードになります。
一部の外部関数でしか引数と戻り値の型を指定していませんが、動作したので今回はこれで良いことにします。

from ctypes import * 
from ctypes.wintypes import *
import subprocess

# リモートプロセスの起動
proc = subprocess.Popen("notepad.exe")
pid = proc.pid

# プロセスハンドルの取得
VM_READ, VM_WRITE, VM_OPERATION = 0x0010, 0x0020, 0x0008 
DesiredAccess = VM_READ|VM_WRITE|VM_OPERATION
kernel32 = WinDLL("kernel32")

handle = kernel32.OpenProcess(DesiredAccess, False, pid)

print("プロセスハンドル: {}".format(hex(handle)))
print("GetLastError:{}\n".format(kernel32.GetLastError()))

# プロセスへのメモリ割り当て(VirutualAllocEx)
size = 0x27
MEM_COMMIT = 0x1000|0x2000
PAGE_READWRITE = 0x04

kernel32.VirtualAllocEx.restype = LPVOID
mem_address = kernel32.VirtualAllocEx(handle, 0, size, MEM_COMMIT, PAGE_READWRITE)

print("VirtualAllocExの戻り値(確保したページ領域のベースアドレス): {}".format(hex(mem_address)))
print("GetLastError:{}\n".format(kernel32.GetLastError()))

# 割り当てたメモリにDLLのパスをコピー(WriteProcessMemory)
dll_pass = "c:\\testdll.dll"

kernel32.WriteProcessMemory.argtypes = [HANDLE, LPVOID, LPCVOID, c_size_t, POINTER(c_size_t)]
kernel32.WriteProcessMemory.restypes = BOOL
ret = kernel32.WriteProcessMemory(handle, mem_address, dll_pass.encode(), c_size_t(len(dll_pass)), byref(c_size_t(0)))

print("WriteProcessMemory(0以外なら成功): {}".format(ret))
print("GetLastError:{}\n".format(kernel32.GetLastError()))

# LoadLibrary関数のアドレスの取得(GetModuleHandleA)
kernel32.GetModuleHandleA.restype = HMODULE
h_module = kernel32.GetModuleHandleA(b"kernel32.dll")

print("kernel32.dll のモジュールハンドル: {}".format(hex(h_module)))
print("GetLastError:{}\n".format(kernel32.GetLastError()))

kernel32.GetProcAddress.argtypes = (HMODULE, LPCSTR)
kernel32.GetProcAddress.restype = LPVOID
func_addr = kernel32.GetProcAddress(h_module, b"LoadLibraryA")

print("LoadLibraryA のアドレス: {}".format(hex(func_addr)))
print("GetLastError:{}\n".format(kernel32.GetLastError()))

# 対象プロセスでリモートスレッドの実行(CreateRemoteThread)
kernel32.CreateRemoteThread.argtypes = [HANDLE, c_size_t, c_size_t, LPVOID, LPVOID, DWORD, POINTER(c_size_t)]
ret = kernel32.CreateRemoteThread(handle, c_size_t(0), c_size_t(0), func_addr, mem_address, 0, byref(c_size_t(0)))

print("CreateRemoteThreadの戻り値: {}".format(hex(ret)))
print("GetLastError:{}\n".format(kernel32.GetLastError()))

動作確認

上記スクリプトを実行すると、notepad.exeの起動後にDLLが読み込まれ、DLLMain内のMessageBoxが表示されることを確認しました。

Windows11+Python 3.10.0の環境だと特に問題ないのですが、Windows10+Python3.10.10の環境だと動作はするのですが、Windows APIの呼び出し結果が122(ERROR_INSUFFICIENT_BUFFER)となります。
以下はWindows10環境での実行時の画像です。


tasklistコマンドでも、notepad.exeに対象のDLLが読み込まれていることを確認できます。

>tasklist /m testdll.dll

イメージ名                     PID モジュール
========================= ======== ============================================
Notepad.exe                  21960 testdll.dll