<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    某APP加固產品的深入分析

    VSole2022-03-09 16:41:55

    樣本

    應用寶隨手下載一個安裝包,本文分析的是作業幫v13.28.0。

    工具

    jadx、idapro、unicorn。

    分析

    直接將apk拖入jadx,找到入口MyWrapperProxyApplication。

    繼承了WrapperProxyApplication,在attachBaseContext中調用了initProxyApplication,而initProxyApplication中調用com.wrapper.proxyapplication.Util.PrepareSecurefiles,然后加載libshell-super.2019.so。

    PrepareSecurefiles這個方法反編譯出來又長又臭,主要就是檢查下/data/data/com.baidu.homework/files/prodexdir目錄下的一些文件是否完整。

    這些文件都是從assets釋放的,主要有兩個文件tosversion和0OO00l111l1l。

    通過后續分析可知tosversion適用于判斷應用升級的,0OO00l111l1l是保存原始dex的加密文件。

    然后把libshell-super.2019.so拖入ida進行分析,首先看看.init_array,有一堆函數。

    把這些函數挨個大概看下,除了最后一個sub_29BC0都是在做字符串解密,這些字符加密的方式為每個字符串和一個固定的字符進行異或。

    雖然解密的方式十分簡單,但是需要處理的字符串數量太多了,手動一個一個處理肯定是不行的,這就需要腳本了。

    腳本處理有兩種方式:

    一種是分析匯編代碼,找出需要解密的字符串起始地址、字符串長度、解密key;

    另一種是unicorn直接運行.init_array中的函數,然后把運行后的內容直接加載到ida中。

    首先來看第一種方法,通過分析可知,每次解密的指令格式都是固定的。

    通過第三條指令LDR可以獲取到字符串起始地址,第四條有可能是跳轉指令B,也可能沒有。

    通過倒數第四條指令SUBS可以獲取到字符串長度,通過EOR.W指令可以獲取到用于解密的字符。

    根據以上分析結果,寫出idc腳本,執行一遍發現,存在解密失敗的。

    找到失敗的地址,發現當字符串長度為1的時候,指令格式不一致。

    把這個情況兼容一下,腳本內容如下:

    import idc  def find_next_chunk(addr):    if not idc.is_code(idc.get_full_flags(addr)):        return False, None, None, None, None     op = idc.print_insn_mnem(addr)    if op != 'MOVS' and op != 'MOV.W':        return False, None, None, None, None     addr = idc.next_head(addr)    op = idc.print_insn_mnem(addr)    if op != 'STRD.W':        return False, None, None, None, None     addr = idc.next_head(addr)    op = idc.print_insn_mnem(addr)    if op != 'LDR':        return False, None, None, None, None    arr_start = idc.get_wide_dword(idc.get_operand_value(addr, 1)) + addr + 2 * 3     addr = idc.next_head(addr)    op = idc.print_insn_mnem(addr)    if op != 'ADD':        return False, None, None, None, None     addr = idc.next_head(addr)    op = idc.print_insn_mnem(addr)    if op == 'B':        addr = idc.get_operand_value(addr, 0)        op = idc.print_insn_mnem(addr)    if op != 'LDRD.W':        return False, None, None, None, None     addr = idc.next_head(addr)    op = idc.print_insn_mnem(addr)    if op != 'LDRB':        return False, None, None, None, None     addr = idc.next_head(addr)    op = idc.print_insn_mnem(addr)    if op != 'EOR.W':        return False, None, None, None, None    xor_ch = idc.get_operand_value(addr, 2)     addr = idc.next_head(addr)    op = idc.print_insn_mnem(addr)    if op != 'STRB':        return False, None, None, None, None     addr = idc.next_head(addr)    op = idc.print_insn_mnem(addr)    if op != 'ADDS':        return False, None, None, None, None     addr = idc.next_head(addr)    op = idc.print_insn_mnem(addr)    if op == 'ADC.W':        addr = idc.next_head(addr)        op = idc.print_insn_mnem(addr)        if op != 'STR':            return False, None, None, None, None         addr = idc.next_head(addr)        op = idc.print_insn_mnem(addr)        if op != 'SUBS':            return False, None, None, None, None        arr_len = idc.get_operand_value(addr, 1)         addr = idc.next_head(addr)        op = idc.print_insn_mnem(addr)        if op != 'STR':            return False, None, None, None, None         addr = idc.next_head(addr)        op = idc.print_insn_mnem(addr)        if op != 'SBCS.W':            return False, None, None, None, None         addr = idc.next_head(addr)        op = idc.print_insn_mnem(addr)        if op != 'BCC':            return False, None, None, None, None         next_addr = idc.next_head(addr)        return True, next_addr, xor_ch, arr_start, arr_len     elif op == 'STR':        addr = idc.next_head(addr)        op = idc.print_insn_mnem(addr)        if op != 'ADCS.W':            return False, None, None, None, None         addr = idc.next_head(addr)        op = idc.print_insn_mnem(addr)        if op != 'STR':            return False, None, None, None, None         addr = idc.next_head(addr)        op = idc.print_insn_mnem(addr)        if op != 'ADCS.W':            return False, None, None, None, None         addr = idc.next_head(addr)        op = idc.print_insn_mnem(addr)        if op != 'BNE':            return False, None, None, None, None         next_addr = idc.next_head(addr)        return True, next_addr, xor_ch, arr_start, 1     else:        return False, None, None, None, None  def decode_arr(arr_start, arr_len, xor_ch):    for addr in range(arr_start, arr_start + arr_len):        ch = idc.get_wide_byte(addr)        # print hex(ch)        idc.patch_byte(addr, ch ^ xor_ch)    idc.create_strlit(arr_start, arr_start + arr_len + 1)  def proc_func(func_addr):    if not func_addr:        return    # Thumb, func_start_addr+1==func_addr    func_start_addr = idc.get_func_attr(func_addr, idc.FUNCATTR_START)    func_end_addr = idc.get_func_attr(func_addr, idc.FUNCATTR_END)    print hex(func_start_addr), '----->', hex(func_end_addr)    addr = func_start_addr    while addr < func_end_addr:        succ, next_addr, xor_ch, arr_start, arr_len = find_next_chunk(addr)        if succ:            print hex(addr).ljust(10), hex(xor_ch).ljust(10), hex(arr_start).ljust(10), arr_len            decode_arr(arr_start, arr_len, xor_ch)            addr = next_addr        else:            print '*' * 10, hex(addr)            addr = idc.next_head(addr)    print '' * 3  def decode_str():    idc.auto_wait()    start_addr = idc.get_segm_by_sel(idc.selector_by_name('.init_array'))    end_addr = idc.get_segm_end(start_addr)    addr = start_addr    while addr + 4 <= end_addr:        func_addr = idc.get_wide_dword(addr)        proc_func(func_addr)        addr += 4 decode_str()
    

    ida加載運行該腳本,得到解密后的內容。

    然后是第二種方法,通過unicorn直接運行.init_array中的解密函數,然后把解密后的內容直接加載到ida中。

    import unicorn import idc  def func_block_handle(uc, address, size, user_data):    if address in (0, 0x29BC0):        uc.emu_stop()  def decode_str():    idc.auto_wait()     dir_path = r'/Users/lll19/Downloads/legu/'    bin_len = idc.prev_addr(idc.BADADDR)    bin_len = (bin_len / 0x1000 + (1 if bin_len % 0x1000 else 0)) * 0x1000    bin_path = dir_path + 'elf_bin'    idc.savefile(bin_path, 0, 0, bin_len)    f_bin = open(bin_path, 'rb')    bin_bytes = bytes(f_bin.read())    f_bin.close()     stack_size = 0x100000    stack_top = bin_len    stack_bottom = stack_top + stack_size     uc = unicorn.Uc(unicorn.UC_ARCH_ARM, unicorn.UC_MODE_THUMB)    uc.hook_add(unicorn.UC_HOOK_BLOCK, func_block_handle)    uc.mem_map(0, bin_len)    uc.mem_write(0, bin_bytes)    uc.mem_map(stack_top, stack_size)     start_addr = idc.get_segm_by_sel(idc.selector_by_name('.init_array'))    end_addr = idc.get_segm_end(start_addr)    addr = start_addr    while addr + 4 <= end_addr:        func_addr = idc.get_wide_dword(addr)        addr += 4        if not func_addr:            continue        print hex(func_addr)         func_end_addr = idc.find_func_end(func_addr)        uc.reg_write(unicorn.arm_const.UC_ARM_REG_SP, stack_bottom)        uc.reg_write(unicorn.arm_const.UC_ARM_REG_LR, 0)        uc.emu_start(func_addr, func_end_addr)     f_save = open(bin_path, 'wb')    f_save.write(str(uc.mem_read(0, bin_len)))    f_save.close()    idc.loadfile(bin_path, 0, 0, bin_len) decode_str()
    

    然后開始分析JNI_OnLoad,這個函數被混淆了,于是繼續unicorn走起。

    根據輸出可知,該函數主要執行了以下幾個操作:

    通過RegisterNatives注冊WrapperProxyApplication.Ooo0ooO0oO(),

    調用0x1f668處的函數進行上下文初始化,調用0x2B604處的函數生成解密key,調用0xcea8處的函數加載dex。

    先分析下sub_1f668處的初始化過程,東西比較多就不貼圖了

    首先是一些常規操作,獲取vm類型、PackageInfo、ActivityThread、ClassLoader等。

    然后是通過GetMethodID獲取之前注冊的WrapperProxyApplication.Ooo0ooO0oO()的MethodID,然后在MethodID指向的內存(通過Android源碼可知,MethodID實際是ArtMethod對象的指針)查找之前注冊的函數地址,找到后保存該偏移值,后面會通過這個偏移值對系統native方法進行hook。

    再然后就是加載0OO00l111l1l,解析該文件,把該文件的各種數據指針緩存起來,用于后面數據解密。

    再然后判斷系統是否升級,讀取prodexdir/.updateIV.dat中緩存的數據與libart.so和dex2oat的大小進行比較,如果不相等則將.odex和.vdex文件刪除并更新.updateIV.dat。

    0OO00l111l1l數據結構如下,前4字節為dex的數量,后面分別為三種數據,通過后面分析可知:

    第一部分數據為壓縮的dex,其中的指令被抽取了;第二部分為壓縮且加密的索引數據;第三部分為壓縮且加密的指令數據。

    看下sub_2B604的代碼,比較簡單,讀取文件內容與byte_31391處的字符處理后存放在byte_36A8C。

    大概分析下sub_cea8過程,調用sub_1CD90,對DexFile.defineClassNative()進行hook,原理是先獲取defineClassNative的MethodID,然后通過先前獲取到的偏移值,將該函數的本地函數指針保存起來,再通過RegisterNatives重新為該函數注冊一個本地函數。

    調用java方法com.wrapper.proxyapplication.MultiDex.preparetoinstallDexes(),獲取dexElements,hook幾個系統函數mmap、execve、execv,hook的方式是通過遍歷重定位項實現的。這個幾個hook在我分析的過程中沒用上。

    多線程調用函數sub_CE14解密加載dex并opt,等待所有線程結束后,獲取mCookie緩存起來,取消之前hook的幾個系統函數,并設置幾個環境變量,

    構造當前應用的原始application,并調用其attach()方法。

    剩下的流程都不重要了,現在只關心解密函數sub_CE14,

    首先調用sub_CC2C進行dex解壓縮并寫入/data/data/com.baidu.homework/files/prodexdir下的dex文件,再調用java方法com.wrapper.proxyapplication.MultiDex.installDexes()進行dex替換。

    解壓縮函數為sub_2B2AC,看看反編譯代碼,全是各種字符操作,具體算法就不看了,待會兒直接上unicorn。

    dex加載成功后調用sub_10EB4進行指令解密。

    首先對第二部分數據進行解密,解密函數為sub_2315C,解密完成后調用sub_2B2AC解壓縮,再調用sub_115F4生成每個類結構的索引。

    然后對第三部分數據進行解密,解密函數為sub_2315C,解密完成后調用sub_2B2AC解壓縮,得到所有抽取的指令。

    現在只剩最后一步了,那就是在class加載的時候,對抽取的指令進行填充,這里需要分析的是前面hook的DexFile.defineClassNative(),對應的函數為sub_1C900,其主要內容如下。

    首先查找class所在的dex和對應的索引(sub_22B90),然后進行指令填充(sub_101B8),最后調用原來的函數地址。

    sub_22B90的代碼和對應的數據如下,通過分析可知,從偏移8開始是一個hash表,每個表項3個字段,分別為hash值、類名偏移、類結構索引偏移。

    偏移為4的字段為hash表的大小。

     

    函數sub_101B8對class指令進行填充,函數被混淆了,既然這樣,也就不看了,直接unicorn。

    到此為止,所有的流程都分析完了。

    開始準備脫殼腳本,需要模擬執行的函數流程分為以下幾部分:

    1、執行0x2B604處的代碼。讀取tosversion文件的內容,處理后作為解密的key(該處只讀取了16個字節,但是解密的時候,復制了32字節的key,但是不影響,實際執行解密的時候只用了前16字節)。

    2、循環執行0x2B2AC處的代碼。解壓出所有被抽取指令的dex。

    3、循環執行0x2315C、0x2B2AC、0x115F4這三處代碼,分別對應class信息的解密、解壓、建索引。

    4、循環執行0x2315C、0x2B2AC這兩處代碼,分別對應指令的解密、解壓。

    5、遍歷所有dex的hash表,循環執行0x101b8處的代碼進行指令修復。

    完整的脫殼腳本就不貼了,提示超出字數限制了,我把它放在附件里面了。

    樣本打包后附件大小也超限制了。

    樣本我放網盤了,附件只保留了腳本文件。

    貼個脫殼修復后的圖:

    函數調用匯編指令
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    匯編語言是一種用于電子計算機、微處理器、微控制器或其他可編程器件的低級語言,亦稱為符號語言。Smali匯編基礎Smali語言最早是由JesusFreke發布在Google Code上的一個開源項目,并不是擁有官方標準的語言。因此也將Smali語言稱作Android虛擬機的反匯編語言。基本類型Smali基本數據類型中包含兩種類型,原始類型和引用類型。而在Smali中則是以LpackageName/objectName的形式表示對象類型。
    概述在windows系統上,涉及到內核對象的功能函數,都需要從應用層權限轉換到內核層權限,然后再執行想要的內核函數,最終將函數結果返回給應用層。本文就是用OpenProcess函數來觀察函數從應用層到內核層的整體調用流程。OpenProcess函數,根據指定的進程ID,返回進程句柄。NTSTATUS Status; //保存函數執行狀態。OBJECT_ATTRIBUTES Obja; //待打開對象的對象屬性。HANDLE Handle; //存儲打開的句柄。CLIENT_ID ClientId; //進程、線程ID. dwDesiredAccess, //預打開進程并獲取對應的權限。ObjectNamePresent = ARGUMENT_PRESENT ; //判斷對象名稱是否為空
    得益于Unicorn的強大的指令trace能力,可以很容易實現對cpu執行的每一條匯編指令的跟蹤,進而對ollvm保護的函數進行剪枝,去掉虛假塊,大大提高逆向分析效率。
    欺騙防御新方法:rMTD
    2021-09-21 22:41:45
    如果運行的操作系統和應用中存在漏洞,那攻擊者大概率會找到一個利用它的方法。唯一確定的解決隱患方式就是從程序庫中修復問題。但是,在安全補丁發布前,系統依然有被攻陷的風險。許多人不得不接受這種情況。 不過,事情可能出現了轉機:輪換移動目標防御技術(rotational Moving Target Defense, rMTD)。
    前言最近一段時間在研究Android加殼和脫殼技術,其中涉及到了一些hook技術,于是將自己學習的一些hook技術進行了一下梳理,以便后面回顧和大家學習。主要是進行文本替換、宏展開、刪除注釋這類簡單工作。所以動態鏈接是將鏈接過程推遲到了運行時才進行。
    毫無疑問,app逆向的第一步是抓包。通過抓包可以獲取很多有用的線索,比如url,參數名等。再根據url,參數名等,可以逐步抽絲剝繭找到app收發數據的地方,然后就能找到最關鍵的簽名所在的位置。Cronet是從Chrome中抽出的給移動端使用的網絡組件,目前針對Cronet抓包,證書校驗的研究比較少,大多是奇技淫巧,沒能從根本上挖掘。如果沒有錯誤則檢查了證書的發布者是否為known_root,然后返回OK。換句話說,就是證書校驗的過程被bypass了。
    近期突然發現64位APP分析需求激增,然而手邊好用的 inlineHook 只有 Frida 一款,所以打算稍微研究下 Frida 的思路,以作借鑒,然后寫一款滿足簡單自用需求的 AArch64 inlineHook 工具。Step1:首先我們簡單編寫一個 com.example.x64 應用作為目標 APP,且在 libx64.so 中放置一個 native 函數:?
    靜態分析法是在不執行代碼文件的情形下,對代碼進行靜態分析的一種方法。靜態分析時并不執行代碼,而是觀察代碼文件的外部特征,獲取文件的類型(EXE、DLI、DOC、ZIP等)、大小、PE頭信息、Import/ExportAPI內部字符串、是否運行時解壓縮、注冊信息、調試信息、數字證書等多種信息。
    源碼分析1、LLVM編譯器簡介LLVM 命名最早源自于底層虛擬機的縮寫,由于命名帶來的混亂,LLVM就是該項目的全稱。LLVM 核心庫提供了與編譯器相關的支持,可以作為多種語言編譯器的后臺來使用。自那時以來,已經成長為LLVM的主干項目,由不同的子項目組成,其中許多是正在生產中使用的各種 商業和開源的項目,以及被廣泛用于學術研究。
    棧與棧幀的調試
    2022-03-06 16:24:19
    再次執行pop EAX,ESP的值增加4個字節,變為0012FFC4。OD狀態變成最開始的狀態。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类