一、说明
现在的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
=RZ
13ik
0FHP
2IeX
6b
6LsZrDBFM
1yHW
464mobilegwRZ
13;
其中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++) {
-