【Python微信机器人】第四篇:实战发送文本和图片消息(讲解篇)

目录修整

目前的系列目录(后面会根据实际情况变动):

  1. 在windows11上编译python
  2. 将python注入到其他进程并运行
  3. 注入Python并使用ctypes主动调用进程内的函数和读取内存结构体
  4. 调用汇编引擎实战发送文本和图片消息
  5. 允许Python加载运行py脚本且支持热加载,发布成pypi库,可pip安装
  6. 利用beaengine反汇编引擎的c接口写一个pyd库,用于实现inline hook
  7. 利用beaengine反汇编引擎的python接口写一个py库,用于实现inline hook
  8. 使用inline hook实战接收消息
  9. 读取微信内存中的好友联系人列表的信息结构体数据
  10. 根据bug反馈和建议进行细节上的优化
  11. 做一个僵尸粉检测工具
  12. 其他功能慢慢加

温馨提示

该项目仅做学习使用, 不建议用于实际生产中。因为可能会导致封号,如果想学习的,请使用小号进行测试

文章篇幅过长,这里分为了两个部分:使用篇和讲解篇。如果不想知道原理,只是想体验一下的,可以看使用篇。

准备工作

需要的环境

当前官网下载的最新x86版本是3.9.7.28(发文的时候,最新版本已经更新到了3.9.8.12,代码中已经更新,同时支持两个版本),后面的文章也会使用该版本做为案例。

另外,Python也开始使用官网下载的安装版本(没什么区别,就是大家下载方便)。版本随意,只需要是32位的。anaconda创建的虚拟环境也可以。

原理

跟c语言写的发送消息对比,这种方式用来测试call挺方便的,不用去一直编译dll注入卸载,可以动态修改代码来测试。

实现方法和上一篇文章类似,都是调用进程内的函数,只是x86版本的微信函数大部分都不是stdcall和cdecl调用约定。而用ctypes调用这种函数就需要增加一个步骤:内存构建一个stdcall或cdecl的函数,再去调用这个构建好的函数。

先用汇编写一个简单的函数,在通过汇编引擎将汇编代码转成机器码,在申请一段可执行的内存,将机器码写入到这块内存,接着就可以用Python调用这个内存函数了。这个操作我在之前写的一篇文章里也有提过:用Python发送微信消息给好友(优化篇)。

相比之前的那篇文章,现在可以不用自己构造结构体和一些多余操作,因为Python所在进程已经和微信是同一个了。写的代码也会简短很多,也更容易看懂。

发消息函数汇编

先看看新版本发送文本消息的汇编代码,其实push 0x0上面那个call也是发消息的函数之一,但因为只需要运行一次就可以,调不调用都可以:

【Python微信机器人】第四篇:实战发送文本和图片消息(讲解篇)

这个函数的调用约定很像fastcall,因为使用ecx和edx还有堆栈传参,但它又不是由被调用函数来平栈的,它是由调用者使用add来平衡堆栈。

开始写代码

汇编指令转机器码

首先需要一个汇编引擎将汇编转为机器码,不然我们还得在Python里手写机器码。这里我选择的是keystone,安装方法pip install keystone-engine

转换的代码如下:

import keystone

def asm2code(asm_code):
 '''汇编指令转机器码'''
    ks = keystone.Ks(keystone.KS_ARCH_X86, keystone.KS_MODE_32)
    bytes_code, _ = ks.asm(asm_code, as_bytes=True)
    return bytes_code

定义微信使用的结构体字符串

class GeneralStructW(Structure):
    _fields_ = [
        ('content', c_wchar_p),
        ('content_len1', c_uint32),
        ('content_len2', c_uint32),
        ('null_word1', c_int),
        ('null_word2', c_int)
    ]

# 缓存大小给大一点,要是给小了微信会崩溃
buffer_len = 0x500
# 缓存用的数据类型无所谓,这里就用DWORD数组
buffer_struct = DWORD * buffer_len

定义内存函数

正常情况下的函数都需要保存ebp环境,汇编指令如下:

push ebp;
mov ebp, esp;
... // 在这里写你的操作
pop ebp;
ret;

当然如果你还想保存edi的话,就再加一个push edipop edi。如果函数有参数的话,ebp+0x8为第一个参数,ebp+0xC为第二个参数,依次加4。

计算函数地址

# 获取WeChatWin.dll基址
wxwin_base = GetModuleHandleW("WeChatWin.dll")

text_call1 = c_int(wxwin_base + 0xD49990)
text_call2 = c_int(wxwin_base + 0x79D9A0)

开始写汇编

就是把微信里发送文本的汇编指令稍微改一下,然后再把变量改成参数,这里需要注意:text_call1不能在调用内存函数之前被垃圾回收机制给回收了,不能就会报很奇怪的错误

# 先获取text_call的地址
call1_addr = addressof(text_call1)
call2_addr = addressof(text_call2)
# 函数的汇编指令
asm_code = f'''
push ebp;
mov ebp, esp;
push 0x0;
push 0x0;
push 0x1;
push 0x1;
mov eax, dword ptr [ebp+0x8]; // @的结构体
push eax;
mov eax, dword ptr [ebp+0xC]; // 发送的文本内容
push eax;
mov edx, dword ptr [ebp+0x10]; // wxid
mov ecx, dword ptr [ebp+0x14]; // 缓存
call dword ptr ds:[{call1_addr:#02x}];
add esp, 0x18;
mov ecx, dword ptr [ebp+0x14];
call dword ptr ds:[{call2_addr:#02x}];
pop ebp;
ret;
'''

