利用沙盒技術破解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,就能夠去直接調試其他的應用。用來做一些競品性能分析,動態調試,關鍵技術分析等都十分有用。

聲明

本項目僅用於學習使用,嚴禁用於任何商業用途

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章