RedFree 发表于 2020-2-14 15:15

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代码**

![]()

## **四、实际效果演示**

![]()

![]()

夜步城 发表于 2020-2-16 11:20

DDMS的跟踪分析有没有使用过程啊

ccraker 发表于 2020-6-3 10:54

2.6.7版本的,有研究过吗,我用的是frida来做hook的,然后你说的这几个方法都hook了,付款的时候,会显示付款成功,但是页面会卡死……然后只能强制关闭,重新打开之后,充值失败。

h132011424 发表于 2020-2-15 21:24

弱弱的问一句,有成品吗

lies2014 发表于 2020-2-14 19:50

图片都看不到

工程欧巴 发表于 2020-2-14 22:48

属实厉害

夜步城 发表于 2020-2-16 11:14

赞,用到恰处了!!

Yubai 发表于 2020-2-17 12:54

谢谢 我来支持一下看看

18372261830 发表于 2020-2-17 17:15

这个厉害,但是要自己做模块

Aim灬血饮 发表于 2020-2-18 13:40

是个狼灭

Shepherdmyq 发表于 2020-2-19 11:46

厉害厉害了
页: [1] 2 3
查看完整版本: Xposed绕过元气骑士内购验证