【Python微信機器人】第三篇:使用ctypes調用進程函數和讀取內存結構體

目錄修整

目前的系列目錄(後面會根據實際情況變動):

  1. 在windows11上編譯python
  2. 將python注入到其他進程並運行
  3. 注入Python並使用ctypes主動調用進程內的函數和讀取內存結構體
  4. 使用匯編引擎調用進程內的任意函數
  5. 利用beaengine反彙編引擎的c接口寫一個pyd庫,用於實現inline hook
  6. 利用beaengine反彙編引擎的python接口寫一個py庫,用於實現inline hook
  7. 注入python到微信實現簡單的收發消息
  8. Bug修復和細節優化,允許Python加載運行py腳本並且支持熱加載
  9. 讀取微信內存中的好友聯繫人列表的信息結構體數據
  10. 做一個殭屍粉檢測工具

ctypes的主要功能

ctypes是Python與c寫的文件做交互的庫,能和Python直接交互的也就是動態庫了。所以在Windows上主要是調用dll,Linux上則是調用so。

不過,在這個系列文章裏,它的作用稍微有些不同。因爲Python已經被注入到其他進程,可以用ctypes隨意操作其他進程的數據和調用其他進程裏的函數,相對於用c寫的dll注入後,只需要把c的接口改成Python的。這樣就能動態操作,不需要頻繁改動dll代碼,注入卸載了

同時它還能調用其他進程裏的任意函數,不過默認只能調用stdcallcdecl兩種調用約定的函數。如果不是這兩種調用約定,則需要使用內聯彙編來調用。當然Python無法直接內聯彙編,但可以通過彙編引擎將彙編指令翻譯成機器能識別的機器碼寫入到內存,達到內聯彙編的效果。也可以不用匯編引擎,直接寫機器碼到內存,只要你能記得彙編指令代表的機器碼(人肉彙編引擎)。

與進程交互

對於調用dll相關的功能,我這裏就不多贅述了,之前寫的一篇文章裏有:Python基礎庫-ctypes

這裏我主要說下ctypes與進程交互方面,比如讀取內存結構體,調用內存中的函數等

寫一個測試程序

先自己寫一個測試程序,然後在自己的程序測試,這樣可以避免很多錯誤,也方便調試。簡單寫了幾個函數和結構體測試,代碼如下:

typedef int(*cdecl_add_pointer)(int, int);
typedef int(__stdcall *stdcall_add_pointer)(int, int);

struct CString
{
   wchar_t* s = nullptr;
   size_t len = 0;
   CString(wchar_t* ss) {
   	s = ss;
   	len = wcslen(ss);
   }
};

CString ccs((wchar_t*)L"aaaaaa這是個全局變量結構體");

int cdecl_add(int a, int b) {
   std::wcout << L"cdecl調用約定\n";
   return a + b;
}

int __stdcall stdcall_add(int a, int b) {
   std::wcout << L"stdcall調用約定\n";
   return a + b;
}

int add_callback(stdcall_add_pointer add, int a, int b) {
   std::wcout << L"add_callback \n";
   return add(a, b);
}

int console_print(CString* cs) {
   std::wcout << L"print CString: ";
   std::wcout << cs->s;
   std::wcout << L"\n";
   return cs->len;
}

調用進程內的函數

這裏就用上一篇的pyexe.dll來將Python注入到目標進程。

現在開始調用cdecl_add和stdcall_add這兩個函數,首先需要找到他們的地址偏移,上面的函數裏都有一個字符串,這也是我爲了方便定位刻意寫的。

在x32dbg裏搜索字符串,就能定位這兩個函數,比如cdecl_add:
file

得出cdecl_add函數的偏移就是00AF4190-00AE0000, 00AE0000是exe的基址。同理可以知道stdcall_add的基址爲0x00AF43B0 - 0x00AE0000

先定義一個GetModuleHandleW函數用於獲取exe的基址

import ctypes

kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
GetModuleHandleW = kernel32.GetModuleHandleW
GetModuleHandleW.argtypes = (ctypes.c_wchar_p, )
GetModuleHandleW.restype = ctypes.c_int
base = GetModuleHandleW("CtypesTest.exe")

以下幾行代碼就是調用cdecl_add的全部代碼,看註釋一行一行解釋:

