精华内容
下载资源
问答
  • 一、说明 现在的App一般都会带有支付功能,而现在比较流行的支付...利用一些特殊的手段获得收款二维码以及收款记录,这样就可以绕过支付平台完成支付过程了,本篇文章的目的就是分析如何完成这样一个流程,本文的...

    一、说明

           现在的App一般都会带有支付功能,而现在比较流行的支付一般有支付宝、微信、银行卡等,一般情况下,应用开发者会直接对接支付宝、微信或者第三方支付公司的Api,以完成支付,但是都需要收取不小的费率,于是,有的第三方支付平台就想到了钻空子的方法,利用一些特殊的手段获得收款二维码以及收款记录,这样就可以绕过支付平台完成支付过程了,本篇文章的目的就是分析如何完成这样一个流程,本文的意图只有一个就是通过分析app学习更多的逆向技术,如果有人利用本文知识和技术进行非法操作进行牟利,带来的任何法律责任都将由操作者本人承担,和本文作者无任何关系,最终还是希望大家能够秉着学习的心态阅读此文:想获得支付宝的个人收款二维码,和用户最近的收款记录,于是研究了一下方法,最终用xposed解决了。流程如下:

    1、获得收款二维码链接流程

           服务器推送金额和备注任务到xposed插件(或者xposed插件主动请求任务)--》xposed插件发送广播通知支付宝--》支付宝打开设置金额页面并自动设置金额和备注,点击确认

           --》xposed hook支付宝处理收款二维码链接的回调方法--》获得收款链接--》发送广播将收款链接回传给xposed插件--》xposed插件将二维码链接发送给服务器

    2、获得账单信息流程

           服务器推送账单任务到xposed插件(或者xposed插件主动请求任务)--》xposed插件发送广播通知支付宝获得账单消息--》支付宝打开账单页面获得账单信息--》xposed hook支付宝处理账单信息的回调方法-->获得账单信息--》发送广播将账单信息回传给xposed插件--》xposed插件将账单信息发送给服务器

    3、自动登录流程

           服务器推送登录任务到xposed插件,信息包括支付宝账号和密码(或者xposed插件主动请求任务)--》xposed插件发送广播通知支付宝自动登录--》支付宝打开登录页面自动设置账号和密码,点击登录--》xposed hook支付宝登录的回调方法-->获得登录状态(是否登录成功)--》发送广播将登录状态回传给xposed插件--》xposed插件将登录状态发送给服务器

    4、自动退出登录流程

           服务器推送退出登录任务到xposed插件(或者xposed插件主动请求任务)--》xposed插件发送广播通知支付宝退出登录--》xposed调用支付宝退出登录的代码完成退出任务--》发送广播通知xposed插件退出任务已经完成

    5、获得当前登录用户信息流程

           服务器推送获得当前登录用户信息任务到xposed插件(或者xposed插件主动请求任务)--》xposed插件发送广播通知支付宝广播需要获得用户信息--》支付宝广播调用获得当前登录用户信息代码获得用户信息--》xposed发送广播通知插件获得了用户信息-->xposed插件广播接收用户信息--》xposed插件将用户信息发送给服务器

    备注:网络通信的过程可以采用推送(websocket长连接)或者轮询(客户端主动发起http请求)的方式,只要能够正常让插件程序和服务端通信就行。

    二、问题分析

    1、支付宝的个人收钱界面

    我用的支付宝版本是10.1.20

    获得个人收钱二维码的流程如下:

    打开支付宝主界面--》点击收钱---》进入到个人收钱界面--》点击设置金额--》进入设置金额界面--》设置金额和理由--》点击确定--》返回个人收钱界面并刷新收钱二维码

    个人收钱界面如下:

    设置金额界面如下:

    点击个人收钱界面下面的收款记录,我们可以看到用户当天的收款情况,如下:

    三、反编译支付宝并分析

    反编译应用的方法可以参考:https://blog.csdn.net/xiao_nian/article/details/79391417,这篇文章反编译的是微信的apk,方法是一样的。

    1、收款二维码

    首先我们用hierarchy view查看设置金额页面,如下:

    在反编译代码中找到PayeeQRSetMoneyActivity类,发现下面有一个方法定义如下:

      protected final void a(ConsultSetAmountRes paramConsultSetAmountRes)
      {
        runOnUiThread(new di(this, paramConsultSetAmountRes));
      }

    而di的定义如下:

    package com.alipay.mobile.payee.ui;
    
    import android.content.Intent;
    import com.alipay.android.hackbyte.ClassVerifier;
    import com.alipay.mobile.commonui.widget.APInputBox;
    import com.alipay.mobile.payee.R.string;
    import com.alipay.mobile.payee.util.Logger;
    import com.alipay.transferprod.rpc.result.ConsultSetAmountRes;
    
    final class di
      implements Runnable
    {
      di(PayeeQRSetMoneyActivity paramPayeeQRSetMoneyActivity, ConsultSetAmountRes paramConsultSetAmountRes)
      {
        if (Boolean.FALSE.booleanValue()) {
          ClassVerifier.class.toString();
        }
      }
      
      public final void run()
      {
        PayeeQRSetMoneyActivity.a.b("call processConsultSetAmountRes(), ConsultSetAmountRes = " + this.a);
        if (this.a != null)
        {
          if (!this.a.success) {
            break label140;
          }
          Intent localIntent = new Intent();
          localIntent.putExtra("codeId", this.a.codeId);
          localIntent.putExtra("qr_money", this.b.g);
          localIntent.putExtra("beiZhu", this.b.c.getInputedText());
          localIntent.putExtra("qrCodeUrl", this.a.qrCodeUrl);
          localIntent.putExtra("qrCodeUrlOffline", this.a.printQrCodeUrl);
          this.b.setResult(-1, localIntent);
          this.b.finish();
        }
        for (;;)
        {
          return;
          label140:
          this.b.alert("", this.a.message, this.b.getString(R.string.payee_confirm), null, null, null);
        }
      }
    }
    

    di的run方法里面主要是设置用户设置的金额,备注,服务端返回的二维码链接(qrCodeUrl)到intent中,然后再传递给个人收款(PayeeQRActivity)页面,可以看一下个人收款页面的onActivityResult方法

      public void onActivityResult(int paramInt1, int paramInt2, Intent paramIntent)
      {
        super.onActivityResult(paramInt1, paramInt2, paramIntent);
        if ((paramInt1 == 10) && (paramInt2 == -1) && (paramIntent != null)) {}
        try
        {
          this.c = paramIntent.getStringExtra("qr_money");
          this.d = paramIntent.getStringExtra("beiZhu");
          this.i = paramIntent.getStringExtra("qrCodeUrl");
          this.j = paramIntent.getStringExtra("qrCodeUrlOffline");
          e();
          return;
        }
        catch (Exception paramIntent)
        {
          for (;;)
          {
            LoggerFactory.getTraceLogger().warn(a, paramIntent);
          }
        }
      }

    这里主要是根据设置金额页面传过来的qrCodeUrl刷新收款二维码。

    经过上面分析,可以有这样一种思路,当手机接收要生成收款二维码的请求后,可以启动支付宝的设置金额页面,然后在自动将金额和备注设置到页面上,最后在模拟点击确定按钮,这个时候支付宝就会将备注和金额发送给服务端,请求二维码链接,请求回来后,会调用PayeeQRSetMoneyActivity的

    protected final void a(ConsultSetAmountRes paramConsultSetAmountRes)

    方法,ConsultSetAmountRes paramConsultSetAmountRes里面有服务端返回的二维码链接信息,ConsultSetAmountRes类定义如下:

    package com.alipay.transferprod.rpc.result;
    
    import com.alipay.android.hackbyte.ClassVerifier;
    
    public class ConsultSetAmountRes
      extends RPCResponse
    {
      public String codeId;
      public String printQrCodeUrl;
      public String qrCodeUrl;
      
      public ConsultSetAmountRes()
      {
        if (Boolean.FALSE.booleanValue()) {
          ClassVerifier.class.toString();
        }
      }
      
      public String toString()
      {
        return "ConsultSetAmountRes{codeId='" + this.codeId + '\'' + ", qrCodeUrl='" + this.qrCodeUrl + '\'' + ", printQrCodeUrl='" + this.printQrCodeUrl + '\'' + "} " + super.toString();
      }
    }
    

    其中qrCodeUrl即服务端返回的收款二维码链接,我们只需要hook设置金额界面(PayeeQRSetMoneyActivity)的

    protected final void a(ConsultSetAmountRes paramConsultSetAmountRes)

    方法,即可得到收款二维码链接

                // hook获得二维码url的回调方法
                findAndHookMethod("com.alipay.mobile.payee.ui.PayeeQRSetMoneyActivity", lpparam.classLoader, "a",
                        findClass("com.alipay.transferprod.rpc.result.ConsultSetAmountRes", lpparam.classLoader), new XC_MethodHook() {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                        log("com.alipay.mobile.payee.ui.PayeeQRSetMoneyActivity a" + "\n");
                   
                        Object consultSetAmountRes = param.args[0];
                        String consultSetAmountResString = "";
                        if (consultSetAmountRes != null) {
                            consultSetAmountResString = (String) callMethod(consultSetAmountRes, "toString");
                        }
                        log("consultSetAmountResString:" + consultSetAmountResString + "\n");
                    }
                });
    

    安装插件并重启手机后,打开支付宝界面,弹出非法操作弹框,并且不让操作支付宝界面,我擦,支付宝看来是有反hook机制的

    那么如何解决呢?支付宝肯定也是通过代码去检查应用是否被hook了,我们只需要用xposed hook住支付宝的检测方法,并且修改返回值,这样就可以骗过支付宝了。代码如下:

                XposedHelpers.findAndHookMethod(Application.class,
                        "attach",
                        Context.class, new XC_MethodHook() {
                            @Override
                            protected void afterHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
                                super.afterHookedMethod(param);
                                Context context = (Context) param.args[0];
                                ClassLoader appClassLoader = context.getClassLoader();
                                securityCheckHook(appClassLoader);
                            }
                        });
    
        // 解决支付宝的反hook
        private void securityCheckHook(ClassLoader classLoader) {
            try {
                Class securityCheckClazz = XposedHelpers.findClass("com.alipay.mobile.base.security.CI", classLoader);
                XposedHelpers.findAndHookMethod(securityCheckClazz, "a", String.class, String.class, String.class, new XC_MethodHook() {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                        Object object = param.getResult();
                        XposedHelpers.setBooleanField(object, "a", false);
                        param.setResult(object);
                        super.afterHookedMethod(param);
                    }
                });
    
                XposedHelpers.findAndHookMethod(securityCheckClazz, "a", Class.class, String.class, String.class, new XC_MethodReplacement() {
                    @Override
                    protected Object replaceHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
                        return (byte) 1;
                    }
                });
                XposedHelpers.findAndHookMethod(securityCheckClazz, "a", ClassLoader.class, String.class, new XC_MethodReplacement() {
                    @Override
                    protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
                        return (byte) 1;
                    }
                });
                XposedHelpers.findAndHookMethod(securityCheckClazz, "a", new XC_MethodReplacement() {
                    @Override
                    protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
                        return false;
                    }
                });
    
            } catch (Error | Exception e) {
                e.printStackTrace();
            }
        }

    在应用加载完成后hook住支付宝的检测是否被hook的方法,修改返回值。重新运行并重启手机,发现没有弹出非法操作弹框。

    或者将securityCheckHook代码修改为如下代码也可以:

        // 解决支付宝的反hook
        private void securityCheckHook(ClassLoader classLoader) {
    
            try {
                Class securityCheckClazz = XposedHelpers.findClass("com.alipay.mobile.base.security.CI", classLoader);
    
                XposedHelpers.findAndHookMethod(securityCheckClazz, "a", securityCheckClazz, Activity.class, new XC_MethodReplacement() {
                    @Override
                    protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
                        return null;
                    }
                });
            } catch (Error | Exception e) {
            }
        }

    第一种方式是通过修改支付宝检查是否被hook的方法的返回值来骗过支付宝,第二种方式是通过替换支付宝弹出非法操作弹框方法执行逻辑的方式来屏蔽非法操作弹框弹出。

    下面我们来分析一下怎样找到支付宝反hook的代码的,首先用hierarchy view查看非法操作弹框布局,如下:

    反编译代码中全局搜索"非法操作,当前手机不安全!",没有找到对应的信息,全局搜索"R.id.message",发现有好几个地方有用到这个id,经过加入log测试都不是非法操作弹框使用的,换一种思路,既然是弹框,肯定会继承"android.app.Dialog"类,弹框显示的时候肯定会调用其"show"方法,我们只需要hook住"android.app.Dialog"类的"show"方法,然后打印出方法调用的堆栈来跟踪代码调用逻辑,不就可以知道支付宝弹框非法操作弹框的代码了吗?代码如下:

            findAndHookMethod(Dialog.class, "show", new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    super.afterHookedMethod(param);
                    try {
                        throw new NullPointerException(); // 故意抛出一个异常以便打印堆栈信息
                    } catch (Exception e) {
                        XposedLogUtils.log("securityCheckHook:" + Log.getStackTraceString(e)); // 打印堆栈信息分析代码的调用逻辑
                    }
                }
            });

    打开支付宝,弹出非法操作弹框后,可以看到以下日志:

    06-02 15:26:23.449 I/Xposed  ( 5792): securityCheckHook:java.lang.NullPointerException
    06-02 15:26:23.449 I/Xposed  ( 5792): 	at com.hhly.pay.alipay.Main$6.afterHookedMethod(Main.java:266)
    06-02 15:26:23.449 I/Xposed  ( 5792): 	at de.robv.android.xposed.XposedBridge.handleHookedMethod(XposedBridge.java:374)
    06-02 15:26:23.449 I/Xposed  ( 5792): 	at android.app.Dialog.show(<Xposed>)
    06-02 15:26:23.449 I/Xposed  ( 5792): 	at android.app.AlertDialog.show(AlertDialog.java:1246)
    06-02 15:26:23.449 I/Xposed  ( 5792): 	at android.app.AlertDialog$Builder.show(AlertDialog.java:1126)
    06-02 15:26:23.449 I/Xposed  ( 5792): 	at com.alipay.mobile.base.security.CI.a(CI.java:2463)
    06-02 15:26:23.449 I/Xposed  ( 5792): 	at com.alipay.mobile.base.security.CI$1.run(CI.java:114)
    06-02 15:26:23.449 I/Xposed  ( 5792): 	at android.os.Handler.handleCallback(Handler.java:739)
    06-02 15:26:23.449 I/Xposed  ( 5792): 	at android.os.Handler.dispatchMessage(Handler.java:95)
    06-02 15:26:23.449 I/Xposed  ( 5792): 	at android.os.Looper.loop(Looper.java:148)
    06-02 15:26:23.449 I/Xposed  ( 5792): 	at android.app.ActivityThread.main(ActivityThread.java:5666)
    06-02 15:26:23.449 I/Xposed  ( 5792): 	at java.lang.reflect.Method.invoke(Native Method)
    06-02 15:26:23.449 I/Xposed  ( 5792): 	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:775)
    06-02 15:26:23.449 I/Xposed  ( 5792): 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:665)
    06-02 15:26:23.449 I/Xposed  ( 5792): 	at de.robv.android.xposed.XposedBridge.main(XposedBridge.java:107)

    在日志中,我们可以看到,弹出非法操作的弹框的代码在"com.alipay.mobile.base.security.CI"的"a"方法中,打开lipay.mobile.base.security.CI"类,发现其中有如下方法定义:

        static /* synthetic */ void a(CI ci, Activity activity) {
            try {
                // 显示非法操作弹框
                Builder builder = new Builder(activity);
                builder.setMessage(new String(Base64.decode("6Z2e5rOV5pON5L2c77yM5b2T5YmN5omL5py65LiN5a6J5YWo77yB", 0), SymbolExpUtil.CHARSET_UTF8)); // 弹框提示内容,这里支付宝对提示文字进行了加密
                builder.setPositiveButton(new String(Base64.decode("56Gu5a6a", 0), SymbolExpUtil.CHARSET_UTF8), new c(ci, activity)); // 确认按钮
                builder.setNegativeButton(R.string.detail, new d(ci, activity)); // 查看详情按钮
                builder.setCancelable(false);
                builder.show();
            } catch (Exception e) {
            }
        }
    

    其中确认按钮和查看详情按钮的点击事件最终都会调用到"com.alipay.mobile.base.security.CI"的下面方法:

        static /* synthetic */ void a(Activity activity) {
            try {
                AlipayApplication.getInstance().getMicroApplicationContext().exit(); // 退出应用
            } catch (Throwable th) {
                activity.finish(); // 退出应用
                System.exit(-1);
            }
        }
    

    在"com.alipay.mobile.base.security.CI"类中,还有一些检查是否被hook的方法,这里不具体分析了。

    打开支付宝设置金额界面,设置金额和备注并点击确认,在xposed的log中可以看到以下日志:

    04-10 17:11:09.647 I/Xposed  ( 7116): consultSetAmountResString:ConsultSetAmountRes{codeId='1804106465231431', qrCodeUrl='HTTPS://QR.ALIPAY.COM/FKX007021VPOLKNEMJRV5C', printQrCodeUrl='HTTPS://QR.ALIPAY.COM/FKX024385RNIN3NEYG3MDD'} RPCResponse{success=true, code='null', message='null'}

    其中qrCodeUrl即收款二维码的支付链接,可以通过支付链接生成一个二维码,然后用支付宝客户端扫码即可向用户付款。

    2、用户的收款记录

    点击收款记录进入收款记录页面,发现是一个h5的页面,用Charles抓包工具抓包,发现收款记录的请求信息如下:

    url
    https://mbillexprod.alipay.com/enterprise/simpleTradeOrderQuery.json?beginTime=1523289600000&limitTime=1523376000000&pageSize=20&pageNum=1&channelType=ALL&ctoken=Sf6-M33mBqAxZZKNtUxr8BfA
    
    Referer
    https://render.alipay.com/p/z/merchant-mgnt/simple-order.html?beginTime=2018-04-10&endTime=2018-04-10&fromBill=true&channelType=ALL
    
    Cookie
    JSESSIONID=RZ13WJ3MUC3KkSLP9Hl0p50jfGkM8464mobilegwRZ13; session.cookieNameId=ALIPAYJSESSIONID; JSESSIONID=DB2789AEA01160BC04A582168D1E5F56; devKeySet={"apdidToken":"2TvE1a0uTmOgw66ehO7iVGekSrqGuHzgMYEaoqbZS\/mgr+jE6sCfYgEB"}; ALIPAYJSESSIONID=RZ13xrqd7gCXa98nzw9FjaXQj5XCC564mobilegwRZ13GZ00; ctoken=Sf6-M33mBqAxZZKNtUxr8BfA; zone=RZ13B; rtk=z02vdaECH12mfnbsHEjoVSXRlX+5t9MESl8UVjAWb0Pkt9vKHEK; ssl_upgrade=0; spanner=B6pqxJF5iOiQ90i4CSoZsIIs1GQtygX7
    
    Method
    GET
    
    User-Agent
    Mozilla/5.0 (Linux; U; Android 6.0.1; zh-CN; PRO 6 Plus Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/40.0.2214.89 UCBrowser/11.6.4.950 UWS/2.11.1.49 Mobile Safari/537.36 UCBS/2.11.1.49_180322095406 NebulaSDK/1.8.100112 Nebula AlipayDefined(nt:WIFI,ws:360|0|4.0) AliApp(AP/10.1.20.556) AlipayClient/10.1.20.556 Language/zh-Hans useStatusBar/true

    其中beginTime表示查询的开始时间,limitTime表示查询的截止时间,将上面的信息用浏览器请求,发现能够返回数据,注意编辑请求设置上面的信息,如下:

     

     

     

     

     

    经过尝试发现url中的ctoken可以去除,并且Referer可以简化成Referer: https://render.alipay.com/p/z/merchant-mgnt/simple-order.html,后面的参数全部去除,然后Cookie中只需要设置ALIPAYJSESSIONID就可以了,User-Agent可以不修改,最终请求信息如下:

    url: https://mbillexprod.alipay.com/enterprise/simpleTradeOrderQuery.json?beginTime=1522425600000&limitTime=1523289600000&pageSize=20&pageNum=1&channelType=ALL
    Cookie: ALIPAYJSESSIONID=RZ115A3WmakZXV6KlujBgYoG0I9HoS31mobilegwRZ11GZ00;
    Referer: https://render.alipay.com/p/z/merchant-mgnt/simple-order.html
    user-agent:Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1

    也就是说,我们只需要ALIPAYJSESSIONID就可以获得到用户的收款记录了,查询时间可以自己设置,这个查询时间间隔好像不能超过1个月,超过了就会返回{"exception_marking":"搜索条件的范围过大"}。

    我们再postman中模拟发送数据:

    可以看到,我们拿到了支付宝的账单数据,但是账单数据里面没有备注,要想获得备注信息,我们还需要查询单个账单的详情,接口如下:

    https://tradeeportlet.alipay.com/wireless/tradeDetail.htm?tradeNo=2018040421001004450524815080
    Cookie:ALIPAYJSESSIONID=RZ13ik0FHP2IeX6b6LsZrDBFM1yHW464mobilegwRZ13;

    其中tradeNo表示订单号,这个在账单列表中有返回,其他的只需要设置Cookie就可以了,用Postman模拟请求:

    返回的是一个html页面,我们再页面中可以找到订单的备注信息,上面的订单对应的备注信息是“收款”。

    接下来我们只需要想办法获得ALIPAYJSESSIONID就可以了,在反编译代码中全局搜索“ALIPAYJSESSIONID”,发现AmnetUserInfo类中有相关的信息,ALIPAYJSESSIONID类中有如下代码:

      private static String getSessionid()
      {
        for (;;)
        {
          try
          {
            if (MiscUtils.isInAlipayClient(ExtTransportEnv.getAppContext())) {
              continue;
            }
            str1 = "";
          }
          catch (Throwable localThrowable)
          {
            String str1;
            LogCatUtil.error("ext_AmnetUserInfo", "getSessionid ex:" + localThrowable.toString());
            LogCatUtil.debug("ext_AmnetUserInfo", "getSessionid return null");
            String str2 = "";
            continue;
            str2 = getSessionidFromCookiestr(CookieAccessHelper.getCookie((String)localObject, ExtTransportEnv.getAppContext()));
            if (TextUtils.isEmpty(str2)) {
              continue;
            }
            Object localObject = new java/lang/StringBuilder;
            ((StringBuilder)localObject).<init>("sessionidFromCookieStore:");
            LogCatUtil.debug("ext_AmnetUserInfo", str2);
            continue;
          }
          return str1;
          localObject = ReadSettingServerUrl.getInstance().getGWFURL(ExtTransportEnv.getAppContext());
          str1 = getSessionidFromCookiestr(GwCookieCacheHelper.getCookie((String)localObject));
          if (TextUtils.isEmpty(str1)) {
            continue;
          }
          localObject = new java/lang/StringBuilder;
          ((StringBuilder)localObject).<init>("sessionidFromCache:");
          LogCatUtil.debug("ext_AmnetUserInfo", str1);
        }
      }
      
      private static String getSessionidFromCookiestr(String paramString)
      {
        try
        {
          if (!TextUtils.isEmpty(paramString)) {
            break label12;
          }
          paramString = "";
        }
        catch (Throwable paramString)
        {
          for (;;)
          {
            label12:
            int j;
            int i;
            LogCatUtil.error("ext_AmnetUserInfo", "getAlipayJsessionidFromCookiestr ex:" + paramString.toString());
            label96:
            paramString = "";
          }
        }
        return paramString;
        paramString = paramString.split("; ");
        j = paramString.length;
        for (i = 0;; i++)
        {
          if (i >= j) {
            break label96;
          }
          CharSequence localCharSequence = paramString[i];
          if ((!TextUtils.isEmpty(localCharSequence)) && (localCharSequence.contains("ALIPAYJSESSIONID")))
          {
            paramString = localCharSequence.substring(localCharSequence.indexOf("=") + 1);
            break;
          }
        }
      }

    其中getSessionid方法感觉就是获得ALIPAYJSESSIONID的方法,在xposed中调用该静态方法并打印返回值,发现返回的是字符串“ALIPAYJSESSIONID”,在hook getSessionidFromCookiestr方法,打印传入的参数,结果就是我们抓包获得的cookie,而cookie中是包含ALIPAYJSESSIONID的信息的,通过

          localObject = ReadSettingServerUrl.getInstance().getGWFURL(ExtTransportEnv.getAppContext());
          str1 = getSessionidFromCookiestr(GwCookieCacheHelper.getCookie((String)localObject));

    这两行代码,我们知道可以通过如下代码获得cookie

          cookieStr = getSessionidFromCookiestr(GwCookieCacheHelper.getCookie((String)ReadSettingServerUrl.getInstance().getGWFURL(ExtTransportEnv.getAppContext())));

    在xposed中对应的代码如下:

                        String cookieStr = "";
                        // 获得cookieStr
                        Context context = (Context) callStaticMethod(findClass("com.alipay.mobile.common.transportext.biz.shared.ExtTransportEnv", lpparam.classLoader), "getAppContext");
                        if (context != null) {
                            Object readSettingServerUrl = callStaticMethod(findClass("com.alipay.mobile.common.helper.ReadSettingServerUrl", lpparam.classLoader), "getInstance");
                            if (readSettingServerUrl != null) {
                                String gWFURL = (String) callMethod(readSettingServerUrl, "getGWFURL", context);
                                cookieStr = (String) callStaticMethod(findClass("com.alipay.mobile.common.transport.http.GwCookieCacheHelper", lpparam.classLoader), "getCookie", gWFURL);
                            }
                        }

    打印日志如下:

    04-10 17:11:09.647 I/Xposed  ( 7116): cookieStr:session.cookieNameId=ALIPAYJSESSIONID; ssl_upgrade=0; spanner=PWDKfHD/i7Rh9gQCMkMP+DTzT8PATh824EJoL7C0n0A=; ctoken=Sf6-M33mBqAxZZKNtUxr8BfA; rtk=vokrGCgjMQ9UdSJNIgY0Tnw6Os8MF2zV3TThTYLGJohQF2zBIgB; ALIPAYJSESSIONID=RZ13nkgR2GBxkRKbRrX11rVYzOI6Vi64mobilegwRZ13; devKeySet={"apdidToken":"oBdC1a0uTmOgw66ehO7iVGekSnlK3Y00XLuw5BGCZ6yVyRla+q2qYgEB"}; zone=RZ13A

    可以看到其中包含了ALIPAYJSESSIONID的信息。

    备注:上面获得的账单信息有一个明显的缺点,就是接口返回的账单数据中没有备注信息,而一般我们是需要根据备注信息来确认账单的唯一性,从而判断是否收款成功,之后会进行优化。

     

    四、xposed插件和支付宝应用通信

    我们写的插件是单独一个进程,而支付宝也是单独一个进程,两个进程之间的通信有很多方法,比如Binder,Socket,BroadcastReceiver等,这里选择最简单的BroadcastReceiver。

    xposed插件的主界面如下:

    添加收钱按钮的点击事件:

            mShouQianButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent intent = getPackageManager().getLaunchIntentForPackage(ALIPAY_PACKAGE_NAME);
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    startActivity(intent);
    
                    Intent broadCastIntent = new Intent();
                    Random random = new Random();
                    broadCastIntent.putExtra("qr_money", String.valueOf(random.nextInt(100) + 1));
                    broadCastIntent.putExtra("beiZhu", "测试");
                    broadCastIntent.setAction(AlipayBroadcast.INTENT_FILTER_ACTION);
                    sendBroadcast(broadCastIntent);
                }
            });

    点击收钱按钮后,会切换到支付宝应用,并且随机生成一个1-100的金额,设置备注,然后将信息通过广播的形式发送出去,支付宝要收到对应的广播,必须先要注册广播,我们可以在支付宝的主界面注册广播,hook支付宝主界面的onCreate方法,注册广播,hook支付宝主界面的onDestory方法,销毁广播,代码如下:

                // hook 支付宝主界面的onCreate方法,获得主界面对象并注册广播
                findAndHookMethod("com.alipay.mobile.quinox.LauncherActivity", lpparam.classLoader, "onCreate", Bundle.class, new XC_MethodHook() {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                        log("com.alipay.mobile.quinox.LauncherActivity onCreated" + "\n");
                        launcherActivity = (Activity) param.thisObject;
                        alipayBroadcast = new AlipayBroadcast();
                        IntentFilter intentFilter = new IntentFilter();
                        intentFilter.addAction(AlipayBroadcast.INTENT_FILTER_ACTION);
                        launcherActivity.registerReceiver(alipayBroadcast, intentFilter);
                    }
                });
    
                // hook 支付宝的主界面的onDestory方法,销毁广播
                findAndHookMethod("com.alipay.mobile.quinox.LauncherActivity", lpparam.classLoader, "onDestroy", new XC_MethodHook() {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                        log("com.alipay.mobile.quinox.LauncherActivity onDestroy" + "\n");
                        if (alipayBroadcast != null) {
                            ((Activity) param.thisObject).unregisterReceiver(alipayBroadcast);
                        }
                        launcherActivity = null;
                    }
                });


    广播类定义如下:

    package com.hhly.pay.alipay.boradcast;
    
    import android.content.BroadcastReceiver;
    import android.content.Context;
    import android.content.Intent;
    
    import com.hhly.pay.alipay.Main;
    
    import de.robv.android.xposed.XposedHelpers;
    
    import static de.robv.android.xposed.XposedBridge.log;
    
    /**
     * Created by dell on 2018/4/4.
     */
    
    public class AlipayBroadcast extends BroadcastReceiver{
        public static String INTENT_FILTER_ACTION = "com.hhly.pay.alipay.info";
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().contentEquals(INTENT_FILTER_ACTION)) {
                String qr_money = intent.getStringExtra("qr_money");
                String beiZhu = intent.getStringExtra("beiZhu");
                log("AlipayBroadcast onReceive " + qr_money + " " + beiZhu + "\n");
                if (!qr_money.contentEquals("")) {
                    Intent launcherIntent = new Intent(context, XposedHelpers.findClass("com.alipay.mobile.payee.ui.PayeeQRSetMoneyActivity", Main.launcherActivity.getApplicationContext().getClassLoader()));
                    launcherIntent.putExtra("qr_money", qr_money);
                    launcherIntent.putExtra("beiZhu", beiZhu);
                    Main.launcherActivity.startActivity(launcherIntent);
                }
            }
        }
    }

    可以看到,支付宝在接受到广播后会打开设置金额页面,并且将金额和备注传过去,接下来我们需要hook住设置金额页面的onCreate方法,取得金额和备注,设置到界面上并且模拟点击确认按钮,这样我们只需要hook住设置金额的"a"方法,

    protected final void a(ConsultSetAmountRes paramConsultSetAmountRes)

    就获得到付款链接了,可以在这里顺便获得cookie,然后通过广播的形式发送给xposed插件,代码如下:

              findAndHookMethod("com.alipay.mobile.payee.ui.PayeeQRSetMoneyActivity", lpparam.classLoader, "a",
                        findClass("com.alipay.transferprod.rpc.result.ConsultSetAmountRes", lpparam.classLoader), new XC_MethodHook() {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                        log("com.alipay.mobile.payee.ui.PayeeQRSetMoneyActivity a" + "\n");
                        String cookieStr = "";
                        // 获得cookieStr
                        Context context = (Context) callStaticMethod(findClass("com.alipay.mobile.common.transportext.biz.shared.ExtTransportEnv", lpparam.classLoader), "getAppContext");
                        if (context != null) {
                            Object readSettingServerUrl = callStaticMethod(findClass("com.alipay.mobile.common.helper.ReadSettingServerUrl", lpparam.classLoader), "getInstance");
                            if (readSettingServerUrl != null) {
                                String gWFURL = (String) callMethod(readSettingServerUrl, "getGWFURL", context);
                                cookieStr = (String) callStaticMethod(findClass("com.alipay.mobile.common.transport.http.GwCookieCacheHelper", lpparam.classLoader), "getCookie", gWFURL);
                            }
                        }
                        Object consultSetAmountRes = param.args[0];
                        String consultSetAmountResString = "";
                        if (consultSetAmountRes != null) {
                            consultSetAmountResString = (String) callMethod(consultSetAmountRes, "toString");
                        }
                        Intent broadCastIntent = new Intent();
                        broadCastIntent.putExtra("consultSetAmountResString", consultSetAmountResString);
                        broadCastIntent.putExtra("cookieStr", cookieStr);
                        broadCastIntent.setAction(PluginBroadcast.INTENT_FILTER_ACTION);
                        Activity activity = (Activity) param.thisObject;
                        activity.sendBroadcast(broadCastIntent);
                        log("consultSetAmountResString:" + consultSetAmountResString + "\n");
                        log("cookieStr:" + cookieStr + "\n");
                    }
                });

    同样,在xposed插件中需要注册广播:

    在xposed插件的MainActivity的onCreate方法中注册广播,并在其onDestory中销毁广播,如下:

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
    
            pluginReceiver = new PluginBroadcast();
            IntentFilter intentFilter = new IntentFilter();
            intentFilter.addAction(PluginBroadcast.com.eg.android.AlipayGphone.info);
            registerReceiver(pluginReceiver, intentFilter);
        }
    
    
    
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            unregisterReceiver(pluginReceiver);
        }

    PluginBroadcast的定义如下:

    package com.hhly.pay.alipay.boradcast;
    
    import android.content.BroadcastReceiver;
    import android.content.Context;
    import android.content.Intent;
    import com.hhly.pay.alipay.App;
    
    /**
     * Created by dell on 2018/4/4.
     */
    
    public class PluginBroadcast extends BroadcastReceiver{
        public static String INTENT_FILTER_ACTION = "com.eg.android.AlipayGphone.info";
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().contentEquals(INTENT_FILTER_ACTION)) {
                App.dealAlipayInfo(context, intent);
            }
        }
    }

    dealAlipayInfo方法的定义:

        public static void dealAlipayInfo(Context context, Intent intent) {
            String consultSetAmountResString = intent.getStringExtra("consultSetAmountResString");
            String cookieStr = intent.getStringExtra("cookieStr");
            String toastString = consultSetAmountResString + " " + cookieStr;
            Log.i("liunianprint:", toastString);
            Toast.makeText(context, toastString, Toast.LENGTH_SHORT).show();
        }

    这里只是打印了consultSetAmountResString和cookieStr,正常流程是应该将信息上传给服务端,打印的日志如下:

    04-10 18:47:03.288 7097-7097/? I/liunianprint:: ConsultSetAmountRes{codeId='1804106465250342', qrCodeUrl='HTTPS://QR.ALIPAY.COM/FKX03573WKXOYREEFL2686', printQrCodeUrl='HTTPS://QR.ALIPAY.COM/FKX01907CEYS5GOWTI9PB1'} RPCResponse{success=true, code='null', message='null'} session.cookieNameId=ALIPAYJSESSIONID; ssl_upgrade=0; spanner=PWDKfHD/i7Rh9gQCMkMP+DTzT8PATh824EJoL7C0n0A=; ctoken=Sf6-M33mBqAxZZKNtUxr8BfA; rtk=vokrGCgjMQ9UdSJNIgY0Tnw6Os8MF2zV3TThTYLGJohQF2zBIgB; ALIPAYJSESSIONID=RZ13nkgR2GBxkRKbRrX11rVYzOI6Vi64mobilegwRZ13; devKeySet={"apdidToken":"oBdC1a0uTmOgw66ehO7iVGekSnlK3Y00XLuw5BGCZ6yVyRla+q2qYgEB"}; zone=RZ13A
    

    到此为止,我们已经可以在插件中获得收款链接和ALIPAYJSESSIONID,只需要将其发送给服务端就可以了,服务端可以根据收款链接生成收款二维码,根据ALIPAYJSESSIONID请求到收款记录。

    顺便说一句,支付宝请求收款二维码链接是通过rpc协议进行的,在PayeeQRSetMoneyActivity如下方法:

      final void a()
      {
        ConsultSetAmountReq localConsultSetAmountReq = new ConsultSetAmountReq();
        localConsultSetAmountReq.amount = this.g;
        localConsultSetAmountReq.desc = this.c.getUbbStr();
        localConsultSetAmountReq.sessionId = this.h;
        new RpcRunner(new dk(this), new dj(this)).start(new Object[] { localConsultSetAmountReq });
      }

    点击确认按钮后会调用该方法去向支付宝的服务器请求支付链接,用Charles抓取不到rpc的请求,后面可以考虑直接模拟rpc请求直接向支付宝的服务器请求付款链接。

     

    五、优化账单

    通过上面的接口获得的账单信息中是没有备注的,估计支付宝为了安全没有将这块信息加入到接口中,但是在服务端判断收款是否到账就是根据收款记录中的备注信息确认的,只需要将设置金额页面的备注信息设置为每个账单唯一,就可以根据备注信息确认收款是否到账,在支付宝的账单页面,我们可以看到账单的备注信息,如下:

    那下面就从账单页面入手,找到带备注信息的账单数据,用hierarchy view看一下账单界面,如下:

    可以看到账单页面对应的activity为"com.alipay.mobile.bill.list.ui.BillListActivity_",在反编译代码中搜索"BillListActivity_"类,发现找不到这个类,通过xposed hook这个类,也提示无法找到该类。代码如下:

            findAndHookMethod("com.alipay.mobile.bill.list.ui.BillListActivity_", mClassLoader, "onCreate", Bundle.class, new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    XposedLogUtils.log("com.alipay.mobile.bill.list.ui.BillListActivity_" + ":onCreated");
                    mBillActivity = (Activity) param.thisObject;
                }
            });

    报错信息如下:

    05-24 17:07:14.977 E/Xposed  ( 6047): de.robv.android.xposed.XposedHelpers$ClassNotFoundError: java.lang.ClassNotFoundException: com.alipay.mobile.bill.list.ui.BillListActivity_
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at de.robv.android.xposed.XposedHelpers.findClass(XposedHelpers.java:71)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at de.robv.android.xposed.XposedHelpers.findAndHookMethod(XposedHelpers.java:260)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at com.sunny.aliplugin.hook.AliHook.o(AliHook.java:497)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at com.sunny.aliplugin.hook.AliHook.b(AliHook.java:58)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at com.sunny.aliplugin.hook.AliHook$6.afterHookedMethod(AliHook.java:245)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at de.robv.android.xposed.XposedBridge.handleHookedMethod(XposedBridge.java:374)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at com.alipay.mobile.quinox.classloader.BundleClassLoader.<init>(<Xposed>)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at com.alipay.mobile.quinox.classloader.c.run(BundleClassLoaderFactory.java:213)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at com.alipay.mobile.quinox.asynctask.PipelineRunnable.run(PipelineRunnable.java:124)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at java.lang.Thread.run(Thread.java:818)
    05-24 17:07:14.977 E/Xposed  ( 6047): Caused by: java.lang.ClassNotFoundException: com.alipay.mobile.bill.list.ui.BillListActivity_
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at java.lang.Class.classForName(Native Method)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at java.lang.Class.forName(Class.java:324)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at external.org.apache.commons.lang3.ClassUtils.getClass(ClassUtils.java:823)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at de.robv.android.xposed.XposedHelpers.findClass(XposedHelpers.java:69)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	... 11 more
    05-24 17:07:14.977 E/Xposed  ( 6047): Caused by: java.lang.ClassNotFoundException: Didn't find class "com.alipay.mobile.bill.list.ui.BillListActivity_" on path: DexPathList[[zip file "/system/framework/org.simalliance.openmobileapi.jar", zip file "/data/app/com.eg.android.AlipayGphone-1/base.apk"],nativeLibraryDirectories=[/data/user/0/com.eg.android.AlipayGphone/app_plugins_lib, /data/app/com.eg.android.AlipayGphone-1/lib/arm, /data/app/com.eg.android.AlipayGphone-1/base.apk!/lib/armeabi, /vendor/lib, /system/lib]]
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at java.lang.ClassLoader.loadClass(ClassLoader.java:511)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	at java.lang.ClassLoader.loadClass(ClassLoader.java:469)
    05-24 17:07:14.977 E/Xposed  ( 6047): 	... 15 more
    05-24 17:07:14.977 E/Xposed  ( 6047): 	Suppressed: java.lang.ClassNotFoundException: HostClassLoader

    那这样就奇怪了,既然支付宝能够显示账单页面,那么对应的代码肯定是存在的,考虑一下,是不是支付宝用了分包技术,账单页面在其他的jar包中呢?在需要显示账单页面时才去加载对应的jar包,如果是这样,那么我们在当前apk的主ClassLoader就无法找到账单页面。现在需要想办法验证一下我们的想法,首先如果我们能够知道账单页面对应的ClassLoader,就可以打印出ClassLoader的名称,就能知道账单页面对应的代码所在位置。那么如何获得账单页面对应的ClassLoader呢?我们知道,Class类有一个getClassLoader()方法,如果能够获得账单页面对应的对象,然后通过getClass()方法获得其对应的Class,再调用Class的getClassLoader()方法,就可以知道账单页面对应的ClassLoader了。那么如何获得账单页面Activity对象呢?我们现在是不知道账单页面对应的ClassLoader的,也就找不到其对应的Class,无法在Xposed中注册Hook它的方法,额,感觉有点麻烦了,换个角度思考一下,BillListActivity_是一个Activity,那么它肯定是要继承Activity类的,BillListActivity_在创建时肯定会调用到Activity的onCreate方法,我们可以Hook Activity类的onCreate方法,获得当前Activity对象,然后获得当前Activity对象对应的类名,再来比较类名是不是账单页面对应的类名,如果是,那么当前Activity对象就是BillListActivty_对象,那么就可以打印出其对应的ClassLoader了。代码如下:

            findAndHookMethod(Activity.class, "onCreate", Bundle.class, new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    if (param.thisObject != null && param.thisObject.getClass().getName().contentEquals("com.alipay.mobile.bill.list.ui.BillListActivity_")) {
                        XposedLogUtils.log(param.thisObject.getClass().getClassLoader().toString());
                    }
                }
            });

    打印结果如下:

    05-24 17:43:17.051 I/Xposed  ( 6055): BundleClassLoader[/data/user/0/com.eg.android.AlipayGphone/lib/libandroid-phone-wallet-billlist.so]

    可以看到账单页面的代码在lib目录的"libandroid-phone-wallet-billlist.so"这个库中,打开apk的lib目录下,发现可以找到这个库:

    并且ClassLoader对应的类名为"BundleClassLoader",我擦,居然是一个so库,我们知道,so库是不能直接看到代码的,如果是so库就难搞了,尝试将"libandroid-phone-wallet-billlist.so"重命名为"libandroid-phone-wallet-billlist.zip"并且解压,对应目录如下:

    呃呃呃,原来是一个假的so库,其实和apk包是一样的,反编译一下classes.dex,获得账单页面对应的jar包,如下:

    获得账单页面对应的jar包并且用jd-gui打开,搜索BillListActivity_,可以看到BillListActivity_的代码:

    ok,现在我们获得账单页面的代码了,接下来就是Hook账单页面对应的方法然后获得账单数据,搞到这里又会发现一个问题,想要hook账单页面的方法首先需要获得账单页面对应的class,而class是需要从ClassLoader中找的,账单页面的ClassLoader是动态加载的,只有当启动支付宝后首次打开账单页面时,才会去加载账单页面对应的库,从而生成对应的ClassLoader,我们上面获得账单页面对应的ClassLoader的步骤如下:

    1、hook Activity的onCreate方法

    2、通过类名判断当前Activity是否是账单页面

    3、如果是账单页面,则可以通过getClass().getClassLoader()获得账单页面对应的ClassLoader

    4、记录账单页面的ClassLoader当静态变量中

    5、之后要使用账单页面的ClassLoader就可以直接使用静态变量中的ClassLoader了

    上面方法有一个问题是我们必须要在支付宝启动后手动打开一次账单页面,这样支付宝才会去加载账单页面对应的库,然后才能找到其对应的ClassLoader,有没有什么办法可以不用手动打开账单页面呢?通过查看点击进入账单页面的方法,最终我们发现可以通过下面的代码打开账单页面:

        LauncherAppUtils.a("20000003");

    LauncherAppUtils应该是支付宝为了启动动态库中的Activity而写的一个辅助类,"20000003"应该代表账单页面,这个在支付宝AppId类中有配置:

    我们可以在支付宝的首页启动时调用该方法,从而实现在支付宝启动后自动打开支付宝的账单页面,这样就可以获得账单页面对应的ClassLoader了。代码如下:

            // hook 支付宝主界面的onCreate方法,获得主界面对象并注册广播
            findAndHookMethod(AliParamUtils.mLauncherActivityClassfullName, classLoader, "onCreate", Bundle.class, new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    XposedLogUtils.log(AliParamUtils.mLauncherActivityClassfullName + ":onCreated方法");
                    mLauncherActivity = (Activity) param.thisObject;
    
                    if (AliParamUtils.mBillListActivityIsFromSoLib) {
                        // 打开账单页面,并加载其对应的库
                        callStaticMethod(findClass("com.alipay.android.phone.home.manager.LauncherAppUtils", classLoader), "a", "20000003", null);
                    }
                }
            });

    另外一个问题是,我们不想通过hook Activity的onCreate方法,然后判断类名的方式获得账单页面对应的ClassLoader,那么有没有其他的办法呢?通过上面的对应账单页面ClassLoader的打印,我们知道,账单页面的ClassLoader对应的类名为"BundleClassLoader",在反编译代码中搜索"BundleClassLoader",可以看到如下代码:

    观察"BundleClassLoader"所有的构造方法,发现其最终都会调用下面的这个构造方法:

      @SuppressLint({"DefaultLocale"})
      public BundleClassLoader(ClassLoader paramClassLoader, Bundle paramBundle, BundleManager paramBundleManager, HostClassLoader paramHostClassLoader)

    并且在该构造方法中可以看到如下代码:

     if ((Build.HARDWARE.toLowerCase().contains("mt6592")) && (paramBundle.getLocation().endsWith(".so")))

    猜想paramBundle.getLocation()应该是获得动态库的路径,我们可以打印paramBundle.getLocation()的值,在首次打开账单页面时可以看到如下日志:

    05-12 10:55:28.861 I/Xposed  ( 6891): ------------so库    /data/user/0/com.eg.android.AlipayGphone/lib/libandroid-phone-wallet-billlist.so

    说明paramBundle.getLocation()就是获得动态库的路径,既然这样,我们就可以通过动态库的名称来判断当前的ClassLoader是否是账单页面的ClassLoader,代码如下:

            // hook BundleClassLoader构造方法,获得so库对应的classloader并hook来自so库中的类
            findAndHookConstructor("com.alipay.mobile.quinox.classloader.BundleClassLoader", classLoader,
                    ClassLoader.class,
                    findClass("com.alipay.mobile.quinox.bundle.Bundle", classLoader),
                    findClass("com.alipay.mobile.quinox.bundle.BundleManager", classLoader),
                    findClass("com.alipay.mobile.quinox.classloader.HostClassLoader", classLoader),
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                            try {
                                if (param.args[1] != null) {
                                    String soLibName = (String) XposedHelpers.callMethod(param.args[1], AliParamUtils.mBundleGetLocationMethodName); // 获得so库名称
                                    if (soLibName != null) {
                                        if (soLibName.contains("wallet-billlist")) {
                                            mBillListActivityClassLoader = (ClassLoader) param.thisObject;
                                            XposedLogUtils.log("账单页面classloader: " + mBillListActivityClassLoader.toString());
                                            hookBillListActivityMethod();
                                        }
                                    }
                                }
                            } catch (Exception e) {
                            }
                        }
                    });

    首次打开账单页面,可以看到如下日志:

    05-12 10:55:28.861 I/Xposed  ( 6891): 账单页面classloader: BundleClassLoader[/data/user/0/com.eg.android.AlipayGphone/lib/libandroid-phone-wallet-billlist.so]

    到现在为止,我们已经可以在支付宝应用启动后自动获得账单页面的ClassLoader并将其保存在静态变量mBillListActivityClassLoader中,终于能够正常hook账单页面的方法了,接下来我们就通过hook账单页面来获得账单数据。

    用hierarchy view查看账单界面,如下:

    发现其账单信息是在一个APListView控件中,并且ApListView外面又套了一个APPullRefreshView,这个应该是可以猜到的,账单页面是一个列表,并且支持下拉刷新和上拉加载更多,一般的套路就是下拉刷新控件套上一个ListView或者RecyclerView,既然这样,那么如果我们找到ListView对应的Adpater,账单的数据应该就存在Adpater中的某个类型为List的对象中,直接在反编译代码中查看BillListActivity_的代码,如下:

    并没有找到对应的APPullRefreshView或者ApListView之类的信息,打开其父类BillListActivity,如下:

    @EActivity(resName="activity_bill_list")
    public class BillListActivity
      extends BillListBaseActivity
    {
      private BroadcastReceiver A;
      private BroadcastReceiver B;
      private boolean C = false;
      private boolean D = false;
      private boolean E = false;
      private boolean F = false;
      private long G = 0L;
      private String H;
      private boolean I = false;
      private RpcRunner J;
      private String K;
      private String L;
      private String M;
      private boolean N;
      private String O;
      private boolean P;
      private String Q;
      private RpcRunner R;
      private RpcRunner S;
      private List<EntrancePBModel> T;
      private SelectDateWindow U;
      private CategoryListRes V;
      private boolean W;
      private String X;
      private AUFloatMenu Y;
      private BillCacheManager Z;
      private BillCacheManager aa;
      private BillListNewCategoryManager ab;
      private boolean ac = false;
      private FilterPopUpWindow ad;
      private NewCategoryFilterPopUpWindow ae;
      private boolean af;
      private String ag;
      private String ah;
      private boolean ai = true;
      private boolean aj = false;
      private boolean ak = false;
      private String al;
      private boolean am = false;
      private boolean an = true;
      @ViewById(resName="bill_list_title_bar")
      protected AUTitleBar c;
      @ViewById(resName="bill_list_view")
      protected APListView d;
      @ViewById(resName="bill_list_container")
      protected View e;
      @ViewById(resName="bill_list_month_header")
      protected ViewGroup f;
      @ViewById(resName="bill_list_pull_refresh")
      protected APPullRefreshView g;
      @ViewById(resName="bill_list_loading")
      protected View h;
      protected BillListFilterBar i;
      protected BillListFilterBar j;
      protected TextView k;
      private ViewGroup l;
      private ViewGroup m;
      private TextView n;
      private View o;
      private BadgeView p;
      private String q;
      private APOverView r;
      private AuthService s;
      private String t = "NO";
      private QueryListReq u;
      private boolean v = false;
      private boolean w = true;
      private boolean x = false;
      private BillListViewFooterView y;
      private BillListAdapter z;

    可以看到其中有如下字段的定义:

      @ViewById(resName="bill_list_pull_refresh")
      protected APPullRefreshView g;
      @ViewById(resName="bill_list_view")
      protected APListView d;
      private BillListAdapter z;

    可以看到账单页面对应的Adapter为BillListAdater,字段名称为"z",打开BillListAdater类,可以看到如下代码:

        public List<SingleListItem> a = new ArrayList();
    
        public final void a(List<SingleListItem> paramList)
        {
            this.a.addAll(paramList);
            notifyDataSetChanged();
        }

    其中字段"a"应该就是账单列表的数据,而方法"a"应该是用来添加账单数据到列表中的方法,在这个类中搜索"this.a.add",发现只有这个方法中有添加账单数据到列表中,由此,我们可以判断,只要账单数据有增加,肯定会调用该方法。我们可以通过hook 该方法,一旦账单数据有添加,我们就可以监控到,代码如下:

            findAndHookMethod("com.alipay.mobile.bill.list.ui.adapter.BillListAdapter", mBillListActivityClassLoader, "a", List.class, new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    XposedLogUtils.log("com.alipay.mobile.bill.list.ui.adapter.BillListAdapter" + "a called" + "\n");
                    if (param.args[0] != null) {
                        Field billListFiled = XposedHelpers.findField(param.thisObject.getClass(), "a"); // 通过反射找到账单数据列表对应的字段
                        final Object billList = billListFiled.get(param.thisObject); // 获得账单数据列表
                        List<Object> bList_obj = (List) billList;
                        if (bList_obj != null) {
                            sendBillListBroadCast(bList_obj); // 将账单数据列表通过广播发送回给插件程序
                        }
                    }
                }
            });

    其中sendBillListBroadCast(bList_obj)的作用是将账单数据列表通过广播发送回给插件程序,通过上面的分析我们知道账单数据的类型为SingleListItem,打开SingleListItem类,可以看到如下字段的定义:

      @ProtoField(tag=15)
      public ActionParam actionParam;
      @ProtoField(tag=1, type=Message.Datatype.STRING)
      public String bizInNo;
      @ProtoField(tag=6, type=Message.Datatype.STRING)
      public String bizStateDesc;
      @ProtoField(tag=10, type=Message.Datatype.STRING)
      public String bizSubType;
      @ProtoField(tag=9, type=Message.Datatype.STRING)
      public String bizType;
      @ProtoField(tag=11, type=Message.Datatype.BOOL)
      public Boolean canDelete;
      @ProtoField(tag=23, type=Message.Datatype.STRING)
      public String categoryName;
      @ProtoField(tag=3, type=Message.Datatype.STRING)
      public String consumeFee;
      @ProtoField(tag=4, type=Message.Datatype.STRING)
      public String consumeStatus;
      @ProtoField(tag=2, type=Message.Datatype.STRING)
      public String consumeTitle;
      @ProtoField(tag=26, type=Message.Datatype.INT32)
      public Integer contentRender;
      @ProtoField(tag=8, type=Message.Datatype.STRING)
      public String createDesc;
      @ProtoField(tag=16, type=Message.Datatype.STRING)
      public String createTime;
      @ProtoField(tag=14, type=Message.Datatype.STRING)
      public String destinationUrl;
      @ProtoField(tag=7, type=Message.Datatype.INT64)
      public Long gmtCreate;
      @ProtoField(tag=18, type=Message.Datatype.BOOL)
      public Boolean isAggregatedRec;
      @ProtoField(tag=17, type=Message.Datatype.STRING)
      public String memo;
      @ProtoField(tag=13, type=Message.Datatype.STRING)
      public String month;
      @ProtoField(tag=5, type=Message.Datatype.STRING)
      public String oppositeLogo;
      @ProtoField(tag=20, type=Message.Datatype.STRING)
      public String oppositeMemGrade;
      @ProtoField(tag=12, type=Message.Datatype.ENUM)
      public RecordType recordType;
      @ProtoField(tag=19, type=Message.Datatype.STRING)
      public String sceneId;
      @ProtoField(tag=25, type=Message.Datatype.STRING)
      public String statistics;
      @ProtoField(tag=24, type=Message.Datatype.STRING)
      public String subCategoryName;
      @ProtoField(label=Message.Label.REPEATED, tag=21, type=Message.Datatype.STRING)
      public List<String> tagNameList;
      @ProtoField(tag=22, type=Message.Datatype.INT32)
      public Integer tagStatus;

    根据名称,我们大致可以猜测出每一个字段代表的意思,比如consumeFee应该代表进账或者消费的金额,我们在插件程序中创建一个BillObject类,并且实现Parcelable接口,以便其能够被序列化,如下:

       /**
         * 账单信息对象
         */
        public static class BillObject implements Parcelable {
            public String bizInNo;
            public String bizStateDesc;
            public String bizSubType;
            public String canDelete;
            public String bizType;
            public String consumeFee;
            public String consumeStatus;
            public String consumeTitle;
            public String createDesc;
            public String createTime;
            public String destinationUrl;
            public String gmtCreate;
            public String isAggregatedRec;
            public String memo;
            public String month;
            public String oppositeLogo;
            public String oppositeMemGrade;
            public String sceneId;
    
            public BillObject() {
    
            }
    
            protected BillObject(Parcel in) {
                bizInNo = in.readString();
                bizStateDesc = in.readString();
                bizSubType = in.readString();
                canDelete = in.readString();
                bizType = in.readString();
                consumeFee = in.readString();
                consumeStatus = in.readString();
                consumeTitle = in.readString();
                createDesc = in.readString();
                createTime = in.readString();
                destinationUrl = in.readString();
                gmtCreate = in.readString();
                isAggregatedRec = in.readString();
                memo = in.readString();
                month = in.readString();
                oppositeLogo = in.readString();
                oppositeMemGrade = in.readString();
                sceneId = in.readString();
            }
    
            public static final Creator<BillObject> CREATOR = new Creator<BillObject>() {
                @Override
                public BillObject createFromParcel(Parcel in) {
                    return new BillObject(in);
                }
    
                @Override
                public BillObject[] newArray(int size) {
                    return new BillObject[size];
                }
            };
    
            @Override
            public String toString() {
                return "bizInNo:" + bizInNo + "," +
                        "bizStateDesc:" + bizStateDesc + "," +
                        "bizSubType:" + bizSubType + "," +
                        "canDelete:" + canDelete + "," +
                        "bizType:" + bizType + "," +
                        "consumeFee:" + consumeFee + "," +
                        "consumeStatus:" + consumeStatus + "," +
                        "consumeTitle:" + consumeTitle + "," +
                        "createDesc:" + createDesc + "," +
                        "createTime:" + createTime + "," +
                        "destinationUrl:" + destinationUrl + "," +
                        "gmtCreate:" + gmtCreate + "," +
                        "isAggregatedRec:" + isAggregatedRec + "," +
                        "memo:" + memo + "," +
                        "month:" + month + "," +
                        "oppositeLogo:" + oppositeLogo + "," +
                        "oppositeMemGrade:" + oppositeMemGrade + "," +
                        "sceneId:" + sceneId + "\n";
            }
    
            @Override
            public int describeContents() {
                return 0;
            }
    
            @Override
            public void writeToParcel(Parcel dest, int flags) {
                dest.writeString(bizInNo);
                dest.writeString(bizStateDesc);
                dest.writeString(bizSubType);
                dest.writeString(canDelete);
                dest.writeString(bizType);
                dest.writeString(consumeFee);
                dest.writeString(consumeStatus);
                dest.writeString(consumeTitle);
                dest.writeString(createDesc);
                dest.writeString(createTime);
                dest.writeString(destinationUrl);
                dest.writeString(gmtCreate);
                dest.writeString(isAggregatedRec);
                dest.writeString(memo);
                dest.writeString(month);
                dest.writeString(oppositeLogo);
                dest.writeString(oppositeMemGrade);
                dest.writeString(sceneId);
            }
        }

    然后将账单数据存到我们自己的Object对象中,并且发送广播给插件:

        /**
         * 发送账单数据广播
         *
         * @param objectList
         * @throws IllegalAccessException
         */
        private void sendBillListBroadCast(List<Object> objectList) throws IllegalAccessException {
            boolean isFound = false; // 是否查到相应位置
            boolean isLast = false;  // 已是最新数据
            int invalidCount = 0; //
            ArrayList<BillObject> billObjectList = new ArrayList<>();
    
            XposedLogUtils.log("objectList size:" + objectList.size());
    
            for (int i = 0; i < objectList.size(); i++) {
                Object obj = objectList.get(i);
                BillObject billObject = new BillObject();
                billObject.bizInNo = getStringField(obj, "bizInNo");
                billObject.bizStateDesc = getStringField(obj, "bizStateDesc");
                billObject.bizSubType = getStringField(obj, "bizSubType");
                billObject.canDelete = getStringField(obj, "canDelete");
                billObject.bizType = getStringField(obj, "bizType");
                billObject.consumeFee = getStringField(obj, "consumeFee");
                billObject.consumeStatus = getStringField(obj, "consumeStatus");
                billObject.consumeTitle = getStringField(obj, "consumeTitle");
                billObject.createDesc = getStringField(obj, "createDesc");
                billObject.createTime = getStringField(obj, "createTime");
                billObject.destinationUrl = getStringField(obj, "destinationUrl");
                billObject.gmtCreate = getStringField(obj, "gmtCreate");
                billObject.isAggregatedRec = getStringField(obj, "isAggregatedRec");
                billObject.memo = getStringField(obj, "memo");
                billObject.month = getStringField(obj, "month");
                billObject.oppositeLogo = getStringField(obj, "oppositeLogo");
                billObject.oppositeMemGrade = getStringField(obj, "oppositeMemGrade");
                billObject.sceneId = getStringField(obj, "sceneId");
    
                XposedLogUtils.log("bizInNo:" + billObject.bizInNo + ", consumeFee:" + billObject.consumeFee + ", gmtCreate:" + billObject.gmtCreate);
                billObjectList.add(billObject);
            }
    
    
            Intent broadCastIntent = new Intent();
            broadCastIntent.putParcelableArrayListExtra(AliParamUtils.mBillListParcelString, billObjectList);
            broadCastIntent.setAction(PluginReceiver.BILL_LIST_INTENT_FILTER_ACTION);
            mBillActivity.sendBroadcast(broadCastIntent);
        }
    
        private String getStringField(final Object obj, final String fieldName) throws IllegalAccessException {
            Field sField = XposedHelpers.findField(obj.getClass(), fieldName);
            if (sField == null) {
                return null;
            }
            return String.valueOf(sField.get(obj));
        }

    我们在插件的广播接收类中取出账单数据并打印出来:

        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().contentEquals(BILL_LIST_INTENT_FILTER_ACTION)) {
                List<AliHook.BillObject> billObjectList = intent.getParcelableArrayListExtra(AliParamUtils.mBillListParcelString);
                LogUtils.e("billObjectListISNull ", billObjectList == null ? "true" : "false" + billObjectList.size());
    
                if (billObjectList != null && billObjectList.size() > 0) {
                    for (AliHook.BillObject billObject : billObjectList) {
                        LogUtils.e("billObject:", billObject.toString());
                    }
                }
            }
        }

    可以看到如下日志:

    bizInNo:2018050521001004450538532688,bizStateDesc:null,bizSubType:1041,canDelete:true,bizType:TRADE,consumeFee:+0.01,consumeStatus:2,consumeTitle:信息,createDesc:周六,createTime:05-05,destinationUrl:null,gmtCreate:1525488487000,isAggregatedRec:false,memo:null,month:null,oppositeLogo:http://tfs.alipayobjects.com/images/partner/TB1GZtrXXmb81Jjme7TXXc6FpXa_160X160,oppositeMemGrade:golden,sceneId:null
    bizInNo:2018050521001004640504157182,bizStateDesc:null,bizSubType:1041,canDelete:true,bizType:TRADE,consumeFee:-0.01,consumeStatus:2,consumeTitle:小鸡蛋,createDesc:周六,createTime:05-05,destinationUrl:null,gmtCreate:1525488253000,isAggregatedRec:false,memo:null,month:null,oppositeLogo:https://gw.alipayobjects.com/zos/mwalletmng/mukPPhtdXrnqECpCXXDq.png,oppositeMemGrade:null,sceneId:null
    bizInNo:2018050521001004640504337006,bizStateDesc:null,bizSubType:1041,canDelete:true,bizType:TRADE,consumeFee:-0.01,consumeStatus:2,consumeTitle:收款,createDesc:周六,createTime:05-05,destinationUrl:null,gmtCreate:1525488220000,isAggregatedRec:false,memo:null,month:null,oppositeLogo:https://gw.alipayobjects.com/zos/mwalletmng/mukPPhtdXrnqECpCXXDq.png,oppositeMemGrade:null,sceneId:null
    bizInNo:2018050421001004640500816650,bizStateDesc:null,bizSubType:73,canDelete:true,bizType:TRADE,consumeFee:-31.40,consumeStatus:2,consumeTitle:家家乐生活超市,createDesc:周五,createTime:05-04,destinationUrl:null,gmtCreate:1525432915000,isAggregatedRec:false,memo:null,month:null,oppositeLogo:1lhro7mDT_S35zjkEc5I9AAAACMAAQQD,oppositeMemGrade:null,sceneId:null

    这不就是账单信息吗,其中"consumeFee"对应的是收账或者消费的金额,"consumeTitle"对应的是备注,"bizInNo"对应的是订单编号,"gmtCreate"对应的账单的时间戳,如果我们只想要收款信息,可以判断"consumeTitle"字段的值是否以"+"开头,

    到此,我们已经可以获得账单数据了,但是还有问题,账单数据时分页加载的,每次大概加载20条数据,要想获得全部的账单数据,我们必须要模拟下拉获得更多数据,通过查看账单页面的代码,我们最终找到下拉加载更多是通过调用下面的方法来请求更多账单数据的:

      public final void e()
      {
        this.w = false;
        if (this.v) { // 判断是否加载到底了
          d(); // 生成请求获取账单数据
        }
      }

    其中d()方法是真正的请求数据的方法:

      protected final void d()
      {
        Object localObject;
        if (this.w)
        {
          this.u.pageType = "WaitPayConsumeQuery";
          localObject = new PagingCondition();
          this.u.paging = ((PagingCondition)localObject);
        }
        if ((this.u.startTime != null) && (this.u.startTime.longValue() != 0L) && (this.u.endTime != null) && (this.u.endTime.longValue() != 0L))
        {
          this.u.needMonthSeparator = Boolean.valueOf(false);
          boolean bool = s();
          if (this.w)
          {
            if (this.J != null)
            {
              this.J.getRpcSubscriber().cancelRpc();
              this.J.getRpcSubscriber().getRpcUiProcessor().hideFlowTipViewIfShow();
              if (this.R != null) {
                this.R.getRpcSubscriber().cancelRpc();
              }
            }
            localObject = new GetBillListDataRunnable();
            o localo = new o(this, this, bool);
            RpcRunConfig localRpcRunConfig = new RpcRunConfig();
            localRpcRunConfig.showFlowTipOnEmpty = true;
            localRpcRunConfig.showNetError = true;
            localRpcRunConfig.exceptionMode = "exception_all";
            localRpcRunConfig.loadingMode = LoadingMode.SILENT;
            localRpcRunConfig.cacheKey = this.K;
            localRpcRunConfig.flowTipHolderViewId = R.id.bill_list_flow_tip;
            localRpcRunConfig.cacheType = new p(this);
            this.J = new RpcRunner(localRpcRunConfig, (RpcRunnable)localObject, localo, new QueryListResProcessor()); // 生成一个RpcRunner对象
          }
          if ((!bool) || (!this.w)) {
            break label380;
          }
          if (this.aj) {
            break label364;
          }
          this.J.getRpcRunConfig().cacheMode = CacheMode.CACHE_AND_RPC;
          label292:
          if (!bool) {
            break label410;
          }
          if ((!this.w) || (this.z == null) || (this.z.getCount() != 0)) {
            break label396;
          }
          this.J.getRpcRunConfig().showFlowTipOnEmpty = true;
        }
        for (;;)
        {
          this.J.start(new Object[] { this.u }); // 发起rpc请求,其中this.u是请求参数
          return;
          this.u.needMonthSeparator = Boolean.valueOf(true);
          break;
          label364:
          this.J.getRpcRunConfig().cacheMode = CacheMode.RPC_AND_SAVE_CACHE;
          break label292;
          label380:
          this.J.getRpcRunConfig().cacheMode = CacheMode.NONE;
          break label292;
          label396:
          this.J.getRpcRunConfig().showFlowTipOnEmpty = false;
          continue;
          label410:
          if (this.w) {
            this.J.getRpcRunConfig().showFlowTipOnEmpty = true;
          } else {
            this.J.getRpcRunConfig().showFlowTipOnEmpty = false;
          }
        }
      }

    我们在需要加载更多账单数据时只需要调用e()方法,即可加载更多账单数据

    callMethod(mBillActivity, "e");

    其中mBillActivity是账单页面对象,我们hook BillListActivity_的onCreate方法,然后记录账单Activity对象,并且在onDestory是将mBillActivity设置为null,代码如下:

            findAndHookMethod("com.alipay.mobile.bill.list.ui.BillListActivity_", mBillListActivityClassLoader, "onCreate", Bundle.class, new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    XposedLogUtils.log("com.alipay.mobile.bill.list.ui.BillListActivity_" + ":onCreated");
                    mBillActivity = (Activity) param.thisObject;
                }
            });
    
            // hook BaseActivity onDestroy方法,BaseActivity是支付宝Activity共有的父类,通过类名判断是否是账单页面被销毁,因为BillListActivity_并没有重写onDestory方法,所有是不能直接hook它的onDestory方法的
            findAndHookMethod("com.alipay.mobile.framework.app.ui.BaseActivity", classLoader, "onDestroy", new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
    
                    if (param.thisObject.getClass().getName().equals("com.alipay.mobile.bill.list.ui.BillListActivity_")) {
                        XposedLogUtils.log("com.alipay.mobile.bill.list.ui.BillListActivity_" + ":onDestroy方法");
                        mBillActivity = null;
                    }
                }
            });
    

    ok,加载更多账单数据的问题解决,在考虑一下,我们是否能够在每次获得最新的账单数据时不需要重新打开账单页面,而是模拟账单页面的下拉刷新呢?我们知道账单页面是通过APListView外面套了一个APPullRefreshView来实现下拉刷新的,打开APPullRefreshView的代码:

    public class APPullRefreshView
      extends APFrameLayout
      implements GestureDetector.OnGestureListener
    {
      private APPullRefreshView.RefreshListener d;
      // 应该是这下拉刷新的监听类    
      public void setRefreshListener(APPullRefreshView.RefreshListener paramRefreshListener)
      {
        if (this.mOverView != null) {
          removeView(this.mOverView);
        }
        this.d = paramRefreshListener;
        this.mOverView = this.d.getOverView();
        paramRefreshListener = new FrameLayout.LayoutParams(-1, -2);
        addView(this.mOverView, 0, paramRefreshListener);
        getViewTreeObserver().addOnGlobalLayoutListener(new APPullRefreshView.1(this));
      }
    }

    在看一下APPullRefreshView.RefreshListener的代码:

    package com.alipay.mobile.commonui.widget;
    
    public abstract interface APPullRefreshView$RefreshListener
    {
      public abstract boolean canRefresh();
      
      public abstract APOverView getOverView();
      
      public abstract void onRefresh();
    }

    可以看到APPullRefreshView.RefreshListener中有一个onRefresh()方法,猜想这个应该是控件下拉刷新的回调方法,在具体的下拉刷新监听类中肯定要去实现该方法,完成下拉刷新的回调。现在我们可以获得账单页面的APPullRefreshView对象,然后又可以通过反射获得其APPullRefreshView.RefreshListener对象,那么我们直接调用其onRefresh方法,不就可以模拟下拉刷新了吗?代码如下:

        Field aPPullRefreshViewFiled = XposedHelpers.findField(mBillActivity.getClass().getSuperclass(), "g"); // 获得账单页面APPullRefreshView对应的Field
        final Object aPPullRefreshView = aPPullRefreshViewFiled.get(mBillActivity); // 获得账单页面APPullRefreshView对象
        Field refreshListenerField = XposedHelpers.findField(findClass(AliParamUtils.mAPPullRefreshViewClassFullName, mClassLoader), "d"); // 获得账单页面APPullRefreshView.RefreshListener对应的Field
        final Object refreshListener = refreshListenerField.get(aPPullRefreshView); // 获得账单页面APPullRefreshView.RefreshListener对象
        callMethod(refreshListener, "onRefresh"); // 调用账单页面APPullRefreshView.RefreshListener对象的onRefresh方法,模拟下拉刷新

    到这里,已经可以通过调用下拉刷新的回调方法获得最新的账单数据,通过调用上拉加载更多的回调方法获得更多的账单数据,然而,支付宝每次请求账单数据时,大概只会请求20条左右的数据,如果账单数据量比较大,那么就需要发送多次网络请求才能获得全部账单数据。我们知道支付宝是通过Rpc来进行网络传输的,而要发起网络请求,会调用RpcRunner的start方法:

      public void start(Object... paramVarArgs)
      {
        if ((this.rpcTask != null) && (paramVarArgs != null)) {
          this.rpcTask.setParams(paramVarArgs);
        }
        start(this.rpcTask);
      }

    其中"Object... paramVarArgs"是请求参数,例如请求账单数据调用的代码如下:

    this.J.start(new Object[] { this.u });

    其中this.J是一个RpcRunner对象,this.u是账单页面的请求参数,定义如下:

    private QueryListReq u;
    package com.alipay.mobilebill.common.service.model.pb;
    
    import com.squareup.wire.Message;
    import com.squareup.wire.Message.Datatype;
    import com.squareup.wire.Message.Label;
    import com.squareup.wire.ProtoField;
    import java.util.Collections;
    import java.util.List;
    
    public final class QueryListReq
      extends Message
    {
      @ProtoField(tag=28, type=Message.Datatype.INT64)
      public Long asyncQueryTaskId;
      @ProtoField(tag=27, type=Message.Datatype.STRING)
      public String batchTagId;
      @ProtoField(tag=24, type=Message.Datatype.STRING)
      public String billMonthCategoryId;
      @ProtoField(tag=25, type=Message.Datatype.STRING)
      public String billMonthSubCategoryId;
      @ProtoField(tag=14, type=Message.Datatype.STRING)
      public String bizState;
      @ProtoField(tag=8, type=Message.Datatype.STRING)
      public String bizSubType;
      @ProtoField(tag=7, type=Message.Datatype.STRING)
      public String bizType;
      @ProtoField(tag=1, type=Message.Datatype.STRING)
      public String category;
      @ProtoField(tag=20, type=Message.Datatype.STRING)
      public String categoryId;
      @ProtoField(tag=23, type=Message.Datatype.STRING)
      public String ceilAmount;
      @ProtoField(tag=15, type=Message.Datatype.STRING)
      public String consumeStatus;
      @ProtoField(tag=10, type=Message.Datatype.STRING)
      public String date;
      @ProtoField(tag=29, type=Message.Datatype.STRING)
      public String dateType;
      @ProtoField(tag=18, type=Message.Datatype.INT64)
      public Long endTime;
      @ProtoField(tag=16, type=Message.Datatype.STRING)
      public String extReq;
      @ProtoField(tag=26, type=Message.Datatype.STRING)
      public String extraFilter;
      @ProtoField(tag=22, type=Message.Datatype.STRING)
      public String floorAmount;
      @ProtoField(tag=11, type=Message.Datatype.STRING)
      public String inout;
      @ProtoField(tag=6, type=Message.Datatype.STRING)
      public String month;
      @ProtoField(tag=5, type=Message.Datatype.BOOL)
      public Boolean needMonthSeparator;
      @ProtoField(tag=30, type=Message.Datatype.STRING)
      public String oldCategoryName;
      @ProtoField(tag=13, type=Message.Datatype.STRING)
      public String oppositeCardNo;
      @ProtoField(tag=2, type=Message.Datatype.STRING)
      public String pageType;
      @ProtoField(tag=3)
      public PagingCondition paging; // 配置当前页信息,比如页面条数,是否有下一页等
      @ProtoField(tag=12, type=Message.Datatype.STRING)
      public String product;
      @ProtoField(tag=9, type=Message.Datatype.STRING)
      public String scene;
      @ProtoField(tag=4, type=Message.Datatype.STRING)
      public String searchKeyWords;
      @ProtoField(tag=17, type=Message.Datatype.INT64)
      public Long startTime;
      @ProtoField(tag=21, type=Message.Datatype.STRING)
      public String subCategoryId;
      @ProtoField(label=Message.Label.REPEATED, tag=19, type=Message.Datatype.STRING)
      public List<String> tagIdList;
    }
    
    
    public final class PagingCondition
      extends Message
    {
      @ProtoField(tag=7, type=Message.Datatype.STRING)
      public String defQueryEndTime;
      @ProtoField(tag=4, type=Message.Datatype.BOOL)
      public Boolean hasNextPage;
      @ProtoField(tag=5, type=Message.Datatype.INT32)
      public Integer listQueryTime;
      @ProtoField(tag=6, type=Message.Datatype.INT32)
      public Integer nextPageDate;
      @ProtoField(tag=3, type=Message.Datatype.INT32)
      public Integer nextPageMonth;
      @ProtoField(tag=2, type=Message.Datatype.INT32)
      public Integer nextPageNum;
      @ProtoField(tag=1, type=Message.Datatype.INT32)
      public Integer pageSize; // 每页请求的数据条数
    }

    我们只关注当前页的请求数据条数,通过日志打印我们知道QueryListReq.paging.pageSize配置的就是当前页请求的数据条数,我们只需要在发起请求前修改这个参数,就可以修改每页请求的数据条数了。那么怎样修改呢?可以考虑hook RpcRunner的start方法,判断参数是否是QueryListReq类型,如果是,则表示是在请求账单数据,然后修改参数中的pageSize,最后再发起请求,即可达到修改参数的目的,代码如下:

           // hook rpc执行请求方法,可以在这里修改请求参数
            findAndHookMethod(AliParamUtils.mRpcRunnerFullClassName, classLoader, "start", Object[].class, new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    if (param.args[0] != null) {
                        XposedLogUtils.log(param.args[0].toString());
                        XposedLogUtils.log(param.args[0].getClass().getName());
                        if (param.args[0] instanceof Object[]) {
                            Object[] objects = (Object[]) param.args[0];
                            if (objects.length > 0) {
                                Object queryListReq = objects[0];
                                if (queryListReq != null && queryListReq.getClass().getName().contentEquals("com.alipay.mobilebill.common.service.model.pb.QueryListReq")) {
                                    Object queryListReqPaging = XposedHelpers.getObjectField(queryListReq, "paging");
                                    if (queryListReqPaging != null) {
                                        XposedHelpers.setObjectField(queryListReqPaging, "pageSize", 200); // 修改账单请求参数,自定义每次请求数据
                                    }
                                }
                            }
                        }
                    }
                    super.beforeHookedMethod(param);
                }
            });

    上面将每页的请求数据修改为200条,经过测试是没有问题的,成功一次请求到了200条账单数据,但当将其修改为1000条时,提示获得账单数据失败,估计支付宝后台有限制,毕竟1000条账单数据量还是挺大的,至于最大可以设置为多少有兴趣可以自己测试。到此,我们实现了可以动态的修改每次账单数据的请求条数。

    另外一种情况是,当没有账单数据时,是不会调用BillListAdapter的"a"方法来添加数据,这个时候我们就需要hook其他的方法,来判断是否有账单数据,通过观察,我们找到了账单页面的在收到账单数据的回调方法,如下:

     @UiThread
      protected void a(QueryListRes paramQueryListRes, boolean paramBoolean);

    也就是说,只要账单数据返回来了,一定是会调用该方法的,我们可以在这个方法中判断账单数据是否为空,并且判断账单数据是否加载完成:

            Method method = billActivityClass.getDeclaredMethod("a", findClass("com.alipay.mobilebill.common.service.model.pb.QueryListRes", mClassLoader), boolean.class);
    
            if (method != null) {
                XposedBridge.hookMethod(method, new XC_MethodHook() {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                        XposedLogUtils.log(AliParamUtils.mBillListActivityClassFullName + ":called a");
                        Activity activity = (Activity) param.thisObject;
                        if (activity != null && mLastTimestamp > 0) {
                            int noRecordId = FindResourceIdUtils.getFieldId("com.alipay.mobile.bill.list", "id", "bill_list_flow_tip", mBillListActivityClassLoader);
                            View noRecordView = activity.findViewById(noRecordId);
                            if (noRecordView != null && noRecordView.getVisibility() == View.VISIBLE) {
                                XposedLogUtils.log("------" + "用户账单数据为空" + "-----------");
                            } else {
                                boolean isCanLoadMore = (boolean) callMethod(param.thisObject, AliParamUtils.mBillListCanLoadMoreMethodName);
                                boolean isFromCache = (boolean) param.args[1];
                                XposedLogUtils.log("------" + isCanLoadMore + " " + isFromCache + "-----------");
                                if (!(isCanLoadMore || isFromCache)) {
                                    XposedLogUtils.log("------没有更多数据了,到底了");
                                }
                            }
    
                        }
                    }
                });
            }
    

    通过查找账单页面是否存在账单为空的view来判断账单数据是否为空。

    六、优化设置金额页面

    现在每次获得二维码链接时,都必须打开和关闭一次设置金额页面,这个对速度是有影响的,那么我们是否能够只打开一次设置金额页面,获得二维码链接后不关闭页面呢?下面来分析一下设置金额页面在获得服务端返回的二维码链接数据后的回调方法:

    public class PayeeQRSetMoneyActivity
      extends BaseActivity
    
      protected final void a(ConsultSetAmountRes paramConsultSetAmountRes)
      {
        runOnUiThread(new ct(this, paramConsultSetAmountRes));
      }
    }
    final class ct
      implements Runnable
    {
      ct(PayeeQRSetMoneyActivity paramPayeeQRSetMoneyActivity, ConsultSetAmountRes paramConsultSetAmountRes) {}
      
      public final void run()
      {
        PayeeQRSetMoneyActivity.a.b("call processConsultSetAmountRes(), ConsultSetAmountRes = " + this.a);
        if (this.a != null)
        {
          if (!this.a.success) {
            break label140;
          }
          Intent localIntent = new Intent();
          localIntent.putExtra("codeId", this.a.codeId);
          localIntent.putExtra("qr_money", this.b.g);
          localIntent.putExtra("beiZhu", this.b.c.getInputedText());
          localIntent.putExtra("qrCodeUrl", this.a.qrCodeUrl);
          localIntent.putExtra("qrCodeUrlOffline", this.a.printQrCodeUrl);
          this.b.setResult(-1, localIntent); // 设置数据到intent,回传给收款页面生成对应的二维码
          this.b.finish(); // 关闭设置金额页面
        }
        for (;;)
        {
          return;
          label140:
          this.b.alert("", this.a.message, this.b.getString(R.string.payee_confirm), null, null, null);
        }
      }
    }

    通过上面的代码我们知道,在收款二维码的回调方法中,会在UI线程关闭当前页面,并将数据传递给收款页面,我们只需要替换掉a方法,就可以不关闭设置金额页面了,代码如下:

            // hook获得二维码url的回调方法(替换方法 )
            findAndHookMethod(findClass("com.alipay.mobile.payee.ui.PayeeQRSetMoneyActivity", classLoader), "a", findClass("com.alipay.transferprod.rpc.result.ConsultSetAmountRes", classLoader), new XC_MethodReplacement() {
                @Override
                protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {
                    Object consultSetAmountRes = methodHookParam.args[0];
                    String consultSetAmountResString = "";
                    if (consultSetAmountRes != null) {
                        consultSetAmountResString = (String) callMethod(consultSetAmountRes, "toString");
                    }
                    // 获得返回数据后发送广播将数据传给支付宝插件程序
                    Activity activity = (Activity) methodHookParam.thisObject;
                    sendQrLinkBroadCast(activity, consultSetAmountResString, mJinEr, mBeiZhu);
                    return null;
                }
            });

    七、自动登录

    从服务端获得账户名密码后,我们需要调用支付宝的方法来实现自动登录,首先用hierarchy view看一下登录页面:

    首先可以看到登录页面名称为"com.alipay.mobile.security.login.ui.AlipayUserLoginActivity",在新的支付宝版本,添加了刷脸登录功能,如果之前登录过支付宝,它会让最近登录的账号选择是刷脸登录还是密码登录,选择密码登录就会显示密码登录的布局,注意,这个时候并没有跳转到另外的Activity,只是改变了界面的布局而已,看一下密码登录界面的布局:

    用hierarchy view看一下布局结构:

    可以看到账号和密码的输入框对应的类型是AUInputBox类,账号的id为"userAccountInput",密码的id为"userPasswordInput",我们可以通过findViewById找到这两个控件,再打开AUInputBox类,看一下其有没有设置内容的方法,通过查找,发现有如下方法:

      public void setText(CharSequence paramCharSequence)
      {
        this.mInputEditText.setText(paramCharSequence);
        paramCharSequence = this.mInputEditText.getSafeText();
        if ((paramCharSequence instanceof Spannable)) {
          Selection.setSelection((Spannable)paramCharSequence, paramCharSequence.length());
        }
      }

    猜想这个应该就是设置输入框内容的方法。

    再看一下选择登录方式页面,如果需要选择登录方式,我们首先要选择密码登录切换到密码登录布局,然后才能设置账号和密码输入框的内容,最后再找到登录,调用其performClick模拟点击登录按钮,具体代码如下:

        // hook 支付宝登陆界面的onCreate方法,获得主界面对象并注册广播
        findAndHookMethod("com.alipay.mobile.security.login.ui.AlipayUserLoginActivity", mClassLoader, "onCreate", Bundle.class, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                XposedLogUtils.log("com.alipay.mobile.security.login.ui.AlipayUserLoginActivity" + ":onCreated方法");
                mLoginActivity = (Activity) param.thisObject;
                Intent intent = ((Activity) param.thisObject).getIntent();
                if (intent != null) {
                    String account = intent.getStringExtra("account");
                    String password = intent.getStringExtra("password");
                    XposedLogUtils.log("account:" + account);
                    XposedLogUtils.log("password:" + password);
                    executeLogin(account, password); // 执行模拟登录
                }
            }
        });
    
        // hook 支付宝的登陆界面的onDestory方法,将保持的登录页面对象设置为空
        findAndHookMethod("com.alipay.mobile.security.login.ui.AlipayUserLoginActivity", mClassLoader, "onDestroy", new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                XposedLogUtils.log("com.alipay.mobile.security.login.ui.AlipayUserLoginActivity" + ":onDestroy方法");
                mLoginActivity = null;
            }
        });
    
    
        /**
         * 模拟登录
         *
         * @param account
         * @param password
         */
        public static void startLogin(String account, String password) {
            XposedLogUtils.log("mLauncherActivity :" + mLauncherActivity);
            if (mLauncherActivity != null) {
                if (mLoginActivity != null) {  // 如果登录界面已经启动则直接在当前登录界面模拟登录
                    executeLogin(account, password);
                } else { // 如果登录页面没有启动则先启动登录页面
                    XposedLogUtils.log("startLogin" + " " + account + " " + password);
                    Class alipayUserLoginActivityClass = XposedHelpers.findClass(AliParamUtils.mAlipayUserLoginActivityClassFullName, mClassLoader);
                    Intent launcherIntent;
                    launcherIntent = new Intent(mLauncherActivity, alipayUserLoginActivityClass);
                    launcherIntent.putExtra("account", account);
                    launcherIntent.putExtra("password", password);
                    mLauncherActivity.startActivity(launcherIntent);
                }
            }
        }
    
    
        /**
         * 模拟登录
         *
         * @param account
         * @param password
         */
        public static void executeLogin(String account, String password) {
            if (mLoginActivity != null && !TextUtils.isEmpty(account) && !TextUtils.isEmpty(password)) {
                XposedLogUtils.log("executeLogin" + " " + account + " " + password);
                mAccount = account;
                mPassword = password;
                View switchToPasswordLogin = mLoginActivity.findViewById(FindResourceIdUtils.getFieldId("com.ali.user.mobile.security.ui", "id", "switchToPasswordLogin", mClassLoader)); // 找到切换到密码登录按钮
                if (switchToPasswordLogin != null) { // 要判断该按钮是否存在,存在则需要模拟点击切换到密码登录
                    switchToPasswordLogin.performClick();
                    switchToPasswordLogin.postDelayed(new Runnable() { // 这里最好做一下延时处理,因为后面的密码登录布局是动态添加到布局中的,延时一下能够确保密码登录布局已经加载
                        @Override
                        public void run() {
                            executeLogin();
                        }
                    }, 200);
                } else {
                    executeLogin(); // 如果找不到切换到密码登录按钮则说明当前密码登录布局已经加载了
                }
            }
        }
    
        /**
         * 自动登录
         */
        private static void executeLogin() {
            View userAccountInput = mLoginActivity.findViewById(FindResourceIdUtils.getFieldId("com.ali.user.mobile.security.ui", "id", "userAccountInput", mClassLoader));
            View userPasswordInput = mLoginActivity.findViewById(FindResourceIdUtils.getFieldId("com.ali.user.mobile.security.ui", "id", "userPasswordInput", mClassLoader));
            XposedHelpers.callMethod(userAccountInput, "setText", mAccount);
            XposedHelpers.callMethod(userPasswordInput, "setText", mPassword);
            XposedHelpers.callMethod(userAccountInput, "setText", mAccount); // 要在设置一遍,否则账号数据无法设置上去,可能是账号输入框上保留了上一次的登录账号导致的,具体原因还不知道
            View loginButton = mLoginActivity.findViewById(FindResourceIdUtils.getFieldId("com.ali.user.mobile.security.ui", "id", "loginButton", mClassLoader));
            loginButton.performClick();
        }

    上面就是所有的自动登录的代码,其中在查找控件id的时候用到了一个辅助类,定义如下:

    public class FindResourceIdUtils {
        /**
         * 根据给定的类型名和字段名,返回R文件中的字段的值
         */
        public static int getFieldId(String rPackageName, String typeName, String fieldName, ClassLoader classLoader){
            int i = -1;
            try {
                Class<?> clazz = XposedHelpers.findClass(rPackageName + ".R$" + typeName, classLoader);
                i = clazz.getField(fieldName).getInt(null);
            } catch (Exception e) {
                log("没有找到"+  rPackageName +".R$" + typeName + "类型资源 " + fieldName + "请copy相应文件到对应的目录.");
                return -1;
            }
            return i;
        }
    }

    八、模拟退出

    在收到服务器退出当前账号的消息后,需要调用支付宝的方法退出登录,一样的,首先看一下支付宝自己的退出方式,在设置界面的最底部有一个退出登录按钮,如下:

    用hierarchy view看一下布局结构:

    可以看到,设置页面对应的Class名称为UserSettingActivity_,退出登录控件的id为logout,打开UserSettingActivity_类,可以找到如下代码:

    public final class UserSettingActivity_
      extends UserSettingActivity
      implements HasViews, OnViewChangedListener
    {
      private final OnViewChangedNotifier i = new OnViewChangedNotifier();
      
      public final void onViewChanged(HasViews paramHasViews)
      {
        this.e = ((UserContainer)paramHasViews.findViewById(R.id.user_container));
        this.f = paramHasViews.findViewById(R.id.logout); // 找到退出登录按钮
        if (this.f != null) {
          this.f.setOnClickListener(new h(this)); // 设置点击监听
        }
        ...
      }
    }

    可以看到退出登录按钮的点击监听类为"h"类,"h"类定义如下:

    package com.alipay.android.phone.home.user.ui;
    
    import android.view.View;
    import android.view.View.OnClickListener;
    
    final class h
      implements View.OnClickListener
    {
      h(UserSettingActivity_ paramUserSettingActivity_) {}
      
      public final void onClick(View paramView)
      {
        this.a.a();
      }
    }

    点开this.a.a()方法:

      @Click(resName={"logout"})
      protected final void a()
      {
        try
        {
          ((LogoutService)this.mApp.getMicroApplicationContext().getExtServiceByInterface(LogoutService.class.getName())).showChangeAccountDialog(this);
          SpmLogUtil.p();
          return;
        }
        catch (Exception localException)
        {
          for (;;)
          {
            LoggerFactory.getTraceLogger().error(this.i, localException.toString());
          }
        }
      }

    大致的意思是调用了退出登录的服务的显示切换登录账号的弹框,注意,我们在点击退出登录的时候是不会直接退出登录的,而是会显示一个弹框,如下:

    再点击退出登录才能真正的退出登录,试想一下,既然支付宝有提供退出登录的服务,那么退出登录的功能应该也是在该服务中实现的,点击showChangeAccountDialog方法,发现打开了一个抽象类,定义如下:

    package com.alipay.mobile.framework.service.ext.security;
    
    import android.app.Activity;
    import android.content.Intent;
    import com.alipay.mobile.framework.service.ext.ExternalService;
    import com.alipay.mobile.framework.service.ext.security.bean.UserInfo;
    
    public abstract class LogoutService
      extends ExternalService
    {
      public static final String TYPE_DEVICE_LOCK = "SingleDevice";
      public static final String TYPE_LOGOUT = "Logout";
      public static final String TYPE_NO_TOKEN = "LogoutNoToken";
      
      public abstract void localLogout(String paramString);
      
      public abstract void logout();
      
      public abstract void showChangeAccountDialog(Activity paramActivity);
      
      public abstract void syncLogout(String paramString, UserInfo paramUserInfo, Intent paramIntent);
    }
    

    其中有一个logout()方法,猜想真正的退出登录应该就是调用的该方法,现在我们只需获得退出登录的服务,并且调用其logout方法,即可完成退出登录,看上面的代码,是通过如下代码获得退出登录服务的:

    this.mApp.getMicroApplicationContext().getExtServiceByInterface(LogoutService.class.getName()))

    先要获得App对象,然后调用其getMicroApplicationContext()方法获得一个MicroApplicationContext对象,在调用MicroApplicationContext对象的getExtServiceByInterface方法,传入服务的类名即可获得对应的服务,而这里的mApp对象已经在界面中初始化好了,通过查看代码,我们有其他的方式获得App对象,通过调用AlipayApplication的getInstance方法,可以获得一个App对象的实例,然后就可以获得退出登录的服务了,最后在调用其logout方法即可实现退出登录:

        /**
         * 退出登录
         */
        public static void startLogout() {
            XposedLogUtils.log("startLogout");
            Object alipayApplication = callStaticMethod(findClass("com.alipay.mobile.framework.AlipayApplication", mLoadPackageParam.classLoader), "getInstance");
            Object microApplicationContext = callMethod(alipayApplication, "getMicroApplicationContext");
            Object logoutService = callMethod(microApplicationContext, "getExtServiceByInterface", "com.alipay.mobile.framework.service.ext.security.LogoutService");
            callMethod(logoutService, "logout");
        }

    九、获得用户信息

    想要获得的用户信息包括当前登录用户的账号,头像,登录token等,首先我们在支付宝"我的"页面"是可以看到当前登录用户的账号,头像等信息的,如下:

    用hierarchy view看一下布局结构:

    发现这一块信息是在一个叫"AccountInfoView"的控件中,在反编译代码中搜索AccountInfoView类,发现有如下代码:

     public void refreshUserData()
      {
        Object localObject = UserInfoCacher.a().a;
        ...
      }

    而用户的信息都是从这个localObject中获取到的,点开"UserInfoCacher.a().a"的定义,如下:

    package com.alipay.android.widgets.asset.utils;
    
    import com.alipay.mobile.common.helper.UserInfoHelper;
    import com.alipay.mobile.framework.AlipayApplication;
    import com.alipay.mobile.framework.MicroApplicationContext;
    import com.alipay.mobile.framework.service.ext.security.bean.UserInfo;
    
    public class UserInfoCacher
    {
      private static UserInfoCacher b;
      public UserInfo a;
      private MicroApplicationContext c = AlipayApplication.getInstance().getMicroApplicationContext();
      
      public static UserInfoCacher a()
      {
        try
        {
          if (b == null)
          {
            localUserInfoCacher = new com/alipay/android/widgets/asset/utils/UserInfoCacher;
            localUserInfoCacher.<init>();
            b = localUserInfoCacher;
          }
          UserInfoCacher localUserInfoCacher = b;
          return localUserInfoCacher;
        }
        finally {}
      }
      
      public final void b()
      {
        this.a = UserInfoHelper.getInstance().getUserInfo(this.c);
      }
    }
    

    可以看到对象"a"的类型为UserInfo,猜想其应该是对应的用户信息类,而UserInfoCacher类应该是用户信息的缓存类,打开UserInfo类,定义如下:

    package com.alipay.mobile.framework.service.ext.security.bean;
    
    import android.os.Parcel;
    import android.os.Parcelable;
    import android.os.Parcelable.Creator;
    import android.text.TextUtils;
    import com.ali.user.mobile.utils.StringUtil;
    import com.alipay.mobile.security.util.AuthUtil;
    import com.j256.ormlite.field.DatabaseField;
    import java.io.Serializable;
    
    public class UserInfo
      implements Parcelable, Serializable, Cloneable
    {
      public static final Parcelable.Creator<UserInfo> CREATOR = new a();
      public static final String GENDER_FEMALE = "f";
      public static final String GENDER_MALE = "m";
      private static final long serialVersionUID = 1L;
      @DatabaseField
      private boolean autoLogin;
      @DatabaseField
      private String colorStr;
      @DatabaseField
      private String customerType;
      @DatabaseField
      private String extern_token;
      private String fingerprintAuthInfo;
      @DatabaseField
      private String gender;
      @DatabaseField
      private String gestureAppearMode;
      @DatabaseField
      private String gestureErrorNum;
      @DatabaseField
      private boolean gestureOrbitHide;
      @DatabaseField
      private String gesturePwd;
      @DatabaseField
      private boolean gestureSkip = false;
      @DatabaseField
      private String gestureSkipStr;
      @DatabaseField
      private String havanaId;
      @DatabaseField
      private boolean isBindCard;
      @DatabaseField
      private String isCertified;
      @DatabaseField
      private boolean isNewUser;
      @DatabaseField
      private boolean isShowWalletEditionSwitch = false;
      @DatabaseField
      private boolean isWirelessUser;
      @DatabaseField
      private String loginEmail;
      @DatabaseField
      private String loginMobile;
      @DatabaseField
      private String loginTime;
      @DatabaseField
      private String loginToken;
      @DatabaseField
      private String logonId;
      @DatabaseField
      private String memberGrade;
      @DatabaseField
      private String mobileNumber;
      @DatabaseField
      private String nick;
      @DatabaseField
      private boolean noPayPwd;
      @DatabaseField
      private String noQueryPwdUser;
      @DatabaseField
      private String otherLoginId;
      @DatabaseField
      private String realName;
      @DatabaseField
      private String realNamed;
      @DatabaseField
      private String sessionId;
      @DatabaseField
      private String shippingAddressCount;
      @DatabaseField
      private String studentCertify;
      @DatabaseField
      private String taobaoNick;
      @DatabaseField
      private String taobaoSid;
      @DatabaseField
      private String userAvatar;
      @DatabaseField(id=true)
      private String userId;
      @DatabaseField
      private String userName;
      @DatabaseField
      private String userType;
      @DatabaseField
      private String walletEdition;
      ...
    }

    在其中可以看到很多用户信息的定义,比如用户名称userName,用户登录账号logonId,用户登录token对应的是loginToken,还有其他的比如用户头像url对应的是userAvatar,我们可以模拟支付宝获得用户信息的代码来获得用户信息,代码如下:

        /**
         * @return
         */
        public static UserInfo getUserInfo() {
            UserInfo userInfo = new UserInfo();
            // 获得用户信息
            if (mLoadPackageParam != null) {
                XposedLogUtils.log("getUserInfo");
                Object userInfoCacherObject = callStaticMethod(findClass("com.alipay.android.widgets.asset.utils.UserInfoCacher", mClassLoader), "a");
                callMethod(userInfoCacherObject, "b");
                Field userInfoField = findField(findClass("com.alipay.android.widgets.asset.utils.UserInfoCacher", mClassLoader), "a");
                try {
                    Object userInfoObject = userInfoField.get(userInfoCacherObject);
                    if (userInfoCacherObject != null) {
                        userInfo.userName = getStringField(userInfoObject, "userName");
                        userInfo.userAvatar = getStringField(userInfoObject, "userAvatar");
                        userInfo.userId = getStringField(userInfoObject, "userId");
                        userInfo.nick = getStringField(userInfoObject, "nick");
                        userInfo.loginToken = getStringField(userInfoObject, "loginToken");
                        userInfo.loginEmail = getStringField(userInfoObject, "loginEmail");
                        userInfo.loginMobile = getStringField(userInfoObject, "loginMobile");
                        userInfo.mobileNumber = getStringField(userInfoObject, "mobileNumber");
                        userInfo.userType = getStringField(userInfoObject, "userType");
                        userInfo.taobaoNick = getStringField(userInfoObject, "taobaoNick");
                        userInfo.customerType = getStringField(userInfoObject, "customerType");
                        userInfo.logonId = getStringField(userInfoObject, "logonId");
                        XposedLogUtils.log(userInfo.toString());
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
            return userInfo;
        }
    
    
        private static String getStringField(final Object obj, final String fieldName) throws IllegalAccessException {
            Field sField = XposedHelpers.findField(obj.getClass(), fieldName);
            if (sField == null) {
                return null;
            }
            return String.valueOf(sField.get(obj));
        }
    

    其中UserInfo类是我们自己定义的用来保存需要的用户信息类,定义如下:

        /**
         * 用户信息类
         */
        public static class UserInfo implements Parcelable{
            public String nick; // 昵称
            public String userName; // 用户名
            public String userId; // 用户id
            public String loginToken; // 登录token
            public String userAvatar; // 头像
            public String loginEmail;
            public String loginMobile;
            public String mobileNumber;
            public String userType;
            public String taobaoNick;
            public String customerType;
            public String logonId; // 登录账号
    
            public UserInfo() {
    
            }
    
            protected UserInfo(Parcel in) {
                nick = in.readString();
                userName = in.readString();
                userId = in.readString();
                loginToken = in.readString();
                userAvatar = in.readString();
                loginEmail = in.readString();
                loginMobile = in.readString();
                mobileNumber = in.readString();
                userType = in.readString();
                taobaoNick = in.readString();
                customerType = in.readString();
                logonId = in.readString();
            }
    
            public static final Creator<UserInfo> CREATOR = new Creator<UserInfo>() {
                @Override
                public UserInfo createFromParcel(Parcel in) {
                    return new UserInfo(in);
                }
    
                @Override
                public UserInfo[] newArray(int size) {
                    return new UserInfo[size];
                }
            };
    
            @Override
            public String toString() {
                return "nick:" + nick + "," +
                        "userName:" + userName + "," +
                        "userId:" + userId + "," +
                        "loginToken:" + loginToken + "," +
                        "loginEmail:" + loginEmail + "," +
                        "loginMobile:" + loginMobile + "," +
                        "mobileNumber:" + mobileNumber + "," +
                        "userType:" + userType + "," +
                        "taobaoNick:" + taobaoNick + "," +
                        "customerType:" + customerType + "," +
                        "userAvatar:" + userAvatar + "," +
                        "logonId:" + logonId + "\n";
            }
    
            @Override
            public int describeContents() {
                return 0;
            }
    
            @Override
            public void writeToParcel(Parcel dest, int flags) {
                dest.writeString(nick);
                dest.writeString(userName);
                dest.writeString(userId);
                dest.writeString(loginToken);
                dest.writeString(userAvatar);
                dest.writeString(loginEmail);
                dest.writeString(loginMobile);
                dest.writeString(mobileNumber);
                dest.writeString(userType);
                dest.writeString(taobaoNick);
                dest.writeString(customerType);
                dest.writeString(logonId);
            }
        }

    注意,因为这里获得用户信息是从支付宝缓存中获得的,在退出状态时,登录账号、用户名称等信息还是保存的上一次登录的用户信息,如果要判断当前是否有用户登录,可以直接判断loginToken是否为空,当在退出登录状态下时,loginToken是为空的,而登录状态下loginToken是不为空的。

     

    十、总结

    1、进程直接的通信有很多种方式,比如Binder,Socket,BroadcastReceiver等,后面需要详细了解一下。这里我们使用广播的方式实现进程间的通信,有两个原因,第一是因为广播简单;第二是因为可以动态注册,不需要在AndroidManifest.xml中注册,而使用Binder的方式需要注册Service,而Service又只能在AndroidManifest.xml文件中注册,但是我们是无法修改支付宝的AndroidManifest.xml文件的;

    2、支付宝有反hook机制,可以利用xposed hook住支付宝的hook检测方法,修改返回参数来解决;

    3、支付宝的收款记录页面是一个h5页面,这个时候可以通过抓包工具来拦截网络请求,分析请求信息,然后模拟请求来获得收款记录(没有备注信息);

    4、支付宝的账单页面代码在动态库中,在第一次启动账单页面时才会去加载,它对应的ClassLoader和支付宝主程序的ClassLoader不是同一个ClassLoader对象,我们可以找到启动账单页面的代码,在打开支付宝主界面后直接启动账单页面,然后hook支付宝加载动态库时创建的BundleClassLoader对象的构造方法,通过判断动态库名称来确认是否是账单页面的动态库,这样在支付宝打开主界面后自动获得账单页面对应的ClassLoader了;

    5、支付宝的动态库在lib目录下,都是以so文件结尾,但是其实它是假的so库,应该属于apk包之类的,将so文件重命名为.zip文件,解压之后通过反编译就可以看到对应的代码;

    6、利用xposed hook有很多技巧,下面举例说明:

    a、当查看代码分析不清楚流程时,可以尝试将对应类的全部方法都hook住,然后打印方法的调用流程,这样就能知道对应操作的方法调用流程;

    b、当实在找不到对应的反编译代码时,可以通过反射将对应类的所有字段(包括字段名称和类型)、方法(包括方法名称、传入参数、返回参数)都打印出来,这样对类的结构能有一个大致的印象(之前一直以为账单动态库是so库,无法反编译,就是通过这种方式分析账单页面的)

    c、当一个问题正面不好突破时,可以尝试反向思维,比如我们最开始通过hook Activity的onCreate方法,然后判断类名的方式获得了账单页面的对象,并且知道了其动态加载库的位置和其ClassLoader的类型

    d、hierarchy view是一个很好的查看布局的工具,我们只有对界面的布局有所了解之后,才能更好的找到突破点

    e、在分析反编译代码时,全局搜索觉得是一个好办法,可以在搜索内容中找到很多相关的信息;

    e、最后一点,一定要大胆猜想和尝试,通过不断的打印日志来验证自己的猜想,这样才能找到对应的解决办法。

    最后附上源码地址:https://github.com/2449983723/alipay-master

    (源码未更新,只有最基本的获得二维码链接功能)

     

     

    严重声明

    本文的意图只有一个就是通过分析app学习更多的逆向技术,如果有人利用本文知识和技术进行非法操作进行牟利,带来的任何法律责任都将由操作者本人承担,和本文作者无任何关系,最终还是希望大家能够秉着学习的心态阅读此文。

     


     

     

     

     

     

     


     

     

     

          

    展开全文
  • 一、说明  现在的App一般都会带有支付功能,而现在比较流行的支付...利用一些特殊的手段获得收款二维码以及收款记录,这样就可以绕过支付平台完成支付过程了,本篇文章的目的就是分析如何完成这样一个流程,本文...

    一、说明

            现在的App一般都会带有支付功能,而现在比较流行的支付一般有支付宝、微信、银行卡等,一般情况下,应用开发者会直接对接支付宝、微信或者第三方支付公司的Api,以完成支付,但是都需要收取不小的费率,于是,有的第三方支付平台就想到了钻空子的方法,利用一些特殊的手段获得收款二维码以及收款记录,这样就可以绕过支付平台完成支付过程了,本篇文章的目的就是分析如何完成这样一个流程,本文的意图只有一个就是通过分析app学习更多的逆向技术,如果有人利用本文知识和技术进行非法操作进行牟利,带来的任何法律责任都将由操作者本人承担,和本文作者无任何关系,最终还是希望大家能够秉着学习的心态阅读此文,支付宝的相关文章可以参考:https://blog.csdn.net/xiao_nian/article/details/79881274,这篇文章是获得支付宝的个人收款二维码和账单信息,而我们现在是要获得微信的个人收款二维码,和用户的收款记录。本篇文章只分析hook 部分的代码。

    二、问题分析

    1、微信的二维码收款

    我用的微信版本是6.6.2

    获得个人收钱二维码的流程如下:

    打开微信主界面--》点击右上角的收付款---》进入到收付款界面--》点击二维码收款--》进入二维码收款页面--》点击设置金额--》进入设置金额界面--》设置金额和备注--》点击确定--》返回二维码收钱界面并刷新收钱二维码

    二维码收款界面如下:

    设置金额页面如下:

    点击收款小账本进入收款小账本页面,再点击收款记录即可进入收款记录界面:

     

    三、反编译微信并分析

    反编译应用的方法可以参考:https://blog.csdn.net/xiao_nian/article/details/79391417,这篇文章介绍了如何反编译微信的apk。

    1、hook微信主界面注册广播

    类似支付宝的过程,我们首先需要在微信主界面创建时注册广播并保存其实例,并在其销毁时销毁广播并清空对其实例的引用

            // hook 微信主界面的onCreate方法,获得主界面对象并注册广播
            findAndHookMethod("com.tencent.mm.ui.LauncherUI", classLoader, "onCreate", Bundle.class, new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    XposedLogUtils.log("com.tencent.mm.ui.LauncherUI onCreated" + "\n");
                    weiXinLauncherActivity = (Activity) param.thisObject;
                    // 注册广播
                    weiXinBroadcast = new WeiXinBroadcast();
                    IntentFilter intentFilter = new IntentFilter();
                    intentFilter.addAction(WeiXinBroadcast.WEIXIN_QR_CODE_URL_STRING_INTENT_FILTER_ACTION);
                    intentFilter.addAction(WeiXinBroadcast.WEIXIN_BILLLIST_INTENT_FILTER_ACTION);
                    weiXinLauncherActivity.registerReceiver(weiXinBroadcast, intentFilter);
                }
            });
    
            // hook设置金额和备注的onDestroy方法
            findAndHookMethod("com.tencent.mm.ui.LauncherUI", classLoader, "onDestroy", new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    XposedLogUtils.log("com.tencent.mm.ui.LauncherUI onDestroy" + "\n");
                    weiXinLauncherActivity.unregisterReceiver(weiXinBroadcast);
                    weiXinBroadcast = null;
                    weiXinLauncherActivity = null;
                }
            });

     

    2、设置金额界面

     

    用hierarchy view查看设置金额界面布局,如下:

    可以看到设置金额界面对应的Activity为"com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI",再次查看设置金额界面的布局,找到确认按钮,如下:

    可以看到"确认"按钮的id为"ak_",在微信apk的public.xml(解压apk后的res/values/public.xml文件,记录资源id)中搜索"ak_",可以搜到如下代码:

        <public type="id" name="ak_" id="0x7f1006ec" />

    也就是说id"ak_"对应的值为"0x7f1006ec",将"0x7f1006ec"转换为10进制,结果为"2131756780",在反编译代码中全局搜索"2131756780",可以搜到如下内容:

    Searching 16831 files for "2131756780"
    
    E:\tools\xpose\classes.jar.src\com\tencent\mm\plugin\facedetect\a.java:
       61:     public static final int cAg = 2131756780;
    
    E:\tools\xpose\classes.jar.src\com\tencent\mm\plugin\wxpay\a.java:
      292:     public static final int cAg = 2131756780;
    E:\tools\xpose\classes.jar.src\com\tencent\mm\R.java:
     1975:     public static final int cAg = 2131756780;
    
    3 matches across 3 files

    可以"2131756780"这个值在三个地方都有定义,但是名称都为"cAg",打开CollectCreateQRCodeUI反编译代码,搜索"cAg"字段,结果如下:

         ((Button)findViewById(a.f.cAg)).setOnClickListener(new View.OnClickListener()
          {
            public final void onClick(View paramAnonymousView)
            {
              double d = bh.getDouble(CollectCreateQRCodeUI.a(CollectCreateQRCodeUI.this).getText(), 0.0D);
              g.Dk();
              int i = ((Integer)g.Dj().CU().get(w.a.xrD, Integer.valueOf(0))).intValue();
              x.i("MicroMsg.CollectCreateQRCodeUI", "wallet region: %s", new Object[] { Integer.valueOf(i) });
              if (!CollectCreateQRCodeUI.a(CollectCreateQRCodeUI.this).XO()) {
                com.tencent.mm.ui.base.u.makeText(CollectCreateQRCodeUI.this.mController.xIM, a.i.uPA, 0).show();
              }
              for (;;)
              {
                return;
                if (d < 0.01D) {
                  com.tencent.mm.ui.base.u.makeText(CollectCreateQRCodeUI.this.mController.xIM, a.i.uMS, 0).show();
                } else if (i == 8) {
                  CollectCreateQRCodeUI.this.r(new m(Math.round(d * 100.0D), CollectCreateQRCodeUI.b(CollectCreateQRCodeUI.this), q.FZ()));
                } else {
                  CollectCreateQRCodeUI.this.l(new s(d, "1", CollectCreateQRCodeUI.b(CollectCreateQRCodeUI.this)));
                }
              }
            }
          });

    很明显,这段代码应该是在布局中找到确认按钮,并给其设置监听事件,经分析,发现下面代码是发起获得二维码链接的请求:

    CollectCreateQRCodeUI.this.l(new s(d, "1", CollectCreateQRCodeUI.b(CollectCreateQRCodeUI.this)));

    "s"应该是请求参数,定义如下:

    public final class s
      extends i
    {
      public String desc;
      public String fpP;
      public String ljf = null;
      public double ljg;
      // 构造函数的三个参数类型,第一个为金额,第二个为二维码类型,我们一直传入"1"就可以了,第三个为备注信息
      public s(double paramDouble, String paramString1, String paramString2)
      {
        HashMap localHashMap = new HashMap();
        try
        {
          StringBuilder localStringBuilder = new java/lang/StringBuilder;
          localStringBuilder.<init>();
          localHashMap.put("fee", Math.round(100.0D * paramDouble));
          localHashMap.put("fee_type", paramString1);
          localHashMap.put("desc", URLEncoder.encode(paramString2, "UTF-8"));
          this.ljg = paramDouble;
          this.fpP = paramString1;
          this.desc = paramString2;
          D(localHashMap);
          return;
        }
        catch (UnsupportedEncodingException localUnsupportedEncodingException)
        {
          for (;;)
          {
            x.printErrStackTrace("Micromsg.NetSceneTenpayRemittanceQuery", localUnsupportedEncodingException, "", new Object[0]);
          }
        }
      }
      ...
    }

    再来看一下发起请求的"l"方法,定义如下:

      public final void l(k paramk)
      {
        cCe();
        this.zIY.a(paramk, true, 1);
      }

    现在我们只需要hook住CollectCreateQRCodeUI的onCreate方法,获得其实例对象保存在静态变量中,并在onDestory是设置为空,这样就可以获得CollectCreateQRCodeUI的实例了,然后在需要获得二维码链接时,调用其"l"方法发起请求即可,代码如下:

            // hook设置金额和备注的onCreate方法,自动填写数据并点击
            findAndHookMethod("com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI", classLoader, "onCreate", Bundle.class, new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    XposedLogUtils.log("com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI" + ":onCreated方法");
                    mWeiXinCreateQRCodeActivity = (Activity) param.thisObject;
    
                    Intent intent = ((Activity) param.thisObject).getIntent();
                    String jinEr = intent.getStringExtra("qr_money");
                    String beiZhu = intent.getStringExtra("beiZhu");
    
                    // 连续生成二维码链接时,是否需要关闭金额设置界面
                    executeWeiXinGenerateQrCodeMethod(jinEr, beiZhu);
                }
            });
    
            // hook设置金额和备注的onDestroy方法
            findAndHookMethod("com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI", classLoader, "onDestroy", new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    XposedLogUtils.log("com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI onDestroy" + "\n");
                    mWeiXinCreateQRCodeActivity = null;
                }
            });
    
    
        /**
         * 当设置金额界面已经打开的情况下,可以直接执行生成二维码方法
         */
        public static void executeWeiXinGenerateQrCodeMethod(String amount, String beiZhu) {
            if (mWeiXinCreateQRCodeActivity != null) {
                XposedLogUtils.log("executeWeiXinGenerateQrCodeMethod " + amount + " " + beiZhu);
                if (!TextUtils.isEmpty(amount)) {
                    Object paramObject = null;
                    Class paramObjectClass = findClass("com.tencent.mm.plugin.collect.b.s", mWeiXinClassLoader);
                    try {
                        Constructor paramObjectConstructor = paramObjectClass.getDeclaredConstructor(double.class, String.class, String.class);
                        paramObject = paramObjectConstructor.newInstance(Double.valueOf(amount), "1", beiZhu);
                        callMethod(mWeiXinCreateQRCodeActivity, "l", paramObject);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        /**
         * 打开设置金额界面
         *
         * @param qr_money
         * @param beiZhu
         */
        public static void startWeiXinCreateQRCodeActivity(String qr_money, String beiZhu) {
            Class weiXinCreateQRCodeActivityClass;
            Intent launcherIntent;
    
    
            if (weiXinLauncherActivity != null) {
                XposedLogUtils.log("launcher WeiXinCreateQRCodeActivity");
                weiXinCreateQRCodeActivityClass = XposedHelpers.findClass("com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI", mWeiXinClassLoader);
                launcherIntent = new Intent(weiXinLauncherActivity, weiXinCreateQRCodeActivityClass);
                launcherIntent.putExtra("qr_money", qr_money);
                launcherIntent.putExtra("beiZhu", beiZhu);
                weiXinLauncherActivity.startActivity(launcherIntent);
                return;
            }
        }
    
    

    在支付宝的广播接收类中,定义如下:

    public class WeiXinBroadcast extends BroadcastReceiver{
        public static String WEIXIN_QR_CODE_URL_STRING_INTENT_FILTER_ACTION = "com.hhly.pay.weixin.info.qrCodeUrl";
    
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().contentEquals(WEIXIN_QR_CODE_URL_STRING_INTENT_FILTER_ACTION)) {
                String qr_money = intent.getStringExtra("qr_money");
                String beiZhu = intent.getStringExtra("beiZhu");
                XposedLogUtils.log("WeiXinBroadcast onReceive " + qr_money + " " + beiZhu + "\n");
                if (!TextUtils.isEmpty(qr_money)) {
                    if (Main.mWeiXinCreateQRCodeActivity != null) { // 如果已经有设置金额,则直接使用其实例,不需要重复启动
                        Main.executeWeiXinGenerateQrCodeMethod(qr_money, beiZhu);
                    } else { // 生成二维码链接
                        Main.startWeiXinCreateQRCodeActivity(qr_money, beiZhu);
                    }
                }
            }
        }
    }

    我们hook了设置金额界面的onCreate方法并保存了其实例,这样就不用每次生成二维码链接都打开设置金额界面了,下面再来看一看当服务端返回二维码链接之后设置金额界面的回调方法,经观察,回调方法定义如下:

     public final boolean d(int paramInt1, int paramInt2, final String paramString, k paramk)
      {
        boolean bool2 = false;
        boolean bool1;
        if ((paramk instanceof s))
        {
          bool1 = bool2;
          if (paramInt1 == 0)
          {
            bool1 = bool2;
            if (paramInt2 == 0)
            {
              paramString = (s)paramk;
              paramk = new Intent();
              paramk.putExtra("ftf_pay_url", paramString.ljf); // 获得二维码链接
              paramk.putExtra("ftf_fixed_fee", paramString.ljg); // 获得金额
              paramk.putExtra("ftf_fixed_fee_type", paramString.fpP); // 获得类型
              paramk.putExtra("ftf_fixed_desc", paramString.desc); // 获得备注
              setResult(-1, paramk); // 将参数回传给二维码收款界面
              finish(); // 关闭当前界面
              bool1 = true;
            }
          }
        }
        ...
     }

    分析上面的代码,该方法会将二维码链接等数据放入intent中并回传给收款二维码界面,最后再关闭设置金额界面,我们只需要hook住该方法,并替换其执行逻辑,就可以在获得二维码链接后不销毁设置金额界面了。其中,最后一个参数"k paramk"中定义了二维码链接信息,代码如下:

             findAndHookMethod("com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI", classLoader, "d", int.class, int.class, String.class,
                    findClass("com.tencent.mm.ae.k", classLoader), new XC_MethodReplacement() {
                        @Override
                        protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {
                            XposedLogUtils.log("com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI" + ":d方法");
                            XposedLogUtils.log(methodHookParam.args[0] + " " + methodHookParam.args[1] + " " + methodHookParam.args[2]);
                            if (methodHookParam.args[3] != null) {
                                WeiXinQrCode weiXinQrCode = new WeiXinQrCode();
                                weiXinQrCode.ftf_pay_url = getStringField(methodHookParam.args[3], "ljf"); // 获得二维码链接
                                weiXinQrCode.ftf_fixed_fee = getStringField(methodHookParam.args[3], "ljg"); // 获得金额
                                weiXinQrCode.ftf_fixed_fee_type = getStringField(methodHookParam.args[3], "fpP"); // 获得类型
                                weiXinQrCode.ftf_fixed_desc = getStringField(methodHookParam.args[3], "desc"); // 获得备注
                                XposedLogUtils.log(weiXinQrCode.toString() + "\n");
    
                                // 发送广播将二维码链接信息传给xposed插件
                                Intent broadCastIntent = new Intent();
                                broadCastIntent.putExtra("weiXinQrCode", weiXinQrCode);
                                broadCastIntent.setAction(PluginBroadcast.WEIXIN_QR_CODE_URL_STRING_INTENT_FILTER_ACTION);
                                Activity activity = (Activity) methodHookParam.thisObject;
                                activity.sendBroadcast(broadCastIntent);
    
                            }
                            return true;
                        }
                    });

    其中WeiXinQrCode类是我们自己定义的类,用来保存二维码信息,定义如下:

        public static class WeiXinQrCode implements Parcelable {
            public String ftf_pay_url;
            public String ftf_fixed_fee;
            public String ftf_fixed_fee_type;
            public String ftf_fixed_desc;
    
            public WeiXinQrCode() {
    
            }
    
            protected WeiXinQrCode(Parcel in) {
                ftf_pay_url = in.readString();
                ftf_fixed_fee = in.readString();
                ftf_fixed_fee_type = in.readString();
                ftf_fixed_desc = in.readString();
            }
    
            public static final Creator<WeiXinQrCode> CREATOR = new Creator<WeiXinQrCode>() {
                @Override
                public WeiXinQrCode createFromParcel(Parcel in) {
                    return new WeiXinQrCode(in);
                }
    
                @Override
                public WeiXinQrCode[] newArray(int size) {
                    return new WeiXinQrCode[size];
                }
            };
    
            @Override
            public int describeContents() {
                return 0;
            }
    
            @Override
            public void writeToParcel(Parcel dest, int flags) {
                dest.writeString(ftf_pay_url);
                dest.writeString(ftf_fixed_fee);
                dest.writeString(ftf_fixed_fee_type);
                dest.writeString(ftf_fixed_desc);
            }
    
            @Override
            public String toString() {
                return "ftf_pay_url:" + ftf_pay_url + "," +
                        "ftf_fixed_fee:" + ftf_fixed_fee + "," +
                        "ftf_fixed_fee_type:" + ftf_fixed_fee_type + "," +
                        "ftf_fixed_desc:" + ftf_fixed_desc + "\n";
            }
        }

    然后在xposed插件的广播接收类中接收二维码链接信息,如下:

    public class PluginBroadcast extends BroadcastReceiver{
        public static String WEIXIN_QR_CODE_URL_STRING_INTENT_FILTER_ACTION = "com.tencent.mm.info.qrCodeUrl";
    
        @Override
        public void onReceive(Context context, Intent intent) {
     	if (intent.getAction().contentEquals(WEIXIN_QR_CODE_URL_STRING_INTENT_FILTER_ACTION)) {
                App.dealWeiXinQrCodeUrlString(intent);
            }
        }
    }
    
    
    public static void dealWeiXinQrCodeUrlString(Intent intent) {
        Main.WeiXinQrCode weiXinQrCode = intent.getParcelableExtra("weiXinQrCode");
        Log.i("aaaaa:", weiXinQrCode.toString() + " " + weiXinTotalCount);
    }

    在xposed插件中发送获得二维码链接广播,打印信息如下:

        05-31 16:58:42.181 12445-12445/? I/aaa:: ftf_pay_url:wxp://f2f1dsW3BhPFHjfKRd2Royq4-jp0yy_JUxxe,ftf_fixed_fee:5.0,ftf_fixed_fee_type:1,ftf_fixed_desc:你好5

    可以看到二维码链接已经打印出来了。

    三、获得账单信息

    首先,我们来看一下收款记录对应的页面,如下:

    用hierarchy view查看其布局,如下:

    可以看到收款记录页面所在Activity为"com.tencent.mm.plugin.appbrand.ui.AppBrandUI",而具体的收款信息显示在"InnerWebView"中,也就是说收款记录是在一个WebView中显示的,那么想从WebView中获得收款信息就比较复杂了,一般的,如果页面显示在WebView中,应该是通过http请求获得h5页面并加载,那么我们可以尝试一下抓包来获得收款信息,打开抓包Charles抓包工具,然后再打开收款记录页面,可以抓到如下请求:

    可以看到成功抓取到了账单数据,分析请求参数,总结如下:

        url:https://payapp.weixin.qq.com/qrappzd/user/incomelist?sid=AAHE8SPddES5nFhpG3Cm-7zVE2XqeHLx3PzD78ZL33nzvA&v=3.4.3
        请求方式:post
        请求参数:{
    				"v": "3.4.3", // 请求接口版本
    				"start_time": 0, // 查询收款数据开始时间
    				"end_time": 1527760717, // 查询收款结束时间
    				"last_bill_id": null, // 请求这个账单编号对应账单之后的数据
    				"page_size": 10, // 每次请求数据条数
    				"sort": "desc", // 排序方式
    				"is_first": true, // 是否是第一个请求
    				"sid": "AAHE8SPddES5nFhpG3Cm-7zVE2XqeHLx3PzD78ZL33nzvA" // 用户信息id
    			}

    用postman模拟请求,发现是可以成功请求到数据的:

    上面的请求信息中,最关键的部分为"sid",这个应该类似loginToken一样的信息,用来验证用户信息的,也就是说,我们只需要找到获得"sid"的办法,就能够获得到收款记录。然后,微信的收款记录是用微信小程序实现的,我找了一天也没有找到获得"sid"的方法,后面有时间可以尝试反编译微信小程序的代码,看能不能找到获得"sid"的方法。看来这个办法暂时行不通,那么有没有其他的办法获得收款数据呢?在微信的交易记录中我们也可以获得收款信息,如下:

    查看其布局,如下:

    麻蛋,又是一个WebView页面,同样,抓包获得了如下数据:

    总结请求数据:

        url:https://wx.tenpay.com/userroll/userrolllist?classify_type=0&count=20&exportkey=A%2F4YTLDXVQMB1IScuR4uDa8%3D&sort_type=1
        请求方式:get
        Cookie:export_key=A/4YTLDXVQMB1IScuR4uDa8=; userroll_encryption=5Wyzpp5yYABVU2ZLQ9ueNA5LT466SEtjxY5Z1L0bhnnzcWwYEij0Q0T+ZeUfu0T4qFTAELJEZYvclmmtF39a/mW5syVtXHUYstaIYCrEVcOb0yfR6OkVpqb1xUE5p3rCXT3OF8YwcgoIDCS5PepNkg==;

    上面得参数中,export_key和userroll_encryption是关键参数,有了这两个参数我们就可以获得账单数据,用postman模拟请求如下:

    我在反编译代码中找了很久,同样没有找到获得export_key和userroll_encryption的办法,到现在为止,问题好像卡住了,然而,一次偶然的操作,我打开了微信交易记录的原生页面,也就是说,微信交易记录的页面是可以配置使用原生的页面还是使用h5页面的,原生的交易记录页面如下:

    查看其布局,如下:

    可以看到原生交易记录页面对应的类为"com.tencent.mm.plugin.order.ui.MallOrderRecordListUI",首先分析一下其布局结构:

    可以看到,其账单数据其实是在MMLoadMoreListView控件中显示的,应该是一个可以支持加载更多的ListView控件,这个我们是能够猜到。查看"MallOrderRecordListUI"类的代码:

    public class MallOrderRecordListUI
      extends WalletBaseUI
    {
      ...
      public MMLoadMoreListView ldB; // 交易记录页面对应的ListView
      public a pcp = null; // listview adapter引用
      public List<i> pcq = new ArrayList(); // 交易记录数据
      public int wn = 0; // 记录已经加载的数据条数
      protected String pcr = null; // 服务端返回的已经加载过的账单数据的条数,会在请求参数中传入该值
      ...
    
    
      // 初始化布局
      protected final void initView()
      {
        // 这里有一个判断,只有用对应的方法打开交易记录页面才会去加载数据,我们再xposed中打开交易记录界面,这个条件是不成立的,也就是说打开交易记录界面时,不会主动的去加载数据
        if ((com.tencent.mm.wallet_core.a.ag(this) instanceof com.tencent.mm.plugin.order.a.a))
        {
          this.acS = true;
          biJ(); // 加载数据
        }
        ...
        this.ldB = ((MMLoadMoreListView)findViewById(a.f.uqt)); // 在布局中找到listview
        this.pcp = new a(); // 新建adpater实例
        this.ldB.setAdapter(this.pcp); // 给listview设置adapter
        // 设置listview的item点击监听
        this.ldB.setOnItemClickListener(new AdapterView.OnItemClickListener()
        {
          public final void onItemClick(AdapterView<?> paramAnonymousAdapterView, View paramAnonymousView, int paramAnonymousInt, long paramAnonymousLong)
          {
            ...
          }
        });
        // 设置listview的item长按监听
        this.ldB.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener()
        {
          public final boolean onItemLongClick(AdapterView<?> paramAnonymousAdapterView, View paramAnonymousView, final int paramAnonymousInt, long paramAnonymousLong)
          {
     		...
          }
        });
        // 给listview绑定加载更多的监听
        this.ldB.ybX = new MMLoadMoreListView.a()
        {
          public final void axW()
          {
            if (!MallOrderRecordListUI.this.acS)
            {
              MallOrderRecordListUI.this.acS = true;
              MallOrderRecordListUI localMallOrderRecordListUI = MallOrderRecordListUI.this;
              localMallOrderRecordListUI.wn += 10; // 已经加载的数据条数
              MallOrderRecordListUI.this.biJ(); // 调用该方法加载数据
            }
          }
        };
        ...
      }
    
    
      // 请求账单数据方法
      public void biJ()
      {
        // 调用"l"方法请求账单数据,传入请求参数,其中this.wn为我们ListView中数据的条数,this.pcr为服务端返回给我们的已经加载的数据条数
        l(new com.tencent.mm.plugin.order.model.e(this.wn, this.pcr));
      }
    
      // 获得账单数据之后的回调方法
      public boolean d(int paramInt1, int paramInt2, String paramString, k paramk)
      {
        boolean bool;
        if ((paramk instanceof com.tencent.mm.plugin.order.model.e))
        {
          if (this.mzP != null)
          {
            this.mzP.dismiss();
            this.mzP = null;
          }
          paramString = (com.tencent.mm.plugin.order.model.e)paramk; // 账单数据
          this.pcr = paramString.pbf; // 服务端返回的已经加载过的账单数据条数,这个值
          bl(paramString.pbd); // 具体账单数据在paramk.pbd中
          bm(paramString.pbe);
          this.mCount = this.pcq.size(); // 现在已经加载过的账单数据
          if (paramString.liB > this.mCount) // paramString.liB返回的是用户当前账单数据总条数
          {
          	...
          }
        }
        ...
      }
    
      // 交易记录列表对应的adapter
      protected final class a
        extends BaseAdapter
      {
        protected a() {}
        
        private i uA(int paramInt)
        {
          return (i)MallOrderRecordListUI.this.pcq.get(paramInt);
        }
        
        public final int getCount()
        {
          return MallOrderRecordListUI.this.pcq.size();
        }
        
        public final long getItemId(int paramInt)
        {
          return paramInt;
        }
        
        public final View getView(int paramInt, View paramView, ViewGroup paramViewGroup)
        {
          ...
        }
      }
     }

    可以看到,微信最终是调用"l"方法来请求账单数据的,其中参数"e"的定义如下:

    package com.tencent.mm.plugin.order.model;
    
    import com.tencent.mm.sdk.platformtools.x;
    import java.util.HashMap;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.Map;
    import org.json.JSONArray;
    import org.json.JSONException;
    import org.json.JSONObject;
    
    public final class e
      extends com.tencent.mm.wallet_core.tenpay.model.i
    {
      public int liB;
      private int ocK;
      public List<i> pbd = null; // 账单数据
      public List<d> pbe = null; // 账单列表上的悬浮日期数据,我们不关心
      public String pbf;
      
      public e(int paramInt, String paramString)
      {
        // 设置请求参数
        HashMap localHashMap = new HashMap();
        localHashMap.put("Limit", "10"); // 每次请求数据的条数,微信写死了就是10条,我们可以通过其他方法修改这个值
        localHashMap.put("Offset", String.valueOf(paramInt)); // listview中数据的条数
        localHashMap.put("Extbuf", paramString); // 服务端返回的已经加载过的数据条数
        D(localHashMap); // 调用D方法构造请求参数
      }
      
      // 数据回来后的回调方法
      public final void a(int paramInt, String paramString, JSONObject paramJSONObject)
      {
        int i = 0;
        x.d("MicroMsg.NetScenePatchQueryUserRoll", "errCode " + paramInt + " errMsg: " + paramString);
        this.pbd = new LinkedList();
        // 解析json数据,转换为对象形式
        try
        {
          this.liB = paramJSONObject.getInt("TotalNum");
          this.ocK = paramJSONObject.getInt("RecNum");
          this.pbf = paramJSONObject.optString("Extbuf");
          JSONArray localJSONArray = paramJSONObject.getJSONArray("UserRollList");
          if (localJSONArray != null) {
            for (paramInt = 0; paramInt < localJSONArray.length(); paramInt++)
            {
              paramString = new com/tencent/mm/plugin/order/model/i;
              paramString.<init>();
              JSONObject localJSONObject = localJSONArray.getJSONObject(paramInt);
              paramString.pbq = localJSONObject.optInt("PayType");
              paramString.pbi = localJSONObject.optString("Transid");
              paramString.pbj = localJSONObject.optDouble("TotalFee");
              paramString.pbk = localJSONObject.optString("GoodsName");
              paramString.pbl = localJSONObject.optInt("CreateTime");
              paramString.pbn = localJSONObject.optInt("ModifyTime");
              paramString.pbo = localJSONObject.optString("FeeType");
              paramString.pbt = localJSONObject.optString("AppThumbUrl");
              paramString.pbm = localJSONObject.optString("TradeStateName");
              paramString.pby = localJSONObject.optString("StatusColor");
              paramString.pbz = localJSONObject.optString("FeeColor");
              paramString.pbA = localJSONObject.optDouble("ActualPayFee");
              paramString.pbB = localJSONObject.optString("BillId");
              this.pbd.add(paramString);
            }
          }
          ...
          return;
        }
        catch (JSONException paramString)
        {
          x.e("MicroMsg.NetScenePatchQueryUserRoll", "Parse Json exp:" + paramString.getLocalizedMessage());
        }
      }
      ...
    }

    微信将每次请求的数据条数写死了,每次只能请求10条数数据,从上面的代码中可以看到最终是通过调用"D(localHashMap);"来构造请求数据的,它是在"com.tencent.mm.plugin.order.model.e"的父类"com.tencent.mm.wallet_core.c.h"中定义的,其定义如下:

      public final void D(Map<String, String> paramMap) {
       ...
      }

    那么只要我们考虑hook住该方法,判断如果当前类的类名是"com.tencent.mm.plugin.order.model.e",就修改参数paramMap中key值为"Limit"的value值,这样不就修改了每次请求账单数据的条数了吗?

    考虑另外一个问题,我们现在想屏蔽掉用户对交易记录页面的操作,因为我们可以自己调用对应的方法来获得账单数据,而不需要让账单数据显示在界面上,或者让用户滑动账单页面就加载更多数据了,通过上面的分析我们知道,微信是通过调用"biJ()"来加载数据的,我们只需要replace掉该方法,不就可以让用户操作界面无法加载数据了吗?而账单数据的显示是在交易记录界面账单数据返回的回调方法中实现的,同样我们可以replace掉该方法,就可以让账单数据不显示在界面上了。

    整个处理账单页面的代码如下:

     

        public static Activity weiXinMallOrderRecordListUI = null; // 微信账单页面
        public static List<WeiXinBillObject> weiXinBillObjectList = null;
    
    
        // hook交易记录页面的onCreate方法,保存其对象实例并调用获得账单数据方法
        findAndHookMethod("com.tencent.mm.plugin.order.ui.MallOrderRecordListUI", classLoader, "onCreate", Bundle.class, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                XposedLogUtils.log("com.tencent.mm.g.a.st.MallOrderRecordListUI" + ":onCreate");
                weiXinMallOrderRecordListUI = (Activity) param.thisObject;
                startGetWeiXinBillList();
            }
        });
    
        // hook交易记录页面的onDestory方法,释放对其对象实例的引用
        findAndHookMethod("com.tencent.mm.plugin.order.ui.MallOrderRecordListUI", classLoader, "onDestroy", new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                XposedLogUtils.log("com.tencent.mm.plugin.order.ui.MallOrderRecordListUI onDestroy" + "\n");
                weiXinMallOrderRecordListUI = null;
            }
        });
    
        // 替换掉交易记录页面的加载数据方法,让用户操作界面不能加载数据
        findAndHookMethod("com.tencent.mm.plugin.order.ui.MallOrderRecordListUI", classLoader, "biJ", new XC_MethodReplacement() {
            @Override
            protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {
                XposedLogUtils.log("com.tencent.mm.plugin.wallet_core.model.ae" + ":bLH");
                return null;
            }
        });
    
        // hook D方法,修改每次请求账单数据的条数
        findAndHookMethod("com.tencent.mm.wallet_core.c.h", classLoader, "D", Map.class, new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                if (param.thisObject.getClass().getName().contentEquals("com.tencent.mm.plugin.order.model.e")) {
                    if (param.args[0] != null) {
                        Map<String, String> paramMap = (Map<String, String>)param.args[0];
                        if (paramMap.containsKey("Limit")) {
                            paramMap.put("Limit", "100"); // 修改每次请求账单数据的条数,这里修改为100条
                        }
                    }
                }
                super.beforeHookedMethod(param);
            }
        });
    
        // hook交易记录页面数据返回的回调方法,替换其原来的执行逻辑,让账单数据不显示到界面上,并且获得账单数据
        findAndHookMethod("com.tencent.mm.plugin.order.ui.MallOrderRecordListUI", classLoader, "d", int.class, int.class, String.class,
                findClass("com.tencent.mm.ae.k", classLoader), new XC_MethodReplacement() {
                    @Override
                    protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {
                        XposedLogUtils.log("com.tencent.mm.plugin.order.ui.MallOrderRecordListUI:" + "d" + "\n");
                        XposedLogUtils.log(methodHookParam.args[0] + " " + methodHookParam.args[1] + " " + methodHookParam.args[2] + "\n");
                        if (methodHookParam.args[3] != null) {
                            Object resObject = methodHookParam.args[3];
                            String pcr = getStringField(resObject, "pbf"); // 服务端返回的已经加载过的数据条数
                            Integer totalCount = 0;
                            if (getField(resObject, "liB") != null) {
                                totalCount = (Integer) getField(resObject, "liB"); // 服务端返回的当前用户账单数据总条数
                            }
                            XposedLogUtils.log("pcr:" + pcr + " " + "totalCount:" + totalCount + "\n");
                            // 获得账单数据
                            Field billListFiled = XposedHelpers.findField(findClass("com.tencent.mm.plugin.order.model.e", classLoader), "pbd");
                            final Object billList = billListFiled.get(resObject);
                            // 转换账单数据到我们自己的对象中
                            if (billList != null) {
                                List<Object> bList_obj = (List) billList;
                                for (Object object : bList_obj) {
                                    WeiXinBillObject weiXinBillObject = new WeiXinBillObject();
                                    weiXinBillObject.payType = getStringField(object, "pbq");
                                    weiXinBillObject.transid = getStringField(object, "pbi");
                                    weiXinBillObject.totalFee = getStringField(object, "pbj");
                                    weiXinBillObject.goodsName = getStringField(object, "pbk");
                                    weiXinBillObject.createTime = getStringField(object, "pbl");
                                    weiXinBillObject.modifyTime = getStringField(object, "pbn");
                                    weiXinBillObject.feeType = getStringField(object, "pbo");
                                    weiXinBillObject.appThumbUrl = getStringField(object, "pbt");
                                    weiXinBillObject.tradeStateName = getStringField(object, "pbm");
                                    weiXinBillObject.statusColor = getStringField(object, "pby");
                                    weiXinBillObject.feeColor = getStringField(object, "pbz");
                                    weiXinBillObject.actualPayFee = getStringField(object, "pbA");
                                    weiXinBillObject.billId = getStringField(object, "pbB");
                                    weiXinBillObjectList.add(weiXinBillObject);
                                }
                                // 连续加载1000条数据或者加载完成则不再继续加载,在这里判断是否加载到了所要的账单数据,我这里设置的条件是加载超过了1000条数据
                                if (weiXinBillObjectList.size() >= 1000 || bList_obj.size() == 0 || bList_obj.size() >= totalCount) {
                                    for (WeiXinBillObject weiXinBillObject : weiXinBillObjectList) {
                                        XposedLogUtils.log(weiXinBillObject.toString());
                                    }
                                } else { // 继续加载账单数据
                                    getWeiXinBillList(weiXinBillObjectList.size(), pcr);
                                }
                            }
                        }
                        return true;
                    }
                });
    
    
         /**
         * 加载账单数据
         * @param offset
         * @param extbuf
         */
        private static void getWeiXinBillList(int offset, String extbuf) {
            if (weiXinMallOrderRecordListUI != null) {
                Object paramObject = null;
                Class paramObjectClass = findClass("com.tencent.mm.plugin.order.model.e", mWeiXinClassLoader);
                try {
                    Constructor paramObjectConstructor = paramObjectClass.getDeclaredConstructor(int.class, String.class);
                    paramObject = paramObjectConstructor.newInstance(offset, extbuf);
                    callMethod(weiXinMallOrderRecordListUI, "l", paramObject);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        /**
         * 启动微信交易记录页面
         */
        public static void startWeiXinBillListActivity() {
            Intent launcherIntent;
            if (weiXinLauncherActivity != null) {
                XposedLogUtils.log("launcher MallOrderRecordListUI");
                launcherIntent = new Intent(weiXinLauncherActivity, findClass("com.tencent.mm.plugin.order.ui.MallOrderRecordListUI", mWeiXinClassLoader));
                weiXinLauncherActivity.startActivity(launcherIntent);
                return;
            }
        }
        /**
         * 开始获得账单数据
         */
        public static void startGetWeiXinBillList() {
            if (weiXinBillObjectList == null) {
                weiXinBillObjectList = new ArrayList<>();
            }
            weiXinBillObjectList.clear(); // 清空原来的数据
            getWeiXinBillList(0, null); // 加载账单数据
        }
    
    
        private static String getStringField(final Object obj, final String fieldName) throws IllegalAccessException {
            Field sField = XposedHelpers.findField(obj.getClass(), fieldName);
            if (sField == null) {
                return null;
            }
            return String.valueOf(sField.get(obj));
        }
    
        private static Object getField(final Object obj, final String fieldName) throws IllegalAccessException {
            Field sField = XposedHelpers.findField(obj.getClass(), fieldName);
            if (sField == null) {
                return null;
            }
            return sField.get(obj);
        }
    
         /**
         * 微信账单对象
         */
        public static class WeiXinBillObject implements Parcelable {
            public String payType;
            public String transid;
            public String totalFee;
            public String goodsName;
            public String createTime;
            public String modifyTime;
            public String feeType;
            public String appThumbUrl;
            public String tradeStateName;
            public String statusColor;
            public String feeColor;
            public String actualPayFee;
            public String billId;
    
            public WeiXinBillObject() {
    
            }
    
            protected WeiXinBillObject(Parcel in) {
                payType = in.readString();
                transid = in.readString();
                totalFee = in.readString();
                goodsName = in.readString();
                createTime = in.readString();
                modifyTime = in.readString();
                feeType = in.readString();
                appThumbUrl = in.readString();
                tradeStateName = in.readString();
                statusColor = in.readString();
                feeColor = in.readString();
                actualPayFee = in.readString();
                billId = in.readString();
            }
    
            public static final Creator<WeiXinBillObject> CREATOR = new Creator<WeiXinBillObject>() {
                @Override
                public WeiXinBillObject createFromParcel(Parcel in) {
                    return new WeiXinBillObject(in);
                }
    
                @Override
                public WeiXinBillObject[] newArray(int size) {
                    return new WeiXinBillObject[size];
                }
            };
    
            @Override
            public int describeContents() {
                return 0;
            }
    
            @Override
            public void writeToParcel(Parcel dest, int flags) {
                dest.writeString(payType);
                dest.writeString(transid);
                dest.writeString(totalFee);
                dest.writeString(goodsName);
                dest.writeString(createTime);
                dest.writeString(modifyTime);
                dest.writeString(feeType);
                dest.writeString(appThumbUrl);
                dest.writeString(tradeStateName);
                dest.writeString(statusColor);
                dest.writeString(feeColor);
                dest.writeString(actualPayFee);
                dest.writeString(billId);
            }
    
            @Override
            public String toString() {
                return "payType:" + payType + "," +
                        "transid:" + transid + "," +
                        "totalFee:" + totalFee + "," +
                        "createTime:" + createTime + "," +
                        "modifyTime:" + modifyTime + "," +
                        "feeType:" + feeType + "," +
                        "appThumbUrl:" + appThumbUrl + "," +
                        "tradeStateName:" + tradeStateName + "," +
                        "statusColor:" + statusColor + "," +
                        "feeColor:" + feeColor + "," +
                        "actualPayFee:" + actualPayFee + "," +
                        "billId:" + billId + "," +
                        "goodsName:" + goodsName + "\n";
            }
        }

    在广播中定义如下:

        public class WeiXinBroadcast extends BroadcastReceiver{
    	    public static String WEIXIN_BILLLIST_INTENT_FILTER_ACTION = "com.tencent.mm.info.billlist";
    	    @Override
    	    public void onReceive(Context context, Intent intent) {
    	    	if (intent.getAction().contentEquals(WEIXIN_BILLLIST_INTENT_FILTER_ACTION)) {
    	            if (Main.weiXinMallOrderRecordListUI != null) {
    	                Main.startGetWeiXinBillList();
    	            } else {
    	                Main.startWeiXinBillListActivity();
    	            }
    	        }
    	    }
    	}

     

    最后在Xposed插件程序发送一个广播,可以看到如下日志打印:

     

    06-01 10:27:46.137 I/Xposed  (10765): payType:0,transid:4200000119201806015247324551,totalFee:9800.0,createTime:1527783145,modifyTime:1527783151,feeType:CNY,appThumbUrl:,tradeStateName:支付成功,statusColor:#888888,feeColor:#000000,actualPayFee:9800.0,billId:e91e105b20a10700d55d1574,goodsName:京东-订单编号76517324492
    06-01 10:27:46.137 I/Xposed  (10765): payType:5,transid:100005020118053100078332119850436866,totalFee:52000.0,createTime:1527774458,modifyTime:1527774458,feeType:1,appThumbUrl:,tradeStateName:等待朋友确认收钱,statusColor:#888888,feeColor:#000000,actualPayFee:52000.0,billId:fafc0f5b20a10700d55d1574,goodsName:微信转账
    06-01 10:27:46.137 I/Xposed  (10765): payType:2,transid:4200000144201805318564157226,totalFee:680.0,createTime:1527771799,modifyTime:1527771799,feeType:CNY,appThumbUrl:,tradeStateName:支付成功,statusColor:#888888,feeColor:#000000,actualPayFee:680.0,billId:97f20f5b20a10700d55d1574,goodsName:深圳市南山家家乐生活超市消费
    06-01 10:27:46.137 I/Xposed  (10765): payType:2,transid:4200000116201805312045134300,totalFee:1900.0,createTime:1527768245,modifyTime:1527768245,feeType:,appThumbUrl:,tradeStateName:支付成功,statusColor:#888888,feeColor:#000000,actualPayFee:1900.0,billId:b5e40f5b20a10700d55d1574,goodsName:万连佳 收银员:005-102
    06-01 10:27:46.137 I/Xposed  (10765): payType:6,transid:100005030118053100078332117578249866,totalFee:1800.0,createTime:1527768062,modifyTime:1527768062,feeType:1,appThumbUrl:,tradeStateName:已转账,statusColor:#888888,feeColor:#000000,actualPayFee:1800.0,billId:fee30f5b20a10700d55d1574,goodsName:二维码收款
    ...

    ok,问题解决。

    利用账单cookie来获得账单

    上面说到过,微信账单h5页面请求账单数据的接口总结如下:

        url:https://wx.tenpay.com/userroll/userrolllist?classify_type=0&count=20&sort_type=1
        请求方式:get
        Cookie:export_key=A/4YTLDXVQMB1IScuR4uDa8=; userroll_encryption=5Wyzpp5yYABVU2ZLQ9ueNA5LT466SEtjxY5Z1L0bhnnzcWwYEij0Q0T+ZeUfu0T4qFTAELJEZYvclmmtF39a/mW5syVtXHUYstaIYCrEVcOb0yfR6OkVpqb1xUE5p3rCXT3OF8YwcgoIDCS5PepNkg==;

    其中最关键的就是要获得Cookie,只要得到Cookie,就可以利用接口查询账单,之前一直没有找到获得cookie的方式,现在我们仔细分析一下这个问题,微信账单页面是一个h5页面,而h5页面在Android中都是通过WebView加载的,而我们在开发中一般都会给WebView设置WebViewClient来监控页面的加载情况,WebViewClient的onPageFinished是页面加载完成的回调方法,我们可以在该方法中获得请求页面所需要的Cookie信息,这是一种获得H5页面的Cookie的通用方法。

    经过上面的分析,我们知道可以Hook住微信账单页面的WebViewClient的onPageFinished方法,然后通过判断加载页面的url是否是微信账单页面的url来判断加载的是否是微信账单页面,如果是,则获得cookie并返回。这里还要解决一个问题就是如何自动打开账单页面,只有打开了账单页面,才会去加载账单。具体分析过程就不说明了,这里只贴出代码:

        /**
         * 启动微信交易记录页面
         */
        public static void startWeiXinBillListActivity() {
            XposedLogUtils.log("startWeiXinBillListActivity" + weiXinLauncherActivity);
            if (weiXinLauncherActivity != null) {
                // 打开账单页面
                callStaticMethod(findClass(VersionParam.weiXinBillListUIFullClassName, mWeiXinClassLoader), VersionParam.weiXinBillListMethodName, weiXinLauncherActivity);
            }
        }
            findAndHookMethod(VersionParam.weiXinWebViewClientFullClassName, classLoader, VersionParam.weiXinWebViewClientLoadFinishMethodName,
                    findClass(VersionParam.weiXinWebViewFullClassName, classLoader), String.class, new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                            String url = (String) param.args[1];
                            if (url != null && url.contains("wx.tenpay.com/userroll/readtemplate")) { // 交易记录url
                                XposedLogUtils.log("onPageFinished:" + url);
                                String cookie = (String) callMethod(callStaticMethod(findClass(VersionParam.weiXinWebViewGetCookieFullClassName, classLoader),
                                        VersionParam.weiXinWebViewGetCookieInstanceMethodName), VersionParam.weiXinWebViewGetCookieMethodName, url);
                                XposedLogUtils.log("cookie:" + cookie);
                                if (cookie != null) {
                                    if (param.thisObject != null) {
                                        Activity webViewUI = (Activity) getField(param.thisObject, VersionParam.weiXinWebViewObjectFeildName);
                                        if (webViewUI != null) {
                                            Intent broadCastIntent = new Intent();
                                            broadCastIntent.putExtra("cookieStr", cookie);
                                            broadCastIntent.setAction(WebSocketService.WEIXIN_BILLLIST_COOKIE_INTENT_FILTER_ACTION);
                                            webViewUI.sendBroadcast(broadCastIntent);
                                            webViewUI.finish();
                                        }
                                    }
                                }
                            }
                        }
                    });
    
            
            // 这个是为了确保在账单页面加载失败的时候能够自动关闭账单页面,页面打开多个账单页面,影响性能
            findAndHookMethod(VersionParam.weiXinWebViewUIFullClassName, classLoader, "b",
                    findClass("com.tencent.mm.plugin.webview.stub.c", classLoader), new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                            XposedLogUtils.log("WeiXin WebViewUI b");
                            Activity webViewUI = (Activity) param.thisObject;
                            try {
                                if (webViewUI != null && !webViewUI.isFinishing()) {
                                    View refresh_mask = (View) getField(webViewUI, "pYL");
                                    if (refresh_mask != null && refresh_mask.getVisibility() == View.VISIBLE) {
                                        XposedLogUtils.log("WeiXin WebViewUI refresh_mask visibile");
                                        webViewUI.finish();
                                    }
                                }
                            } catch (Exception e) {
    
                            }
                        }
                    });

     用的的一些全局静态变量的定义

        public static String WEIXIN_PACKAGE_NAME = "com.tencent.mm";
    
        public static String UNIONPAY_PACKAGE_NAME = "com.unionpay";
    
        public static String weiXinReturnParamFullClassName = "com.tencent.mm.ae.k";
        public static String weiXinBillStartRequestMethodName = "biJ";
        public static String weiXinCreateBillRequestParamFullClassName = "com.tencent.mm.wallet_core.c.h";
        public static String weiXinCreateBillRequestParamMethodName = "D";
    
        public static String ftf_pay_url_field_name = "ljf";
        public static String ftf_fixed_fee_field_name = "ljg";
        public static String ftf_fixed_fee_type_field_name = "fpP";
        public static String ftf_fixed_desc_field_name = "desc";
        public static String weiXinIsLoginFullClassName = "com.tencent.mm.kernel.a";
        public static String weiXinIsLoginMethodName = "Dz";
        public static String weiXinCreateQRCodeUIFullClassName = "com.tencent.mm.plugin.collect.ui.CollectCreateQRCodeUI";
        public static String weiXinWebViewUIFullClassName = "com.tencent.mm.plugin.webview.ui.tools.WebViewUI";
        public static String weiXinCreateQRCodeUICallBackMethodName = "d";
        public static String weiXinLauncherUIFullClassName = "com.tencent.mm.ui.LauncherUI";
        public static String weiXinBillListUIFullClassName = "com.tencent.mm.plugin.mall.ui.MallIndexBaseUI";
        public static String weiXinBillListMethodName = "u";
        public static String weiXinCreateQRCodeRequestParamFullClassName = "com.tencent.mm.plugin.collect.b.s";
        public static String weiXinWebViewClientFullClassName = "com.tencent.mm.plugin.webview.ui.tools.WebViewUI$i";
        public static String weiXinWebViewClientLoadFinishMethodName = "a";
        public static String weiXinWebViewFullClassName = "com.tencent.xweb.WebView";
        public static String weiXinWebViewGetCookieFullClassName = "com.tencent.xweb.b";
        public static String weiXinWebViewGetCookieInstanceMethodName = "cIi";
        public static String weiXinWebViewGetCookieMethodName = "getCookie";
        public static String weiXinWebViewObjectFeildName = "pZJ";
        // 微信当前版本中的登录界面类全名
        public static String mWeiXinLoginActivityClassFullName = "com.tencent.mm.plugin.account.ui.LoginUI";
        public static String mWeiXinSQLiteDatabaseClassFullName = "com.tencent.wcdb.database.SQLiteDatabase";
        public static String mWeiXinSQLiteDatabaseInsertMethodName = "insertWithOnConflict";

    由于微信的WebView和WebViewClient都是自定义的,所有在onPageFinished方法中获得Cookie是通过调用微信提供的方法获得的,一般情况下,我们要在onPageFinished中获得Cookie,只需要执行如下代码即可:

        CookieManager cookieManager = CookieManager.getInstance();
        String cookieStr = cookieManager.getCookie(url);

    获得Cookie后,我们就可以将Cookie发送给服务端,服务端通过接口来请求微信账单数据了,这个Cookie是有失效时长的,如果服务端发现Cookie过期,则重新向App请求Cookie,App会再次打开微信账单页面,刷新Cookie并返回给服务端。

    四、总结

          需要总结的不多,基本都是https://blog.csdn.net/xiao_nian/article/details/79881274里面使用的技巧,不得不说微信的混淆还是做得很好的,代码里面基本都是adcd之类的。

     

    严重声明

    本文的意图只有一个就是通过分析app学习更多的逆向技术,如果有人利用本文知识和技术进行非法操作进行牟利,带来的任何法律责任都将由操作者本人承担,和本文作者无任何关系,最终还是希望大家能够秉着学习的心态阅读此文。

    展开全文
  • 收款码截取二维码

    2020-02-15 16:46:03
    收款码截取二维码 由于支付宝微信生成的二维码,都会很大,这里我们只需要最关键的部分截取即可。这里写一篇博客主要是记录相关大小定位。其实这个定位我们可以打开window自带的画图工具得知。 不带金额的支付宝 ...

    收款码截取二维码

    由于支付宝微信生成的二维码,都会很大,这里我们只需要最关键的部分截取即可。这里写一篇博客主要是记录相关大小定位。其实这个定位我们可以打开window自带的画图工具得知。

    不带金额的支付宝二维码

              
     BufferedImage bufImage = ImageIO.read(new ClassPathResource("static/images/zfb/normal.jpg").getInputStream());
     BufferedImage subimage = bufImage.getSubimage(130, 360, 320, 320);
     ImageIO.write(subimage,"JPEG", new File("D:\\normal.jpg"));
    

    带金额的支付宝二维码

     File []files = new ClassPathResource("static/images/zfb").getFile().listFiles();
     for (int i = 0; i < files.length; i++) {
         BufferedImage bufImage = ImageIO.read(files[i]);
         System.out.println(files[i].getName());
         BufferedImage subimage = bufImage.getSubimage(240, 520, 430, 430);
         ImageIO.write(subimage,"JPEG", new File("D:\\"+files[i].getName()));
    }
    

    微信不带金额的二维码

    BufferedImage bufImage = ImageIO.read(new ClassPathResource("static/images/vx/normal.jpg").getInputStream());
    BufferedImage subimage = bufImage.getSubimage(340, 420, 570, 570);
    ImageIO.write(subimage,"JPEG", new File("D:\\normal.jpg"));
    
    展开全文
  • 微信,支付宝,收款二维码实时生成订单监控,免签支,付支付系统,个人收款,收款二维码 微信和支付宝个人支付二维码生成与监控!有PHP接口回调,个人收款好助手! 实现收款即时到个人微信或支付宝账户!方便安全。 这...

    微信,支付宝,收款二维码实时生成订单监控,免签支,付支付系统,个人收款,收款二维码
    微信和支付宝个人支付二维码生成与监控!有PHP接口回调,个人收款好助手!

    实现收款即时到个人微信或支付宝账户!方便安全。

    这个APP的作用是对自己的网站或者任何业务进行搭建支付系统!可以把钱收到自己的微信或者支付宝,财付通里面!不要在问我这是干什么的了!

    支付宝、微信个人收款二维码实时生成,通过app实现,app对外提供web接口,访问web接口传递金额、备注、类型等参数,app收到请求后会去生成对应金额和备注的收款二维码并且把数据返回到接口,全部采用hook操作,生成速度快,生成一个二维码0.5秒作
    右,非模拟操作
    这里写图片描述

    在这里插入图片描述

    这里写图片描述

    这里写图片描述

    这里写图片描述

    演示地址

    在这里插入图片描述

    转载于:https://www.cnblogs.com/jpfss/p/9764808.html

    展开全文
  • 最近折腾了一下合并收款码,简单记录一下折腾的过程,方法不唯一,只是提供一种思路,如果各位大佬有更加简单粗暴的办法,那就更好了。 原理 首先解析出三个二维码的内容,用 Nginx 判断 User agent 后,返回302,...
  • 移动支付给人们的生活带来了巨大的改变,很多人调侃好几年没“摸过纸钞”了。...目前市面上常用的合并收款app有不少,商家可以通过这些APP将自己的微信收款码和支付宝收款码合并生成一个收钱码,顾客扫码付款
  • 关于微信二维码支付的一点点总结.如上一个博客所说,开始开发前需要前往官网进行一系列的接入,从而得到相关的appid,密钥. 本次的开发中,使用谷歌zxing实现将支付链接字符串转为二维码.附上相关依赖: &lt;!-- ...
  • 二维码

    2012-04-18 14:52:24
    4.物品追踪:会议资料、生产零件、客户服务、邮购运送、维修记录、危险物品、后勤补给、生态研究; 5.资料保密:商业机密、政治情报、军事机密、私人信函。 条码识别  条码识别是采用各种条码扫描器把...
  • Android精选源码 一个漂亮而强大的自定义view SeekBar 适用于Android的简单NFC读取源码 ...android实现收款二维码保存代码 RxBus 一个简易、非反射的Android事件通知库 一个基于谷歌CameraView library...
  • 本文教你如何生成 支付宝 捐赠二维码,转帖请附上出处,谢谢! 撰写了一篇对读者有帮助的博客, 贡献了一套对开发者有用的开源项目, 上架了一个免费的App,用户觉得好用想回报作者, 上架了一个收费的App,小白...
  • 我们有时后会遇到这样一个场景,当进入某一个界面的时候需要这个界面高亮显示,而其他界面则是正常的亮度。比如说,在使用支付宝收付款时,进入二维码展示界面时,页面会变高亮。
  • 主要再次做一下记录,防止以后又重新找一边。主要是支付宝跳转: //微信 //weixin://dl/scan 扫一扫 // "weixin://dl/moments"朋友圈 // String intentFullUrl ="alipayqr://platformapi/startapp?....
  • 微信收款监控

    2018-11-19 14:54:01
    微信收款监控,扫描二维码收款提醒,笔笔秒到账播报,开店的福利
  • 本文教你怎样生成 支付宝捐赠二维码,转帖请附上出处,谢谢! 撰写了一篇对读者有帮助的博客。 贡献了一套对开发人员实用的开源项目, 上架了一个免费的App,用户认为好用想回报作者, 上架了一个收费的App...
  • 它们让我们的生活变得便利,其中尤以二维码为我们的生活提供了巨大的便利,如网址链接二维码, 电子名片二维码, 文件生成二维码(图片,pdf,音频,视频), 文本二维码, 收款二维码, 微信群二维码等等。 二维码大都...
  • 今年9月,智能共享自行车“摩拜单车”正式进入北京,成为越来越多人解决最后一公里...针对上述问题,摩拜公司相关负责人表示,已经和征信机构开展了合作,“恶意滥用、损毁甚至盗窃摩拜单车的行为将影响其征信记录”。
  • 在日常的生活中我们会遇见很多类似微信扫码的功能,比如扫码支付,比如生成个人的二维码,比如利用二维码展示自己的信息,利用二维码收款等等一系列的很多的和二维码息息相关的产品,那么这个二维码是怎么生成的呢?...
  • 支付宝获取二维码充值

    千次阅读 2018-01-02 15:26:26
    获取充值二维码
  • 清华大学中国经济社会数据研究中心与腾讯联合...中关村工信二维码技术研究院院长张超表示,中国已成为二维码应用最广泛的国家,中国二维码应用占全球九成以上。 扫二维码监督后厨 今天带大家了解的是餐饮服务业二维码
  • Java生成艺术二维码也可以很简单

    千次阅读 2019-11-13 19:27:03
    现在二维码可以说非常常见了,当然我们见得多的一般是白底黑块,有的再中间加一个 logo,或者将二维码嵌在一张特定的背景中(比如微信、支付宝的收款码);偶尔也可能看到一些酷炫的二维码,比如非黑白的、渐变色的...
  • 如何测试扫码支付二维码

    千次阅读 2021-02-06 16:07:58
    收款方生成二维码,支付方拿着手机去扫码。 知道使用场景了,接着拆分功能点,从字面上“二维码扫码支付”,这7个字可以拆分成3个关键字:二维码,扫码,支付 二维码场景用例 针对二维码写用例,可以分: 生成的...
  • 在这里记录一下。也方便一下后来的同学。不足之处请指正~1.如何将二维码图片(包括布局整个页面保存下来)保存下来?经过查找资料发现用到的知识点是:将View转化为Bitmap并保存下来有一个方法是使用:View里面有几...
  • 二维码生成机制

    千次阅读 2017-02-15 18:10:05
    版本信息,如图所示的两块3*6模块,用于记录和说明该二维码的版本。一共18个模块,代表着十八位二进制,其中六位二进制用于版本信息的数据编码,另外十二位为版本信息的纠错编码(之所以需要六位二进制,是因为...
  • 微信和支付宝小程序二维码的聚合

    千次阅读 热门讨论 2018-12-25 15:31:22
    这篇文章是用来记录我解决公司需要聚合微信和支付宝小程序的二维码聚合的过程。 首先是微信和支付宝的小程序都各自会有二维码,我们先解析出二维码的内容。都是一个url,我们用各自的浏览器打开,发现支付宝是可以...
  • 二维码的衍生机会

    2020-05-18 03:03:58
    最近呢,有人注意到一个现象,新加坡的app榜单里,排名靠前的,突然出现了一堆二维码的扫码工具。其实这事的原因很简单,因为新加坡疫情实施断路器措施,5.12日有限度解封部分商业设施,那么为...
  • 二维码扫码支付原理

    千次阅读 2019-09-26 18:28:29
    平常我们在购物付款时,使用手机中的微信或支付宝扫一扫即可完成支付,无需像以前携带现金等...线下所有的扫码支付都是以扫二维码开始,通过扫描二维码,我们可以看到付款页面商家的名称,所以二维码在这里承担的角...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,028
精华内容 411
关键字:

二维码收款记录怎么删除