某音adult_version防护分析&加密分析&邀请机制刷vip分析

查看 179|回复 11
作者:red1y   
0. 本文包括
  • 防护分析:模拟器检测、root检测、反hook、反抓包等
  • 加密分析:数据加密分析、身份验证的Token参数生成分析
  • 设备注册逻辑分析:无限获得新帐号、绑定邀请码刷vip
  • b站视频链接:https://www.bilibili.com/video/BV12e411V7a5/?vd_source=23b9de401c27e819abddbd5551eddbda

    一、防护分析
    [ol]

  • 提示不能在模拟器里运行



  • image-20221023120903559.png (53.26 KB, 下载次数: 1)
    下载附件
    2022-10-23 19:08 上传

  • 定位检测代码
    入口:com.niming.weipa.ui.splash.SplashActivity
    home.php?mod=space&uid=1892347  // com.niming.weipa.base.BaseActivity
    protected void onCreate(home.php?mod=space&uid=1043391 Bundle arg2) {
    if(com.blankj.utilcode.util.e.d()) {
         com.blankj.utilcode.util.e.b(this, false);
    }
    super.onCreate(arg2);
    Intent v2 = this.getIntent();
    Intrinsics.checkExpressionValueIsNotNull(v2, "intent");
    if((v2.getFlags() & 0x400000) != 0) {
         this.finish();
    }
    两次super到父类的onCreate函数中,查看整个启动逻辑,定位到initView函数
    @Override  // androidx.appcompat.app.AppCompatActivity
    protected void onCreate(@Nullable Bundle arg5) {
    super.onCreate(arg5);
    this.supportRequestWindowFeature(1);
    /* other codes */
    this.activity = this;
    this.loadingDialogFragment = new com.niming.framework.widget.dialog.LoadingDialogFragment.a().a(true).a();
    this.pageStatusHelper = new PageStatusHelper(this, this.getPageBuilder());
    this.pageStatusHelper.a(this);
    this.init();
    this.initView(arg5); // key
    }
    回到SplashActivity的initView函数,发现this.m()函数
    @Override  // com.niming.weipa.base.BaseActivity
    public void initView(@Nullable Bundle arg1) {
    super.initView(arg1);
    v0.d(this);
    com.blankj.utilcode.util.e.d(this, false);
    com.blankj.utilcode.util.e.a(false);
    this.m(); // key
    }
    进入this.m(),发现检测逻辑
    private final void m() {
         LogUtils.b("niming", new Object[]{"===AppUtils.isAppRoot():" + com.blankj.utilcode.util.c.isAppRoot() + " DeviceUtils.isDeviceRooted() " + w.isDeviceRoot()});
         if(!this.checkRoot() && !n.isEmulator(this.activity)) {
             if(this.checkProxy(this)) {
                 NoticeAppDialogFragment v0 = NoticeAppDialogFragment.a("检测到您使用了代{过}{滤}理软件,不允许继续使用");
                                 /* notice codes */
                 v0.c(this.activity);
                 return;
             }
             h.a().a("api_domain", "");
             this.i();
             return;
         }
         NoticeAppDialogFragment v0_1 = NoticeAppDialogFragment.a("检测到您使用的是模拟器或者设备已经root,不允许继续使用");
         /* notice codes */
         v0_1.c(this.activity);
    }

  • root检测方法
    [ol]
  • 通过getRuntime尝试执行su命令
    [/ol]
    public static b a(String[] arg8, String[] arg9, boolean arg10, boolean arg11) {
        StringBuilder v8_2;
        StringBuilder v11;
        StringBuilder v10_2;
        Process v9 = null;
        BufferedReader v4 = null;
        BufferedReader v5 = null;
        DataOutputStream v10 = null;
        String v1 = "";
        int v2 = -1;
        if(arg8 != null && arg8.length != 0) {
            BufferedReader v3 = null;
            try {
                Runtime v4_1 = Runtime.getRuntime();
                String v10_1 = arg10 ? "su" : "sh";
                v9 = v4_1.exec(v10_1, arg9, null);
                v10 = new DataOutputStream(v9.getOutputStream());
            }
                    /* other codess */
            return new b(v2, v8_6, v1);
        }
        return new b(-1, "", "");
    }
  • 检查特定目录下的su文件是否存在
    [/ol]
    public static boolean isDeviceRoot() {
        String[] v0 = {"/system/bin/", "/system/xbin/", "/sbin/", "/system/sd/xbin/", "/system/bin/failsafe/", "/data/local/xbin/", "/data/local/bin/", "/data/local/", "/system/sbin/", "/usr/bin/", "/vendor/bin/"};
        int v3;
        for(v3 = 0; v3

  • emulator检测方法
    [ol]
  • 检测系统属性
    [/ol]
    private static final boolean c(Context arg1) {
        return "1".equals(n.a(arg1, "ro.kernel.qemu"));
    }
  • 检测特征值
    [/ol]
    if((Build.MANUFACTURER.equals("unknown")) || (Build.MANUFACTURER.equals("Genymotion")) || (Build.MANUFACTURER.contains("Andy")) || (Build.MANUFACTURER.contains("MIT")) || (Build.MANUFACTURER.contains("nox")) || (Build.MANUFACTURER.contains("TiantianVM"))) {
        ++v15;
    }
    /* 很多特征值 */
    if((Build.HARDWARE.equals("goldfish")) || (Build.HARDWARE.equals("vbox86")) || (Build.HARDWARE.contains("nox")) || (Build.HARDWARE.contains("ttVM_x86"))) {
        ++v15;
    }
    /* 很多特征值 */
    if((Build.FINGERPRINT.contains("generic/sdk/generic")) || (Build.FINGERPRINT.contains("generic_x86/sdk_x86/generic_x86")) || (Build.FINGERPRINT.contains("Andy")) || (Build.FINGERPRINT.contains("ttVM_Hdragon")) || (Build.FINGERPRINT.contains("generic_x86_64")) || (Build.FINGERPRINT.contains("generic/google_sdk/generic")) || (Build.FINGERPRINT.contains("vbox86p")) || (Build.FINGERPRINT.contains("generic/vbox86p/vbox86p"))) {
        ++v15;
    }
  • 检测OPENGL的属性值
    [/ol]
    try {
        String v2_1 = GLES20.glGetString(0x1F01);
        if(v2_1 != null) {
            if(v2_1.contains("Bluestacks")) {
                goto label_260;
            }
            else {
                boolean v2_2 = v2_1.contains("Translator");
                goto label_257;
            }
        }
    }
    catch(Exception v2) {
        v2.printStackTrace();
    }
    goto label_262;
    label_257:
    if(v2_2) {
        v15 += 10;
        goto label_262;
        label_260:
        v15 += 10;
    }
  • 检测共享文件夹
    [/ol]
    try {
         label_262:
         boolean v2_4 = new File(Environment.getExternalStorageDirectory().toString() + File.separatorChar + "windows" + File.separatorChar + "BstSharedFolder").exists();
    }
    catch(Exception v2_3) {
        v2_3.printStackTrace();
        n.b = v15;
        return n.b > 3;
    }

  • 处理:hook

  • hook失败,定位反hook代码

  • SplashActivity中没有发现相关代码,到application的app com.niming.weipa.App 的onCreate函数中寻找

  • 发现disableXposed函数 (名字当然是我改的...)
    @Override  // com.niming.framework.base_app.BaseApplication
    public void onCreate() {
    super.onCreate();
    App.u0 = this;
    if(this.a(this)) {
         g1.a(this);
         this.b();
         this.c();
         com.shuyu.gsyvideoplayer.i.c.a(com.niming.weipa.d.c.class);
         com.shuyu.gsyvideoplayer.f.a.a(com.niming.weipa.d.d.class);
                 /* other codes */
         com.lahm.library.d.disableXposed(); // 反hook
                 /* other codes */
    }
    }

  • 查看disableXposed实现,通过反射设置disableHooks字段为true
    public boolean disableXposed() {
    try {
         Field v1_3 = ClassLoader.getSystemClassLoader().loadClass("de.robv.android.xposed.XposedBridge").getDeclaredField("disableHooks");
         v1_3.setAccessible(true);
         v1_3.set(null, Boolean.TRUE);
         return true;
    }
    /* exception codes return false */
    }

  • 处理:hook掉,在函数返回后,同样使用反射将disableHooks设置为false

  • 抓不到包,分析反抓包机制

  • 定位反抓包代码,从app的onCreate入手
    @Override  // com.niming.framework.base_app.BaseApplication
    public void onCreate() {
    super.onCreate();
    App.u0 = this;
    if(this.a(this)) {
         g1.a(this);
         this.b();
         this.c();
         com.shuyu.gsyvideoplayer.i.c.a(com.niming.weipa.d.c.class);
         com.shuyu.gsyvideoplayer.f.a.a(com.niming.weipa.d.d.class);
                 /* other codes */
         com.lahm.library.d.disableXposed(); // 反hook
                 /* other codes */
    }
    }

  • 进入this.c(),进行了大量关于http请求的配置,AntiCapture是我一开关注的类,后来发现这个类什么都没干;最终得出结论:抓不到包是因为设置了NO_PROXY属性,app不走系统代{过}{滤}理,因此抓不到包
    private void c() {
    HttpHeaders v0 = new HttpHeaders();
    v0.put("User-Agent", "Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleDart/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17");
    try {
         this.t0 = new b();
         this.t0.d(30000L, TimeUnit.MILLISECONDS);
         /* other codes */
         AntiCapture v1 = a.a(); // 这里获得有关ssl证书的参数,并在下一行设置
         this.t0.a(v1.a, v1.b); // 经过分析,这两个参数都没用,并不是反抓包的实现机制
         this.t0.a(new c());
         this.t0.a(new com.niming.weipa.app.a.a());
         this.t0.a(new com.niming.weipa.app.a.b());
         /* other codes */
         this.t0.a(new ProxySelector() {
             @Override
             public void connectFailed(URI arg1, SocketAddress arg2, IOException arg3) {
             }
             @Override
             public List select(URI arg1) {
                 /* 抓不到包的原因所在! */
                 return Collections.singletonList(Proxy.NO_PROXY);
                 /* 抓不到包的原因所在! */
             }
         });
         c.f.a.b.k().a(this).a(this.t0.a()).a(CacheMode.NO_CACHE).a(-1L).a(0).a(v0);
    }
    /* other codes */
    }

  • 处理:hook掉,在函数调用前,将参数设置为Proxy.getDefault(),默认走系统代{过}{滤}理

  • 签名校验
  • 生成可调试版本后,app会停在某个页面,通过查看日志发现是native层作了签名校验
  • 我一般不分析签名校验,有兴趣可以自己分析

  • 防护类分析

  • 通过disableXposed那个函数,发现了一个很完整的java层加密类,里面实现了很多的防护函数,这里完全拷贝下来,名称我都作了修改
    package com.lahm.library;
    import android.content.Context;
    import android.content.Intent;
    import android.content.IntentFilter;
    import android.content.pm.PackageManager.NameNotFoundException;
    import android.content.pm.Signature;
    import android.os.Debug;
    import android.os.Process;
    import java.io.BufferedReader;
    import java.io.File;
    import java.io.FileReader;
    import java.io.IOException;
    import java.lang.reflect.Field;
    import java.net.InetAddress;
    import java.net.Socket;
    import java.net.UnknownHostException;
    import java.util.HashSet;
    import java.util.Iterator;
    public class i {
    static class b {
         private static final i a;
         static {
             b.a = new i(null);
         }
    }
    private static final String a = "de.robv.android.xposed.XposedHelpers";
    private static final String b = "de.robv.android.xposed.XposedBridge";
    private i() {
    }
    i(com.lahm.library.i.a arg1) {
    }
    public String getAppMetaData(Context arg1, String arg2) {
         return arg1.getApplicationInfo().metaData.getString(arg2);
    }
    public boolean isDebuggerConnected() { // 检测调试器是否连接
         return Debug.isDebuggerConnected();
    }
    public boolean isPortAvailable(int arg2) { // 检测端口是否被占用,可用于反调试、反hook
         try {
             return this.isNetAddressAvailable("127.0.0.1", arg2);
         }
         catch(Exception unused_ex) {
             return true;
         }
    }
    public boolean isApkDebuggable(Context arg1) { // 检测apk:debuggable属性是否为true
         return (arg1.getApplicationInfo().flags & 2) != 0;
    }
    public boolean isLibOrJarLoaded(String arg6) { // 检测加载的jar/so库
         try {
             HashSet v0 = new HashSet();
             BufferedReader v1 = new BufferedReader(new FileReader("/proc/" + Process.myPid() + "/maps"));
             while(true) {
                 String v2 = v1.readLine();
                 if(v2 != null) {
                     goto label_34;
                 }
                 v1.close();
                 Iterator v0_1 = v0.iterator();
                 while(true) {
                 label_24:
                     if(!v0_1.hasNext()) {
                         return false;
                     }
                     Object v1_1 = v0_1.next();
                     if(!((String)v1_1).contains(arg6)) {
                         goto label_24;
                     }
                     return true;
                 label_34:
                     if(!v2.endsWith(".so") && !v2.endsWith(".jar")) {
                         break;
                     }
                     v0.add(v2.substring(v2.lastIndexOf(" ") + 1));
                     break;
                 }
             }
         }
         catch(Exception unused_ex) {
             return false;
         }
    }
    public boolean isNetAddressAvailable(String arg2, int arg3) throws UnknownHostException {
         InetAddress v2 = InetAddress.getByName(arg2); // 检测网络地址是否可用
         try {
             new Socket(v2, arg3);
             return true;
         }
         catch(IOException unused_ex) {
             return false;
         }
    }
    public boolean isDeviceRooted() { //检测设备是否root,里面两个函数的实现在下面
         return this.isDeviceRootedByRoSecure() == 0 ? true : this.isDeviceRootedByFile();
    }
    public boolean isEmulatorByBattery(Context arg4) { // 通过检查电量检测模拟器
         Intent v4 = arg4.registerReceiver(null, new IntentFilter("android.intent.action.BATTERY_CHANGED"));
         return v4 == null ? false : v4.getIntExtra("plugged", -1) == 2;
    }
    public String getPackageSignature(Context arg5) { // 获取签名
         try {
             Signature[] v5_1 = arg5.getPackageManager().getPackageInfo(arg5.getPackageName(), 0x40).signatures;
             StringBuilder v0 = new StringBuilder();
             int v2;
             for(v2 = 0; v2

    [/ol]
    二、加密分析
    [ol]

  • 定位加密代码

  • 这个app通过拦截器实现请求加密,以及携带特殊参数,通过app的onCreate,进入this.c(),接着定位到加密拦截器
    private void c() {
    HttpHeaders v0 = new HttpHeaders();
    v0.put("User-Agent", "Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleDart/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17");
    try {
        /* other codes */
         this.t0.a(new c()); // TOKEN拦截器
         this.t0.a(new com.niming.weipa.app.a.a()); // 加密拦截器
         this.t0.a(new com.niming.weipa.app.a.b());
        /* other codes */
    }
    catch(Exception v0_1) {
         v0_1.printStackTrace();
    }
    }

  • 进入com.niming.weipa.app.a.a(),发现其中的b函数
    private b0 a(b0 arg9) {
    /* other codes */
    v1.put("data", v2);
    String v1_1 = g.a(v1);
    LogUtils.b(new Object[]{"===realParamsString: " + v1_1});
    String v1_2 = URLEncoder.encode(com.niming.weipa.utils.a.b(v1_1)); // 加密函数
    LogUtils.b(new Object[]{"===encrypt realParamsString: " + v1_2});
    new okhttp3.s.a().b("data", v1_2);
    return arg9;
    }

  • 定位到加密类com.niming.weipa.utils.a,使用AES加密,ECB模式,PKCS7的padding
    package com.niming.weipa.utils;
    import android.text.TextUtils;
    import com.blankj.utilcode.util.y;
    public class a {
    private static String a = "";
    private static String b = "pcv#Cg1vbdl#r2hm";
    private static String c = "AES/ECB/PKCS7Padding"; // 加密算法、加密模式、padding模式
    private static String d; // 加密密钥
    static {
         /* other codes */
             v0 = TextUtils.equals("release", "staging") ? TestUtil.getSecretPre() : TestUtil.getSecret();
         a.d = v0;
    }
    public static String b(String arg3) {
         if(a.c.equals("AES/ECB/NoPadding")) {
             while(arg3.getBytes().length % 16 != 0) {
                 arg3 = arg3 + ' ';
             }
         }
         byte[] v0 = a.d.getBytes(); // 获得密钥
         String v1 = a.c; // 加密方式
         byte[] v3 = y.k(arg3.getBytes(), v0, v1, null);
         return v3 == null ? "" : new String(v3);
    }
    }

  • 获得加密密钥

  • 密钥通过native函数获得,可通过hook方式得到加密密钥
    package com.niming.weipa.utils;
    public class TestUtil {
    static {
         System.loadLibrary("security");
    }
    public static native String getSecret() {
    }
    public static native String getSecret2() {
    }
    public static native String getSecret3() {
    }
    public static native String getSecretPre() {
    }
    public static native String getSecretVP() {
    }
    }

  • 验证加密算法

  • 没有改动加密算法及结果,可直接通过再现网站验证
    数据 {"data":"SaiwLbHeTk0xAbNBv8Q9cnc4os2kD8foP+3Ie57JTkc=","handshake":"v20200429"}


    image-20221023184555550.png (39.7 KB, 下载次数: 2)
    下载附件
    2022-10-23 19:08 上传


    [/ol]
    三、设备注册分析
    [ol]

  • 本地sqlite数据库

  • 用户信息存储在本地sqlite数据库中,可以通过sqlite3或者导出数据的方式查看数据库数据,数据库路径为/data/data/package_name/database/db_name
    package com.niming.framework.basedb;
    import android.content.Context;
    import androidx.room.Database;
    import androidx.room.RoomDatabase;
    import androidx.room.q1;
    @Database(entities = {e.class, a.class}, version = 1)
    public abstract class BaseAppDatabase extends RoomDatabase {
    private static volatile BaseAppDatabase a;
    static BaseAppDatabase a(Context arg2) {
         Class v0 = BaseAppDatabase.class;
         if(BaseAppDatabase.a == null) {
             synchronized(v0) {
                 if(BaseAppDatabase.a == null) {
                     BaseAppDatabase.a = (BaseAppDatabase)q1.a(arg2.getApplicationContext(), v0, "base_database").a().b();
                 }
                 return BaseAppDatabase.a;
             }
         }
         return BaseAppDatabase.a;
    }
    }

  • 如果本地不存在数据库以及用户信息,会触发新设备注册逻辑,可以手动删除数据库进而触发该逻辑

  • 设备注册逻辑

  • 首次注册设备时,向服务器发送的未加密数据格式为,其中device_no唯一标识一个设备,且在客户端生成,可以随意修改
    {"channel":"","code":"","device_no":"cd17c0a9-1d41-3377-8b3c-2599755976f8","device_type":"A","version":"4.0.5"}

  • 在服务器返回的响应中会包含token,与device_no对应,用于生成XTOKEN请求头参数,作为用户身份标识,后续所有请求需携带该字段

  • XTOKEN认证参数生成

  • 可直接通过搜索XTOKEN关键字定位到生成逻辑,很简单,通过注册设备时发送给服务器的deivce信息以及服务器返回的token信息,经序列化后加密即生成了最终的X-TOKEN
    @Override  // okhttp3.w
    public d0 a(a arg6) throws IOException {
    /* other codes */
    HashMap v2 = new HashMap();
    v2.put("token", v0_3);
    v2.put("device_no", h.a().c("device_id"));
    v2.put("device_type", "A");
    v2.put("version", "4.0.5");
    JSONObject v3 = new JSONObject(v2);
    if(!TextUtils.isEmpty(v0_3)) {
         v1.a("X-TOKEN", com.niming.weipa.utils.a.b(String.valueOf(v3)));
    }
    return arg6.a(v1.a());
    }

  • 获得新帐号、绑定邀请码、获得vip
  • 在以上分析的基础上,通过不同的device_no,生成相应的XTOKEN即可拥有新用户帐号,利用该app邀请码绑定送vip机制,可以获得大量用户凭证并绑定自己的邀请码,获得持续vip功能,演示可以观看bilibili视频

    [/ol]

    函数, 设备

  • freehjx1982   

    抖音最好别老是看,1分钟 3分钟给我们大脑一个刺激,长久 我们的大脑就会习惯这种短期刺激
    4825516   

    有成品吗?发这么多也看不懂啊
    ysd254   

    真不建议大家用。让伤害从我为止。。。。。
    jayfox   

    为了反破解 作者是煞费苦心啊   各种机制都用上了
    夏驰   

    感谢分享
    lianquke   

    各种检测代码可以学一下
    emailsina   

    关注学习
    jokertao   

    感谢分享
    戰龍在野   

    虽然看不懂但是要支持楼主的分享
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部