android逆向奇技淫巧八:apk加殼(二代)和通用脫殼分析


  這次同樣以T廠的x固加殼為例:為了方便理解,減少不必要的干擾,這里只寫了一個簡單的apk,在界面靜態展示一些字符串,如下:

       

      用x固加殼后,用jadx打開后,先看看AndroidMainfest這個全apk的配置文件:入口是“MyWrapperProxyApplication”;

<application android:theme="@style/AppTheme" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:name="MyWrapperProxyApplication" android:allowBackup="true" android:supportsRtl="true" android:roundIcon="@mipmap/ic_launcher_round" android:appComponentFactory="androidx.core.app.CoreComponentFactory">
        <activity android:name="com.example.test.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

  進入這個類查看: 先執行attachBaseContext,得到一個context,然后修復名稱,最后初始化proxyApplication;然后執行onCreate,在里面調用了一個名為Ooo0ooO0oO的native方法,這里明顯有問題:正常的開發人員會這樣取名字?

public abstract class WrapperProxyApplication extends Application {
    static Context baseContext = null;
    static String className = "android.app.Application";
    static ClassLoader mLoader = null;
    static Application shellApp = null;
    static String tinkerApp = "tinker not support";

    /* access modifiers changed from: package-private */
    public native void Ooo0ooO0oO();

    /* access modifiers changed from: protected */
    public abstract void initProxyApplication(Context context);

    static Context getWrapperProxyAppBaseContext() {
        return baseContext;
    }

    private synchronized boolean Fixappname() {
        if (className.startsWith(".")) {
            className = super.getPackageName() + className;
        } else if (className.indexOf(".") < 0) {
            className = super.getPackageName() + "." + className;
        }
        return true;
    }

    public static void fixAndroid(Context context, Application application) {
        if (Build.VERSION.SDK_INT == 28) {
            try {
                mLoader = AndroidNClassLoader.inject(context.getClassLoader(), application);
            } catch (Throwable th) {
                th.printStackTrace();
            }
        }
    }

    private static String getVersionCode(Context context) {
        try {
            return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
            return "0";
        }
    }

    /* access modifiers changed from: protected */
    public void attachBaseContext(Context context) {
        super.attachBaseContext(context);
        baseContext = getBaseContext();
        if (shellApp == null) {
            shellApp = this;
        }
        Fixappname();
        initProxyApplication(context);
    }

