某动画APP分析
2020-04-03 17:36:54 Author: bbs.pediy.com(查看原文) 阅读量:811 收藏

前言

用了这个APP很久了,网上找的修改版都然并卵——测试时发现只改了本地几个功能,不支持嘀哩源和超清源播放——最重要的功能被阉割了。
无聊做一下分析,APP是官网的最新版v4.6。

授权分析

反编译后发现APP的类名和方法名都做了混淆,发现smali中的source文件名并未被as去除,可以修复类名混淆。这里为了调试,暂时不修改APP。
载入JEB,定位到关键类info.zzjian.dididh.mvp.model.entity.UserInfo,在getExpire方法下断:

打开APP,JEB调试器附加该进程:

点击APP内头像,成功断下,可以在右边看到此时的局部变量:

双击值可修改,将expire改成 2100-01-01 ,恢复断点并运行:

成功显示会员提示。

按x键交叉引用,定位到另一个方法,该方法判断了是否登录以及VIP是否到期,可以看到许多功能的执行都由该方法决定:

因此只需让该方法返回true,即可免登录使用会员功能。

hook这两个方法即可破解,frida脚本如下:

Java.perform(function () {
    var UserInfo = Java.use("info.zzjian.dididh.mvp.model.entity.UserInfo");
    UserInfo.getExpire.implementation = function () {
        return "2100-01-01";
    };
    var isValid = Java.use("info.zzjian.dididh.util.བཅོམ.ཕྱིན");
    isValid.ཤེ.implementation = function () {
        return true;
    }
});

经测试各种源均正常使用——用frida注入未引起异常,说明APP对签名做了校验。
可能所做的和谐有未考虑到的地方,这里不做进一步分析。

校验分析

APP重签名后安装运行,提示如下:

消息是从服务器返回的,抓包看看:

协议头的imei引起注意,找找在哪:

IDA载入libnative-lib.so,定位函数Java_info_zzjian_dididh_util_CheckUtils_getV:

