微信界面逆向分析

微信界面逆向分析

微信版本:3.9.0.28

下载链接:链接:https://pan.baidu.com/s/1eUR9hoaJ8o_NMRybJ-MYpA?pwd=iofu
提取码:iofu
duilib:链接:https://pan.baidu.com/s/1_cuB7e2bBbIZrKexfmKBag?pwd=qx7x
提取码:qx7x
如造成侵权,请联系作者处理。

一、缘由

微信的白色背景页面实在是对像笔者这样眼睛不太好的用户不太友好,所以就有自己给他改改界面的打算,就改个颜色,应该不会太麻烦,所以就写了这篇文章记录一下过程。受限于笔者水平以及时间问题,这篇文章最终没有实现目的,但是理论上文章中所讲方法是可以实现的,并可以提取大部分微信界面UI布局,并且对其他功能的分析也有帮助。笔者将在文中给出使用frida进行简单内存dump的方式获取xml以及静态解密的方式拿到XML

二、一些已掌握的信息

  1. 微信界面使用了Duilib库
  2. Duilib实现了从XML中加载页面布局的能力
  3. 本质上是封装了一层MFC并自写了解析器来完成相关布局设置
  4. 微信的主要功能模块是WeChatWIn.dll

比较笼统的资料即以上四点,这对我们帮助还是比较多的:

  • Duilib库是开源的,官方已经停止了对Duilib的维护,现在的版本基本上都是由个人维护的
  • Duilib从XML中加载布局,这给了我们很好的着手点,我们可以试着找到XML文件对其更改来达到更改背景颜色的目的
  • 本质是一层MFC封装,那么免不了从资源中加载文件,会不会它吧XML文件放到rc里的呢?
  • 我们的主要分析应该是在WeChatWIn中进行。

三、准备冻手

1)搞一下源码

微信团队之前开源过Duilib的源码,但是现在Github上应该是找不到了,笔者这里有一份,出于学习的目的需要请联系笔者。拿到源码后笔者简单看了一下,对比官方原来来说改动不算太大。

2)资源加载方式

因为我们的关注点是微信是从哪里获取XML资源的,所以我们需要大致了解一下Duilib的资源加载方式,微信之前开源的Duilib中有四种资源加载方式:

enum UILIB_RESTYPE
    {
        UILIB_FILE=1,        // 来自磁盘文件
        UILIB_ZIP,            // 来自磁盘zip压缩包
        UILIB_RESOURCE,        // 来自资源
        UILIB_ZIPRESOURCE,    // 来自资源的zip压缩包
    };

从磁盘上以及从ZIP中加载资源基本上是不可能的,我们在微信的根目录里没有发现相关信息,保险一点,我们通过火绒剑来监控一下文件操作,防止搞错方向。

我们只关注文件操作,设置好相关过滤后,我们记录微信启动完成的相关信息,搜索关键字“.xml”,”zip”,,没有发现什么有用的信息,但是在我们搜索“Resource”时,我们留意到

image-20230313193312742

image-20230313193326814

微信加载了这两个非常可疑的dll,我们索性用Resource Hacker看看里面有什么

首先WeChatResource.dll里面有一些较为简单的资源以及一个不明所以的奇怪资源,资源类型为WXZ,如下图:

image-20230313193735695

WeUIResource.dll中同样含有此种资源类型,如下图:

image-20230313193854862

其中,在其十六进制流中我们可以看到有PNG格式文件头如下image-20230313193952483

我们在010Editor中进行简单的提取

image-20230313194330746

可以看到其中确实隐藏着png图片,但是我们没有发现xml的影子,应该是有加密的(不然也不会有这篇文章)

我们留意资源的类型为wxz,尝试在IDA中搜索相关字符串,并查找相关引用,如下图

image-20230313194754756

可以看到加载了这种资源,但是如果从这里分析的话并不优雅,一般资源的加载是在初始化过程中,从这里跟下去不一定能找到解密点。

比较幸运的是我们有源码,我们可以试着从源码中找到一个xml文件明文出现的位置(对xml文件进行解析渲染,一定在某一个时刻会出现xml文件的明文信息),找到明文出现的位置逆向回溯找出密文解密点应该是比正着死磕分析要简单一点。

3)从源码入手

