Python實現向好友發(fā)送微信消息
前言
原理:Windows逆向,通過內聯(lián)匯編的形式調用發(fā)消息的函數
下面的代碼PC微信版本是:3.7.0.26 , python使用的32位的3.8.13版本
如果只是需要最后的Python代碼請直接翻到最后看 優(yōu)化篇
c語言發(fā)微信消息
因為c語言本身支持內聯(lián)匯編,所以寫發(fā)消息的代碼很簡單,需要找到發(fā)消息的call,構造一下參數即可
// 微信通用結構體
struct WxBaseStruct
{
wchar_t* buffer;
DWORD length;
DWORD maxLength;
DWORD fill1;
DWORD fill2;
WxBaseStruct(wchar_t* pStr) {
buffer = pStr;
length = wcslen(pStr);
maxLength = wcslen(pStr) * 2;
fill1 = 0x0;
fill2 = 0x0;
}
};
void SendText( wchar_t* wsTextMsg) {
// 發(fā)送的好友,filehelper是文件傳輸助手
wchar_t wsWxId[0x10] = L"filehelper";
WxBaseStruct wxWxid(wsWxId);
// 發(fā)送的消息內容
WxBaseStruct wxTextMsg(wsTextMsg);
wchar_t** pWxmsg = &wxTextMsg.buffer;
char buffer[0x3B0] = { 0 };
char wxNull[0x100] = { 0 };
DWORD dllBaseAddress = (DWORD)GetModuleHandleA("WeChatWin.dll");
// 發(fā)消息的函數call地址
DWORD callAddress = dllBaseAddress + 0x521D30;
__asm {
lea eax, wxNull;
push 0x1;
push eax;
mov edi, pWxmsg;
push edi;
lea edx, wxWxid;
lea ecx, buffer;
call callAddress;
add esp, 0xC;
}
}這部分不懂的可以百度pc微信發(fā)消息call,相關文章很多,基本從找call,調用call都有了。
寫好代碼然后封裝成dll注入到微信即可
Python調用
原理:c語言寫的dll將發(fā)消息的函數導出,這樣就能通過符號找到SendText的地址,然后通過CreateRemoteThread來調用,這也就是為什么我SendText里將wsWxId寫死的原因
CreateRemoteThread調用的外部函數只能傳遞一個參數。要想傳遞多個參數,兩種方式:通過傳入結構體的方式;或者寫一個匯編函數調用call轉機器碼后寫入進程空間,然后調用這個寫入的函數。
另外,為了方便我直接使用pymem庫,pip install pymem,也可以自己使用ctypes封裝,參考pymem的源碼,以WriteProcessMemory 為例
import ctypes
dll = ctypes.WinDLL('kernel32.dll')
WriteProcessMemory = dll.WriteProcessMemory
WriteProcessMemory.argtypes = [
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.POINTER(ctypes.c_size_t)
]
WriteProcessMemory.restype = ctypes.c_long這個定義的WriteProcessMemory就是pymem.ressources.kernel32.WriteProcessMemory
import pymem
import ctypes
def start_thread(process_handle, address, params=None):
'''調用CreateRemoteThread
process_handle: 外部進程句柄
address: 要調用的函數地址
params: 參數地址
'''
params = params or 0
NULL_SECURITY_ATTRIBUTES = ctypes.cast(0, pymem.ressources.structure.LPSECURITY_ATTRIBUTES)
thread_h = pymem.ressources.kernel32.CreateRemoteThread(
process_handle,
NULL_SECURITY_ATTRIBUTES,
0,
address,
params,
0,
ctypes.byref(ctypes.c_ulong(0))
)
last_error = ctypes.windll.kernel32.GetLastError()
if last_error:
pymem.logger.warning('Got an error in start thread, code: %s' % last_error)
pymem.ressources.kernel32.WaitForSingleObject(thread_h, -1)
return thread_h
def main(wxpid, content):
# 獲取進程句柄
process_handle = pymem.process.open(wxpid)
# 這里通過加載dll(sendText.dll) ,獲取到 SendText的地址偏移
# 當然你也可以直接用查看pe的軟件直接看SendText函數的偏移
ctypes.CDLL(r'F:\Code\匯編\sendText\Release\sendText.dll')
local_sendText_handle = pymem.ressources.kernel32.GetModuleHandleW("sendText.dll")
SendText_address = pymem.ressources.kernel32.GetProcAddress(local_sendText_handle, b"SendText")
sendText_Offset = SendText_address - local_sendText_handle
# 獲取到微信進程中sendText.dll的句柄
process_sendText_handle = pymem.process.module_from_name(process_handle, "sendText.dll")
# 微信進程內SendText的函數地址就等于 sendText.dll的基址加上偏移
SendText = process_sendText_handle.lpBaseOfDll + sendText_Offset
# 開始構造消息,因為是參數是wchar_t類型的,所以需要編碼為utf-16的
msg = content.encode('utf-16')
# 在微信進程申請一塊內存空間,大小為1000個字節(jié)
address = pymem.memory.allocate_memory(process_handle, 1000)
# 輸出SendText地址和申請的內存地址
print(SendText, "address:", hex(address))
# 往申請的內存地址寫入我們要發(fā)送的消息內容
pymem.ressources.kernel32.WriteProcessMemory(process_handle, address, msg, len(msg), None)
# 調用CreateRemoteThread發(fā)送消息
thread_h = start_thread(process_handle, SendText, address)
print(thread_h)
if __name__ == "__main__":
wxpid = 18196
main(wxpid, "Python test!")不用c編寫dll如何發(fā)消息
用c寫dll再用Python來調用dll里的函數太麻煩了,能不能只用Python就實現發(fā)消息的功能。
回答這個問題前先看看寫的dll里SendText函數的匯編形式

一共有四部分內容,第一部分是地址段,第二部分是機器語言,第三部分是匯編語言,第四部分是注釋。
猜測:既然函數就是第二部分的機器語言組成的,那么我直接將第二部分的內容拷貝到另一個地址,是否能正常執(zhí)行。為了方便和準確,我直接使用代碼拷貝了。
首先在sendtext.dll最下面選一塊沒有被使用的地址(508C1D80)
import pymem
import ctypes
def copy_code(wxpid, source_addr, buffer_len, src_addr):
# 獲取進程句柄
process_handle = pymem.process.open(wxpid)
# 申請一塊臨時內存空間存放讀取的機器碼
buffer = ctypes.create_string_buffer(buffer_len)
# 讀出source_addr地址里的內容到buffer,長度為buffer_len
pymem.ressources.kernel32.ReadProcessMemory(process_handle, source_addr, buffer, buffer_len, None)
# 寫入buffer.raw內容到src_addr,長度為buffer_len
pymem.ressources.kernel32.WriteProcessMemory(process_handle, src_addr, buffer.raw, buffer_len, None)
# 打印內容
print(buffer.raw.hex())
if __name__ == "__main__":
wxpid = 18196
source_addr = 0x508C1010
buffer_len = 0x127
src_addr = 0x508C1D80
copy_code(wxpid, source_addr, buffer_len, src_addr )0x508C1010就是上面那種匯編圖第一段的首地址,buffer_len 就是圖的最后一個地址減第一個地址+1的值,0x508C1D80就是我選擇的一塊空白的空間
復制完成后先不直接調用,先用x64dbg看看翻譯的匯編指令是不是一樣的。我對比了一下,發(fā)現有一點出入,也就是說相同的機器碼放在不同的內存地址解釋出來的匯編指定是不一樣的,主要不同之處在于call指定后面的地址。圖中三個地方:508C10DD、508C10F0和508C112E
要想調用成功,首先得清楚為什么這三個call后面的地址不一樣??梢詮哪夸浱D到call、jmp指令地址計算看看,也就是說E8后面跟的地址是通過call的地址減去E8這條指令所在的地址再減5得出來的
以508C10DD為例,圖中顯示的匯編是call <sendtext._memset>,后面的其實就是一個地址,在x64dbg里點擊這條匯編按空格就可以編輯,看到真實的地址是call 0x508C1D2C
計算表達式:0x508C1D2C-0x508C10DD-5 = 00000C4A,而機器碼是 E8 4A0C0000 基本對上了,順序應該只是大端和小端的問題,這個就不去研究了。這里的5是指E8 4A0C0000的字節(jié)數,其實真正的計算公式是call的地址減去當前指令的下一條指令的地址,而下一條指令的地址就是當前指令的地址+當前指令所占字節(jié)。就像圖中的jne指令則是減2
調用我們寫入的機器碼
上面說我在0x508C1D80寫入的機器碼,call后面的地址不對。先手動用x64dbg按空格鍵將地址修改成和dll中調用原函數的匯編一樣,然后再使用Python調用這個地址發(fā)消息。只需要把SendText = process_sendText_handle.lpBaseOfDll + sendText_Offset改成SendText = 0x508C1D80
測試是調用成功的,也就是說知道函數機器碼的情況下,我們完全可以自己在內存自己構造一個發(fā)消息的函數,而不用c語言寫dll。
要想實現上面的機器碼能完全脫離dll還需要一些操作,因為用x64dbg看到的一些地址前面都帶了sendtext.508C1960,說明這個地址是dll里的地址,如果dll不存在了,則這些地址里的內容也沒有意義了
還是先處理那三個call, call <sendtext._memset>,回車進到該函數發(fā)現是vcruntime140.dll里的memset,那么我把這條指令改成 call vcruntime14.memset應該也是能調用成功的吧,看了下vcruntime14.memset的地址是553DDA10,就改成call 0x553DDA10,試了下,確實是成功的。
508C112E的匯編是call <sendtext.@__security_check_cookie@4> ,回車進入該函數就三條指令
cmp ecx,dword ptr ds:[<___security_cookie>]
bnd jne <sendtext.failure>
bnd ret
是一個判斷,成功就ret,失敗就jne。所以這就第一條指令是有效的,將508C112E的call指令直接改成cmp ecx,dword ptr ds:[<___security_cookie>] 試試,字節(jié)數不一樣,所以我就不去改sendtext函數的原地址,我改0x508C1D80的代碼,也就是我自己寫入的代碼,也是成功調用的
圖中的jne指令并不需要改,這是相對地址,剩下需要改的就是一些內存地址了。比如508C1023出現的0x508C20AC,這個代碼沒太看懂。0x508C209C顯示是filehelper,也就是我們寫死的。用ce搜索utf-16的字符串filehelper, 然后替換這兩個地址(508C1023、508C1028)。假設搜索到的地址是010EE490,則把508C209C改成010EE490,508C20AC改成010EE4A0。再試著發(fā)下消息,也是成功的,其他幾個地址也做類似處理,如果ce沒有搜到,就用Python申請,然后寫入相應的內容即可
處理完后的機器碼寫入到WechatWin.dll調用會崩潰,原因可能是<___security_cookie>的地址也是dll中,我百度了下,這是微軟的GS編譯,用于驗證堆棧平衡的,可以在vs2017中項目-屬性->C/C+±?代碼生成->安全檢查->禁用安全檢查,就可以關閉掉了。接著生成的匯編代碼就不會包含cookie這種東西了
另外寫入到WechatWin.dll的vcruntime14.memset的機器碼也要改下,做完這些就可以調用成功了。
上面代碼有個小錯誤, msg = content.encode('utf-16')這里應該改成msg = content.encode('utf-16le') 兩個的區(qū)別請看:https://blog.csdn.net/QQxiaoqiang1573/article/details/84937863
第一次優(yōu)化
上面折騰了那么多,寫入匯編之后還是要手動改vcruntime14.memset的機器碼,能不能在寫入匯編的時候自動計算機器碼呢?上面已經知道了計算公式,那么自動計算應該也不難
期間又遇到兩個小問題,GetModuleHandleA的地址也是變化的,需要動態(tài)獲取。vcruntime140.dll沒有被加載,是在注入sendtext.dll后才被加載。
完整代碼如下,如果你想在自己電腦上運行,其中有個地方需要改。C:\Software\WeChat\3.7.0.26\vcruntime140.dll這個路徑得改成你自己微信3.7.0.26下的vcruntime140.dll,必須要是微信下的vcruntime140.dll,系統(tǒng)的不行,因為偏移不一樣。用系統(tǒng)的則需要改0xDA10這個偏移量
import os
import pymem
import ctypes
import time
def calc_code(calladdr, codeaddr):
code = calladdr - codeaddr - 5
hex_code = hex(code & 0xFFFFFFFF)
return hex_code
def convert_addr(addr):
if isinstance(addr, int):
addr = hex(addr)
if addr.startswith("0x") or addr.startswith("0X"):
addr = addr[2:]
if len(addr) < 8:
addr = (8-len(addr))*'0' + addr
tmp = []
for i in range(0, 8, 2):
tmp.append(addr[i:i+2])
tmp.reverse()
return ''.join(tmp)
def start_thread(process_handle, address, params=None):
'''調用CreateRemoteThread
process_handle: 外部進程句柄
address: 要調用的函數地址
params: 參數地址
'''
params = params or 0
NULL_SECURITY_ATTRIBUTES = ctypes.cast(0, pymem.ressources.structure.LPSECURITY_ATTRIBUTES)
thread_h = pymem.ressources.kernel32.CreateRemoteThread(
process_handle,
NULL_SECURITY_ATTRIBUTES,
0,
address,
params,
0,
ctypes.byref(ctypes.c_ulong(0))
)
last_error = ctypes.windll.kernel32.GetLastError()
if last_error:
pymem.logger.warning('Got an error in start thread, code: %s' % last_error)
pymem.ressources.kernel32.WaitForSingleObject(thread_h, -1)
return thread_h
def main(wxpid, content):
format_code = '558bec81ecfc040000a1{filehelper10}0f1005{filehelper}8945c466a1{buffer}668945c88d45b48bc866c745d200000f1145b4560f57c08945d457660fd645ca8d5102668b0183c1026685c075f52bcac745e000000000d1f9894dd8c745e4000000008d04098b4d088bd18945dc894de88d7202668b0283c2026685c075f52bd6d1fa8955ec8d5102668b0183c1026685c075f52bcac745f400000000d1f968b00300006a00c745f8000000008d04098945f08d45e88945088d8504fbffff50e8{memset1}68000100008d85b4feffff6a0050e8{memset2}83c41868{wechatwin}ff15{GetModuleHandleA}05301d52008945fc8d85b4feffff6a01508b7d08578d55d48d8d04fbffffff55fc83c40c5f5e8be55dc3'
process_handle = pymem.process.open(wxpid)
filehelper_address = pymem.memory.allocate_memory(process_handle, 50)
text = "filehelper".encode("utf-16le")
pymem.ressources.kernel32.WriteProcessMemory(process_handle, filehelper_address, text, len(text), None)
filehelper_hex_code = convert_addr(filehelper_address)
filehelper10_hex_code = convert_addr(filehelper_address+0x10)
buffer_address = pymem.memory.allocate_memory(process_handle, 16)
buffer_hex_code = convert_addr(buffer_address)
WeChatWin_address = pymem.memory.allocate_memory(process_handle, 100)
msg = "WeChatWin.dll".encode("ascii")
pymem.ressources.kernel32.WriteProcessMemory(process_handle, WeChatWin_address, msg, len(msg), None)
wechatwin_hex_code = convert_addr(WeChatWin_address)
ctypes.CDLL('kernel32.dll')
local_kernel32_handle = pymem.ressources.kernel32.GetModuleHandleW("kernel32.dll")
GetModuleHandleA_address = pymem.ressources.kernel32.GetProcAddress(local_kernel32_handle, b"GetModuleHandleA")
GetModuleHandleA_Offset = GetModuleHandleA_address - local_kernel32_handle
process_kernel32_handle = pymem.process.module_from_name(process_handle, "kernel32.dll")
GetModuleHandleA = process_kernel32_handle.lpBaseOfDll + GetModuleHandleA_Offset
GetModuleHandleA_address = pymem.memory.allocate_memory(process_handle, 4)
pymem.memory.write_int(process_handle, GetModuleHandleA_address, GetModuleHandleA)
GetModuleHandleA_hex_code = convert_addr(GetModuleHandleA_address)
process_vcruntime140_handle = pymem.process.module_from_name(process_handle, "vcruntime140.dll")
if not process_vcruntime140_handle:
pymem.process.inject_dll(process_handle, r'C:\Software\WeChat\3.7.0.26\vcruntime140.dll'.encode("ascii"))
process_vcruntime140_handle = pymem.process.module_from_name(process_handle, "vcruntime140.dll")
memset = process_vcruntime140_handle.lpBaseOfDll + 0xDA10
code_address = pymem.memory.allocate_memory(process_handle, 500)
memset1 = convert_addr(calc_code(memset, code_address+0xBE))
memset2 = convert_addr(calc_code(memset, code_address+0xD1))
hex_code = format_code.format(filehelper10=filehelper10_hex_code, filehelper=filehelper_hex_code,
buffer=buffer_hex_code, wechatwin=wechatwin_hex_code, memset1=memset1, memset2=memset2,
GetModuleHandleA=GetModuleHandleA_hex_code)
hex_code = bytes.fromhex(hex_code)
pymem.ressources.kernel32.WriteProcessMemory(process_handle, code_address, hex_code, len(hex_code), None)
msg = content.encode('utf-16le')
address = pymem.memory.allocate_memory(process_handle, 1000)
pymem.ressources.kernel32.WriteProcessMemory(process_handle, address, msg, len(msg), None)
print(hex(code_address))
# 調用CreateRemoteThread發(fā)送消息
thread_h = start_thread(process_handle, code_address, address)
time.sleep(0.5)
pymem.memory.free_memory(process_handle, filehelper_address)
pymem.memory.free_memory(process_handle, buffer_address)
pymem.memory.free_memory(process_handle, WeChatWin_address)
pymem.memory.free_memory(process_handle, code_address)
pymem.memory.free_memory(process_handle, GetModuleHandleA_address)
if __name__ == "__main__":
wxpid = 24600
process_handle = pymem.process.open(wxpid)
main(wxpid, "你好")第二次優(yōu)化
優(yōu)化主要還有兩個點:發(fā)送的人不要寫死,也可以通過參數傳入,這個實現很簡單,寫死的那個filehelper也是在內存構造的,當然也可以構造任意一個好友的wxid;上面的代碼也很麻煩,還要自己拼裝十六進程的機器碼,能不能只寫匯編,然后自動轉成機器碼呢?先說結果:是可以的,Python就有很多匯編轉機器碼的庫,比如keystone和unicorn等,待我在研究研究
x86/x64 Call Jmp指令區(qū)別
- Call指令主要實現對一個函數的調用。Jmp指令主要實現地址的調轉。
- Call指令和Jmp指令的區(qū)別
1:Call指令和Jmp指令的機器碼不同。
2:Call指令會對當前指令的下一條指令的地址進行壓棧操作,來實現函數的返回。
相當于
Push eip+5
Jmp xxxxxxxx
Call指令的二進制形態(tài)(機器碼)
1:X86
Call --- e8/ ff15(但是其他比如 call eax 等是不相同的)
E8 xxxxxxxx 其中xxxxxxxx是偏移地址
計算方法:目標地址-當前地址-5 = 偏移地址
Ff15 xxxxxxxx 其中xxxxxxxx是絕對地址(FF15會對當前的這個絕對地址解*號,也就是絕對地址[目標地址])
Jmp ---e9 /ff25
E9 xxxxxxxx其中xxxxxxxx是偏移地址
計算方法:目標地址-當前地址-5 = 偏移地址
Ff25 xxxxxxxx其中xxxxxxxx是絕對地址(FF15會對當前的這個絕對地址解*號,也就是絕對地址[目標地址])
2:X64
E8 xxxxxxxx 其中xxxxxxxx是偏移地址
計算方法:目標地址-當前地址-5 = 偏移地址
Ff15 xxxxxxxx 其中xxxxxxxx是相對地址(FF15會對當前的這個相對地址解*號,也就是相對地址[目標地址])
Ff25 xxxxxxxx其中xxxxxxxx是相對地址(FF15會對當前的這個相對地址解*號,也就是相對地址[目標地址])
到此這篇關于Python實現向好友發(fā)送微信消息 的文章就介紹到這了,更多相關Python微信消息內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
django獲取from表單multiple-select的value和id的方法
今天小編就為大家分享一篇django獲取from表單multiple-select的value和id的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-07-07
Python中循環(huán)后使用list.append()數據被覆蓋問題的解決
這篇文章主要給大家介紹了關于Python中循環(huán)后使用list.append()數據被覆蓋問題的解決方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2018-07-07
Python爬蟲實現selenium處理iframe作用域問題
這篇文章主要介紹了Python爬蟲實現selenium處理iframe作用域問題,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-01-01
Python?操作?MongoDB數據庫的方法(非?ODM)
這篇文章主要介紹了Python?操作?MongoDB?----非?ODM的方法,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-03-03

