Xposed绕过元气骑士内购验证
本帖最后由 RedFree 于 2020-2-14 23:10 编辑**申明:本文仅用于技术研究所用,请支持正版游戏!**
## **一、APK反编译**
从官网下载最新版本(2.5.1)的元气骑士apk安装包:
http://www.chillyroom.com/yqqs-2501.apk
解压并保存apk中的文件:`classes.dex`
使用`dex2jar`获取jar文件,到这里便可以使用`jd-gui`打开这个jar包并查看反编译的java代码。为了方便搜索,可以使用`jd-gui`的导出源代码功能导出所有反编译的文件。
![]()
## **二、内购功能点分析**
走正常的购买流程购买任意物品,在付款前返回,得到如下的消息:
![]()
![]()
可以看到返回了两个消息:
```
订单取消user_calcelled.
null 校验订单失败:unpaid
```
**以'订单取消'作为关键字搜索,定位到了以下点:**
![]()
**以'校验订单失败'作为关键字搜索,定位到以下两个点:**
![]()
## **三、代码分析 + Xposed Hook**
### **1、第一个点(订单取消)绕过**
```
protected void onActivityResult(int paramInt1, int paramInt2, Intent paramIntent) {
try {
if (paramInt1 == Pingpp.REQUEST_CODE_PAYMENT && paramIntent != null && paramIntent.getExtras() != null) {
String str3 = paramIntent.getExtras().getString("pay_result");
String str2 = paramIntent.getExtras().getString("error_msg");
String str1 = paramIntent.getExtras().getString("extra_msg");
if (this.payingOrderId != null && !"".equals(this.payingOrderId)) {
if ("success".equals(str3)) {
onPaySuccess(this.payingOrderId, "", "");
Toast.makeText((Context)this, "支付成功", 0).show();
} else {
StringBuilder stringBuilder;
if ("fail".equals(str3)) {
onPayFail(this.payingOrderId, "");
stringBuilder = new StringBuilder();
stringBuilder.append("支付失败:");
stringBuilder.append(str2);
stringBuilder.append(",");
stringBuilder.append(str1);
Toast.makeText((Context)this, stringBuilder.toString(), 1).show();
} else if ("cancel".equals(stringBuilder)) {
onPayCancel(this.payingOrderId, "");
stringBuilder = new StringBuilder();
stringBuilder.append("订单取消");
stringBuilder.append(str2);
stringBuilder.append(",");
stringBuilder.append(str1);
Toast.makeText((Context)this, stringBuilder.toString(), 0).show();
} else if ("invalid".equals(stringBuilder)) {
onPayFail(this.payingOrderId, "");
stringBuilder = new StringBuilder();
stringBuilder.append("支付失败:");
stringBuilder.append(str2);
stringBuilder.append(",");
stringBuilder.append(str1);
Toast.makeText((Context)this, stringBuilder.toString(), 1).show();
} else if ("unknown".equals(stringBuilder)) {
onPayUnknown(this.payingOrderId, "");
stringBuilder = new StringBuilder();
stringBuilder.append("未知错误:");
stringBuilder.append(str2);
stringBuilder.append(",");
stringBuilder.append(str1);
Toast.makeText((Context)this, stringBuilder.toString(), 1).show();
}
}
this.payingOrderId = null;
return;
}
}
} catch (Exception exception) {
Log.i("Unity", exception.toString());
}
}
```
可以看到只要让`paramIntent.getExtras().getString("pay_result");`的值为`success`便通过了此处验证。
**Xposed Hook代码**
![]()
### **2、第二个点(校验订单)绕过**
> com.chillyroomsdk.sdkbridge.order.https_task.OrderCheckTask
```
protected void onPostExecute(String paramString) {
String str = TAG;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("onPostExecute: ");
stringBuilder.append(paramString);
Log.i(str, stringBuilder.toString());
if (paramString != null && !paramString.equals("")) {
OrderInfo orderInfo = new OrderInfo();
orderInfo.orderId = this.m_orderId;
try {
String str1;
JSONObject jSONObject = new JSONObject(paramString);
if (jSONObject.getInt("retcode") == 0) {
jSONObject = jSONObject.getJSONObject("order");
if (jSONObject.getInt("can_send") == 1) {
orderInfo.productName = jSONObject.getString("item_name");
orderInfo.productId = jSONObject.getString("item_id");
float f = jSONObject.getInt("total_price") / 100.0F;
((IPayAgent)UnityPlayer.currentActivity).onPayCheckSuccess(this.m_orderId, orderInfo.productId, orderInfo.productName, f, "");
if (this.m_dialog != null) {
this.m_dialog.dismiss();
return;
}
} else {
if (this.m_count > 0) {
StartTaskOnMainThread(this.m_dialog, this.m_orderId, this.m_count, this.m_time_count);
} else {
Toast.makeText(this.m_context, "订单支付失败。若发生丢单请勿重复支付,并联系客服", 1).show();
if (this.m_dialog != null)
this.m_dialog.dismiss();
}
str1 = TAG;
stringBuilder = new StringBuilder();
stringBuilder.append("此订单支付失败:");
stringBuilder.append(orderInfo.orderId);
Log.i(str1, stringBuilder.toString());
return;
}
} else {
str1 = str1.getString("msg");
Context context = this.m_context;
StringBuilder stringBuilder1 = new StringBuilder();
stringBuilder1.append(orderInfo.productName);
stringBuilder1.append(" 校验订单失败:");
stringBuilder1.append(str1);
Toast.makeText(context, stringBuilder1.toString(), 1).show();
if (this.m_count > 0) {
StringBuilder stringBuilder2 = new StringBuilder();
stringBuilder2.append("count : ");
stringBuilder2.append(this.m_count);
Log.i("Unity", stringBuilder2.toString());
StartTaskOnMainThread(this.m_dialog, this.m_orderId, this.m_count, this.m_time_count);
return;
}
if (this.m_dialog != null) {
this.m_dialog.dismiss();
return;
}
}
} catch (JSONException jSONException) {
jSONException.printStackTrace();
return;
}
} else {
if (this.m_count > 0) {
StartTaskOnMainThread(this.m_dialog, this.m_orderId, this.m_count, this.m_time_count);
return;
}
Toast.makeText(this.m_context, "订单支付失败。若发生丢单请勿重复支付,并联系客服", 1).show();
if (this.m_dialog != null)
this.m_dialog.dismiss();
}
}
```
通过代码逻辑可以分析得出:参数paramString是一个`JSON`字符串,格式如下时便可以通过校验:
```
{"retcode":0,"order":{"can_send":1,"item_name":"商品名称","item_id":商品编号,"total_price":0},"msg":"购买成功"}
```
基中的`商品名称`和`商品编号`暂时未知。
**Xposed Hook代码**
![]()
### **3、第三个点(校验订单)绕过**
> com.chillyroomsdk.sdkbridge.order.https_task.OrderRestoreTask
这个点是用来恢复有问题订单的,原理同第二个点,不再重复说明。
**Xposed Hook代码**
![]()
### **4、商品信息获取**
上面第二和第三个点都需要商品名称和商品编号这个关键信息,通过DDMS的跟踪分析,发现了这个点:
> com.chillyroomsdk.sdkbridge.order.BaseOrderAgent.requestOrder
![]()
![]()
可以看到商品名称、编号、价格这里都有了。因此Hook这个点,设置变量内容便可以在购买成功前设置购买商品的信息。
**Xposed Hook代码**
![]()
## **四、实际效果演示**
![]()
![]() DDMS的跟踪分析有没有使用过程啊 2.6.7版本的,有研究过吗,我用的是frida来做hook的,然后你说的这几个方法都hook了,付款的时候,会显示付款成功,但是页面会卡死……然后只能强制关闭,重新打开之后,充值失败。 弱弱的问一句,有成品吗 图片都看不到 属实厉害
赞,用到恰处了!! 谢谢 我来支持一下看看 这个厉害,但是要自己做模块 是个狼灭 厉害厉害了