笔者结合网上对duilib的一些介绍进行了一些简单分析,我们关注点来到函数签名为 LRESULT WindowImplBase::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)的函数中,这个函数所属的类被创建窗口的类继承,并且创建窗口时应当调用OnCreate()

image-20230313195503764

可以看到关键的函数行为是,分析当前加载的方式选择合适的加载例程,最终会调用Create(),而GetSkinFile().GetData()是将返回xml的一些信息,在某些情况下,是xml内容本身,这里也是比较好定位,我们可以看到字符串“Duilib”,尝试在IDA中搜索,查找引用,我们来到VA:01C80DC5处,如下图:

image-20230313201503565

对比源码,我们很容易确定这就是OnCreate函数内部,而117行对应位置就是Create,我们对其下断,重启微信。

成功断下后跟进,来到VA:1C81B13处,如下图

image-20230313201842219

IDA对这一部分的分析比较模糊,并没有把其视为一个函数,我们可以进行简单的强制函数化,但也没必要fix相关内容,如下图

image-20230313202044916

笔者对其进行了简单的分析,并根据函数行为进行了简单的重命名,这里是OnCreate内部嘛,跟源码进行简单对比,很容易发现并不是,如下

CControlUI* CDialogBuilder::Create(STRINGorID xml, LPCTSTR type, IDialogBuilderCallback* pCallback, 
        CPaintManagerUI* pManager, CControlUI* pParent)
    {
        //资源ID为0-65535,两个字节;字符串指针为4个字节
        //字符串以<开头认为是XML字符串,否则认为是XML文件
        if(HIWORD(xml.m_lpstr) != NULL && *(xml.m_lpstr) != _T('<')) {
            LPCTSTR xmlpath = CResourceManager::GetInstance()->GetXmlPath(xml.m_lpstr);
            if (xmlpath != NULL) {
                xml = xmlpath;
            }
        }

        if( HIWORD(xml.m_lpstr) != NULL ) {
            if( *(xml.m_lpstr) == _T('<') ) {
                if( !m_xml.Load(xml.m_lpstr) ) return NULL;
            }
            else {
                if( !m_xml.LoadFromFile(xml.m_lpstr) ) return NULL;
            }
        }
        else {
            HINSTANCE dll_instence = NULL;
            if (m_instance)
            {
                dll_instence = m_instance;
            }
            else
            {
                dll_instence = CPaintManagerUI::GetResourceDll();
            }
            HRSRC hResource = ::FindResource(dll_instence, xml.m_lpstr, type);
            if( hResource == NULL ) return NULL;
            HGLOBAL hGlobal = ::LoadResource(dll_instence, hResource);
            if( hGlobal == NULL ) {
                FreeResource(hResource);
                return NULL;
            }

            m_pCallback = pCallback;
            if( !m_xml.LoadFromMem((BYTE*)::LockResource(hGlobal), ::SizeofResource(dll_instence, hResource) )) return NULL;
            ::FreeResource(hResource);
            m_pstrtype = type;
        }

        return Create(pCallback, pManager, pParent);
    }

但是我们注意到调用了LoadFromMem,而其内部调用了MultiByteToWideChar,笨点的方法是,我们对这个函数下断,然后触发微信的图形操作,试着找找。

image-20230313204147636

可以看到轻而易举的就断到了我们想要的地方,断下后栈回溯,就找到了比较明显的特征,我们在IDA中转到RVA :1C9F508

image-20230313204331969

对比源码的LoadFromMem:

image-20230313204414255

可以确定这就是LoadFromMem了,对此函数进行简单的重命名。

在LoadFromMem函数头部断下,查看第一个参数(esp+0x4)的值,xml内容已经作为参数传递进来,如下图,说明微信已经完成了对xml内容的解密。请留意xml内容缓冲区的地址。

image-20230314095840691

我们进行回溯,查找第一个参数的值。

来到RVA:01C9D5D6,如下图

image-20230314100127839

可以看到笔者已经根据分析进行了部分注解,我们观察第一个参数*((LPCCH *)v7 + 2048)(thiscall调用约定,第一个参数为对象本身),v7作为stream2xmlbuf的返回值,对于这些重命名的由来,我们可以去源码中查看LoadFromMem的相关调用函数,对比可以确定这个函数的相关信息。如下图:

