Android9.0 hook dlopen問題/如何hook dlopen相關函數

Android9.0中在activity的onCreate之前hook dlopen函數,如果需要返回值(即修改了LR寄存器),那麼會觸發:E/libEGL: EGL_ANDROID_blob_cache advertised, but unable to get eglSetBlobCacheFuncsANDROID。不會crash,但是界面不會繪製出來。

是因爲dlopen(libEGL_adreno.so, 2)、dlopen(libGLESv2_adreno.so, 2)返回都是null。理論上其他任何hook框架也都存在這個問題,測試了幾個也確實都存在。

dlopen函數實現在libdl.so,

.text:0000000000000EF8                 WEAK dlopen
.text:0000000000000EF8 dlopen                                  ; DATA XREF: LOAD:0000000000000508↑o
.text:0000000000000EF8
.text:0000000000000EF8 var_s0          =  0
.text:0000000000000EF8
.text:0000000000000EF8 ; __unwind {
.text:0000000000000EF8                 STP             X29, X30, [SP,#-0x10+var_s0]!
.text:0000000000000EFC                 MOV             X29, SP
.text:0000000000000F00                 MOV             X2, X30 ; a3
.text:0000000000000F04                 BL              .__loader_dlopen
.text:0000000000000F08                 LDP             X29, X30, [SP+var_s0],#0x10
.text:0000000000000F0C                 RET
.text:0000000000000F0C ; } // starts at EF8
.text:0000000000000F0C ; End of function dlopen

這裏把LR寄存器當作第三個參數傳遞給.__loader_dlopen,而我們hook時爲了能拿到返回值是修改了LR寄存器的,這是不可避免的。如果不修改LR寄存器,除非我們知道函數結尾,在函數結尾修改LR寄存器或者跳到shellcode/hook函數,但是通常是不現實的,函數開頭容易確認,函數結尾很難自動化確認。

// Proxy calls to bionic loader
__attribute__((__weak__))
void* dlopen(const char* filename, int flag) {
  const void* caller_addr = __builtin_return_address(0);
  return __loader_dlopen(filename, flag, caller_addr);
}

__builtin_return_address函數應該是gcc內部函數,0獲取的是被調用函數返回後執行的指令地址,至於1之後的數字是否能獲取到函數調用棧待測試。已測試:
1、0獲取的就是進入函LR寄存器的值。
2、1獲取的是r7(thumb)/r12(arm),即ip寄存器的值,所以能不能獲取到正確的值取決於上層是否使用了ip寄存器暫存sp,且ip寄存器之後(棧上)就是在棧上存儲的LR寄存器的值。所以不滿足這些條件的函數是獲取不到的甚至是錯誤的。

爺爺函數:
.text:0000C250 ; __unwind {
.text:0000C250                 PUSH            {R4,R5,R7,LR}
.text:0000C252                 ADD             R7, SP, #8
...
.text:0000C292                 BLX             j__Z12test_replacev ; test_replace(void)

--------------------------------------

父函數:
.text:0000B648                 PUSH            {R7,LR}
.text:0000B64A                 MOV             R7, SP
...
.text:0000B6A6                 BLX             j__Z12dump_replacePvS_PFvP10my_pt_regsP11STR_HK_INFOES5_PKc ;

--------------------------------------

子函數:
.text:0000D268                 PUSH            {R4-R7,LR}
.text:0000D26A                 ADD             R7, SP, #0xC
...
.text:0000D2DA                 MOV             R1, R7
.text:0000D2DC                 LDR             R1, [R1];    父函數傳過來的R7的值
.text:0000D2DE                 LDR             R3, [R1,#4];r3寄存器爲__builtin_return_address(1),即取父函數放在R7之後的LR寄存器的值

.__loader_dlopen就是一個跳板

.plt:0000000000000DD8 ; __int64 __fastcall __loader_dlopen(__int64 a1, __int64 a2, __int64 a3)
.plt:0000000000000DD8 .__loader_dlopen                        ; CODE XREF: dlopen+C↓p
.plt:0000000000000DD8                 ADRP            X16, #off_1FF70@PAGE
.plt:0000000000000DDC                 LDR             X17, [X16,#off_1FF70@PAGEOFF]
.plt:0000000000000DE0                 ADD             X16, X16, #off_1FF70@PAGEOFF
.plt:0000000000000DE4                 BR              X17     ; __loader_dlopen
.plt:0000000000000DE4 ; End of function .__loader_dlopen

導入__loader_dlopen函數,實現在linker/64,所以從libdl.so的got表中可以獲取__loader_dlopen函數的絕對地址。

static void* dlopen_ext(const char* filename,
                        int flags,
                        const android_dlextinfo* extinfo,
                        const void* caller_addr) {
  ScopedPthreadMutexLocker locker(&g_dl_mutex);
  g_linker_logger.ResetState();
  void* result = do_dlopen(filename, flags, extinfo, caller_addr);
  if (result == nullptr) {
    __bionic_format_dlerror("dlopen failed", linker_get_error_buffer());
    return nullptr;
  }
  return result;
}

void* __loader_android_dlopen_ext(const char* filename,
                           int flags,
                           const android_dlextinfo* extinfo,
                           const void* caller_addr) {
  return dlopen_ext(filename, flags, extinfo, caller_addr);
}

void* __loader_dlopen(const char* filename, int flags, const void* caller_addr) {
  return dlopen_ext(filename, flags, nullptr, caller_addr);
}

dlopen_ext函數被__loader_dlopen函數內聯了,所以__loader_dlopen函數足夠被inline hook了。

__int64 __fastcall _loader_dlopen(__int64 a1, unsigned int a2, __int64 a3)
{
  _BYTE *v3; // x21
  __int64 v4; // x19
  unsigned int v5; // w20
  __int64 v6; // x19
  __int64 v7; // x0

  v3 = (_BYTE *)a1;
  v4 = a3;
  v5 = a2;
  _dl_pthread_mutex_lock(&_dl__ZL10g_dl_mutex);
  _dl__ZN12LinkerLogger10ResetStateEv(&_dl_g_linker_logger);
  v6 = _dl__Z9do_dlopenPKciPK17android_dlextinfoPKv(v3, v5, 0LL, v4);
  if ( !v6 )
  {
    v7 = _dl__Z23linker_get_error_bufferv(0LL);
    _dl__ZL23__bionic_format_dlerrorPKcS0_("dlopen failed", v7);
  }
  _dl_pthread_mutex_unlock(&_dl__ZL10g_dl_mutex);
  return v6;
}

首先爲了驗證是不是因爲LR寄存器導致的問題,使用dump函數"(dump((void*)dlopen, onPreCallBack, NULL, “dlopen”)😉",只在函數之前打印下參數寄存器,之後調用原函數,發現可以正常運行了。

使用導入表/導出表hook dlopen也出現該問題。

hook __loader_dlopen代替dlopen,發現dump、replace都可以正常運行,至此可以確定確實是修改了LR寄存器的問題。

而__loader_dlopen函數導出在linker,通過dlsym(RTLD_DEFAULT, “__loader_dlopen”)是獲取不到的,linker不是動態庫,這裏採用

//高版本儘量使用,沒有詳細看各個版本的Android,不過記憶中從Android8.0使用的libdl.so,所以應該8.0及以上
//或者一步到位hook所有dlopen(包括dlopen_ext)都調用的函數do_dlopen,這個函數在linker中,所以需要在linker中找到函數地址,並沒有導出,但是沒有去符號,可以自己解析
//    void *do_dlopen = dlsym(RTLD_DEFAULT, "__dl__Z9do_dlopenPKciPK17android_dlextinfoPKv");
//    LE("do_dlopen=%p", do_dlopen);
//    dump((void*)do_dlopen, onPreCallBack, onCallBack, "do_dlopen");
void test__loader_dlopen(){
    //因爲__loader_dlopen只在libdl.so導入,真正的導出函數是在linker中
    void *__loader_dlopen = dlsym(RTLD_DEFAULT, "__loader_dlopen");//0x7d3ff19dd8
    LE("__loader_dlopen=%p", __loader_dlopen);
    if (!__loader_dlopen) {
        //通過libdl.so拿到的__loader_dlopen地址就是linker導出的函數地址。看源碼的話這個__loader_dlopen函數只是調用dlopen_ext,應該因爲
        //太短無法hook,不過就是因爲太短被和dlopen_ext內聯成一個函數了,dlopen_ext未導出。所以可以hook。
        void *dl = dlopen("libdl.so", RTLD_LAZY);
        LE("libdl.so=%p", dl);
        __loader_dlopen = dlsym(dl, "__loader_dlopen");//0x7d3ff19dd8
        LE("__loader_dlopen=%p", __loader_dlopen);
    }

    if (!__loader_dlopen) {
        return;
    }

    //    dump((void*)__loader_dlopen, onPreCallBack, onCallBack, "__loader_dlopen");
    const RetInfo info = dump_replace((void *) __loader_dlopen, (void *) (my__loader_dlopen), NULL,
                                       NULL, "__loader_dlopen");
    if (info.status != success) {
        LE("hook __loader_dlopen error=%d", info.status);
    }

}

通過第三個參數確定libEGL_adreno.so是被libGLESv2_adreno.so內的函數dlopen;libGLESv2_adreno.so應該是被libadreno_utils.so內的函數dlopen;依此類推libgrallocutils.so、libadreno_utils.so、libboost.so、libgui.so。可能有誤,因爲這個其實不關鍵,我也不關心gui流程,有誰關心這個的可以分析整個流程。

因爲傳入的p(LR寄存器的值)導致返回null,即使不爲null,後面對應的android_namespace_t肯定也不一致。

soinfo* find_containing_library(const void* p) {
  ElfW(Addr) address = reinterpret_cast<ElfW(Addr)>(p);
  for (soinfo* si = solist_get_head(); si != nullptr; si = si->next) {
    if (address >= si->base && address - si->base < si->size) {
      return si;
    }
  }
  return nullptr;
}

最終結果就是已加載的so是其他命名空間、classloader加載的,所以查找不到最終返回null。看到這發現和7.0之後的限制私有api是一回事。那麼即然是根據第三個參數判斷的,那麼hook __loader_dlopen函數傳入一個符合的地址是不是就可以繞過了。

進行測試:
在這裏插入圖片描述
看到日誌linker: library “/system/lib/libnativehelper.so” ("/system/lib/libnativehelper.so") needed or dlopened by可以確定就是android7.0開始採用的命名空間限制私有api調用。

對其進行過濾,把LR寄存器的值改爲符合地址範圍的,這裏我使用dlerror的地址,只要符合即可。通過日誌可以看到成功了。
在這裏插入圖片描述

所以這就是我早期針對Android7.0限制私有api的一種繞過方式,比起自己加載系統so,自己解析再通過偏移值確定地址好些。。。,通過這還可以延伸出我動態注入時做的一件事情全局繞過私有api限制。以前frida是沒有這麼做過的,沒看最近的代碼,不確定是否現在也採用這樣的方法了沒有。

所以對於hook dlopen可以這麼劃分:
1、低於7.0的版本只hook dlopen即可。
2、8.0以上最好hook __loader_dlopen或者do_dlopen。或者hook dlopen過濾發現是系統so(限制的私有so)不取返回值(即不修改LR寄存器),或者自行解析、重組dlopen的指令傳入原來的LR寄存器(不推薦)。
3、7.0-7.1的hook do_dlopen,因爲沒有采用libdl.so做中轉。
4、全局關閉命名空間限制使用私有api,不是特別推薦吧,但其實我是這麼做的。
5、更投機取巧的方法,即然只要LR寄存器合乎規則即可,那麼可以針對dlopen、dlsym從BL指令轉成先設置LR寄存器爲一個libc等so裏面的一個不常用的函數c(常用的也不是不行,但是對效率可能有影響),之後使用B指令跳轉到備份/修復的dlopen原函數執行。c函數進行hook,使用一樣的跳板,只是shellcode有些不一樣。因爲dlopen等函數執行完會跳回c函數,那麼LR寄存器就是c函數的地址,只要c函數不是一個遞歸函數,那麼就可以通過判斷LR寄存器區別出是dlopen等函數的返回還是其他函數正常調用的c函數。當是dlopen函數返回時再跳回dlopen的shellcode處完成返回。

未解決的疑問:
我記得梆梆也hook了dlopen、dlsym,arm使用的是Cydia Substrate框架,在高版本上應該也存在這個問題的,但是以前沒仔細看,我記得應該沒特殊處理的,奇怪。抽時間再逆下,除非他改了生成的shellcode,非自己要hook的so就過濾掉,不修改LR寄存器。

已解決:後來抽時間看下,原來也是解析了__loader_dlopen、__dl__Z9do_dlopenPKciPK17android_dlextinfoPKv等,

if ( sdk_0 >= 24 )
      {
        if ( g_addr_do_dlopen )
        {
            p_dlopen = (void *(**)(const char *, int))g_addr_do_dlopen;

所以也是參照上面的3來做的。不過看其定位了一些安卓源碼中沒有的符號,所以也存在兼容性問題?看來也是存在風險的。畢竟不是標準c/c++ api,確實有可能被魔改。

dlsym等其他幾個函數也是類似的處理,甚至如果只是爲了調用系統的私有api,只hook __loader_dlsym即可,使用dlsym(RTLD_DEFAULT, “xx”)即可,把__loader_dlsym的第三個參數改爲合法的地址。

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