# 定義函數指針類型,第一個參數是返回值類型,後面的都是參數類型
cdecl_add_pfunc = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int)
# 函數的偏移
cdecl_add_offset = 0x00AF4190 - 0x00AE0000
# 通過基址和偏移得到當前函數所在內存地址,然後傳給cdecl_add_pfunc就能得到這個函數
cdecl_add = cdecl_add_pfunc(base + cdecl_add_offset)
# 傳入相應的參數就能調用成功
print("cdecl_add: ", cdecl_add(111, 222))

file

可以看到結果成功輸出,也沒有報錯。沒有打印cdecl調用約定是因爲我們在注入Python是重定向了stdout,如果想要打印目標進程的輸出則需要使用上一篇文章提到的CPython接口重定向stdout。

而調用stdcall_add和它基本一樣,將 ctypes.CFUNCTYPE改成ctypes.WINFUNCTYPE即可
file

構建結構體並調用函數

接着我們開始調用console_print,它的參數類型是一個結構體指針,所以要先在Python構建出結構體

ctypes定義結構體代碼如下:

class CString(ctypes.Structure):
    _fields_ = [
        ('s', ctypes.c_wchar_p),
        ('len', ctypes.c_uint)
    ]

定義console_print函數:

console_print_pfunc = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.POINTER(CString))
console_print_offset = 0x00AF2F10 - 0x00AE0000
console_print = console_print_pfunc(base + console_print_offset)

創建結構體並賦值

cs = CString()
s = "Python結構體字符串"
cs.s = ctypes.c_wchar_p(s)
cs.len = len(s)

爲了確保創建的結構體和目標進程裏的一樣,可以先在Python控制檯創建,然後在x32dbg裏查看。

這裏我爲了避免一直要輸入代碼,使用import sys;sys.path.append(r"T:\Code\PyRobot\part3\py_code")來將目錄添加到sys.path,然後導入我寫的代碼import testa

如果要重新導入:import importlib;importlib.reload(testa),查看Python構建的結構體內存地址有三種方法:

print("ctypes.byref: ", ctypes.byref(cs))
print("ctypes.addressof: ", hex(ctypes.addressof(cs)))
print("ctypes.cast: ", hex(ctypes.cast(ctypes.pointer(cs), ctypes.c_void_p).value))

效果如下:
file

可以看到cs的內存地址是0x1570d40,然後在x32dbg裏查看這個內存地址。

在命令裏輸入dump 0x1570d40或者打開幫助->計算器,輸入這個地址,然後在內存窗口打開:
file

這個地址的內容就是Python構建出的結構體,如果不清楚結構體在內存中長啥樣,可以把c代碼創建的結構體也打印出來,然後在x32dbg中查看

最後調用這個函數,ctypes.byref的作用是傳遞指針的引用,ctypes.pointer也可以,它是構造一個新的指針:

result = console_print(ctypes.byref(cs))
print("console_print result: ", result)

調用成功,說明結構體構造的沒問題:
file

讀取內存中的全局結構體

一樣是先計算偏移

# 全局變量的內存地址一般偏移是固定的,如果是函數內的局部變量就不能這麼計算了
ccs_offset = 0x00AFE2D0 - 0x00AE0000 
css_addr = base + ccs_offset

然後從地址中讀取出結構體裏的字符串和整數

s = ctypes.c_wchar_p.from_address(css_addr)
l = ctypes.c_uint.from_address(css_addr + 0x4)
print("單獨讀取內存結構體: ", s.value, l)

更簡單的方法就是直接轉爲結構體

css = CString.from_address(css_addr)
print("讀取整個結構體: ", css.s, css.len)

執行結果如下圖:
file

調用回調函數

先定義一個Python回調函數

def python_stdcall_add(a:int, b:int):
    print("python_stdcall_add: ", a, b)
    return a-b

定義add_callback函數

add_callback_pfunc = ctypes.CFUNCTYPE(ctypes.c_int, stdcall_add_pfunc, ctypes.c_int, ctypes.c_int) 
add_callback_offset = 0x00AF40D0 - 0x00AE0000
add_callback = add_callback_pfunc(base + add_callback_offset)

因爲回調函數的類型stdcall_add之前已經定義了,這裏就直接用了

result = add_callback(stdcall_add_pfunc(python_stdcall_add), 5, 2)
print("add_callback: ", result)

執行結果:
file

本篇就到此結束了,其他更復雜的數據類型在後面的實戰中再說。

本篇文章用到的文件和代碼

https://github.com/kanadeblisst00/PyRobot-part3

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章