用了这个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编辑 ,原因: