PCXX逆向 --- 分析获取登录二维码的数据
[ 本帖最后由 Kido 于 2019-6-6 18:26 编辑 ]\n\n# PC微信逆向 --- 分析获取登录二维码的数据## 1 前言&准备工作
前段时间有个朋友找我,让我拿下微信登录时侯的二维码数据,好久没有分析PC微信了,先去了解了下微信二维码登录的原理,找到了一篇WEB端的[微信二维码登录的原理](https://www.biaodianfu.com/weixin-qrcode.html):
> ```
> 1、每次用户打开PC端登陆请求,系统返回一个唯一的uid,并将uid的信息绘制成二维码返回给用户。这里的uid一定是唯一的,否则就会造成你登陆了其他用户的账号或者其他用户登陆你的账号。
>
> 2、当用户使用登陆后的微信扫描该二维码的时候,会将这个uid和手机上的微信账号及密码产生的token进行绑定,并上传到服务器。
>
> 3、WEB通过JS不断的向后端发起请求,查询有没有关于uid的登陆记录(uid和token是否存在于服务器上)
> ```
>
虽然该文章讲述以web端角度分析,但是感觉微信的PC端大致流程也是如此,下面我们就准备开干,分析目标:
**PC微信核心模块:wechatwin.dll (2.6.7.57版本)**
相关工具:
- windbg 或者 OD,x64dbg (动态调试工具)
- IDA (静态分析)
- vscode (看mars的源码)
- vs2015(开发hook功能模块)
- dbgview(看调试输出日志)
## 2 打开微信的Xlog日志输出
我们知道腾讯微信开源了 (https://github.com/Tencent/mars),可以看下Mars的框架,下面用官网的图来展示下:
!(https://ae01.alicdn.com/kf/HTB1ytteQmzqK1RjSZPx7634tVXaG.png)
可以看出来Mars的上层就是Wechat或者各种各样的App,Mars提供了xlog组件,这是一个高可靠性高性能的运行期日志组件,也就是说xlog是微信的看门人,我们首先要做的就是先让看门人说话.
1. 一般我们开发log组件的时候,一个基本的功能就是根据log等级来输出
2. log组件除了能根据log等级来输出之外,还有个比较有用的功能是指定输出的方式,是输出到文件中,还是输出到终端中
~~~
typedef enum {
kLevelAll = 0,
kLevelVerbose = 0,
kLevelDebug, // Detailed information on the flow through the system.
kLevelInfo, // Interesting runtime events (startup/shutdown), should be conservative and keep to a minimum.
kLevelWarn, // Other runtime situations that are undesirable or unexpected, but not necessarily "wrong".
kLevelError, // Other runtime errors or unexpected conditions.
kLevelFatal, // Severe errors that cause premature termination.
kLevelNone, // Special level used to disable all log messages.
} TLogLevel;
~~~
xlog定义中定义了上述的log的等级,代码位置(https://github.com/Tencent/mars/blob/a699361a1630b9f9b131ef21fc6040e2668fed14/mars/comm/xlogger/xloggerbase.h#L31) ,一般我们在本地测试的时候会把所有的log都输出,也就是使用kLevelAll,而在线上产品的话一般是设定kLevelInfo以上的输出,而在Mars代码中,这个函数就是[**xlogger_SetLevel**](<https://github.com/Tencent/mars/blob/a699361a1630b9f9b131ef21fc6040e2668fed14/mars/comm/xlogger/xloggerbase.c#L31>)函数,另外指定终端输出方法是[**appender_set_console_log**](https://github.com/Tencent/mars/blob/000d7a586c86e66223c145db38b541741060f991/mars/log/src/appender.cc#L1024)函数。
从Mars的xlog源代码上来说就是设置了两个全局变量 **gs_level**和 **sg_consolelog_open**。
所以这里我们整理一下思路:
> 我们只需要在wechawin.dll里找到对应的 **gs_level**和 **sg_consolelog_open**,并对其修改,就能让微信自身输出功能日志的信息,然后我们通过这些日志信息去查找定位,我们想要分析的功能点
这里,我们开始用IDA载入wechawin.dll,在IDA里静态分析查找这Xlog的两个开关全局变量。
#### 查找定位sg_consolelog_open变量
我们通过mars的源码中知道xlogger_appender函数中出现了sg_consolelog_open全局变量,并且这个函数的特征很好找,有字符串“**xlogger_appender Recursive calls**”,所以我们只需要在IDA里中一搜索就能出现这个字符串,看那个函数在引用这个字符串,就能找到对应的xlogger_appender函数,并且找到这个sg_consolelog_open全局变量的偏移,这里我们已经找到改函数,我们把函数命名下。
!(https://ae01.alicdn.com/kf/HTB1MBRJQhnaK1RjSZFB763W7VXas.png)
!(https://ae01.alicdn.com/kf/HTB1_9XjQbvpK1RjSZPi762mwXXa0.png)
我一般命名的方式的dll名字::函数名字 这样的好处是你一打dll名字在IDA的函数搜索框中,你所有的函数都会展示出来,不用花时间去记
#### 查找定位gs_level变量
从Mars中查找代码,发现gs_level没有什么别的函数用上,那找下xlogger_SetLevel吧,mars的例子中给我们调用xlog初始化的示例,
!(https://ae01.alicdn.com/kf/HTB1SbNnQmzqK1RjSZFp761kSXXa7.png)
也就是说appender_open函数附近会出现xlogger_SetLevel调用,我们找下appender_open的字符串特征“**appender has already been opened**”,在IDA中找到这个字符串的引用的函数,同样命名为
!(https://ae01.alicdn.com/kf/HTB1VftuQgHqK1RjSZFk760.WFXaw.png)
接着,再查找下这个函数的引用
!(https://ae01.alicdn.com/kf/HTB1JMpeQgTqK1RjSZPh760fOFXaq.png)
注意下图片里红框里面的函数,我们进去可以看到这个函数只是给一个全局变量赋值,这个就是xlogger_SetLevel函数了
!(https://ae01.alicdn.com/kf/HTB1T.FlQkvoK1RjSZFD760Y3pXan.png)
至此我们找到了这两个全局变量的偏移
后面等微信进程启动后,我们用windbg或者OD附加上去,修改这两个值,gs_level改成0(表示kLevelAll), sg_consolelog_open改成1(表示打开日志输出),这两个变量都是DWORD,修改完成后调试器退出附加
如果我们不用调试器而是代码处理的话,就是如下图所示了
!(https://ae01.alicdn.com/kf/HTB1pa4lQhTpK1RjSZFK7612wXXag.png)
不管是我们用代码还是调试器处理, 我们都能通过dbgview看到log输出了, 大家可以看下log输出,还是蛮漂亮的
!(https://ae01.alicdn.com/kf/HTB1pRXuQkvoK1RjSZFw763iCFXaK.png)
## 3 通过微信的Log定位关键功能以及获取登录二维码逻辑分析
搞定了PC微信的Log输出后,现在微信这个XLog看门人开始说话,我们就需要仔细的聆听看门人的倾诉,同时,脑子里面记着微信二维码登录的原理,如果幸运的话,我们就能够从看门人的话语中梳理出完整的脉络,我们知道二维码的相关的数据应该是搜索QRcode,所以这里先把log从dbgview中导出,然后搜索关键字QRCODE
我们能看见这样的一条log,这个是第一个带有QRCODE的日志
~~~
[first CGI, to get QRCode Image!
~~~
可以先猜测下这个是整个获取二维码数据逻辑的开头,看样子是说准备向服务器要数据了,我们沿着一路往下看
接下来我们会看到第二条相关的日志
~~~
[getLoginQRcode success, UUID = Ad2ee0iiaXVVEIaiKhP8, checkTime = 15
~~~
哈哈,这里出现了UUID,还记得我们在微信二维码登录原理里面说的那样吗?服务器这个时候给了我们一个UUID了,我们有理由相信,NetSceneGetLoginQRCode::onGYNetEnd有可能就是解码服务器数据的函数,我们接着往下看,看看能不能找到二维码显示的逻辑
~~~
[OnCheckQrCodeTimerCallback
~~~
看看这个,回想下我们在微信二维码登录原理里面所说的,这个应该就是客户端向服务端查询登录状态的逻辑了,也就是说二维码在这个日志出现前就已经显示给用户了,但是我们用QRCODE做关键字,看下来是没有找到显示的逻辑,我们还是往上看看,从二条日志开始的位置到第三条日志出现的位置,我们一条条的看.
在这两条日志之间,我们会看到这样一条日志
~~~
[ load image
~~~
这条很可疑,我们IDA查看下这个log
!(https://ae01.alicdn.com/kf/HTB1gvhMQhjaK1RjSZKz760VwXXaj.png)
进入到sub_1047DDB0函数里面一看,
!(https://ae01.alicdn.com/kf/HTB1Q2llQbvpK1RjSZFq763XUVXaQ.png)
注意上图中的两个标注的函数,其中v28就是原始的二维码数据buffer,v3是size,这里为啥我们会这么肯定呢,可以看下(https://www.experts-exchange.com/questions/23372362/Memory-Buffer-into-GDI-Image-FromStream.html)这个文章的示例
~~~
char *ibuf = new char;
hBuffer = ::GlobalAlloc(GMEM_MOVEABLE,ze.unc_size);
UnzipItem(hz, i, ibuf, ze.unc_size);
if(hBuffer)
{
void* pBuffer = ::GlobalLock(hBuffer);
if(pBuffer)
{
CopyMemory(pBuffer, ibuf, ze.unc_size);
IStream* pStream = NULL;
if(::CreateStreamOnHGlobal(hBuffer,FALSE,&pStream) == S_OK)
{
m_BackImage = Gdiplus::Image::FromStream(pStream,false);
pStream->Release();
~~~
有没有发现文章的例子和我们图片的很相似呢,现在我们能知道了CopyMemory中的第二个参数就是原始的二维码数据,第三个参数是buffer的长度
~~~
void CopyMemory(
_In_ PVOIDDestination,
_In_ const VOID *Source,
_In_ SIZE_T Length
);
~~~
我们再在sub_1047DDB0中查看下v28和v3参数的来源,我们可以看到
!(https://ae01.alicdn.com/kf/HTB1wDtcQb2pK1RjSZFs761NlXXaS.png)
其中v28是a2,v3是a1赋值,而a1是edx寄存器赋值,a2是ecx赋值,也就是sub_1047DDB0的 edx是二维码数据的Size, ecx是二维码数据的buffer, 我们可以看下上层对sub_1047DDB0的调用,确实可以看出来edx是此时的size,ecx是此时的二维码数据buffer
!(https://ae01.alicdn.com/kf/HTB1jGNoQgHqK1RjSZFP763wapXah.png)
至此我们找到了这个二维码数据的显示, 我们的hook点可以是下面这句代码的位置
~~~
mov edx,
~~~
然后hook代{过}{滤}理函数是,我们采用裸函数的写法写这个hook函数,需要自己平衡堆栈
!(https://ae01.alicdn.com/kf/HTB1sH0oQgHqK1RjSZFP763wapXa0.png)
而QRCODEProxy的函数原型如下
~~~
VOID __stdcall QRCODEProxy(LPBYTE lpdata, DWORD dwdatalen, LPVOID lpthis)
~~~
!(https://ae01.alicdn.com/kf/HTB1FkFqQXzqK1RjSZFC762bxVXa4.png)
我们的关键就在替换掉指针这两行代码中, 这个buffer数据就是我们彩色二维码,hook成功后我们相关的log如下
!(https://ae01.alicdn.com/kf/HTB1HFljQhTpK1RjSZR0762EwXXa4.png)
## 4 总结
我们从原理入手,看网上的原理说明,猜测PC的实现逻辑,然后开启log验证我们的想法
逆向大项目的时候,都是先提出假设,然后验证你的假设, 而这个假设的提出,像我们这次,就是在理解原理的情况下完成的,我们逆向分析其实就是还原作者的设计实现,如果你在理解原理的情况下是比较容易去分析出实现的,其实这个是个非常实用的逆向分析思维,这个也是为什么有的人分析速度很快,几个断点下来事情就ok了
那还有的其实这里面我们没有去分析他的回包数据,我们在log中能看到NetSceneGetLoginQRCode::onGYNetEnd函数解包,那么二维码的回包的协议结构是怎么样子的,里面哪个字段是二维码数据的buffer,这个buffer和最终的二维码buffer是一样的嘛,等等这些,我们都没有去分析,感兴趣的朋友也可以发散的逆向下这些逻辑
## 5 demo代码
我们获取了这个显示逻辑能有什么用呢,我们知道微信扫码的二维码是黑白的,很丑,而网络上的二维码有的是变色的,有的还带有logo,我们可以先解码原始微信的二维码数据,然后将这个数据再进行编码,从而让我们的二维码是多彩的
针对2.6.7.57的wechatwin,写了个demo来实现我们刚才所说的4里面的,显示多彩的二维码,代码中的hook只硬编码针对了2.6.7.57,这里没有放上效果图,有兴趣的朋友可以安装下这个插件,直接放到微信安装目录下,version.dll劫持
这个就是我们的二维码数据了
!(https://ae01.alicdn.com/kf/HTB1jD8lQhTpK1RjSZFM762G_VXas.png)
由于附件上传大小限制,所以我放到百度网盘了
链接: [附件下载](https://pan.baidu.com/s/1__B23lHwr0bwiQMpQMf06g)提取码: qn9i
## 6 more and more
微信被人诟病的一点就是文本通讯没有采用端对端加密,我们可以自己动手,简单加以改造,打造自己的微信端对端加密,保护隐私,从我做起,敬请期待!
!(https://ae01.alicdn.com/kf/HTB1aBJnQXzqK1RjSZFo762fcXXab.png)
## 7 参考来源
- https://www.biaodianfu.com/weixin-qrcode.html
- https://github.com/Tencent/mars
- https://www.experts-exchange.com/questions/23372362/Memory-Buffer-into-GDI-Image-FromStream.html
- <https://www.superbed.cn/> 本帖最后由 Brainor 于 2019-10-5 16:18 编辑
我看到你在文中有着一段话
> 后面等微信进程启动后,我们用windbg或者OD附加上去,修改这两个值
请问这一步是怎么做到的? 我在IDE中知道这段代码在哪里之后, 如何在OD中找到它呢? 找到它之后下断点修改这个值对吧?
Thanks!
---
解答:
在IDE中可以看到这段代码的地址, 修正基址为相同之后, 再在OD中找到对应位置下断点即可. Caitingting 发表于 2019-4-9 15:11
喵喵喵。。。我对“WEB通过JS不断的向后端发起请求”这里很感兴趣呢,有可能欺骗服务器从而使不扫码也能登 ...
token的用处就在这里体现了 喵喵喵。。。我对“WEB通过JS不断的向后端发起请求”这里很感兴趣呢,有可能欺骗服务器从而使不扫码也能登录某个微信号么~~~ 666,老铁 非常666啊 大佬进论坛了 喵{:1_893:} 点赞支持,大佬分享的好 本帖最后由 robey 于 2019-4-9 16:34 编辑
期待大佬爆端对端加密黑科技 特别的优秀 前来学习了。好厉害