6

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

 2 years ago
source link: https://segmentfault.com/a/1190000041151012
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

发布于 58 分钟前

【爬虫系列】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编译环境

  • 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逆向的同行。
    敬请期待...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK