zoukankan      html  css  js  c++  java
  • 【爬虫系列】2. 打开App逆向“潘多拉魔盒”

    【爬虫系列】2. 打开App逆向“潘多拉魔盒”
    一、前言

    • 近年大多产品要不Web端功能受限、要不直接就没有Web端,直接从Web端抓数据这个路子越来越难
    • 通过代理软件(whistle | fiddler|Charles)抓HTTP请求基本操作了,相关教程自行学习了。
    • Android 7之后,非Root情况下系统直接不信任用户自行安装的cert证书,直接导致App HTTP抓包更加麻烦了;iOS 抓包倒是简单不少,信任证书之后一路绿灯,甚至还可以花168买个iOS端的本地抓包工具
    • 事与愿违的在于:有时候“辛辛苦苦”搞掂了HTTP抓包,拿过来一看,小小的“sign”字段躺在请求体里面,每个请求都会变化,“数据获取”的大门开了一条缝又关上了,着实让人难受

    那么...
    究竟有没有办法呢?
    下一步怎么办呢?

    PS:本文所有操作均基于Android App,iOS不在本学习教程内(臣妾也不会啊)

    二、先验知识

    2.0 核心思路

    不支持在 Docs 外粘贴 block
    2.1 劝退提示

    • 能看懂Java代码,知道JAVA_HOME 、ANDROID_HOME,能独立配置Android Studio 、Maven仓库
    • 懂一丢丢命令行操作,懂Git基本操作,会从Github捞代码(别TM Download zip)
    • 有一丢丢Android开发知识,至少知道adb 操作,apk文件是什么,能运行起gralwe

    PS:如果都不会,建议早点洗洗睡。
    PPS:或者加钱请我一对一指导。

    2.2 工作环境准备

    • JDK8 本地环境(别问为什么是JDK8,就是任性)
      ➜ ~ java -version
      openjdk version "1.8.0_292"
      OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_292-b10)
      OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.292-b10, mixed mode)
    • skylot/jadx(Dex to Java decompiler)git clone + 配置好jdax工具 -> 用户反编译Apk
    • install Android Studio 下载 + 安装,网络好的情况下,直接把 Android NDK 21.0 也搞掂就最好了
    • 差点忘了说,要有一台Android 手机,有一根数据线...

    三、开搞
    3.0 今天天气真好
    PS: 让我想想,搞谁家的App比较有趣...
    PPS:并不是针对谁,只是“学习学习”一下优秀代码

    3.1 豆瓣App 签名破解(Java原生)

    • 翻了下以前的项目,好像豆瓣App是本地签名,所以就它了
    • 学习目的:豆瓣小组 API 破解
      先捞个curl 看看http接口请求
      curl --location --request GET 'https://frodo.douban.com/api/...' \
      --header 'Authorization;' \
      --header 'User-Agent: api-client/1 com.douban.frodo/7.1.0(205) Android/29 product/perseus vendor/Xiaomi model/Mi MIX 3 rom/miui6 network/wifi udid/a0f9cde79ec841a748625f766273e8f4333ed9c1 platform/mobile nd/1' \
      --header 'Cookie: bid=EGo8Z7aSUAI'

    可以看到这里请求正常返回,也能看到数据了。

    但是我们切换一下其他小组的数据,很快就能看到“invalid_request_996”,签名错误的提示。

    细心的朋友,大概已经看到了“_sig” 这个字段了,明显就是“sign”的简写。
    好了,开始干活。

    豆瓣 App- 下载

    下载apk

    wget https://img2.doubanio.com/dae...

    使用jadx反编译 apk,输出到douban-src文件夹

    jadx -d douban-src com.douban.frodo_douban_7.18.1_231.apk

    使用android-studio 打开douban-src项目

    studio douban-src

    反编译结果

    Android 项目代码

    最简单的办法,直接搜索字段“_sig"

    大概能找到这段代码:

    @android.webkit.JavascriptInterface
    public java.lang.String decoratorUrl(java.lang.String str) {

    try {
        if (!com.douban.frodo.baseproject.rexxar.RexxarConstant.a(android.net.Uri.parse(str).getHost())) {
            return str;
        }
        android.net.Uri.Builder appendQueryParameter = android.net.Uri.parse(str).buildUpon().appendQueryParameter("udid", 
        com.douban.frodo.baseproject.util.FrodoUtils.a()).appendQueryParameter("rom", 
        com.douban.frodo.baseproject.util.Utils.i()).appendQueryParameter("apikey", 
        com.douban.frodo.baseproject.util.FrodoUtils.c()).appendQueryParameter(com.umeng.commonsdk.proguard.d.ao, "rexxar_new");
        if (com.douban.frodo.utils.AppContext.b() != null) {
            appendQueryParameter.appendQueryParameter("channel", com.douban.frodo.utils.AppContext.b().market);
        }
        android.util.Pair<java.lang.String, java.lang.String> a 
               = com.douban.frodo.network.ApiSignatureHelper.a(str, "GET", null);
        if (a != null) {
            appendQueryParameter.appendQueryParameter("_sig", (java.lang.String) a.first);
            appendQueryParameter.appendQueryParameter("_ts", (java.lang.String) a.second);
        }
        str = appendQueryParameter.build().toString();
        return str;
    } catch (java.lang.Exception e) {
        e.printStackTrace();
    }

    }

    明显签名实现在“com.douban.frodo.network.ApiSignatureHelper”这个类,
    使用了a方法对str变量签名,返回了 _sig 和 _ts ,然后扔给QueryParameter。

    package com.douban.frodo.network;

    public class ApiSignatureHelper {

    static android.util.Pair<java.lang.String, java.lang.String> a(okhttp3.Request request) {
        if (request == null) {
            return null;
        }
        java.lang.String header = request.header(com.douban.push.internal.api.Request.HEADER_AUTHORIZATION);
        if (!android.text.TextUtils.isEmpty(header)) {
            header = header.substring(7);
        }
        return a(request.url().toString(), request.method(), header);
    }
    
    public static android.util.Pair<java.lang.String, java.lang.String> a(
            java.lang.String str, java.lang.String str2, java.lang.String str3) {
        if (android.text.TextUtils.isEmpty(str)) {
            return null;
        }
        java.lang.String str4 = com.douban.frodo.network.FrodoApi.a().e.b;
        if (android.text.TextUtils.isEmpty(str4)) {
            return null;
        }
        java.lang.StringBuilder sb = new java.lang.StringBuilder();
        sb.append(str2);
        java.lang.String encodedPath = okhttp3.HttpUrl.parse(str).encodedPath();
        if (encodedPath == null) {
            return null;
        }
        java.lang.String decode = android.net.Uri.decode(encodedPath);
        if (decode == null) {
            return null;
        }
        if (decode.endsWith("/")) {
            decode = decode.substring(0, decode.length() - 1);
        }
        sb.append(jodd.util.StringPool.AMPERSAND);
        sb.append(android.net.Uri.encode(decode));
        if (!android.text.TextUtils.isEmpty(str3)) {
            sb.append(jodd.util.StringPool.AMPERSAND);
            sb.append(str3);
        }
        long currentTimeMillis = java.lang.System.currentTimeMillis() / 1000;
        sb.append(jodd.util.StringPool.AMPERSAND);
        sb.append(currentTimeMillis);
        return new android.util.Pair<>(com.douban.frodo.utils.crypto.HMACHash1.a(
                str4, sb.toString()), java.lang.String.valueOf(currentTimeMillis));
    }

    }

    看到这个代码就应该开心了,17 - 45行都是在处理传入的参数,
    主要用了 android.net.Uri 提取参数之后,拼装成了一个用“&”分割的字符串,用熟悉的语言自行处理就完事了。
    最后,签名是调用了 “com.douban.frodo.utils.crypto.HMACHash1.a”,还需要继续跟进去。
    不过这个名字“ HMAC-SHA1”已经给了不少的信息了。

    跟代码进入到 com.douban.frodo.utils.crypto.HMACHash1类,于是看到了下面的代码:
    package com.douban.frodo.utils.crypto;

    public class HMACHash1 {

    public static final java.lang.String a(java.lang.String str, 
    java.lang.String str2) {
        try {
            javax.crypto.spec.SecretKeySpec secretKeySpec = new javax.crypto.spec.SecretKeySpec(
                    str.getBytes(), com.douban.live.internal.LiveHelper.HMAC_SHA1);
            javax.crypto.Mac instance = javax.crypto.Mac.getInstance(
                    com.douban.live.internal.LiveHelper.HMAC_SHA1);
            instance.init(secretKeySpec);
            return android.util.Base64.encodeToString(
                    instance.doFinal(str2.getBytes()), 2);
        } catch (java.lang.Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    }

    纯Java实现的一个HMAC_SHA1 加密算法。
    str 是SecretKey(ZenoConfig的某个变量),str2是传入的签名数据。

    现在只剩最后一个问题了,SecretKey 怎么拿?
    回到上面代码,可以看到:
    java.lang.String str4 = com.douban.frodo.network.FrodoApi.a().e.b;
    // str4 就是传到HMACHash1的key,e 这里是 ZenoConfig

    这里可以看到ZenoConfig.b 就传入的str3,也就是我们要找的SecretKey
    继续翻就能看到

    java.lang.String d2 = com.douban.frodo.baseproject.util.FrodoUtils.d();

    builder.c = d2;

    com.douban.zeno.ZenoConfig zenoConfig = new com.douban.zeno.ZenoConfig(

        builder.a, builder.b, builder.c, builder.d, builder.e, 
        builder.f, builder.g, builder.h, builder.i, builder.j);
    
    

    package com.douban.frodo.baseproject.util;

    public class FrodoUtils {

    private static java.lang.String a;
    private static java.lang.String b;
    private static java.lang.String c;
    private static java.lang.String d;
    private static java.lang.String e;
    
    public static java.lang.String e() {
        return "frodo://app/oauth/callback/";
    }
    
    public static java.lang.String a() {
        if (android.text.TextUtils.isEmpty(a)) {
            a = com.douban.amonsul.MobileStat.g((android.content.Context) com.douban.frodo.utils.AppContext.a());
        }
        return a;
    }
    
    public static void a(java.lang.String str) {
        e = str;
    }
    
    public static java.lang.String b() {
        return e;
    }
    
    public static java.lang.String c() {
        return b;
    }
    
    public static java.lang.String d() {
        return c;
    }
    
    @android.annotation.SuppressLint({"PackageManagerGetSignatures"})
    public static void a(boolean z) {
        if (android.text.TextUtils.isEmpty(b)) {
            b = "74CwfJd4+7LYgFhXi1cx0IQC35UQqYVFycCE+EVyw1E=";
        }
        if (android.text.TextUtils.isEmpty(c)) {
            c = "bHUvfbiVZUmm2sQRKwiAcw==";
        }
        if (z) {
            try {
                java.lang.String encodeToString = android.util.Base64.encodeToString(
                com.douban.frodo.utils.AppContext.a().getPackageManager().getPackageInfo(
                com.douban.frodo.utils.AppContext.a().getPackageName(),
                 64).signatures[0].toByteArray(), 0);
                b = com.douban.frodo.utils.crypto.AES.a(b, encodeToString);
                c = com.douban.frodo.utils.crypto.AES.a(c, encodeToString);
            } catch (android.content.pm.PackageManager.NameNotFoundException e2) {
                e2.printStackTrace();
            }
        }
    }
    
    public static java.lang.String f() {
        if (android.text.TextUtils.isEmpty(d)) {
            return "d40568d833";
        }
        return d;
    }

    }

    于是我们也就知道了,所谓的“SecretKey”,其实就是c = "bHUvfbiVZUmm2sQRKwiAcw=="; AES加密之后的值,encodeToString就是com.douban.frodo.utils.AppContext.a().getPackageName()包名信息。如果开发过安卓App大概会知道,这段代码用来获取当前应用的签名的,这是安卓的一种防篡改的安全机制。
    虽然我们没办法直接拿到com.douban.frodo.utils.AppContext.a().getPackageName(),不过,其他应用也可以获取已安装应用的签名信息,只需要把对应app的包名作为参数传入。
    于是...
    Application application=(Application)getApplicationContext();
    PackageInfo packageInfo=application.getPackageManager().getPackageInfo("com.douban.frodo",PackageManager.GET_SIGNATURES);
    String sign=Base64.encodeToString(packageInfo.signatures[0].toByteArray(),0);

    最后我们把上面的签名代码跑一下,便可以得到 SecretKey = "bf7dddc7c9cfe6f7"

    很好,很给力,明显可以下班了。
    代码?
    我...
    也...
    懒...
    得...
    写...

    需要的朋友,可以到 豆瓣app签名算法分析与解密 自取。

    3.2 ratel-core Android逆向分析工具套件

    • GitHub - virjarRatel/ratel-core: 平头哥的核心代码
    • 简介 · Ratel文档
      平头哥(ratel)是一个Android逆向分析工具套件,他提供一系列渐进式app逆向分析工具。
      同时平头哥也是一个app二次开发的沙箱环境,支持在免root环境下hook和重定义app功能。
      对于大部分app来说,平头哥打开了潘多拉魔盒,请不要在授权之外违法使用平头哥(仅建议用于个人定制化使用、app攻防安全研究等领域),在ratel官方授权之外违规使用ratel造成的一些后果由使用者自定承担
      平头哥是一个app逆向分析的生态,开发进度历时3年。目前正考虑推出商业版本的开源化。

    同时作为一套完善闭环的工具链,平头哥的相关功能是非常多的。

    • 基本的hook 任意app功能,免root能力
    • 分身和多开能力(目前已经在生产验证过一台手机分身100个设备)
    • 设备指纹对抗能力:实验发现已经可以应对某些大厂
    • 群控能力: 内置SupperAppium模块,其开源方案:https://bbs.pediy.com/thread-...
    • 定时任务管理,大多为了支持无电脑的群控(无USB的脱机群控)
    • 热发布:插件模块通过后端热发,对集群所有设备生效。且支持回滚
    • RDP:目前市面上唯一还可以实现对微信等大型app实现smali重打包的功能模块
    • 脱壳:内置指令dump级别脱壳机,可以免root脱壳
    • 兼容和适配:在2000多台设备,兼容测试过500款来自应用市场的抽样app。覆盖Android5.0-Android10.0(Android11已经在内测中)
    • 免root IDA调试,内置JustTrustMe
    • 内置socketMonitor(比肉丝的R0Cpature早出现3年,早期甚至支持线程跳跃追踪,用以解决异步问题)
    • SplitApk:GoogleAppStore Android 安装包分发格式
    • 多种重打包方案支持: appendDex、rebuildDex、zelda、shell
    • 生态:微信机器人、模拟定位、多开账号资源备份还原

    一句话概括:非ROOT情况下,通过插件机制随便改现有App功能。

    于是,我们来试试水。

    3.2.1 准备ratel-core编译环境

    • JDK8
    • Android Studio + NDK21.0 (NDK 修订历史记录  |  Android NDK  |  Android Developers)
    • adb
      export ANDROID_NDK_HOME=/Users/liguobao/Library/Android/sdk/ndk/21.0.6113669
      export ANDROID_SDK_ROOT="/Users/liguobao/Library/Android/sdk"
      export PATH="${PATH}:${ANDROID_SDK_ROOT}/tools:${ANDROID_SDK_ROOT}/platform-tools"

    参考上面这个配置好本地的ANDROID_NDK_HOME + ANDROID_SDK_ROOT
    PS: 这玩意如果自己不熟悉,多翻翻教程。

    下载https://github.com/virjarRate... 源码
    $ git clone https://github.com/virjarRate...
    $ cd ratel-core/

    编译代码 + 检查环境配置

    $ ./script/create-dist.sh

    以上操作都没问题,都正常编译之后,

    就可以使用 ./script/ratel.sh 来重打包 App了。

    ➜ script git:(master) cd dist
    ➜ dist git:(master) ./ratel.sh ~/Downloads/leyoujia.apk

    正常情况下会生成一个新的apk文件,最后一步就是把这个apk安装到手机上了。

    安装好了之后,在手机端打开App,App不崩溃的话就说明成功了。
    PS:遇到崩溃的情况,去GitHub提Issues。

    3.2.2 准备ratel-module项目

    • 重打包之后的Apk,就是开了“后门”的App
    • 插件就是给了我们自定义App + 操纵App的能力
    • https://github.com/virjarRate...
      同时可以“Ratel Manager”到手机端,通过此App管理插件和查看App感染信息。
    • Ratel Manager 也可以在源码里面编译,或者直接下载老版本。

    “乐有家”就是刚刚重打包感染的App。
    $ git clone https://github.com/virjarRate...
    $ cd ratel-module-template/
    $ ./template.sh ~/Downloads/leyoujia.apk

    然后在 android-studio中打开ratel-module-template整个项目,
    等一下Index代码和还原相关包文件之列的。

    试一下编译安装“crack-乐有家”到手机端是不是OK。

    正常情况下,这个插件App会直接启动,
    而且在 Ratel Manager 的模块里面可以看到新的插件App

    • 开关一下Ratel status,在页面多下拉几次触发刷新机制

    默认插件项目已经有了一个有趣的功能 —“ 插入悬浮按钮”
    // 添加悬浮窗
    private static void addFloatingButtonForActivity(final RC_LoadPackage.LoadPackageParam lpparam) {

    RposedHelpers.findAndHookMethod(Activity.class, "onCreate", Bundle.class, new RC_MethodHook() {
        @Override
        protected void afterHookedMethod(final MethodHookParam param) throws Throwable {
            new Handler(Looper.getMainLooper())
                    .postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            createAndAttachFloatingButtonOnActivity((Activity) param.thisObject);
                        }
                    }, 1000);
        }
    
        private void createAndAttachFloatingButtonOnActivity(Activity activity) {
            Context context = RatelToolKit.ratelResourceInterface.createContext(lpparam.modulePath, HookEntry.class.getClassLoader(), RatelToolKit.sContext);
    
            FrameLayout frameLayout = (FrameLayout) activity.getWindow().getDecorView();
            LayoutInflater.from(context).cloneInContext(context)
                    .inflate(R.layout.float_button, frameLayout);
    
        }
    });}

    到这里,ratel-module 插件项目已经正常运行了,
    Demo代码我们也已经跑起来了。

    3.2.3 再做点有趣的玩意?
    A. 直接信任所有本地用户cert证书
    // package ratel.com.jjs.android.butler;
    // public class HookEntry implements IRposedHookLoadPackage {}
    // handleLoadPackage 函数里面新增一行代码,然后重新编译安装,
    // 手机上操作刷新插件后重新打开App
    JustTrustMe.trustAllCertificate();

    于是HTTPS请求无所遁形。

    curl --location --request POST 'https://steward.leyoujia.com/...' \
    --header 'host: steward.leyoujia.com' \
    --header 'clientid: e14becf6-2e13-43ae-b453-bd9cd35354a4' \
    --header 'd: 0' \
    --header 'latitude;' \
    --header 'channel: online_32' \
    --header 'imsi: 460110136201976' \
    --header 'uuid: e14becf6-2e13-43ae-b453-bd9cd35354a4' \
    --header 'ssid: 00000000378d5761ffffffffa488b92e' \
    --header 'version: 8.1.9' \
    --header 'mac: 64:BC:0C:44:64:01' \
    --header 'network: WIFI' \
    --header 'cit: 001729' \
    --header 'sid: 38e5d3e9be36f0508dff7415ffffc076' \
    --header 'phonemodel: Maru on the Nexus 5X' \
    --header 'phoneos: android' \
    --header 'carries: 0' \
    --header 'imei: 35362607298355' \
    --header 'aid: APP001' \
    --header 'clientsign: 39ea04d3954db18df716e21978f009df' \
    --header 'androidid: aabfdc5bb198e3b7' \
    --header 'oaid: 00000000378d5761ffffffffa488b92e' \
    --header 'longitude;' \
    --header 'timestamp: 1639312110572' \
    --header 'content-type: application/x-www-form-urlencoded' \
    --header 'user-agent: okhttp/3.9.1' \
    --header 'Connection: close' \
    --data-urlencode 'cityCode=001729'

    细心一看,clientsign 签名赫然其中。
    好家伙,又要开始搞事情了....
    回到 3.1的操作,生成一份“leyoujia-src”走起.
    B. 重新开搞leyoujia-src代码
    搜索“clientSign”,大概就能看到代码了

    java.lang.String encode32 = com.jjshome.common.utils.MD5.encode32(
    com.jjshome.common.utils.MD5.encode32(sb.toString()));

    package com.jjshome.common.utils;

    public class MD5 {

    /* JADX WARNING: type inference failed for: r2v2, types: [int] */
    /* JADX WARNING: type inference failed for: r2v5 */
    /* JADX WARNING: Multi-variable type inference failed */
    public static java.lang.String encode32(java.lang.String str) {
        java.lang.StringBuffer stringBuffer = new java.lang.StringBuffer("");
        try {
            java.security.MessageDigest instance = java.security.MessageDigest.getInstance("MD5");
            instance.update(str.getBytes());
            byte[] digest = instance.digest();
            for (int i = 0; i < digest.length; i++) {
                byte b = digest[i];
                if (b < 0) {
                    b += 256;
                }
                if (b < 16) {
                    stringBuffer.append("0");
                }
                stringBuffer.append(java.lang.Integer.toHexString(b));
            }
        } catch (java.lang.Exception e) {
            e.printStackTrace();
        }
        return stringBuffer.toString();
    }

    }

    大概故事又成了什么看到sb字符串怎么来的了。
    此处省略,自行折腾了。
    PS:

    • 其实最后搞这玩意还是花了一个下午,哪些参数怎么拼接实在是有点蛋疼。
    • 好好读代码,好好看逻辑总是能搞掂的,朋友加油!

    N1、总结

    • 学习学习就好,不要有危险的想法。
    • 攻防永远的相对的,永远不可能一劳永逸。
    • 每天距离“提篮桥”更进一步。
      N2、总结
    • 学习了jadx 逆向工具的使用,复习了Java基础
    • 学习了Ratel平头哥工具,学习了Android基础知识

    参考阅读:

    • 豆瓣app签名算法分析与解密 – 天赐网络
    • Android如何调用so文件
    • Linux的so文件到底是干嘛的?浅析Linux的动态链接库
    • Android 的 so 文件加载机制 - 请叫我大苏 - 博客园
    • ratel的使用

    未完待续....

      • Ratel + Sekiro 高阶用法之“卷死”App逆向的同行。
        敬请期待...
  • 相关阅读:
    增加一个基类没有的方法
    修改或加强基类的属性
    linux rm命令详解
    Apache的配置httpd.conf杂谈
    解决 You don't have permission to access / on this server. 错误的另一方法
    ubuntu下成功配置LAMP 并安装PHPMyadmin
    C#连接SQLite的方法
    内存使用大比拼 之 String – StringBuffer
    非常喜欢Gedit,绝不逊色EditPlus!
    关于内存
  • 原文地址:https://www.cnblogs.com/liguobao/p/15713354.html
Copyright © 2011-2022 走看看