一、说明
现在的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学习更多的逆向技术,如果有人利用本文知识和技术进行非法操作进行牟利,带来的任何法律责任都将由操作者本人承担,和本文作者无任何关系,最终还是希望大家能够秉着学习的心态阅读此文。