    public void onCreate() {
        super.onCreate();
        Ooo0ooO0oO();
    }
}

  進入iniProxyApplication函數:這里先是打開一個文件,如果文件打開失敗直接退出(換句話說文件打開失敗的后果很嚴重,直接沒法運行程序了)!最后加載so庫;從整個java代碼執行的過程看,解密dex大概率就是從加載這個so開始的了

    public void initProxyApplication(Context context) {
        ZipFile zipFile;
        try {
            zipFile = new ZipFile(context.getApplicationInfo().sourceDir);
        } catch (IOException e) {
            e.printStackTrace();
            zipFile = null;
        }
        if (zipFile == null) {
            Process.killProcess(Process.myPid());
            System.exit(0);
        }
        Util.PrepareSecurefiles(context, zipFile);
        try {
            zipFile.close();
        } catch (IOException e2) {
            e2.printStackTrace();
        }
        if (Util.CPUABI == "x86") {
            System.load(context.getFilesDir().getAbsolutePath() + "/prodexdir/" + Util.libname);
            return;
        }
        System.loadLibrary(Util.libname);
    }

   libs目錄下面有3個so,很明顯最后一個是解密dex的so, 因為第二個只有1K,哪有這么小的so文件!

   

     用IDA打開,先看看segment的情況:貌似比較正常;

     

     在export這里居然找到了jni_onload函數,用graph view查看,發現幾百個分支,正常人有這樣寫代碼的嘛? 明顯是控制流平坦化了(塊之間的分支)

   

     jni_onload參數是V3,根據V3的值走不同的分支:V3的值只有1個,所以只能走1條分支,其他分支都是干擾靜態分析的:

  

   為了便於靜態跟蹤,先把參數改過來:

  

  vm賦值給了v44,這里改成vm1,便於辨認;jni_onload中,javaVM是最重要的參數(這不廢話么?不重要就沒必要傳進來了):需要 先用vm調用GetEnv得到JNIEnv,然后再通過JNIEnv反射獲取java類、動態注冊native方法vm1只在這里被用到了,而且是3個參數,這到底是個啥函數?又干了啥?

       

       進入這個函數,老規矩,先把第一個JavaVM* 參數改過來,方便分析代碼;追蹤得知vm賦值給了v5+512;這里像極了JNIEnv* 對象+偏移得到vm對象;

       

      先把參數a3改成JNIEnv* 試試了,結果發現不對:

  

        把a3改成char* 試試,因為通過觀察發現,a3(也就是v5)好多次被當成基址,然后加上某個偏移賦值,並且不同偏移的數據類型還不同,如下:

        

         這里大膽猜測:這有可能是個結構體;除此外,再也找不到vm被使用了;接下來怎么繼續分析了? 這里有大量的加密字符串,並在init_array看到了很多異或的解密操作,很有可能是在init_array解密的,所以下一步可以嘗試從內存dump這個so,看看這些字符串到底是啥

       

       運行起來后查pid:9165

       

       把進程在內存的數據全部dump出來:

       

       dump出來的so看看segment:很正常

       

       最重要的是:字符串都解密了成明文的了!

      

      

       接下來就好分析多了:這里打開一個文件,直觀感覺要開始解密文件了!

        

        這個文件剛好在asset目錄下,貌似被加密了,而且很小,應該不是重要的文件;

       

       同在asset目錄,另一個文件就很大了,有906K,試試這個了:

       

       文件頭被抹掉了,從文件大小看,像是被加密的dex了。現在靜態分析階段,暫時無法解密,找找其他的突破口;

  

       上面分析了V5有可能是結構體,紅框框這個函數是第一個使用V5的,進去看看了:

       

       這里根據libdvm、libart這些關鍵的字眼,都能猜到是在獲取虛擬機的版本:把版本信息存放在字符串604偏移的地方;

  

        為什么要找這個了?art和dvm是兩種不同的dex文件加載方式,所以必須要先確定虛擬機類型,才會對dex進一步做操作(所以這兩個分支肯定是成雙成對出現的,缺一不可)!所以解密dex的操作可以直接從這里開始分析了,減少了很多需要分析的代碼!整個代碼用到604偏移的只有3個地方,根據取值不同走不同的分支。我用的的是4.4版本(低版本防護功能弱,利於逆向分析),很顯然用的是dvm,所以選擇下面這個分支繼續:

  

       進入每個函數挨個分析,根據字符串、參數個數等特征,大概猜了一下這些函數和變量的作用,標記到下面了:核心就是找openDexFileNative和openDexFile;

   

       接下來就是關鍵的代碼了:decryptDex(名字是我自己改的),里面有很多calloc函數分配內存,一看就知道要加載dex解密了(三代殼涉及到dex映射、修復和還原);

  

       為了便於理解:這里改個名;這個變量被應用了很多次,每次都是加上一個偏移,就得到函數。然后傳入參數就能使用了,疑似JNIEnv* 變量,這里先改成試試:

      

     這里改變量類型失敗,重新把jni.h導入,然后再改類型,這現在看起來舒服多了:

        

        靜態分析到這里基本基本到頭了,再分析也分析不出個啥了,接下來動態調試:找到剛才分析的shell,記住基址,后面會根據偏移定位關鍵函數和變量;

  

   加上函數的FOA=29DC,絕對地址就是8D2D0000+29DC=8D2D29DC

   

   找到了,下個斷點:看紅框框的地方,字符串還沒被解密了:F9繼續執行

    

   字符串被解密了:可以確定init_array肯定在解密字符串:

   

   從這里一路開始F8,來到了分發的地方:這也是這種混淆最頭疼的地方:這里有大量的分支,根據取值不同走不同的分支;

   

  這里有綠色的線,說明那是下一步跳轉的地方。對於這種控制流平坦化的分支,建議每跳一次,就在ida靜態分析時標注一次,方便后續靜態分析時剔除雜音

   

   然后一路F8,終於來到了另一個很重要的函數:偏移是0x3126(這種情況通過靜態分析時不可能找得到的,只能通過動態調試找到)

   

        繼續動態調試前,先靜態分析一下函數大概是干啥的。看不懂的細節再通過動態調試去理解;這里有點經驗之談:前面這些代碼考前,而且比價“平坦”,沒有較大的分支跳轉,按照一般正向開發經驗來看,大概率做很多基礎性質的工作,比如初始化某些變量,讀取某些關鍵數據,換句話說就是“預處理”

  

   這里也不像是dex加載到內存:

   

   從這里開始又在判斷虛擬機是dvm還是art,兩個分支都考慮了;同時前面也注入了classloader,所以這里有可能是在映射dex(這里ida反編譯有些小問題,看匯編更直接);

  

    如果android版本不是19,那么只調用sub_ad24一個函數,說明這個函數包含了所有dex的處理邏輯,值得進去看看:這種指針加偏移形式的,很有可能是JNIEnv *,可以轉換變量類型試試:

  

  進入mmap函數看看: 看起來還算正常,比jni_onload好看多了!

  

   因為mmap有可能是加載dex的函數,所以可以在函數開始的地方下斷點,但這里現在末尾下斷點,看看此時context的值(尤其時前面幾個傳參的寄存器):好幾次F9后,終於在R0這里看到了希望:

  

   dump到本地看看了:

   

  用jadx打開一看:這又是一個悲傷的故事:關鍵代碼和指令都被抽取了!所以脫殼還未完成,同志仍需努力!說明后面還有指令還原的代碼我們並未執行到,所以繼續往后分析和調試!

   

   重新回到前面幾層:這里有另一個比較關鍵的sub_5110函數,如下:

  (1)這個函數的 參數和剛才動態調試函數的參數大都是一樣的,由此大膽猜測:這兩個函數功能類似(否則為啥參數這么像了?)!
  (2)這 兩個函數在if末尾,並不在前面兩個if中, 說明很重要,需要無條件執行
  (3)sub_5110這個函數還在其他很多地方被調用了(包括剛才單步動態調試的AD24函數)

   

   進入sub_5110,和jni_onload一個鳥樣(甚至更離譜),也被控制流平坦化了,呵呵,又是一個“此地無銀三百兩”!

   

   老規矩:v3有可能是env,先改type,方便理解代碼;

  

   這里明顯是通過反射得到java層的一個installdex的方法:

  

   java層的installdex函數在這里:這就容易看懂了吧?通過classloader加載dex的:

  

   這里實錘了sub_5110就是install dex的方法,先改個名,方便辨認;動態調試時在這里下個斷點,成功斷下;由於1B47C只是dex加載失敗后“善后”的功能,這里直接忽略,看代碼直接跳轉到LABEL_74這里了;

  

   回到sub_CC9C函數,繼續往下走:這里調用fork創建子進程,這里也很可疑:一般一個進程自己跑自己的代碼,有些並行計算的需求就創建線程單獨跑,這里居然新生成一個進程,這種操作不常見,下斷點跟蹤后發現:在sub_10B8C斷下了,這個函數值得進去看看;

  

   本想用老規矩看看有沒有被混淆,結果IDA直接提示這個:不用想了,肯定有問題!

 

   F5的代碼是這樣的,正常人有這么寫代碼的么?

   

   在這個函數繼續下斷點的單步跟蹤:來到了FB64函數的_aeabi_memcpy這里:為啥要重點關注這個函數了? 前面已經把dex解密dump出來了,但是關鍵指令還被抽取掉了;要把指令還原,肯定要copy回去呀

  

   在_aeabi_memcpy這里下個斷點(動態調試居然沒識別函數名.......),R0就是dex的首地址了,同樣導出來:

  

  成功還原dex:

       

 

 

