app逆向之安卓native層安全逆向分析(一):frida 與unidbg

前言

這個專題是根據白龍,龍哥的unidbg博客的案例,進行從0開始到逆向的流程,核心部分會借鑑龍哥的unidbg,通過借鑑大佬的思路,完整的分析某個so層的加密參數

各位朋友也可以直接讀龍哥的博客,我只是用我的角度進一步加工一下

原文地址:SO逆向入門實戰教程一:OASIS_so逆向學習路線_白龍~的博客-CSDN博客

分析

首先拿到這個app,安裝啥的就不多說了。

進入到註冊界面:

 

點擊獲取驗證碼,然後這邊抓包工具抓到的包:

 

然後,這裏面的【sign】就是今天的重點了。

 

用神祕的工具脫殼完之後,有如下的dex:

 

 

把這些dex,全部選中,拖進jadx 

 

 

 

快速定位

接下來開始找sign所在的位置,先看看抓包工具這邊的參數,根據這些鍵名,一頓搜,總能找到一些

 

先搜下【sign】,雖然搜出來的結果不多,但是感覺有很多幹擾項

 進最後那個,到這裏,

 

 

這些參數看着很像,用frida hook,得知,發現並沒有走到這裏

搜【ua】

 

 

進到這裏,發現很可疑,好多參數都對上了

 

 

啥都不說,先用objection hook下,然後app端點擊【重新發送】看看:

 

可以,這不直接就定位邏輯了

 

但是我們要找【sign】,所以還得再看看,jadx查看發現,這個方法反編譯效果不太好,問題不大,記住這個dex名,然後用GDA 打開這個dex,這就挺好,基本都反編譯出來了

 

 

反正看着確實可疑,但是這三個方法,還不確定到底是是哪個方法裏有【sign】部分,所以這裏直接hook 它這個a,b,c三個方法,然後app點下重新獲取

 

 

 

 

ok,發現最後的方法裏基於有我們要的sign,這邊再來下,用抓包工具對比下,謹慎一點,別搞半天沒搞對位置:

 

重新hook下,然後抓包工具打開看看

 

 

 

 

對上了,ok,就是這裏了【g.a.c.g.c.a】

在jadx裏,反編譯失敗:

 

問題不大,用GDA看:

 

 

這不就越來越接近了嗎,嘻嘻,點進去:

 

 

hook下這個c方法看看,ok,對上了

 

 接着一頓分析後再進入這裏

 ok,進到了native層,那麼核心的邏輯就在這裏了。

 

 

 

像這種,常規的方法怎麼解決呢?

 

  • 如果你是爲了拿數據,趕工期的話,那你可以直接主動調用這個方法
  • 如果你是爲了純算還原的話,那就把這個so文件拖進ida一頓分析了
  • 那麼假如,這個方法的純算很複雜,而你又不想主動調用,感覺很low的話,那你就可以嘗試用unidbg了

 

unidbg,就是本系列文章的重點了。

 

unidbg簡介

什麼是unidbg

 

unidbg 是一個基於 unicorn 的逆向工具,可以實現黑盒調用安卓和 iOS 中的 so 文件

unidbg是凱神寫的一個開源的java項目

 

使用場景

因爲現在的大多數 app 把核心的加密算法放到了 so 文件中(比如本例的app),你要想破解簽名算法,必須能夠破解 so 文件。但C++ 的逆向遠比 Java 的逆向要難得多,有各種混淆啊,ollvm啥的,所以好多時候是沒法純算還原破解的

那麼你是否有過一個想法,能不能把安卓的環境模擬出來,但是又脫離了安卓真機的環境,就可以直接使用so裏的方法呢?

unidbg 就是這樣一個工具,它模擬好了好幾種虛擬環境,他不需要直接運行 app,也無需逆向 so 文件,而是直接找到對應的 JNI 接口,然後用 unicorn 引擎(也不止這一個引擎)直接執行這個 so 文件,所以效率也比較高。

 

配置unidbg

 

1.首先,用git 把這個項目clone 下來:

zhkl0228/unidbg: Allows you to emulate an Android native library, and an experimental iOS emulation (github.com)

 

2.再用idea(寫java項目那個編輯器)打開,

 

 

首次打開,右下角會下載很多依賴環境,等待即可 

 

然後隨便找一個項目,執行下main方法,有正常輸出,說明環境配置好了:

 

執行結果:

 

ok,這就很nice。 

 

frida調試

 

接下來,先用ida 打開那個目標so文件

 

 

 

等左下角這個數據沒有再變的時候,說明加載好了

 

 

接下來找導出表【export】,或者在左邊的欄裏搜【java】

 

發現並沒有任何東西,那麼這裏就是動態註冊的so方法了。

 

 

什麼是動態註冊、靜態註冊

 

 