image-20230314100749710

我们观察stream2xmlbuf的行为,在调用他的地方下断

image-20230314100908724

可以看到对应的xml文件名作为参数传入

image-20230314100949831

对应的xml内容作为返回值被传出,我们可以猜测,这个函数完成了对xml内容的读取以及解密,我们跟进这个函数。

需要留意的是stream2xmlbuf的返回值同样为5CC98D60,这与我们之前观察到的xml缓冲区的地址相同,也就是说,duilib极有可能申请了一个公共缓冲区来临时存储xml的内容,那么我们的突破点就找到了,既然用了公共缓冲区,待xml内容解密完成后势必会对这个缓冲区进行写的操作,我们可以对着个缓冲区下硬件写入断点(dbg的内存访问断点是针对整个内存页的,通常情况下这是我们不需要的)。如下图,成功在写入的地方断下,RVA:1F7D053

image-20230314101732542

我们在IDA中同步。

image-20230314101831152

我们在源码中搜索字符串:invalid literal/length code,定位到inflate_codes,其实这里已经比较明显了,inflate是zlib库唯一支持的压缩和解压缩算法,我们在这个函数头部下断,进行回溯。来到RVA:1F76282,如下图:

image-20230314102613074

同样有比较有特征的字符串,同样在源码中搜索。推荐关键字“incorrect header check”如下图

image-20230314102851100

现在看来我们在inflate内部,算法我们不需要分析,我们在inflate函数头部下断,RVA:01F75340,非常容易的断下。

我们需要留意inflate函数签名的结构体z_streamp,其结构如下:

typedef struct z_stream_s {
    Byte    *next_in;  // next input byte
    uInt     avail_in;  // number of bytes available at next_in
    uLong    total_in;  // total nb of input bytes read so far

    Byte    *next_out; // next output byte should be put there
    uInt     avail_out; // remaining free space at next_out
    uLong    total_out; // total nb of bytes output so far

    char     *msg;      // last error message, NULL if no error
    struct internal_state *state; // not visible by applications

    alloc_func zalloc;  // used to allocate the internal state
    free_func  zfree;   // used to free the internal state
    voidpf     opaque;  // private data object passed to zalloc and zfree

    int     data_type;  // best guess about the data type: ascii or binary
    uLong   adler;      // adler32 value of the uncompressed data
    uLong   reserved;   // reserved for future use
} z_stream;

typedef z_stream *z_streamp;

比较关键的字段是

  • next_in,即将要进行解压的缓冲区
  • next_out,此缓冲区将被解压后的内容写入。
  • opaque,这个zlib的源码中的说明不太容易让人理解,但根据之后分析,这个指向的是未解压的数据,也同样是我们之后分析解密过程的突破点

我们在inflate的函数头断下,对应的我们查看第一个参数,即z_streamp结构,如下:

image-20230314103752782

可以看到基本上对应了z_streamp的结构,其中有我们非常眼熟的缓冲区5CC98D60,对应的就是next_out,而opaque指向的是WeChatresource中的数据,我们跟进

image-20230314104029124

复制相应的十六进制流,在WeChatResource.dll中搜索,成功搜索到了对应的十六进制内容,如下图

image-20230314104244610

可见,opaque保存的的是资源文件中的内容。

不要忘了字段next_in,按照解释,其应该指向的是将要解压的内容的缓冲区,我们跟进,如下图

image-20230314104616554

理论上将,将要解压的内容应该与opaque里的内容一致,但这里却不同。经过笔者猜测,这里应该是又一层加密,原因是,当微信加载PNG类型图片时,opaque内容与next_in内容一致,或者在wechatresource.dll中搜索78 9C后,可以发现很多搜索项,如下图:

image-20230314105239359

可以推测,部分资源的部分内容没有被加密。

那么我们怎么找到解密点呢?

4)找到解密点

一种比较朴素的想法是,在没进行解压之前对密文下访问断点,进行解密时必然要访问这篇内存

存在的问题是我们需要找到一个合适的位置,在没进行解压之前下断点,根据之前的分析我们似乎不能很好的确定这个位置,因为我们似乎除了在inflate例程中再没有发现密文出现的地方