總結:

  1、重要的函數都會被混淆,這是一種典型的“此地無銀三百兩”的行為!所以一旦發現函數被混淆,都建議下個斷點調試一下,看看這個函數到底干了啥!

     2、字符串、dex、so這些文件被加密,但是在運行的時候肯定會解密,否則app怎么被cpu、android正確運行了? 所以dump內存是必須的步驟,這個一定不能少(這里抓住了兩個關鍵的函數:mmap和_aeabi_memcpy)!本人以前做windows逆向的,很多時候都是直接用CE搜索內存,找到關鍵數據開始逆向的。android下的逆向也能借鑒類似的思路,后續繼續分享!

   3、還有個比較明顯的通用的dex脫殼處:DexFile,不同版本的系統對這個類的定義可能不完全相同,建議從http://androidxref.cn這里查查類的具體定義,這里以7版本的舉例:http://androidxref.cn/android-7.1.2_r39/xref/art/runtime/dex_file.h,頭文件里面有兩個關鍵的成員變量,如下:

// The base address of the memory mapping.
1235    const uint8_t* const begin_;
1236  
1237    // The size of the underlying memory allocation in bytes.
1238    const size_t size_;

  這兩個分別是dex文件的指針和dex文件的大小,所以只要能得到這個指針,就能得到內存中解密后的dex文件,就可以dump出來!那么脫殼的問題又轉換成了:怎么找到DexFile這個指針了?這個簡單,用ida打開libart.so,函數名用DexFile去搜索,能找到一大堆使用了DexFile作為參數的函數,如下:

       

   這里以LoadMethod方法為例,第二個參數就是DexFile,很容易通過hook這個方法得到內存中的dex;然后在根據dex文件頭得到整個dex的大小,整個過程簡單粗暴,如下:

       

   hook的腳本如下: 這個腳本也能做成通用的dex脫殼方法(注意:4.4-7.0版本的DexFile參數是args[1],8.0-11.0版本的DexFile參數是args[0],其他的都通用)