靜態註冊(又叫靜態綁定),就是,so的方法名直接export導出表裏,且命名格式爲【java_app包名_方法名】,比如 java_com_sina_oasxxx_nativeapi_s

動態註冊(又叫動態綁定)就是export到處表裏沒有的就是動態註冊。

 

 

那麼這裏我們的目標方法就是動態註冊的了,那咋辦,看看JNI_load方法:

 

 

按下【tab】鍵,會由上面的彙編代碼反編譯爲c代碼:

 

 

 

再按下【\】反斜槓,可讀性更強點:

 

但是這裏發現,一頓while 和if,根據龍哥的博客說的,大概率是ollvm混淆。那咋辦?

 

用yang神的hook_native腳本來hook出目標函數的偏移地址 ,然後加上so的基址,就可以得到目標函數的地址了(看是thumb還是arm,thumb要加1,arm不用)

frida_hook_libart/hook_RegisterNatives.js at master · lasting-yang/frida_hook_libart (github.com)

 

用frida hook一下,結果到這就報錯退出了

 

 

 

 

 

 

就很尷尬,看看,他報的哪個class名,用來過濾下試試:

 

 

 

重新運行frida試試,可以,這下直接就定位到我們要的方法的位置了:

 

 

這裏有朋友估計會說,臥槽,這不都出來了嗎,這地址,拿着直接用啊,不急,我重啓腳本看看:仔細看,fnptr地址變了,fnoffset和後面的jni-load + 的地址沒變的

 

 多次hook發現確實如此,因爲這裏就是上面說的動態註冊,所以這個fnptr,也就是so的基址,app沒啓動一次就會變,但是目標方法的偏移值是不會變的。ok

 

用frida hook so看看:

相關的hoo so,有個大佬總結的很好:

分類: frida | 凡牆總是門 (kevinspider.github.io)

 

ok,這裏我們hook下,拿下入參和返回值看看:

function inline_hook() {
    var so_addr = Module.findBaseAddress("liboasiscore.so");
    console.log("so_addr:", so_addr);
    if (so_addr) {
        var sub = so_addr.add(0x116cc); // 不用加1,是arm架構
        console.log("The addr_0x116cc:", sub);
        Java.perform(function () {
            Interceptor.attach(sub,
                {
                    onEnter: function (args) {
                        console.log("addr_0x116cc OnEnter :", this.context.PC,
                            this.context.x1, this.context.x5,
                            this.context.x10);
                    },
                    onLeave: function (retval) {
                        console.log("retval is :", retval)
                    },
                })
        })
    }
}


setTimeout(inline_hook, 1000)

  

運行結果:

 

 發現並不可讀,沒事,反正至少是有了

 

 

function stringToBytes(str) {
  return hexToBytes(stringToHex(str))
}

function stringToHex(str) {
  return str
    .split('')
    .map(function (c) {
      return ('0' + c.charCodeAt(0).toString(16)).slice(-2)
    })
    .join('')
}

function hexToBytes(hex) {
  for (let bytes = [], c = 0; c < hex.length; c += 2) bytes.push(parseInt(hex.substr(c, 2), 16))
  return bytes
}

function hexToString(hexStr) {
  let hex = hexStr.toString()
  let str = ''
  for (let i = 0; i < hex.length; i += 2) str += String.fromCharCode(parseInt(hex.substr(i, 2), 16))
  return str
}


  

function inline_hook() {
    var so_addr = Module.findBaseAddress("liboasiscore.so");
    console.log("so_addr:", so_addr);
    if (so_addr) {
        // var sub = so_addr.add(0x116cc); // 不用加1,是arm架構
        console.log("The addr_0x116cc:", so_addr);
        var ss = "aid=01A8SBOtNRVqsR1ywgkR4tHsZEsgXkGDrgKO2OvFBeThKWZDE.&cfrom=28B5295010&cuid=0&noncestr=L83x8Z40132Wan450y736563n3kmWj&phone=138469655665&platform=ANDROID&timestamp=1681790293128&ua=Xiaomi-MI6__oasis__3.5.8__Android__Android9&version=3.5.8&vid=2010511512550&wm=20004_90024";
        var add_addr = so_addr.add(0x116cc); // 32位需要加1
        var add = new NativeFunction(add_addr, 'pointer', ['pointer', 'int']);
        console.log(add)
        var result = add(stringToByte(Memory.allocUtf8String(ss)), false)
        console.log("add2 result is ->" + result.readCString());
    }

}
function stringToByte(str) {
    var ch, st, re = [];
    for (var i = 0; i < str.length; i++) {
        ch = str.charCodeAt(i);
        st = [];
        do {
            st.push(ch & 0xFF);
            ch = ch >> 8;
        } while (ch);
        re = re.concat(st.reverse());
    }   // return an array of bytes
    return re;
}

setTimeout(inline_hook, 2000)

  

嘗試主動調用,調試了很久,就是不行

 

