利用沙盒技术破解APP的API协议加密

项目地址:https://github.com/tbruceyu/AppCaller

无聊的需求

前段时间闲的没事,经常刷某视频App。里面有很多有才的人,突然想把他们的视频都给下载下来在电脑上面存起来慢慢看。正好这段时间比较空闲,就尝试去破解一下它的Http加密协议。
作为一个主要工作在客户端上的码农,第一时间想到了去抓包看一下他们的App协议。
在这里插入图片描述
可以看到,通过调用http://103.107.217.65/rest/n/feed/profile2这个接口就可以去获取到这个主播的视频了。然后通过分页加载即可下载所有的视频。但是通常的App都会对请求做保护,防止被爬虫抓取。看了下我们的请求里面有一个sig xxxxxx的字段。看来我们需要去破解这个签名算法才行。

通用操作

通常的破解都是用Java的反编译工具去找到App的上层加密调用函数,找到Java层的核心加密函数的位置,然后再用静态分析和动态调试汇编代码,最后自己手动实现这个加密算法。这个方法是最完美的破解方案,但是难度巨大,而且如果App升级后,又需要重新去完整分析一遍。对技术的要求还是很高的。而且现在越来越多的APP会把so里面的符号表去掉,还会混淆核心函数的实现,加大分析难度。以某音的加密libcms.so为例,里面的汇编代码都被混淆了,用IDA的F5插件转化后的代码也根本读不懂。里面连RegisterNative函数调用都找不到。
在这里插入图片描述
如果要用这种方式去破解移动App的协议,无疑要花费大量的时间和精力,而且我自己本身也没有太多逆向分析的经验,那我们能不能用其他的方式去做呢?

新的思路

在Android开发生态下,有很多的黑科技,其中一种就是应用双开技术,比如BLE平行空间、VirtualApp等。好在16年的时候VirtuapApp开源后我有去研究过它的代码,核心原理就是利用Java动态代理和反射技术,自己模拟一个Android的Framework,架设在App和系统之间,达到欺骗App和系统的目的。知道了沙盒技术的原理。思路来了:

利用沙盒把App放到自己能够控制的环境里面去执行,然后在里面启动一个Web服务器,通过远程Http调用访问沙盒里的App,去调用核心加密逻辑,计算出密钥,然后返回给客户端。这样就能够达到破解App的加密算法的目的。
废话不说,开干!

分析上层加密函数

apktool 工具先把这个App的包解出来:

xxx@bogon:~/temp/kuaishou$ apktool  d kuaishou.apk
I: Using Apktool 2.4.0 on kuaishou.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /Users/xxx/Library/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Baksmaling classes10.dex...
I: Baksmaling classes11.dex...
I: Baksmaling classes12.dex...
I: Baksmaling classes13.dex...
I: Baksmaling classes14.dex...
I: Baksmaling classes2.dex...
I: Baksmaling classes3.dex...
I: Baksmaling classes4.dex...
I: Baksmaling classes5.dex...
I: Baksmaling classes6.dex...
I: Baksmaling classes7.dex...
I: Baksmaling classes8.dex...
I: Baksmaling classes9.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
xxx@bogon:~/temp/kuaishou$

然后我们去里面搜索sig字符串,看在哪里加入到Http请求里的:

xxx@bogon:~/temp/kuaishou$ cd kuaishou/
xxx@bogon:~/temp/kuaishou/kuaishou$ grep sig . -rw
./smali_classes10/com/xiaomi/push/service/p.smali:    const-string/jumbo v0, "sig"
./smali_classes10/com/xiaomi/push/service/XMPushService$c.smali:    const-string/jumbo v3, "invalid-sig"
./smali_classes10/com/xiaomi/push/service/XMPushService$c.smali:    const-string/jumbo v4, "SMACK: bind error invalid-sig token = "
Binary file ./lib/armeabi-v7a/libgodzilla.so matches
Binary file ./lib/armeabi-v7a/libBugly.so matches
Binary file ./lib/armeabi-v7a/libksimage.so matches
Binary file ./lib/armeabi-v7a/libCGE.so matches
Binary file ./lib/armeabi-v7a/libawesomecache.so matches
Binary file ./lib/armeabi-v7a/libkwsgmain.so matches
Binary file ./lib/armeabi-v7a/libxylivesdk.so matches
Binary file ./lib/armeabi-v7a/libquic.so matches
Binary file ./lib/armeabi-v7a/libYTFaceReflect.so matches
./smali_classes2/com/yxcorp/gifshow/retrofit/k.smali:    const-string/jumbo v2, "sig"
xxx@bogon:~/temp/kuaishou/kuaishou$