申请内存将机器码写进去

# 汇编转机器码
shellcode = asm2code(asm_code.encode())
# 申请一块可读可写可执行的内存区域,大写是4096个字节
mem_buf = VirtualAlloc(04096, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
print("构建发送文本函数的内存地址: ", hex(mem_buf))
# 将机器码写到这块内存里
memmove(mem_buf, shellcode, len(shellcode))

用Python封装这个函数调用

args_type = (
    buffer_struct, # @的结构体,这里用DWORD数组
    POINTER(GeneralStructW), # 发送的文本内容结构体
    POINTER(GeneralStructW), # wxid结构体
    buffer_struct  # 缓存,也用DWORD数组
)
mem_call = CFUNCTYPE(void, *args_type)(mem_buf)

构造结构体参数

# 构建微信id结构体
wxid = "filehelper" # 这是文件传输助手的wxid
wxid_struct = GeneralStructW()
c_wxid = c_wchar_p(wxid)
wxid_struct.content = c_wxid
wxid_struct.content_len1 = len(wxid)
wxid_struct.content_len2 = len(wxid)
# 构建文本结构体
text_struct = GeneralStructW()
text = "测试消息!"
c_text = c_wchar_p(text)
text_struct.content = c_text
text_struct.content_len1 = len(text)
text_struct.content_len2 = len(text)
# 创建DWROD数组,并赋值为0。相对于c语言中的 DWORD buffer1[buffer_len] = {0};
buffer1 = buffer_struct(*[0]*buffer_len)
buffer2 = buffer_struct(*[0]*buffer_len)

你可以使用addressof输出构建的结构体地址,然后在x32dbg里查看是否和发消息的参数一样的

最后发送消息

mem_call(buffer1, byref(text_struct), byref(wxid_struct), buffer2)

【Python微信机器人】第四篇:实战发送文本和图片消息(讲解篇)

发送图片基本上是一样的,图片可以是正常的图片格式,也可以是微信加密的dat格式。这里就不重复放代码了,感兴趣的可以看github的源码

64位程序

这里稍微提一下怎么注入Python到64位程序,并且调用内部函数。因为之后的inline hook我不会64位的,所以只是简单的说一遍,后面的内容就没有64位的参与了。也说不准哪天我就找到可以在Python中hook 64位的方法,找到的话到时候就会更新。

微信的版本就选择我已经安装的3.9.7.29,发消息的汇编代码如下:

【Python微信机器人】第四篇:实战发送文本和图片消息(讲解篇)

在64位程序里,没有了奇奇怪怪的调用约定,ecx、edx、r8、r9依次是前四个参数,后面的参数通过堆栈传递。所以调用x64的函数更简单,不用写汇编代码。

定义结构体

class GeneralStructW(Structure):
    _fields_ = [
        ('content', c_wchar_p),
        ('content_len1', c_uint64),
        ('content_len2', c_uint64),
        ('null_word1', c_uint64),
        ('null_word2', c_uint64)
    ]

# 缓存大小给大一点,要是给小了微信会崩溃
buffer_len = 0x500
# 缓存用的数据类型无所谓,这里就用DWORD数组
buffer_struct = DWORD * buffer_len

定义发消息函数

args_type = (
    buffer_struct,
    POINTER(GeneralStructW),
    POINTER(GeneralStructW),
    buffer_struct,
    c_int64,
    c_int64,
    c_int64,
    c_int64,
)
wxwin_base = GetModuleHandleW("WeChatWin.dll")
sendmsg_call_addr = wxwin_base + 0x1024370
sendmsg_call = CFUNCTYPE(void, *args_type)(sendmsg_call_addr)

构建参数

wxid = "filehelper" # 这是文件传输助手的wxid
wxid_struct = GeneralStructW()
c_wxid = c_wchar_p(wxid)
wxid_struct.content = c_wxid
wxid_struct.content_len1 = len(wxid)
wxid_struct.content_len2 = len(wxid)

text_struct = GeneralStructW()
text = "测试消息!"
c_text = c_wchar_p(text)
text_struct.content = c_text
text_struct.content_len1 = len(text)
text_struct.content_len2 = len(text)

buffer1 = buffer_struct(*[0]*buffer_len)
buffer2 = buffer_struct(*[0]*buffer_len)

发送消息

sendmsg_call(buffer1, byref(wxid_struct), byref(text_struct), buffer2, 1100)

下篇预告

下一篇先实现一下热加载Python脚本,毕竟在控制台下操作不太方便。如果注入Python之后能直接运行我的Python代码,并且可以在我修改代码之后,可以自动重新加载,那样才方便测试。顺便把代码发布到pypi上,支持用pip安装,到时候就可以import使用了

github源码

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


原文始发于微信公众号(Python成长路):【Python微信机器人】第四篇:实战发送文本和图片消息(讲解篇)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/196110.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!