int __fastcall Java_info_zzjian_dididh_util_CheckUtils_getV(_JNIEnv *a1, int a2, unsigned int a3, unsigned int a4)
{
  _JNIEnv *env; // r4
  jclass v5; // r0
  void *v6; // r5
  jmethodID v7; // r0
  void *currentApplication; // r9
  jclass app__; // r11
  jclass app; // r0
  void *app_; // r8
  jmethodID v12; // r0
  void *ApplicationInfo; // r0
  void *ApplicationInfo_; // r5
  jclass v15; // r0
  struct _jfieldID *v16; // r0
  jobject className; // r5
  jmethodID v18; // r0
  jmethodID v19; // r0
  void *PackageManager; // r8
  jmethodID v21; // r0
  void *PackageName; // ST04_4
  jclass v23; // r0
  jmethodID v24; // r0
  void *PackageInfo; // r0
  void *PackageInfo_; // r6
  jclass v27; // r0
  struct _jfieldID *v28; // r0
  jobject v29; // r0
  jobject v30; // r0
  jobject v31; // r6
  jclass v32; // r0
  jmethodID v33; // r0
  jclass v34; // r0
  jmethodID v35; // r5
  int v36; // r8
  int v37; // r0
  int v38; // r6
  int v39; // r1

  env = a1;
  v5 = a1->functions->FindClass(&a1->functions, "android/app/ActivityThread");
  if ( v5 )
  {
    v6 = v5;
    v7 = env->functions->GetStaticMethodID(&env->functions, v5, "currentApplication", "()Landroid/app/Application;");
    if ( v7 )
      currentApplication = (void *)_JNIEnv::CallStaticObjectMethod(env, v6, v7);
    else
      currentApplication = 0;
    env->functions->DeleteLocalRef(&env->functions, v6);
  }
  else
  {
    currentApplication = 0;
  }
  app__ = env->functions->GetObjectClass(&env->functions, currentApplication);
  app = env->functions->GetObjectClass(&env->functions, currentApplication);
  app_ = app;
  v12 = env->functions->GetMethodID(
          &env->functions,
          app,
          "getApplicationInfo",
          "()Landroid/content/pm/ApplicationInfo;");
  ApplicationInfo = (void *)_JNIEnv::CallObjectMethod(env, currentApplication, v12);
  ApplicationInfo_ = ApplicationInfo;
  v15 = env->functions->GetObjectClass(&env->functions, ApplicationInfo);
  v16 = env->functions->GetFieldID(&env->functions, v15, "className", "Ljava/lang/String;");
  className = env->functions->GetObjectField(&env->functions, ApplicationInfo_, v16);
  env->functions->GetObjectClass(&env->functions, className);
  v18 = env->functions->GetMethodID(&env->functions, app_, "hashCode", "()I");
  _JNIEnv::CallIntMethod(env, className, v18);
  v19 = env->functions->GetMethodID(
          &env->functions,
          app__,
          "getPackageManager",
          "()Landroid/content/pm/PackageManager;");
  PackageManager = (void *)_JNIEnv::CallObjectMethod(env, currentApplication, v19);
  v21 = env->functions->GetMethodID(&env->functions, app__, "getPackageName", "()Ljava/lang/String;");
  PackageName = (void *)_JNIEnv::CallObjectMethod(env, currentApplication, v21);
  v23 = env->functions->GetObjectClass(&env->functions, PackageManager);
  v24 = env->functions->GetMethodID(
          &env->functions,
          v23,
          "getPackageInfo",
          "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
  PackageInfo = (void *)_JNIEnv::CallObjectMethod(env, PackageManager, v24);
  PackageInfo_ = PackageInfo;
  v27 = env->functions->GetObjectClass(&env->functions, PackageInfo);
  v28 = env->functions->GetFieldID(&env->functions, v27, "signatures", "[Landroid/content/pm/Signature;");
  v29 = env->functions->GetObjectField(&env->functions, PackageInfo_, v28);
  v30 = env->functions->GetObjectArrayElement(&env->functions, v29, 0);
  v31 = v30;
  v32 = env->functions->GetObjectClass(&env->functions, v30);
  v33 = env->functions->GetMethodID(&env->functions, v32, "hashCode", "()I");
  _JNIEnv::CallIntMethod(env, v31, v33);
  v34 = env->functions->GetObjectClass(&env->functions, PackageName);
  v35 = env->functions->GetMethodID(&env->functions, v34, "concat", "(Ljava/lang/String;)Ljava/lang/String;");
  v36 = sub_BDC(env);
  sub_BDC(env);
  sub_BDC(env);
  env->functions->NewStringUTF(&env->functions, "dt8re");
  v37 = _JNIEnv::CallObjectMethod(env, v36, v35);
  v38 = _JNIEnv::CallObjectMethod(env, v37, v35);
  env->functions->NewStringUTF(&env->functions, "rt9ws");
  v39 = _JNIEnv::CallObjectMethod(env, v38, v35);
  return _JNIEnv::CallObjectMethod(env, v39, v35);
}

int __fastcall sub_BDC(_JNIEnv *env)
{
  _JNIEnv *env_; // r6
  jclass v2; // r0
  jclass v3; // r4
  jmethodID v4; // r0

  env_ = env;
  v2 = env->functions->FindClass(&env->functions, "java/lang/Long");
  v3 = v2;
  v4 = env_->functions->GetStaticMethodID(&env_->functions, v2, "toHexString", "(J)Ljava/lang/String;");
  return _JNIEnv::CallStaticObjectMethod(env_, v3, v4);
}

首先从ActivityThread.currentApplication()获取到了Application对象,接着调用Application.getPackageName()、Application.getApplicationInfo().className.hashCode()以及Application.getPackageManager().getPackageInfo().signatures[0].hashCode(),通过计算后拼接字符串返回给了java层。

计算结果也同传给native的参数有关,用frida打印下参数及返回值:

Java.perform(function () {
    var CheckUtils = Java.use("info.zzjian.dididh.util.CheckUtils");
    CheckUtils.getV.overload("long").implementation = function (j) {
        var ret = this.getV(j);
        console.log(j, ret);
        return (ret);
    };
});

输出如下:

1585646273280 2e59c8af99fdt8re174deeabb0crt9ws1752f0c063f
1585646273281 2e59c8af9a1dt8re174deeabb0drt9ws1752f0c0640
1585646273282 2e59c8af9a3dt8re174deeabb0ert9ws1752f0c0641
1585646276553 2e59c8b1331dt8re174deeac7d5rt9ws1752f0c1308
1585646276562 2e59c8b1343dt8re174deeac7dert9ws1752f0c1311

传入的参数为时间戳,dt8re和rt9ws只参与连接,总共有三处数据的计算。
指定下参数为0:

Java.perform(function () {
    var CheckUtils = Java.use("info.zzjian.dididh.util.CheckUtils");
    CheckUtils.getV.overload("long").implementation = function (j) {
        var ret = this.getV(j);
        console.log(j, this.getV(0), ret);
        return (ret);
    };
});

输出如下:

1585646644557 33cc6f39fdt8re3af08b80crt9ws3ff2a033f 2e59c964e39dt8re174def06559rt9ws1752f11b08c
1585646645258 33cc6f39fdt8re3af08b80crt9ws3ff2a033f 2e59c9653b3dt8re174def06816rt9ws1752f11b349
1585646645259 33cc6f39fdt8re3af08b80crt9ws3ff2a033f 2e59c9653b5dt8re174def06817rt9ws1752f11b34a
1585646646040 33cc6f39fdt8re3af08b80crt9ws3ff2a033f 2e59c9659cfdt8re174def06b24rt9ws1752f11b657
1585646646041 33cc6f39fdt8re3af08b80crt9ws3ff2a033f 2e59c9659d1dt8re174def06b25rt9ws1752f11b658

拆分字符串后得到native中三组数据的校验值。
用java重写该校验类,固定住原版数据,算法如下:

package info.zzjian.dididh.util;
public class CheckUtils {
    public CheckUtils() {
    }

    public String getV(long ts) {
        String check1 = Long.toHexString(13904573343L + ts * 2);
        String check2 = Long.toHexString(15821486092L + ts);
        String check3 = Long.toHexString(17165845311L + ts);
        return check1 + "dt8re" + check2 + "rt9ws" + check3;
    }
}

将该类编译后放入包名对应的文件夹内,用dx转为dex,再用baksmali转为smali。

这里提供个自己写的java类转为smali的shell脚本,java类不能引入外部包:

#!/bin/bash
dxpath='/Users/akari/Library/Android/sdk/build-tools/29.0.2/dx'
baksmalipath='/Users/akari/dex2jar/d2j-baksmali.sh'

name=${1##*/}
p1=$(grep 'package' ${1}| cut -d ' ' -f 2)
p2=${p1//.//}
pack=${p2%%;*}
mkdir -p ${pack}
javac ${1} -d ./
${dxpath} --dex --output=tmp.dex $pack/${name%.*}.class
${baksmalipath} tmp.dex
save=${1%/*}
if [[ "${save}" != *"/"* ]]; then
        save=./${save%%.*}.smali
fi
mv tmp-out/${pack}/${name%.*}.smali ${save}
rm -rf tmp-out tmp.dex ${pack%%/*}

执行后会在源码同目录生成该类对应的smali:

.class public Linfo/zzjian/dididh/util/CheckUtils;
.super Ljava/lang/Object;
.source "CheckUtils.java"

.method public constructor <init>()V
    .registers 1
    .prologue
    .line 3
    invoke-direct { p0 }, Ljava/lang/Object;-><init>()V
    .line 4
    return-void
.end method

.method public getV(J)Ljava/lang/String;
    .registers 8
    .prologue
    .line 7
    const-wide v0, 13904573343L
    const-wide/16 v2, 2
    mul-long/2addr v2, p1
    add-long/2addr v0, v2
    invoke-static { v0, v1 }, Ljava/lang/Long;->toHexString(J)Ljava/lang/String;
    move-result-object v0
    .line 8
    const-wide v2, 15821486092L
    add-long/2addr v2, p1
    invoke-static { v2, v3 }, Ljava/lang/Long;->toHexString(J)Ljava/lang/String;
    move-result-object v1
    .line 9
    const-wide v2, 17165845311L
    add-long/2addr v2, p1
    invoke-static { v2, v3 }, Ljava/lang/Long;->toHexString(J)Ljava/lang/String;
    move-result-object v2
    .line 10
    new-instance v3, Ljava/lang/StringBuilder;
    invoke-direct { v3 }, Ljava/lang/StringBuilder;-><init>()V
    invoke-virtual { v3, v0 }, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v0
    const-string v3, "dt8re"
    invoke-virtual { v0, v3 }, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v0
    invoke-virtual { v0, v1 }, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v0
    const-string v1, "rt9ws"
    invoke-virtual { v0, v1 }, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v0
    invoke-virtual { v0, v2 }, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v0
    invoke-virtual { v0 }, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
    move-result-object v0
    return-object v0
.end method

将其替换原来的CheckUtils.smali,重新打包签名,测试一下:

成功过native层校验,该so的功能已被取代。

协议分析

算法不难,随手写了份协议,看代码即可理解:

import base64
import random
import time

import requests


class didi:
    def __init__(self):
        self.root = 'https://dili-api.zzjian.club/'
        self.ts = int(round(time.time() * 1000))
        self.uid = 0
        self.header = {'app-version': '1107',
                       'deviceId': str(random.randint(10000000, 99999999)),
                       'imei': hex(13904573343 + self.ts * 2) + 'dt8re' + hex(15821486092 + self.ts) + 'rt9ws' + hex(
                           17165845311 + self.ts),
                       }

    def get(self, func, params=None):
        if params is None:
            params = {}
        return requests.get(url=self.root + func,
                            params=params,
                            headers=self.header).json()

    def post(self, func, data=None):
        if data is None:
            data = {}
        return requests.post(url=self.root + func,
                             data=data,
                             headers=self.header).json()

    def register(self, nickname, email, password, invitecode=''):
        ret = self.post('account/register',
                        {'nickname': nickname, 'email': email, 'password': password, 'inviteCode': invitecode})
        self.init(ret)
        return ret

    def login(self, email, password):
        ret = self.post('account/login', {'email': email, 'password': password})
        self.init(ret)
        return ret

    def setAvatar(self, picUrl):
        return self.post('account/avatar', {'uid': self.uid, 'url': picUrl + '?t=' + str(self.ts)}) == {}

    def updatePassword(self, oldPw, newPw):
        return self.post('account/updatePassword', {'oldPw': oldPw, 'newPw': newPw}) == {}

    def addScore(self, type, func='account'):
        return self.get(func + '/addScore', {'type': type}) == {}

    def getScore(self):
        return self.get('wx/getScore')['score']

    def init(self, ret):
        try:
            self.uid = ret['uid']
            self.header['session'] = base64.b64encode(bytes(str(self.ts - self.uid).encode('utf-8')))
            self.header['UID'] = str(self.uid)
        except Exception:
            pass


def RandomEmail():
    return "".join(random.choice("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPWRSTUVWXYZ") for i in
                   range(random.randint(4, 10))) + random.choice(["@qq.com", "@163.com", "@126.com", "@189.com"])


dd = didi()
# print(dd.register(str(random.randint(10000000, 99999999)), RandomEmail(), '123456x'))  # 注册账号
print(dd.login("[email protected]", "123456x"))  # 登录账号
print(dd.addScore("BANNER_AD"))  # 每日10积分
print(dd.addScore("VIDEO_AD"))  # 每日50积分
print(dd.addScore("WX_SIGN_IN", 'wx'))  # 每日10积分
print(dd.addScore("WX_BANNER_AD", 'wx'))  # 每日50积分
print(dd.getScore())  # 取积分
print(dd.setAvatar('http://xxx.gif'))  # 可设置动态头像,APP里需要6000积分
print(dd.updatePassword("123456x", '123456'))  # 改密码

其他功能留给各位自己探索。

最后

该APP为良心软件,也不贵(永久会员50块钱),希望有能力的支持下正版。

2020安全开发者峰会(2020 SDC)议题征集 中国.北京 7月!

最后于 16小时前 被Fireeye编辑 ,原因:


文章来源: https://bbs.pediy.com/thread-258593.htm
如有侵权请联系:admin#unsafe.sh