function getDexFile() {
//32 _ZN3art11ClassLinker10LoadMethodERKNS_7DexFileERKNS_21ClassDataItemIteratorENS_6HandleINS_6mirror5ClassEEEPNS_9ArtMethodE
//64 _ZN3art11ClassLinker10LoadMethodERKNS_7DexFileERKNS_21ClassDataItemIteratorENS_6HandleINS_6mirror5ClassEEEPNS_9ArtMethodE
    var loadmethodaddr = Module.getExportByName("libart.so", "_ZN3art11ClassLinker10LoadMethodERKNS_7DexFileERKNS_21ClassDataItemIteratorENS_6HandleINS_6mirror5ClassEEEPNS_9ArtMethodE");
    console.log(Process.arch + "--loadmethod:" + loadmethodaddr);//Process.arch:運行模式是32還是64位
    Interceptor.attach(loadmethodaddr, {
        onEnter: function (args) {
            var dexfileptr = args[1];

            console.log("DexFile pointer:" + dexfileptr);

            var begin_ = ptr(dexfileptr).add(Process.pointerSize).readPointer();
            var size_ = ptr(dexfileptr).add(Process.pointerSize * 2).readU32();
            console.log(hexdump(ptr(begin_)));
            console.log("dexfile begin:" + begin_ + "--size:" + size_);
            //console.log(hexdump(ptr(dexfileptr)));


        }, onLeave: function (retval) {

        }
    });

}

setImmediate(getDexFile);

   4、個人的一點感悟:windows下3環和0環是嚴格分開的:3環是一般的exe或dll,0環就是驅動下的sys,有嚴格的隔離;要想hook操作系統內核,必須通過驅動進入0環;但是android下貌似簡單一些:只要手機root,就能hook libart這種系統級別的so庫,感覺簡單多了!一旦修改系統級別的so庫,和修改操作系統的源代碼已經沒有本質區別了,利用這一點可以做好多有趣的應用!

  

 

 

 

參考:

1、https://blog.csdn.net/m0_37344790/article/details/79102031   動態調試脫殼

2、https://github.com/maiyao1988/elf-dump-fix dump工具


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM