首发地址: https://mp.weixin.qq.com/s/Nik8fBF3hxH5FPMGNx3JFw
前言
最近想写一个免费的微信公众号自动采集的工具,我看公众号文章下载需求还挺多的。搜了下github,免费的工具思路大多都是使用浏览器打开公众号主页获取到需要的请求参数,例如key、uin等,然后再用参数请求历史。
优化
这些工具都需要自己复制公众号主页的链接发给文件传输助手然后打开,才能拦截到请求参数,那么能不能让这一步也让程序来完成呢?
简单的方式可以使用模拟点击,这个有兴趣的自行实现。这篇文章我说下基于逆向的方式在微信内置浏览器打开某个链接,这样实现可以更自动化一点,下面是具体的逆向和分析过程。
其他采集方案
在之前的一篇文章里我也说了其他一些采集方案,例如微信公众平台、微信读书等,有兴趣的可以看【Python微信机器人】写一个监控采集公众号文章的插件。不过听说微信读书已经开始加密了,而且采集的历史也不是很全,较早的文章可能获取不到。
对于微信公众平台来采集的可以参考aardio爬虫) 实战篇:采集自己的公众号粉丝列表,原理基本类似,都是先扫码获取到cookie,请求对应的接口,公众平台的接口都没加密。
还有基于逆向hook的采集方案,这个暂不公开。如果只是平时下载一下公众号文章的话,上面两种足够用了
方案优缺点
拦截参数对比逆向来说优点在于不需要固定微信的版本,更适合普通用户。因为逆向hook只能适用于特定版本,想不限版本,需要为每个版本做一遍适配,这明显不太可能。
而且基于逆向的方式采集还有可能有封号的风险,对于只想下几篇公众号文章的人来说,这个代价还是挺大的。当然逆向采集优点也很明显:稳定、无需人工操作、采集的数据量更多等。
逆向过程
下面逆向的版本选择的是3.9.6.32
,选这个版本主要是之前的写的机器人用的这个版本,很多东西有现成的。最新的也没变,想看64位最新版的翻到最后,也会提几句。
日志定位
微信定位一个功能call最好也最方便的方式就是日志(肯定不是因为我只会这个)。打开链接的地方有多处,比如以下两个:

还有就是点击聊天记录里的链接来打开,这三种方式都试一下,看看日志之间有什么共同点,这样更容易定位到关键日志点。



前两个都有openUrlWithExtraWebview
,看名称应该就是微信在调用打开浏览器,但是这里还处于比较上层的位置,如果你在这里打断点的话,会触发多次,不止打开浏览器才触发,并且看了下参数比较复杂,有一堆句柄和回调不好处理,所以这里先不考虑了。
WebViewMgr:user setting
这个日志比较重要,因为三个日志里都有它,而且正好处于openUrlWithExtraWebview
和AddTab
的中间位置,看上去像是打开浏览器时初始化配置。
感觉在这里打上断点应该能在调用堆栈里找到打开浏览器的call。先在IDA里搜索user setting
(x64dbg里搜索这个字符串也可以,我比较喜欢看IDA,有伪C代码看比汇编直观一点),然后定位到下面的位置:

接着翻到函数头,在x64dbg里函数头的位置打上断点,接着打开一个链接让断点断下

右键右下角堆栈的返回地址选择在反汇编中转到指定DWORD
(也就是函数调用的地方),然后在IDA中查看这个地址,翻到函数头的位置看到了比较关键的日志信息(showWebView
),在函数头继续打上断点

查看调用的位置,先看看IDA里这里所在的函数在做些什么操作,看上去是在解析json,而且看到了日志有OpenUrlWithExtraWebviewHandler
,看名称这里就是处理OpenUrlWithExtraWebview
事件的回调,那前面解析json就是在解析之前日志里的json

在头部打上断点,看看传入的参数是不是就是之前的json。如下图,虽然参数不是之前看到的json字符串,但是和json内容基本一样,估计上层函数又对json做了解析,那这里肯定是处理OpenUrlWithExtraWebview
事件的回调函数,也就是在这个函数里打开的链接

那关键位置基本就是上面提到的showWebView
函数了,下面开始分析函数的参数和几个需要调用的call。

分析参数

这里只有ecx是未知的,看了下ebp-0x90C
在上面就有,估计是上面赋值的,打上断点看一下,一般复制的结构体都有函数可以生成,不需要去关心怎么构造。当然,如果构造的call离的太远且结构体也不负责的话也可以自己构造。

分析的结果如上,这里没什么复杂的参数,只需要传一个url就可以。前面在查看函数引用的时候,看到有的地方调用6CC52610
时不是push的这几个值,也就是说这四个值并不是固定的,而是用于控制某些变量。
比如用系统默认浏览器打开则是下面的参数:

可能的组合(每行一种,最后一位是edx)
0 1 0 0 4
0 1 1 0 4
0 1 0 0 2
0 1 0 1 4
0 1 0 0 0
0 1 0 1 2
1 1 0 0 4
0 1 1 0 5
0 1 1 0 8
1 1 0 0 0
1 1 0 0 4
可以自己都测试一遍,不过看日志有的可能是打开小程序相关的,参数不一样也许会崩溃,具体案例具体分析吧。还有一个点,调用这四个call,堆栈不平衡,需要加上add esp, 0x10;
(不一定这么处理,也可能是少call,单步走看看到哪个call平栈了),我看其他地方调用这个call都是使用的add esp, 0x10;
,所以猜测这里加上这个也能运行。下面还有一个call是释放ebp-0x90C
结构体里的内存,也需要调用一下。
完整代码
DWORD ShowVebView(wchar_t* url) {
size_t urlLen = wcslen(url);
DWORD WeChatWinBase = GetWeChatWinBase();
DWORD dwCall1 = WeChatWinBase + 0x77A430;
DWORD dwCall2 = WeChatWinBase + 0xF67310;
DWORD dwCall3 = WeChatWinBase + 0x76CC70;
DWORD dwCall4 = WeChatWinBase + 0x1602610;
DWORD dwCall5 = WeChatWinBase + 0x77A790;
DWORD ebp_0x90C[0x500] = { 0 };
__asm {
pushad;
lea ecx, ebp_0x90C;
call dwCall1;
push urlLen;
push url;
lea eax, ebp_0x90C;
lea ecx, [eax + 0x56C];
call dwCall2;
call dwCall3;
push 0x0;
push 0x1;
push 0x0;
push 0x0;
xor edx, edx;
lea ecx, ebp_0x90C;
call dwCall4;
add esp, 0x10;
lea ecx, ebp_0x90C;
call dwCall5;
popad;
}
return 0;
}
编译成dll注入到进程,调用ShowVebView
正常打开链接。
64位
以能下载到的最新版为例(3.9.10.27
),其实32位和64位微信逻辑是一样的,同样在x64dbg里搜索user setting
,在引用的函数头打断点找到调用点,然后接着在函数头打断点找到调用点,关键位置如下图(基址是00007FF86A160000
):

逻辑一模一样,只是64位无法内联汇编,可以用函数指针来调用。大概代码如下:
typedef UINT64 (*dwCall1Ptr)(UINT64);
dwCall1Ptr dwCall1 = (dwCall1Ptr)0x7FF86BD82C70;
DWORD rbp_0x100[0x500] = {0};
UINT64 addr = dwCall1(&rbp_0x100);
typedef UINT64 (*dwCall2Ptr)(UINT64,wchar_t*,UINT64);
dwCall2Ptr dwCall2 = (dwCall2Ptr)0x7FF86C840C10;
wchar_t* url = (wchar_t*)L"";
dwCall2(addr+0x***, url, wcslen(url));
typedef UINT64 (*dwCall3Ptr)();
dwCall3Ptr dwCall3 = (dwCall3Ptr)0x7FF86BD72AE0;
dwCall3();
typedef UINT64 (*dwCall4Ptr)(UINT64,UINT64,UINT64,UINT64,UINT64,UINT64);
dwCall4Ptr dwCall4 = (dwCall4Ptr)0x7FF86D24E9B0;
dwCall4(addr, 0,0,0,1,0);
typedef UINT64 (*dwCall5Ptr)(UINT64);
dwCall5Ptr dwCall5 = (dwCall5Ptr)0x7FF86BD82EA0;
dwCall5(addr);
上面只是伪代码,需要自己调试改成能运行的代码。
网友评论