前面几个smali文件明显是小米的渠道Push相关的代码,可以忽略,肯定是./smali_classes2/com/yxcorp/gifshow/retrofit/k.smali这一个retrofit里面加入的了。我们打开这个文件,果然发现这个sig就是里面的一个叫computeSignature的方法返回的Pair的first,并且参数就是okhttp的Request,很明显是用来计算密钥的。

# virtual methods
.method public final computeSignature(Lokhttp3/Request;Ljava/util/Map;Ljava/util/Map;)Landroid/util/Pair;
    .locals 5
    .annotation system Ldalvik/annotation/Signature;
        value = {
            "(",
            "Lokhttp3/Request;",
            "Ljava/util/Map",
            "<",
            "Ljava/lang/String;",
            "Ljava/lang/String;",
            ">;",
            "Ljava/util/Map",
            "<",
            "Ljava/lang/String;",
            "Ljava/lang/String;",
            ">;)",
            "Landroid/util/Pair",
            "<",
            "Ljava/lang/String;",
            "Ljava/lang/String;",
            ">;"
        }
    .end annotation

    .prologue
    .line 23
    const-string/jumbo v0, ""

    invoke-static {p2, p3}, Lcom/yxcorp/retrofit/f/a;->b(Ljava/util/Map;Ljava/util/Map;)Ljava/util/List;

    move-result-object v1

    invoke-static {v0, v1}, Landroid/text/TextUtils;->join(Ljava/lang/CharSequence;Ljava/lang/Iterable;)Ljava/lang/String;

    move-result-object v0

    .line 24
    new-instance v1, Landroid/util/Pair;

    const-string/jumbo v2, "sig"

    .line 25
    invoke-static {}, Lcom/yxcorp/gifshow/b;->a()Lcom/yxcorp/gifshow/e;

    move-result-object v3

    invoke-interface {v3}, Lcom/yxcorp/gifshow/e;->b()Landroid/app/Application;

    move-result-object v3

    sget-object v4, Lorg/apache/internal/commons/io/a;->f:Ljava/nio/charset/Charset;

    invoke-virtual {v0, v4}, Ljava/lang/String;->getBytes(Ljava/nio/charset/Charset;)[B

    move-result-object v0

    sget v4, Landroid/os/Build$VERSION;->SDK_INT:I

    invoke-static {v3, v0, v4}, Lcom/yxcorp/gifshow/util/CPU;->a(Landroid/content/Context;[BI)Ljava/lang/String;

    move-result-object v0

    invoke-direct {v1, v2, v0}, Landroid/util/Pair;-><init>(Ljava/lang/Object;Ljava/lang/Object;)V

    .line 24
    return-object v1
.end method

这段Smili转化成Java大致如下:

    public final Pair<String, String> computeSignature(Request request, Map<String, String> map, Map<String, String> map2) {
        return new Pair<>("sig", CPU.a(com.yxcorp.gifshow.b.a().b(), TextUtils.join("", a.b(map, map2)).getBytes(org.apache.internal.commons.io.a.f), VERSION.SDK_INT));
    }

这里可以看到,计算签名和request参数根本没有关系,那么map和map2分别是什么呢?此时我们可以祭出Hook大法。我们就可以去写一个插件去Hook看一下方法参数里面的两个Map分别是什么。站在巨人的肩膀上(用现成的),我这里用的VirtualHook这个库来Hook打印的。当然我们也可以用Xposed或者VirtualPosed去Hook。

package lab.galaxy.demeHookPlugin;

import android.util.Log;
import android.util.Pair;

import java.util.Map;

public class Hook_computeSignature {
    public static String className = "com.yxcorp.gifshow.retrofit.k";
    public static String methodName = "computeSignature";
    public static String methodSig = "(Lokhttp3/Request;Ljava/util/Map;Ljava/util/Map;)Landroid/util/Pair;";

    public static Pair<String, String> hook(Object thiz, Object request, Map<String, String> map, Map<String, String> map2) {
        Log.d("YAHFA", "map:" + map.toString());
        Log.d("YAHFA", "map2:" + map2.toString());
        return backup(thiz, request, map, map2);
    }

    public static Pair<String, String> backup(Object thiz, Object request, Map<String, String> map, Map<String, String> map2) {
        Log.e("YAHFA", "should not be here");
        return null;
    }
}

配合抓包工具和第二,三个map的打印结果可以发现其实map就是所有的URL Query,map2就是所有的POST的内容

10-14 15:50:01.003 30075-30253/com.smile.gifmaker D/YAHFA: map:{isp=, mod=vivo(vivo X7), pm_tag=, lon=333.355416, country_code=CN, kpf=ANDROID_PHONE, extId=85db4aca4443a3c46a5bac6ed1c78836, did=ANDROID_0b171a80c3ff2d40, kpn=KUAISHOU, net=WIFI, app=0, oc=BAIDU, ud=0, hotfix_ver=, c=BAIDU, sys=ANDROID_5.1.1, appver=6.4.0.9003, ftt=, language=zh-cn, iuid=, lat=33.33333, did_gt=1571111544356, ver=6.4, max_memory=256}
10-14 15:50:01.003 30075-30253/com.smile.gifmaker D/YAHFA: map2:{source=1, volume=0.19, browseType=1, seid=52203040-6dce-1111-b08d-d9ae060c2718, pv=false, needInterestTag=false, client_key=3c2cd3f3, coldStart=false, count=20, pcursor=, os=android, refreshTimes=1, id=10, type=7, page=1}

field。有了这些分析,我们接下来就开始去开始实现我们的想法了。

反射调用加密函数

VirtualApp实现了一套很方便的反射架构,我们可以直接拿来用。不过需要注意一下,由于这里需要反射的是应用的类,而VirtualApp的反射框架是直接反射的系统的类,加载时机是virtualapp自身被fork出来的时候,所以是可以直接加载使用的,而我们要反射的是应用内的类,这些类是在应用的Application创建的时候新建的ClassLoader加载的(LoadedApk.makeApplication)。而我们在VirtualApp里的类环境是相对于这个app的父环境。所以我们需要用新创建的这个ClassLoader才能够加载App内我们需要调用到的类。关于这块有疑惑的同学可以去了解一下Java类的双亲委派的加载机制。代码如下:

package plugins.kuaishou.com.yxcorp.gifshow.retrofit;

import android.util.Pair;
import mirror.RefClass;
import mirror.RefMethod;

public class k {
    public static void init(ClassLoader classLoader) {
        try {
            TYPE = RefClass.load(k.class, classLoader.loadClass("com.yxcorp.gifshow.retrofit.k"));
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    public static Class<?> TYPE;

    public static RefMethod<Pair<String, String>> computeSignature;
}

接下来然后我们可以写一个简单的测试函数把抓包工具抓取到的GET Query和POST Fields去除POST里的sig组合成两个map,然后通过

Pair<String, String> res = k.computeSignature.call(instance, null, queryParams, fieldParams);
Log.d("test", "sig:" + res.second);

去调用看一下结果是否和抓包工具里面的相同。我这里已经验证了这个方法就是计算签名的,就不做过多的赘述了。

实现一个简单的Http服务

在Java世界里的现成的Http服务库太多了,我们这里选择了使用Netty库来架设Http服务。Netty库是业界广泛使用的一个NIO实现的异步事件驱动网络框架,并且Netty自身就带了Http的Handler,不用花什么功夫就能做出来。大致就下面这么几行代码
在这里插入图片描述

在VirtualApp的镜像App里加入服务

考虑到大部分的应用的加密模块都会做签名校验,一般会在Application创建的时候去初始化,所以我们的VirtualApp里面多开启动的App都是在一个新的子进程里。在VirtualApp环境里里子App的Application的onCreate调用就在VClientImpl的bindApplicationNoCheck里。所以我们的服务需要是在这之后启动。我们这里简单的抽象包装了一下,万一以后要增加其他的App呢。

package plugins;

import android.content.Context;

import com.tby.http.SmHttpRequest;

import java.util.List;
import java.util.Map;

public interface IPlatformPlugin {
    void init(Context context);

    SmHttpRequest process(String url, String commonParams, Map<String, String> fields, Map<String, List<String>> headers);
}

我们把这个功能抽象成了init和process两步初始化的时候会把App的Application传入进去,而处理就是具体要做的工作。要新增加一个App直接去实现这两个接口即可。
我们简单的设计了客户端一个请求协议:
在这里插入图片描述
里面包含请求的URL,commonParams用于区分App的一些公共参数,field是所有的POST Fields,header就是所有的HTTP请求头。服务器返回就直接在这个格式里面移除commonParams,插入签名字段就可以了。
接下来就是体力活编写启动Http服务的工作了:

MainServerThread thread = new MainServerThread(port);
thread.addPathHandler("/api", new IPathHandler() {
    @Override
    public void handleRequest(ChannelHandlerContext ctx, FullHttpRequest httpRequest) throws Exception {
        String body = HttpServerUtils.getBody(httpRequest);

        JSONObject jsonObject = new JSONObject(body);
        String url = jsonObject.getString("url");
        String commonParams = jsonObject.getString("commonParams");
        Map<String, List<String>> headers = JsonUtil.parseHeaders(jsonObject.getJSONObject("header"));
        Map<String, String> fields = JsonUtil.parseFields(jsonObject.optJSONObject("field"));
        SmHttpRequest request = plugin.process(url, commonParams, fields, headers);

        HttpServerUtils.send(ctx, packageResult(request).toString(), HttpResponseStatus.OK);
    }
});
thread.start();

下面就是去PC环境上运行这个架设在手机端上的远程服务了。我这手机肯定很高兴:平时都是我去调用服务,这次终于轮到你们来调用我了: )
现在App上面把我们的App运行起来,如下图所示:

在这里插入图片描述

接下来在终端下去测试一下。先去查一下手机的IP地址,然后用curl请求:

xxx@bogon:~$ curl http://xxx.xxx.xxx.xxx:8889/api -X POST -d ‘{ “url”: “http://api.ksapisrv.com/rest/n/feed/profile2?app=0&kpf=ANDROID_PHONE&ver=6.4&c=BAIDU”, “commonParams”: “mod=Xiaomi%28MI%208%29&appver=6.4.0.9003&ftt=K-F-T&isp=CTCC&kpn=KUAISHOU&lon=333.337841&language=zh-cn&sys=ANDROID_9&max_memory=512&ud=0&country_code=cn&pm_tag=11694575470&oc=BAIDU&hotfix_ver=&did_gt=153xxxxx91012&iuid=&net=WIFI&did=ANDROID_a1dfcb2b57035073&lat=333.979012”, “field”: { “token”:"", “user_id”:“3848”, “lang”:“zh”, “count”:“30”, “privacy”:“public”, “referer”:“ks%3A%2F%2Fprofile%2F3848%2F5246693579318535881%2F1_i%2F1633500022418448386_h61%2F8”, “browseType”:“1”, “client_key”: “xxx”, “os”: “android” }, “header”: { “User-Agent”: [“kwai-android”], “Accept-Language”: [“zh-cn”], “X-REQUESTID”: [“193601906”], “Host”: [“api.ksapisrv.com”] } }’
{“url”:“http://api.ksapisrv.com/rest/n/feed/profile2?app=0&kpf=ANDROID_PHONE&ver=6.4&c=BAIDU&mod=Xiaomi%28MI%208%29&appver=6.4.0.9003&ftt=K-F-T&isp=CTCC&kpn=KUAISHOU&lon=333.337841&language=zh-cn&sys=ANDROID_9&max_memory=512&ud=0&country_code=cn&pm_tag=11694575470&oc=BAIDU&hotfix_ver=&did_gt=1539073091012&iuid=&net=WIFI&did=ANDROID_a1dfcb2b57035073&lat=333.979012”,“header”:{“User-Agent”:[“kwai-android”],“Accept-Language”:[“zh-cn”],“X-REQUESTID”:[“193601906”],“Host”:[“api.ksapisrv.com”]},“method”:0,“field”:{“token”:"",“user_id”:“3848”,“lang”:“zh”,“count”:“30”,“privacy”:“public”,“referer”:“ks%3A%2F%2Fprofile%2F3848%2F5246693579318535881%2F1_i%2F1633500022418448386_h61%2F8”,“browseType”:“1”,“client_key”:“xxx”,“os”:“android”,“sig”:"8a2fb705bed7471328f99d7f3d91f928"}}
xxx@bogon:~$

如何防范

我们这个方案是基于VirtualApp的沙盒环境做的,直接用现有的双开检测方案即可。通过检测应用的工作目录路径是否是正常的/data/data/目录是否正常即可。

题外话

在实现这个项目的过程中愈发觉得在Android平台下的App的安全真的很难做。这次破解的这个App本身是很容易破解的,后来我又尝试去破解了另外一个很火的短视频App,过程比较曲折一些,但是一样很快就分析破解了。就算我们能够通过双开检测来从上层去防范应用被注入,但是Android平台本身是开源的,我们完全可以在系统上面直接注入类似这样的Hack代码去直接调用某一个应用的一些敏感函数,如何防范这种方式的注入?看起来只能混淆得更深才行了。不过始终没有绝对的安全,最重要的还是服务端的防爬取做好,端控、频控做好。而在客户端上能够提高破解门槛,杜绝大部分的心怀不轨的用户就行了。
另外关于Android的沙盒程序真是一个神器,用来分析和其他应用都十分有用。试想一下你编译了一个Debug版本的VirtualApp,就能够去直接调试其他的应用。用来做一些竞品性能分析,动态调试,关键技术分析等都十分有用。

声明

本项目仅用于学习使用,严禁用于任何商业用途

  • 4
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值