[JavaScript] 纯文本查看 复制代码
var MMProtocalJni=Java.use("com.tencent.mm.protocal.MMProtocalJni");
[/color][/size][/color][/size][size=5][color=red][size=3][color=black] MMProtocalJni.pack.implementation=function(){
console.log("MMpack:"+bytesToHex(arguments[0]));
return this.pack(arguments[0],arguments[1],arguments[2],arguments[3],arguments[4],arguments[5],arguments[6],arguments[7],arguments[8],arguments[9],arguments[10],arguments[11],arguments[12],arguments[13]);
}
首先我们扫付款码,这是之前用xp hook的日志,前面一些基本都是环境设备信息,deviceId,clientVersion之类的,和其他功能组包类似,我们所需要关注的是后面的字符串。明显的是transfer_url就是二维码的字串,进行了简单的编码。WCPaySign是本地计算出来的一个值,而WCPaySign以及channel之间还有一段序列特征,具体是啥也没深入研究,有知道的大佬可以告诉我一下。测试中有时会有encrypt_key以及encrypt_userinfo字段,具体也是由WCPaySign以及channel之间字段决定的。transfer_url是直接通过java库函数URLEncoder解析得到的:
我们可以在这里替换用户扫描的二维码,使得对方不管扫什么码都会跳到我们自己的二维码上。比较简单的做法就是直接hook URLEncoder,通过收款码特征或者堆栈进行判断过滤,当然也可以自己重写这个w类构造,构造一个hashMap传入
[JavaScript] 纯文本查看 复制代码
URLEN.encode.overload("java.lang.String").implementation=function(str){
console.log("URLEN");
var res=this.encode(str);
var stack=instance.currentThread().getStackTrace();
var full_call_stack=where(stack);
return res.indexOf("wxp%3A%2F%2F")==0&&full_call_stack.indexOf("com.tencent.mm.plugin.remittance.model.w.<init>")?
"wxp%3A%2F%2Ff2f0NtReekKHV87BM0pY6k3TVjHlljtYL4sQ":res
}
不过如果这样做有一个问题,就是不管是扫谁的二维码,都会跳转到自己的付款页面。
那么能不能实现,扫谁的码就出现给谁付款的页面,但实际转账却并不是给他转?换句话说就是页面上显示的一切都是正常的,但实际却转给了另一个人?当然是可以的,不过我们需要先进行模拟扫我们码的操作,然后获取到返回的openid,ticket等。之后在后面付款的时候将正常的这些字段替换成我们的就可以了。扯远了,下面进行定位,寻找WCPaySign的算法。调用堆栈如下,直接调用函数为com.tencent.mm.ak.t.a
这个函数比较长,jadx反编译的有问题需要修改设置选项,也可以用jd直接查看调用位置
接着找参数赋值的地方,由一个成员变量req实例化为l.b类型后,进行了序列化
而req的初始化在t构造函的数中
同样的方法hook其构造函数,调用堆栈如下
前两个不用看了,u的构造参数qVar.getReqObj也是t的构造参数,而u的构造是在com.tencent.mm.ak.m.dispatch中,构造参数是它的第二个参数q.getReqObj()的返回值。
也是com.tencent.mm.wallet_core.c.u.dispatch的第二个参数
具体赋值的地方在com.tencent.mm.wallet_core.tenpay.model.m.doScene中的rr
这里的rr,之前的q,以及最初的req,他们的类型其实都不一样。rr实际上是com.tencent.mm.wallet_core.tenpay.model.m所继承的父类的成员,该类也正是我们所需要找的,赋值的地方在setRequestData中的最后。在此之前,正是计算了WCPaySign。
getEncryptUrl是q中一个抽象的方法,实现如下
从名称看就是一个3DES(md5(str))的算法,实际上也确实如此
其md5计算只是在Java层调用的标准库
3DES算法Java层调用接口为encrypt,前一个参数是key,没有则默认
除此之外,接口类q中提供了getUri接口,其返回值是post的cgi目录,在com.tencent.mm.wallet_core.c.u onGYNetEnd中的第四个参数传入了q的实例
通过反射获取到post接口为/cgi-bin/mmpay-bin/transferscanqrcode
接下来进入so层
调用的so为libtenpay_utils.so,也是标准生成的C函数
在encrypt中很容易发现默认密钥
但用这key尝试了几种加密方式,结果都不正确
可能并非标准的算法,看了一下置换表也没发现有什么变化,直接将其算法抠出来
Des3Str就是分组加密函数,Des3进行了单个分组加密
流程就是3DES的加密方式:EK3(Dk2(Ek1(P)))
DES_Encode:
代码太长就不贴了,当时用的IDA6.8,其反编译的还是多多少少有些坑的,现在用的7.0但也懒得去这部分反编译是不是一样的。
注意内存结构就行,然后再用frida对so中的调用依次hook定位问题函数,动态调试即可。这里提一点,sub_D86C函数中反编译的代码中有很多__PAIR__,如果直接用网上ida头文件中的宏定义的话会有问题。而且这个问题并非语法问题,而是ida反编译得不够准确,只能通过调试或看反汇编解决
仔细一点观察,就会发现其伪代码逻辑比较奇怪,实际汇编代码如下:
可以将那句伪代码直接用x86内联汇编给替代了
[Asm] 纯文本查看 复制代码
push eax;
mov eax,r5;
sub eax,1;
sbb r5,eax;
shl r5,1;
mov eax,r5;
mov v21,eax;
pop eax;
其实稍作分析,其真实逻辑只是在判断r5是否为0,可以将伪代码改成:
v21=(BYTE2(v49)&0x20)?2:0;到此,paysign算出的结果总算正确了。
再通过有源码的协议进行封包发送及解包,可以获取到返回的各字段
[C] 纯文本查看 复制代码
{{
"retcode": "0",
"retmsg": "",
"user_name": "wxid_7vf3tr41v3g921",
"true_name": "**鹏",
"fee": "0",
"desc": "",
"scene": "32",
"transfer_qrcode_id": "aOqTgOotZtAyyz2gsfUHWPV9hsUkxMEHkCVpM5OynlvT6Q2fy6Cwv1ffb7NLyPf9PNB-CY1mWSZW0YqQjo39TbJJWLdpPDnX2EROxb1aHTx1FKd6jqZf1wgFS98q0D32",
"rcvr_ticket": "Y4pH5nL20VA7CcRPboeyg-4PBk3ma7_U_vksGZzWTBYE4ioVEcz_v6PrG_ZS1QtY",
"get_pay_wifi": 1,
"receiver_openid": "oX2-vjvhwAutxXTxz85dJeSzzG-k",
"scan_scene": 1,
"favor_list": [],
"amount_remind_bit": 4
}}
不过12月开始好像就不返回wxid了,也就是user_name返回的是空""
类似的,在转账的时候,还有一个密码的算法,快速地说一下,还是一样的通过调用栈去找
hook com.tencent.mm.plugin.wallet.pay.a.a.b构造
密码是第一个参数authen的成员变量
com.tencent.mm.plugin.wallet.pay.a.a.a同理也是一样
con.tencent.mm.plugin.wallet.pay.a.a.e里有post地址,当然之前paysign里面那个hook也能获取到:
再上一层调用:
Authen为cZw()返回值
密码通过成员变量hef进行赋值
hef赋值的地方
再往上找密码字串
又找到一个字段vfo
密码加密后字符串由getText()得到,具体是com.tencent.mm.wallet_core.ui.formview.c.a.a的返回值
跟进去看到,payu和tenpay有两种返回值
我们好友转账,扫码转账,包括708新加入的手机号转账走的都是tenpay,i大概是触发的类型,确定交易时是1,输入金额时是100,其实现部分
当输入金额时,返回的就是输入的明文数字,其他类型会进行加密。实际上com.tencent.mm.wallet_core.ui.formview.WalletFormView以及com.tencent.mm.wallet_core.ui.formview.EditHintPasswdView的两个getText实现分别返回了输入的金额以及密此码明文,通过此可以修改转账金额,再将生成订单金额还原成之前的金额,即可控制实际转账金额
好友转账可以通过CGI_TENPAY直接传对方wxid然后返回req_key,扫码比较麻烦些,之前WCPaySign那步获取到openid,ticket,qrcode_id等字段,再通过这些字段去生成订单走/cgi-bin/mmpay-bin/busif2fplaceorder获取到req_key。但在12月之前,扫码仍有wxid返回时,可以投机走旧版的协议/cgi-bin/micromsg-bin/tenpay,逻辑与之前setRequestData找的一样,大概就是将map中元素拼接成字符串,最后再计算一个paysign,而map中元素也就是扫码返回的数据
获取到req_key,最终完成转账操作,bank_type和bind_serial是绑定银行卡的类型id,都为CFT时使用零钱,获取bind_serial也很简单,这里就不讨论了。
708新增的通过手机号转账也是分为三步,通过/cgi-bin/mmpay-bin/transferphonegetrcvr获取到openid等信息,再通过/cgi-bin/mmpay-bin/transferphoneplaceorder生成订单,获取到req_key,最后再由/cgi-bin/mmpay-bin/tenpay/sns_tf_authen确认订单完成转账。组包中的金额好像进行了一种序列化之类的操作,但还是很容易看得出来的,其算法我们可以自己实现:
[C#] 纯文本查看 复制代码
private int Pow(int x, int n)
{
int res = 1;
while (n > 0)
{
if ((n & 1) == 1) res = res * x;//转化为二进制
x = x * x;//将x平方
n >>= 1;
}
return res;
}
public string getFee(int money,bool isfirst=false,int sign=-1)
{
if (money < 0x80 && isfirst == true)
return String.Format("{0:X2}", money);
int i = 0;
int temp = (int)money;
while ((temp /= 0x80)>=1)
i++;//递归次数
if (isfirst == true) sign = i;
int pow= Pow(128, i);//128 i次方
int dwRes = (pow==1)? money:money/pow;
money = (pow == 1) ?0: money-dwRes * pow;
if (isfirst == false) dwRes += 0x80;
string res = String.Format("{0:X2}", dwRes);
return sign == 0?res: getFee(money, false, --sign)+res;
}
手机转账其实并没有分析很多,很多数据没有分析,只是写死的,但也是能实现手机转账的功能。
太晚了不写了,其实也就初探了下微信支付的流程,加密的算法。当然后续还需要进一步的封包压缩加密,但这都有现成的协议代码。虽然wx是一款社交软件,但其加密强度目前来看也是很高的,但在本地上,我们仍能做很多有趣的事情,所以建议大家不要使用wx模块之类的插件