一种或许可行但不太优雅的方法是记录一下加载某个页面时的密文缓冲区,在wechatresources中定位,然后在加载资源的地方(LoadResource)找到资源缓冲区,继而定位到加载那个指定界面时对应的密文缓冲区,对其下硬件访问断点,再次触发其页面加载,理论上可以断在解密的地方。

这里笔者使用的是一种非常笨拙的方法,赌解密的地方离解压的地方不远,即回溯,根据源码定位z_streamp结构的组装过程,根据内存访问关系找到解密资源的点,这个过程相当笨拙且没什么技术含量,所以这里笔者不再赘述

最后,笔者定位到RVA:01E103F1,如下图:

image-20230314110821656

根据函数名读者可以猜到,密文用的是异或加密,我们跟进这个函数

image-20230314111237882

根据伪代码分析,这段代码主要是对a1所指向的内存区域进行加密。下面是一些推测:

变量v4是一个密文缓冲区,初始值为a1+32,即指向a1之后的第32个字节的地址。
加密算法中使用了SSE指令集中的_XMMINTRIN_H头文件中的_mm_xor_si128()函数进行异或操作,xmmword_2B01A80是一个128位的常量值,可能是加密算法中使用的密钥。
在主循环中,每次处理64字节的数据,并且对这64字节中的每个128位(即16字节)进行加密。加密的方式是将128位的数据与xmmword_2B01A80进行异或操作,并将结果存储回内存中。
当a2的大小小于64字节时,程序会跳到LABEL_9处,进入另一个加密算法中,该算法对剩下的不足64字节的数据进行单字节异或加密。
函数的返回值为result,表示加密完成的数据的字节数。
综上所述,这段代码可能是一种基于SSE指令集的异或加密算法,密钥为xmmword_2B01A80

在这里秘钥为0x63,即字符‘c’,这个秘钥在不同wxid下是否不同笔者没有测试。

四、解密

下面我们测试一下结论是否正确

我们在资源中赋值一个加密chunk,chunk以1B FF作为文件头,转储到一个名为“dump.xml”的文件中,然后尝试解密解压,并将其写入“decode.txt”中下面给出不成熟的脚本:

#include <iostream>
#include "zconf.h"
#include "zlib.h"//包含zlib
#include <filesystem>//需要c++17标准支持
#include <fstream>

using namespace std;

void UnCompress()
{
	uLong file_size = std::filesystem::file_size("dump.xml");
	uLong unfile_size = file_size * 10;

	std::fstream fp("dump.xml", ios::in | ios::binary);
	char* buf = new char[file_size] {0};

	fp.read(buf, file_size);
	for (int i = 0; i < file_size; i++)
	{
		buf[i] ^= 0x63;
	}

	char* de_buf = new char[unfile_size] {0};

	int x = uncompress((Bytef *)de_buf, &unfile_size, (Bytef *)buf, file_size);
	std::fstream de_fp("decode.txt", ios::out | ios::binary);
	de_fp.write(de_buf, unfile_size);
	de_fp.close();
	fp.close();
    
	delete[] buf;
	delete[] de_buf;
	buf = nullptr;
	de_buf = nullptr;
}
void Compress()
{
	uLong file_size = std::filesystem::file_size("decode.txt");
	uLong unfile_size = file_size * 10;

	std::fstream fp("decode.txt", ios::in | ios::binary);
	char* buf = new char[file_size] {0};

	fp.read(buf, file_size);

	char* de_buf = new char[unfile_size] {0};

	int x = compress((Bytef *)de_buf, &unfile_size, (Bytef *)buf, file_size);
	for (int i = 0; i < unfile_size; i++)
	{
		de_buf[i] ^= 0x63;
	}
	std::fstream de_fp("dump.xml", ios::out | ios::binary);
	de_fp.write(de_buf, unfile_size);
	de_fp.close();
	fp.close();

	delete[] buf;
	delete[] de_buf;
	buf = nullptr;
	de_buf = nullptr;
}
int main()
{
	//Compress();
	UnCompress();
	return 0;
}

image-20230314112211970

image-20230314112349865

可以看到我们拿到了xml的内容,证实我们之前的分析时正确的。

