微信安卓客户端逆向分析

原文 https://impakho.com/post/wechat-android-client-hack

前言

微信6.5.4版APK(2017-1-20更新版本)

更多微信开发技术

https://github.com/WeMobileDev/article

工具:Apktool / ShakaApktool、smali、baksmali、IDA、dex2jar、xsearch、jd-gui、Luyten、jadx、keytool、jarsigner

文件结构分析

众所周知,安卓的APK文件就是MIME为ZIP的压缩包。我们可以修改文件后缀名为zip,使用WinRAR打开来查看APK的文件结构。

通过查看微信APK的文件结构,我们发现存在三个不同的dex文件,分别是classes.dex、classes2.dex、classes3.dex。dex文件是Android平台上(Dalvik虚拟机)的可执行文件,也叫作Dalvik字节码文件。通过Dalvik字节码我们不能直接查看原来的逻辑代码,但可以借助其它工具来帮助查看。

assets: 存放资源文件
lib:存放.so动态链接库
META-INF:存放工程的属性文件,例如签名文件
AndroidManifest.xml:Android工程的基础配置属性文件
resources.arsc:主资源文件,相当于资源索引表

因此我们逆向的重点通常放在dex文件上,这里我们需要两个工具:apktool、dex2jar。

apktool可以对整个apk文件进行反编译,命令:java -jar apktool.jar d weixin.apk

反编译后得到如下文件,其中smali、smali_classes2、smali_classes3下分别存放classes.dex、classes2.dex、classes3.dex文件反编译后的smali文件

smali文件就是Dalvik虚拟机内部执行的核心代码文件,里面存放着虚拟机指令,它有自己的一套语法。但是smali文件阅读起来并不那么直观,因此我们需要另外提取那三个dex文件,使用dex2jar将dex文件转换成jar文件,使用相应软件打开jar文件,生成更加方便阅读的java伪代码。

逆向大致思路

对于Android代码文件的整个变化过程,大致上可以看作,java转换成smali文件,所有smali文件可以打包成一个jar文件,jar文件可以转换成dex文件,dex文件就存放在apk文件里。而多个dex文件是运用了dex分包技术,不同dex的逻辑代码可以相互调用,但是不会出现重复。

我们可以结合jar和smali文件,对微信代码进行逆向分析。综合使用三个jar文件阅读器jd-gui、Luyten、jadx,使用阅读器自带的文本查找功能定位jar代码。另外综合使用XSearch对所有smali文件进行文本查找,方便定位smali代码。

逆向分析需要进行smali注入又称smali插桩,它是在保证被测程序原有逻辑完整性的基础上在程序中插入一些探针,通过探针的执行并抛出程序运行的特征数据,通过对这些数据的分析,可以获得程序的控制流和数据流信息,进而得到逻辑覆盖等动态信息,从而实现测试目的的方法。这个方法需要对原代码进行修改,需要先定位到相应的smali文件,再按照语法修改虚拟机指令。

smali文件修改后需要把smali文件转换回dex文件,使用如下命令

ShakaApktool: java -jar apktool.jar s [目录或smali文件]
Smali: java -jar smali-[版本号].jar a [目录或smali文件]

使用WinRAR将dex文件重新放回到apk文件中,覆盖更新原有文件。修改后的apk原有签名将会被破坏,需要重新签名才能被安装到Android设备上。删除apk文件里的META-INF文件夹,删除原有签名文件。使用keytool生成自签证书,命令:keytool -genkey -alias android.keystore -keyalg RSA -validity 20000 -keystore android.keystore。然后使用jarsigner对apk进行签名,命令:jarsigner -verbose -keystore android.keystore -signedjar weixin_signed.apk weixin.apk android.keystore

apk文件签名后,需要使用adb install weixin_signed.apk命令安装修改后的apk至Android设备上,安装前需要使用adb uninstall com.tencent.mm命令卸载原有apk。

逆向目标

对于Android设备本地存储的微信聊天记录,网上有不少方法可以提取。这里尝试逆向分析微信APK,调试输出实时聊天记录。在此之前,我们知道聊天记录存在于网络通信数据包当中,经过之前对微信mmtls协议的研究,通过中间人获取网络明文传输数据的方法,在实际操作上难度比较大。因此,退而求其次,在客户端上进行逆向分析,得到明文数据包应该能够实现。

Android LogCat可以输出系统及其程序的log信息,那么微信也许会输出自己的log信息。同样,我们也可以使用smali插桩,输出自己想要的信息,比如明文通信数据。那么整个逆向分析的关键就是获取微信log信息。

逆向过程

探寻微信的看门人

Android APP开发者都知道,通常情况下,app的log输出由app的一个类负责管理,这个类通常会调用系统函数log()输出log信息,那么我们使用XSearch在smali文件中查找一下“log(”,得到许多匹配的结果。

我们发现有一个名为XLogSetup.smali的文件,看文件名的意思像是负责Log的Setup。使用jar代码查看器打开,查看一下它的代码。

这个文件确实是负责微信Log的Setup的,而微信的Log系统被命名为XLog。

Xlog.setConsoleLogOpen(isLogcatOpen.booleanValue());看上去这句像是负责LogCat输出Log的开关,那么我们先尝试修改这行对应的smali代码,设置函数的传参为True。

Xlog.appenderOpen(toolsLevel.intValue(), 1, cachePath, logPath, nameprefix);后面的这行调用了appenderOpen函数,这个函数位于com/tencent/mars/xlog/Xlog.smali文件。

Xlog.smali文件就是Xlog的主文件

System.loadLibrary("tencentxlog");调用了tencentxlog动态链接库,而上面提到的appenderOpen函数也被声明为native,显然这个函数是存在于动态链接库里的。

private static String decryptTag(String paramString)这里的decryptTag函数很显眼,因为LogCat的Tag和Text分别是项名和项值,而decrypt和encrypt也很能引起大家的注意。结合这两点,可以估计log的项名是被加密的。

com/tencent/mm/booter/c.class中可以看到,微信boot阶段会检查com.tentcent.mm.coolassistcom.tentcent.mm.debug.log.tag.skey,后者同于tag的解密。

每一种类型log都调用了logWrite2函数,logWrite2函数被声明为native,可以推测调用系统函数输出log由tencentxlog负责。

使用XSearch分别尝试搜索每一种类型log的函数名(logD、logE、logF、logI、logV、logW),发现v.smali每次都出现。

使用jar代码查看器查看一下com/tencent/mm/sdk/platformtools/v.class的代码

发现多次调用public static int getLogLevel()函数,每一种类型的log在输出之前需要检查loglevel(log级别),一共1到7级,1为最高级别。那么我们可以使用smali插桩,输出所有类型的Log。

插入log函数:

.method public static log(Ljava/lang/String;Ljava/lang/String;)V
    .locals 2
    .prologue
    const v0, true
    invoke-static {p0, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
    return-void
.end method

删除所有调用getLogLevel函数及其判断结构的代码:

if ((nMo != null) && (nMo.getLogLevel() <= X))

在需要输出log的地方插入调用log函数的代码:

invoke-static {p0, p2}, Lcom/tencent/mm/sdk/platformtools/v;->
log(Ljava/lang/String;Ljava/lang/String;)V

安装修改后的APK,过滤tag:MicroMsg,可以在LogCat中看到微信输出的Log信息

捕获微信的通讯员

微信APK动态链接库里libwechatnetwork.solibMMProtocalJni.so引起我的注意,看名字前者像是负责微信的网络连接,后者像是负责微信的协议。

使用 IDA6.6 反编译这两个so文件

libwechatnetwork.so:结合mmtls协议和抓包分析,微信网络使用HTTP短连接和私有协议长连接,Signaling是负责推送的函数,网络传输任务作为Task进入链接库进行Queue。而且可以自定义设置远程服务器IP,获取当前网络连接状态。

libMMProtocalJni.so:分析函数名,本库进行aes加解密,密钥的协商管理(生成和销毁),数据包的封装和解封装。

我们把重点放在pack*和unpack,即数据包的封装和解封装。

使用XSearch查找所有“;->pack”和“;->unpack(”,对所有调用libMMProtocalJni.so的pack*和unpack函数进行smali插桩,log输出函数传参。

注:经过测试,应该重点插桩下面四个文件
com/tencent/mm/u/r.class
com/tencent/mm/u/t.class
com/tencent/mm/protocal/ab.class关键代码位于ab$b.smali
com/tencent/mm/model/ak.class关键代码位于ak$3.smali

传参有PByteArray、Int、Byte、String四种类型,这里log输出PByteArray、Byte和String三种类型,其中PByteArray为自定义类型,封装着一个Byte类型。

插桩方法:在smali中插入下面三个函数,其中com/tencent/mm/sdk/platformtools/bf;->bm(可以将Byte类型格式化为16进制String类型,com/tencent/mm/pointers/PByteArray;->value:[B可以读取PByteArray封装的Byte。

.method public static log(Ljava/lang/String;)V
    .locals 2
    .prologue
    const-string v0, "Test"
    invoke-static {v0, p0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
    return-void
.end method

.method public static log([B)V
    .locals 2
    .prologue
    const-string v0, "Test"
    move-object v1, p0
    invoke-static {v1}, Lcom/tencent/mm/sdk/platformtools/bf;->bm([B)Ljava/lang/String;
    move-result-object v1
    invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
    return-void
.end method

.method public static log(Lcom/tencent/mm/pointers/PByteArray;)V
    .locals 2
    .prologue
    const-string v0, "Test"
    iget-object v1, p0, Lcom/tencent/mm/pointers/PByteArray;->value:[B
    invoke-static {v1}, Lcom/tencent/mm/sdk/platformtools/bf;->bm([B)Ljava/lang/String;
    move-result-object v1
    invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
    return-void
.end method

然后在pack*和unpack对应位置插入调用log函数的代码:

    invoke-static {v1}, Lcom/tencent/mm/u/r;->log(Ljava/lang/String;)V
    invoke-static {v2}, Lcom/tencent/mm/u/r;->log([B)V
    invoke-static {v3}, Lcom/tencent/mm/u/r;->log(Lcom/tencent/mm/pointers/PByteArray;)V

代码按需修改,最后修改所在函数的寄存器声明数.locals。

安装修改后的APK,过滤tag:Test,可以在LogCat中看到微信网络数据包的16进制信息,包含发送和接收的密文和明文数据。

在两台手机上分别登录两个微信账号,“测试”向“测试2”发送三条信息

LogCat输出了下面三段数据包的16进制数据

每一段的第一句分别是发送数据包的16进制明文数据

0801124d0a150a13777869645f71797131737566683030667132311221e590ace8afb4e4bda0e59ca8e5819ae5beaee4bfa1e98086e59091e58886e69e90180120d2a9b6c505289891d9fefdffffffff01

0801122f0a150a13777869645f717971317375666830306671323112085be5beaee7ac915d180120f6a9b6c50528b4d19c8e01

080112330a150a13777869645f717971317375666830306671323112073233333333333318012082aab6c50528f1e2e5f6ffffffffff01

“wxid_”开头的是微信id,0000001ch是聊天发送数据的长度,0000001dh是聊天发送数据的开始位置。

提取出聊天发送数据的16进制,分别进行分析

e590ace8afb4e4bda0e59ca8e5819ae5beaee4bfa1e98086e59091e58886e69e90

5be5beaee7ac915d

32333333333333

经过尝试,确定是UTF8编码,只需在开头插入UTF-8编码标志EFBBBF即可。

efbbbfe590ace8afb4e4bda0e59ca8e5819ae5beaee4bfa1e98086e59091e58886e69e90

efbbbf5be5beaee7ac915d

efbbbf32333333333333

结语

目前所做的逆向分析只是冰山一角,微信客户端还是有许多地方值得分析和借鉴的。

逆向分析是一个学习的过程,同时也是一个考验安全性的过程。

navicat 无法设置decimal默认值的问题

https://www.zhangshuxian.top/article/102

SELECT
        CONCAT("ALTER TABLE ",TABLE_NAME," MODIFY COLUMN ",COLUMN_NAME," ",COLUMN_TYPE,(case when IS_NULLABLE = "NO" THEN " NOT NULL" ELSE "" END)," DEFAULT 0 COMMENT '",COLUMN_COMMENT,"';") AS 'sql',
    TABLE_SCHEMA AS '库名',
    TABLE_NAME AS '表名',
    COLUMN_NAME AS '列名',
    ORDINAL_POSITION AS '列的排列顺序',
    COLUMN_DEFAULT AS '默认值',
    IS_NULLABLE AS '是否为空',
    DATA_TYPE AS '数据类型',
    CHARACTER_MAXIMUM_LENGTH AS '字符最大长度',
    NUMERIC_PRECISION AS '数值精度(最大位数)',
    NUMERIC_SCALE AS '小数精度',
    COLUMN_TYPE AS '列类型',
    COLUMN_KEY 'KEY',
    EXTRA AS '额外说明',
    COLUMN_COMMENT AS '注释'
FROM
    information_schema.`COLUMNS`
WHERE
    TABLE_SCHEMA = 'zxcrm-demo'
    AND DATA_TYPE = 'decimal'
    AND COLUMN_DEFAULT IS NULL and IS_NULLABLE = "NO";

常见网络摄像机的端口及RTSP地址

来源:https://www.jiangyu.org/port-and-rtsp-address-of-several-ipcams/

备份一下

海康威视
默认IP地址:192.168.1.64/DHCP 用户名admin 密码自己设
端口:“HTTP 端口”(默认为 80)、“RTSP 端口”(默认为 554)、“HTTPS 端 口”(默认 443)和“服务端口”(默认 8000),ONVIF端口 80。
RTSP地址:rtsp://[username]:[password]@[ip]:[port]/[codec]/[channel]/[subtype]/av_stream
说明:
username: 用户名。例如admin。
password: 密码。例如12345。
ip: 为设备IP。例如 192.0.0.64。
port: 端口号默认为554,若为默认可不填写。
codec:有h264、MPEG-4、mpeg4这几种。
channel: 通道号,起始为1。例如通道1,则为ch1。
subtype: 码流类型,主码流为main,辅码流为sub。
例如,请求海康摄像机通道1的主码流,Url如下
主码流:
rtsp://admin:12345@192.0.0.64:554/h264/ch1/main/av_stream
子码流:
rtsp://admin:12345@192.0.0.64/mpeg4/ch1/sub/av_stream

大华
默认IP地址:192.168.1.108 用户名/密码:admin/admin
端口:TCP 端口 37777/UDP 端口 37778/http 端口 80/RTSP 端口号默认为 554/HTTPs 443/ONVIF 功能默认为关闭,端口80
RTSP地址:rtsp://username:password@ip:port/cam/realmonitor?channel=1&subtype=0
说明:
username: 用户名。例如admin。
password: 密码。例如admin。
ip: 为设备IP。例如 10.7.8.122。
port: 端口号默认为554,若为默认可不填写。
channel: 通道号,起始为1。例如通道2,则为channel=2。
subtype: 码流类型,主码流为0(即subtype=0),辅码流为1(即subtype=1)。
例如,请求某设备的通道2的辅码流,Url如下
rtsp://admin:admin@10.12.4.84:554/cam/realmonitor?channel=2&subtype=1

雄迈/巨峰
默认IP地址:192.168.1.10 用户名admin 密码空
端口:TCP端口:34567 和 HTTP端口:80,onvif端口是8899
RTSP地址:rtsp://10.6.3.57:554/user=admin&password=&channel=1&stream=0.sdp?
10.6.3.57这个是被连接的设备的IP
554这个是RTSP服务的端口号,可以在设备的网络服务里面更改
user=admin这个是设备的登录用户名
password= 密码空
channel=1 第一通道
stream=0.sdp?主码流
stream=1.sdp?副码流
图片抓拍地址:http://ip/webcapture.jpg?command=snap&channel=1

天视通
默认IP地址:192.168.0.123 用户名admin 密码123456
端口:http端口80 数据端口8091 RTSP端口554 ONVIF端口 80
RTSP地址:主码流地址:rtsp://192.168.0.123:554/mpeg4
子码流地址:rtsp://192.168.0.123:554/mpeg4cif
需要入密码的地址: 主码流 rtsp://admin:123456@192.168.0.123:554/mpeg4
子码流 rtsp://admin:123456@192.168.0.123:554/mpeg4cif
图片抓拍地址:http://ip/snapshot.cgi

中维/尚维
默认IP地址:DHCP 默认用户名admin 默认密码 空
RTSP地址:rtsp://0.0.0.0:8554/live1.264(次码流)
rtsp://0.0.0.0:8554/live0.264 (主码流)

九安
RTSP地址:rtsp://IP:port(website port)/ch0_0.264(主码流)
rtsp://IP:port(website port)/ch0_1.264(子码流)

技威/YOOSEE
默认IP地址:DHCP 用户名admin 密码123
RTSP地址:主码流:rtsp://IPadr:554/onvif1
次码流:rtsp://IPadr:554/onvif2
onvif端口是5000
设备发现的端口是3702

V380
默认IP地址:DHCP 用户名admin 密码空/admin
onvif端口8899
RTSP地址:主码流rtsp://ip//live/ch00_1
子码流rtsp://ip//live/ch00_0

宇视
默认IP地址: 192.168.0.13/DHCP 默认用户名 admin 和默认密码 123456
端口:HTTP 80/RTSP 554/HTTPS 110(443)/onvif端口 80
RTSP地址:rtsp://用户名:密码@ip:端口号/video123 123对应3个码流

天地伟业
默认IP地址:192.168.1.2 用户名“Admin”、密码“1111”
onvif端口号“8080”
RTSP地址:rtsp://192.168.1.2

巨龙/JVT
默认IP地址:192.168.1.88 默认用户名 admin 默认密码admin
RTSP地址:
主码流地址:rtsp://IP地址/av0_0
次码流地址:rtsp://IP地址/av0_1
onvif端口 2000
图片抓拍地址:http://ip/capture/webCapture.jpg?channel=1&FTpsend=0&checkinfo=0
(http://ip/cgi-bin/images_cgi?channel=1&user=admin&pwd=admin)

php ubuntu 多版本安装

php 多版本安装 from ppa

https://launchpad.net/~ondrej/+archive/ubuntu/php

sudo add-apt-repository ppa:ondrej/php
sudo apt update

然后就可以用sudo apt isntall php-7.4这种方式安装对应版本php

扩展可以直接用apt install php-7.4-redis这种方式安装

不确定的扩展名可以用apt search php-版本-redis搜索包名

  • 多版本切换

如果需要多版本切换,可以使用ubuntu 的update-alternatives实现

用上面ppa的包安装的php都会作为php的候选项,比如使用命令

update-alternatives --list php
/usr/bin/php7.4
/usr/bin/php8.2

需要切换php命令的版本可以使用

update-alternatives --config php
 2 个候选项可用于替换 php (提供 /usr/bin/php)。

  选择       路径           优先级  状态
------------------------------------------------------------
* 0            /usr/bin/php8.2   82        自动模式
  1            /usr/bin/php7.4   74        手动模式
  2            /usr/bin/php8.2   82        手动模式

如果有应用不是使用apt安装的,可以使用下面的命令添加到候选项目

# 链接  名称  (实际)路径   优先级
sudo update-alternatives --install /usr/bin/vim vim /usr/bin/vim.nox 40

  • php-fpm 多版本

如果fpm也需要多版本,可以先apt安装多版本的fpm

使用systemctl start php7.4-fpm 启动对应版本的php-fpm

查看目录/etc/php/7.4/fpm/pool.d/www.conf

 listen = /run/php/php7.4-fpm.sock  //可以看到监听地址

nginx设置php文件的fastcgi=/run/php/php7.4-fpm.sock

需要切换php-fpm版本就找到对应配置的fpm监听地址,修改nginx对应网站的转发配置

PHP 对html文件进行处理

有一批静态文件需要删除一些元素,考虑使用php脚本批量处理

step1 扫描目录

use Masterminds\HTML5;
$dir = __DIR__ . '/path/to/html';
$fileNames = scandir($dir);
$html5 = new HTML5(['disable_html_ns'=> true]);

foreach ($fileNames as $fileName) {
    if (in_array($fileName, ['.', '..'])) {
        continue;
    }
    $filePath = $dir . '/' . $fileName;
    if (!is_file($filePath)) {
        continue;
    }
}

这里使用Masterminds\HTML5这个包,直接使用DOMdocument读取h5会出现问题

step2 加载文件

$document = $html5->loadHTMLFile($filePath);
$xpath = new DOMXPath($document);

step3 修改节点

clearNodes($xpath);

function clearNodes($xpath) {
    clearElements($xpath, '//div[@class="panel panel-default tab-content"]');
}

step4 保存文件

$html5->save($document, $filePath);

微信模板消息改版后发送规则

改版后发送的字段需要跟小程序订阅消息一样遵守字段规则,文档还没更新,这里记录一下,如果不按规则发送会提示参数有问题。

订阅消息参数值内容限制说明

参数类别参数说明参数值限制说明
thing.DATA事物20个以内字符可汉字、数字、字母或符号组合
number.DATA数字32位以内数字只能数字,可带小数
letter.DATA字母32位以内字母只能字母
symbol.DATA符号5位以内符号只能符号
character_string.DATA字符串32位以内数字、字母或符号可数字、字母或符号组合
time.DATA时间24小时制时间格式(支持+年月日),支持填时间段,两个时间点之间用“~”符号连接例如:15:01,或:2019年10月1日 15:01
date.DATA日期年月日格式(支持+24小时制时间),支持填时间段,两个时间点之间用“~”符号连接例如:2019年10月1日,或:2019年10月1日 15:01
amount.DATA金额1个币种符号+10位以内纯数字,可带小数,结尾可带“元”可带小数
phone_number.DATA电话17位以内,数字、符号电话号码,例:+86-0766-66888866
car_number.DATA车牌8位以内,第一位与最后一位可为汉字,其余为字母或数字车牌号码:粤A8Z888挂
name.DATA姓名10个以内纯汉字或20个以内纯字母或符号中文名10个汉字内;纯英文名20个字母内;中文和字母混合按中文名算,10个字内
phrase.DATA汉字5个以内汉字5个以内纯汉字,例如:配送中
enum.DATA枚举值只能上传枚举值范围内的字段值调用接口获取参考枚举值

符号表示除中文、英文、数字外的常见符号,不能带有换行等控制字符。 时间格式支持HH:MM:SS或者HH:MM。 日期包含年月日,为y年m月d日,y年m月、m月d日格式,或者用‘-’、‘/’、‘.’符号连接,如2018-01-01,2018/01/01,2018.01.01,2018-01,01-01。 每个模板参数都会以类型为前缀,例如第一个数字模板参数为number01.DATA,第二个为number02.DATA

wsl 静态ip启动

添加文件

/etc/profile.d/static-ip.sh

ip addr del $(ip addr show eth0 | grep 'inet\b' | awk '{print $2}' | head -n 1) dev eth0
ip addr add 192.168.1.177/24 broadcast 192.168.1.255 dev eth0
ip route add 0.0.0.0/0 via 192.168.1.1 dev eth0

重启后就会按照192.168.1.177这个ip启动了

wsl连接的虚拟网卡使用hyper-v管理器设置为直接连接外部网络

微信支付v3对接

证书解析链接,查看证书序列号 https://myssl.com/cert_decode.html

平台证书获取

php版本工具 https://github.com/wechatpay-apiv3/wechatpay-php

composer 安装后,进入bin目录

执行命令

php CertificateDownloader.php -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}

mchSerialNo用上面提供的解析工具解析获得序列号

mchId 商户号