突然反應過來,這個so文件有ollvm混淆啊,雖然用yang神的代碼hook到了偏移地址,但是他內部可能並不是這些參數,而我們又沒法直接分析,那沒法了。其實以上的代碼,在其他地方是可以用的,

 

那接下來咋辦?上unidbg吧

 

unidbg調試

 

上面用了frida+ida調試,發現有的時候沒法搞啊,那麼這裏,終於要用unidbg來模擬執行生成上面目標app的sign了

 

 先創建一個文件,把該有的都放進去

 

然後再oasis裏寫代碼,照着龍哥的搞就完了:注意文件路徑,跟你實際的路徑保持一致

package com.sina;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class oasis extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    oasis() {
        // 創建模擬器實例,進程名建議依照實際進程名填寫,可以規避針對進程名的校驗
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.sina.oasis").build();
        // 獲取模擬器的內存操作接口
        final Memory memory = emulator.getMemory();
        // 設置系統類庫解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 創建Android虛擬機,傳入APK,Unidbg可以替我們做部分簽名校驗的工作
        vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\sina\\lvzhou.apk"));
        // 加載目標SO
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\sina\\liboasiscore.so"), true); // 加載so到虛擬內存
        //獲取本SO模塊的句柄,後續需要用它
        module = dm.getModule();
        vm.setJni(this); // 設置JNI
        vm.setVerbose(true); // 打印日誌

        dm.callJNI_OnLoad(emulator); // 調用JNI OnLoad
    };

    public static void main(String[] args) {
        oasis test = new oasis();
    }
}

  

 然後運行一下,沒啥問題,說明架子是搭上了

 

 

仔細看這裏,這樣也把我們要的方法的地址拿到了。舒服啊

 

用前面frida 的對比,好像不太一樣,問題不大

 

再來看看代碼:

 

 

 

再來仔細看看他這個main幹了啥:

 

調用

架子搭好了,接下來調用,調用有兩種方式,一種是符號(symbol)調用,一種是地址調用,符號調用對應靜態註冊,地址調用對應動態註冊。那麼根據前面的解析,這裏我們只能選用地址調用了

 

先看看我們要調用的方法:

 

有兩個參數,一個byte數組,一個boolean,然後native層的方法,默認前面會自動加兩個參數 ,一個jni env,一個jobject

 

還是上面的frida腳本,先hook下,看看入參和返回:

 

ok,直接拿着這個str參數的值去unidbg構造:

注意,如果你用的舊版,也就是龍哥案例的代碼,直接報錯:

 

 

新版得這麼用,運行結果:

 

package com.sina;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.sun.jna.Pointer;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.lang.Number;

public class oasis extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    oasis() {
        // 創建模擬器實例,進程名建議依照實際進程名填寫,可以規避針對進程名的校驗
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.sina.oasis").build();
        // 獲取模擬器的內存操作接口
        final Memory memory = emulator.getMemory();
        // 設置系統類庫解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 創建Android虛擬機,傳入APK,Unidbg可以替我們做部分簽名校驗的工作
        vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\sina\\lvzhou.apk"));
        // 加載目標SO
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\sina\\liboasiscore.so"), true); // 加載so到虛擬內存
        //獲取本SO模塊的句柄,後續需要用它
        module = dm.getModule();
        vm.setJni(this); // 設置JNI
        vm.setVerbose(true); // 打印日誌

        dm.callJNI_OnLoad(emulator); // 調用JNI OnLoad
    };

    public static void main(String[] args) {
        oasis test = new oasis();
        System.out.println(test.getSign());
    }
    public String getSign(){
        List<Object> list = new ArrayList<>(10);
        list.add(vm.getJNIEnv()); // arg1,env
        list.add(0); // arg2,jobject
        String keywords = "aid=01A8SBOtNRVqsR1ywgkR4tHsZEsgXkGDrgKO2OvFBeThKWZDE.&cfrom=28B5295010&cuid=0&noncestr=L83x8Z40132Wan450y736563n3kmWj&phone=138469655665&platform=ANDROID&timestamp=1681790293128&ua=Xiaomi-MI6__oasis__3.5.8__Android__Android9&version=3.5.8&vid=2010511512550&wm=20004_90024";
        byte[] keyB = keywords.getBytes(StandardCharsets.UTF_8);
        ByteArray inbarr = new ByteArray(vm,keyB);
        list.add(vm.addGlobalObject(inbarr)); //arg3
        list.add(0); //arg4
//        Number number = module.callFunction(emulator,0xC365,list.toArray())[0];
        Number number = module.callFunction(emulator,0xC365,list.toArray());
        String result = vm.getObject(number.intValue()).getValue().toString();
        return result;
    }
}

  

 

驗證下結果,對上了,舒服:

 

 

結語

unidbg的實用之處就在這裏,相信不用我多說,你已經發現了很多妙用之處

 

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