至此,静态解密得到xml的目的已经实现,出于笔者水平原因,并不能拿到xml对应的文件名字。有兴趣的读者可以继续向下分析,如果想要拿到xml的名字,比较简单的方法是内存dump。下面笔者将使用frida来HOOK相关函数,缺点也是比较明显,我们通过HOOK很那实现完全的dump出所有的xml,因为解密渲染例程只有在被调用时我们才能拿到xml。

这里,我们选择的HOOK点为调用LoadFromMem的点上方,对应RVA:1C9D5C3,这里有xml缓冲区,缓冲区+0x2004指明了其大小,并有xml对应的名字,如下图

image-20230314160919626

给出一下frida脚本(需要frida环境):

let file_name;
let file_size=0;

function GetVAByModuleNM(moduleName,offset)
{
    var base=Module.findBaseAddress(moduleName);
    if(base==null)
        {
            base=enum_to_find+moudle(moduleName);
        };
    console.log(moduleName + ":" + base);
    var target_addr=base.add(offset);
    return target_addr;
}

let Addr=GetVAByModuleNM("WeChatWin.dll",0x1C9D5C3);
Interceptor.attach(Addr,{
  onEnter:function(arg)
  {
    file_name=ptr(this.context.esp).readPointer().readUtf16String();

    var f = new File("XML\\"+file_name.toString().replace(/\\/g,"_")+'.txt', 'wb');
                  f.write(ptr(ptr(this.context.eax).add(0x3)).readByteArray(this.context.eax.add(0x2004).readU32()-0x3));
                  f.flush();
                  f.close();
    console.log(file_name);
  }

});

调用命令:

frida WeChat.exe -l 脚本名字

效果如下:

image-20230314161306750

image-20230314161324150

五、总结

  • 微信的资源放在WeUIResource.dllWeChatResource.dll
  • xml资源使用了异或加密,需要注意的是资源里的部分PNG没有加密,里面也有zip文件
  • xml资源使用zlib解压缩
  • 在内存中修改xml中的内容可以实现修改微信UI,也就是说,我们可以通过frida脚本定位到需要修改UI,然后再资源里查找,解密,修改,再加密替换理论上可以实现更改UI的目的,但是限制比较多,理想的方法是能够完全解压资源文件修改后再根据文件格式打包回去,受限于技术原因,笔者暂时无法实现,非常乐意有想法的朋友交流,也期望各位师傅指点。
  • 类似于electron这种框架或者java虚拟机,将必要文件在运行时加载解析的先天诟病就是很容易在内存中拿到相关信息,而frida就是这方面的瑞士军刀。
  • 这些xml布局中有很多关于功能,例如在布局中有“send_btn”,那么在代码中将会有`if(tmp==“send_btn”),这离具体功能应该相当近,也就是说,了解微信的xml文件也给我们分析一些微信的功能提供了一种思路。
微信PC版逆向获取小程序源码是指通过逆向工程的方法,对微信PC版进行分析和研究,以获取小程序的源码。 首先,逆向工程是对软件进行反向分析的过程,通过分析软件的二进制代码、反编译和调试,以获取软件的内部实现和源码。 要逆向获取小程序源码,我们首先需要通过一些工具和技术手段对微信PC版进行逆向分析。这可能涉及使用反编译工具、调试器和逆向工程技术等。 通过逆向分析微信PC版,我们可以获取到小程序在PC端的运行环境和相关的源码信息。可以分析小程序的请求和响应数据,了解小程序的工作原理以及和微信PC版的交互过程。 然后,我们可以对获取到的源码进行分析和研究。通过阅读源码,可以了解小程序的业务逻辑、界面设计、数据交互等方面的实现细节。这有助于我们更深入地理解小程序的运行机制,并为后续的开发和优化提供参考。 需要注意的是,逆向获取小程序源码存在法律和道德风险。对于商业小程序来说,它们的源码是开发者的知识产权,未经许可获取源码可能侵犯了知识产权法。此外,在逆向工程过程中,可能会对软件的安全性进行攻击或者破坏,这也是不被允许的行为。 因此,在合法和道德的前提下,如果我们需要获取小程序的源码,应该通过与开发者合作、遵守开发者协议等方式来获取。这样不仅可以保证合法性和安全性,还能与开发者建立良好的合作关系,促进行业的良性发展。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值