前言
原理:Windows逆向,通过内联汇编的形式调用发消息的函数
下面的代码PC微信版本是:3.7.0.26 , python使用的32位的3.8.13版本。
微信一定要和这个版本一样(其他版本需要自己找偏移量),python只需要32位,版本随意。
如果只是需要最后的Python代码请直接翻到最后看 优化篇
c语言发微信消息
因为c语言本身支持内联汇编,所以写发消息的代码很简单,需要找到发消息的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) {
// 发送的好友,filehelper是文件传输助手
wchar_t wsWxId[0x10] = L"filehelper";
WxBaseStruct wxWxid(wsWxId);
// 发送的消息内容
WxBaseStruct wxTextMsg(wsTextMsg);
wchar_t** pWxmsg = &wxTextMsg.buffer;
char buffer[0x3B0] = { 0 };
char wxNull[0x100] = { 0 };
DWORD dllBaseAddress = (DWORD)GetModuleHandleA("WeChatWin.dll");
// 发消息的函数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微信发消息call
,相关文章很多,基本从找call,调用call都有了。
写好代码然后封装成dll注入到微信即可
Python调用
原理:c语言写的dll将发消息的函数导出,这样就能通过符号找到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个字节
address = pymem.memory.allocate_memory(process_handle, 1000)
# 输出SendText地址和申请的内存地址
print(SendText, "address:", hex(address))
# 往申请的内存地址写入我们要发送的消息内容
pymem.ressources.kernel32.WriteProcessMemory(process_handle, address, msg, len(msg), None)
# 调用CreateRemoteThread发送消息
thread_h = start_thread(process_handle, SendText, address)
print(thread_h)
if __name__ == "__main__":
wxpid = 18196
main(wxpid, "Python test!")
不用c编写dll如何发消息
用c写dll再用Python来调用dll里的函数太麻烦了,能不能只用Python就实现发消息的功能。
回答这个问题前先看看写的dll里SendText函数的汇编形式
一共有四部分内容,第一部分是地址段,第二部分是机器语言,第三部分是汇编语言,第四部分是注释。
猜测:既然函数就是第二部分的机器语言组成的,那么我直接将第二部分的内容拷贝到另一个地址,是否能正常执行。为了方便和准确,我直接使用代码拷贝了。
首先在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看看翻译的汇编指令是不是一样的。我对比了一下,发现有一点出入,也就是说相同的机器码放在不同的内存地址解释出来的汇编指定是不一样的,主要不同之处在于call指定后面的地址。图中三个地方:508C10DD、508C10F0和508C112E
要想调用成功,首先得清楚为什么这三个call后面的地址不一样。可以看看这个:call、jmp指令地址计算,也就是说E8后面跟的地址是通过call的地址减去E8这条指令所在的地址再减5得出来的
以508C10DD为例,图中显示的汇编是call <sendtext._memset>
,后面的其实就是一个地址,在x64dbg里点击这条汇编按空格就可以编辑,看到真实的地址是call 0x508C1D2C
计算表达式:0x508C1D2C-0x508C10DD-5 = 00000C4A
,而机器码是 E8 4A0C0000
基本对上了,顺序应该只是大端和小端的问题,这个就不去研究了。这里的5是指E8 4A0C0000
的字节数,其实真正的计算公式是call的地址减去当前指令的下一条指令的地址,而下一条指令的地址就是当前指令的地址+当前指令所占字节。就像图中的jne
指令则是减2
调用我们写入的机器码
上面说我在0x508C1D80写入的机器码,call后面的地址不对。先手动用x64dbg按空格键将地址修改成和dll中调用原函数的汇编一样,然后再使用Python调用这个地址发消息。只需要把SendText = process_sendText_handle.lpBaseOfDll + sendText_Offset
改成SendText = 0x508C1D80
测试是调用成功的,也就是说知道函数机器码的情况下,我们完全可以自己在内存自己构造一个发消息的函数,而不用c语言写dll。
要想实现上面的机器码能完全脱离dll还需要一些操作,因为用x64dbg看到的一些地址前面都带了sendtext.508C1960,说明这个地址是dll里的地址,如果dll不存在了,则这些地址里的内容也没有意义了
还是先处理那三个call, call <sendtext._memset>,回车进到该函数发现是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>]
试试,字节数不一样,所以我就不去改sendtext函数的原地址,我改0x508C1D80的代码,也就是我自己写入的代码,也是成功调用的
图中的jne指令并不需要改,这是相对地址,剩下需要改的就是一些内存地址了。比如508C1023出现的0x508C20AC,这个代码没太看懂。0x508C209C显示是filehelper,也就是我们写死的。用ce搜索utf-16的字符串filehelper, 然后替换这两个地址(508C1023、508C1028)。假设搜索到的地址是010EE490,则把508C209C改成010EE490,508C20AC改成010EE4A0。再试着发下消息,也是成功的,其他几个地址也做类似处理,如果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')
两个的区别请看:https://blog.csdn.net/QQxiaoqiang1573/article/details/84937863
第一次优化
上面折腾了那么多,写入汇编之后还是要手动改vcruntime14.memset的机器码,能不能在写入汇编的时候自动计算机器码呢?上面已经知道了计算公式,那么自动计算应该也不难
期间又遇到两个小问题,GetModuleHandleA的地址也是变化的,需要动态获取。vcruntime140.dll没有被加载,是在注入sendtext.dll后才被加载。
完整代码如下,如果你想在自己电脑上运行,其中有个地方需要改。C:\Software\WeChat\3.7.0.26\vcruntime140.dll
这个路径得改成你自己微信3.7.0.26下的vcruntime140.dll,必须要是微信下的vcruntime140.dll,系统的不行,因为偏移不一样。用系统的则需要改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发送消息
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, "你好")
第二次优化
优化主要还有两个点:发送的人不要写死,也可以通过参数传入,这个实现很简单,写死的那个filehelper也是在内存构造的,当然也可以构造任意一个好友的wxid;上面的代码也很麻烦,还要自己拼装十六进程的机器码,能不能只写汇编,然后自动转成机器码呢?先说结果:是可以的,Python就有很多汇编转机器码的库,比如keystone和unicorn等,待我在研究研究