上周用ida調試x音的時候遇到了大量的彈窗,要是一不小心選擇了“pass to app”,結果大概率直接崩掉......... 彈窗這個問題困擾我好久了,如果不解決,后面的trace就沒法做了,該怎么解決了?這就要從彈窗的原理說起了!近期用ida調試時遇到的彈窗提示整理如下:
3B745B60: got SIGSEGV signal (Segmentation violation) (exc.code b, tid 17222) 3B745B60: got SIGSEGV signal (Segmentation violation) (exc.code b, tid 17222 C2087AB6: got SIGILL signal (Illegal instruction) (exc.code 4, tid 23457) C197C3BA: got SIGSEGV signal (Segmentation violation) (exc.code b, tid 8991) C1984B64: got SIGCHLD signal (Child status has changed) (exc.code 11, tid 8973) C1710B64: got SIGCHLD signal (Child status has changed) (exc.code 11, tid 23546) C17083BA: got SIGSEGV signal (Segmentation violation) (exc.code b, tid 23565) D736D93C: got SIGSTOP signal (Stop unblockable) (exc.code 13, tid 23384) C1FEF3BA: got SIGSEGV signal (Segmentation violation) (exc.code b, tid 3838) F739B2B8: got SIGABRT signal (Abort) (exc.code 6, tid 3980)
主要有segmentation violation、illegal instrction、child status has changed、abort、stop unblocked等,可謂是五花八門,什么樣的都有!大家有沒有想過為什么會彈窗了(這不廢話么,當然是客戶端為了保護自己故意反調試的啦)? 彈窗本質上也是一段代碼,既然展示了出來,說明這段代碼肯定被執行了!可是我們明明正在調試主線程,這些彈窗的代碼都是怎么執行的了? 那就只能時另一種可能了:其他線程執行的!這些彈窗都是通過信號量提示的,說明不同的線程在利用信號量通信!在導入表里面搜索,確實能找到sigaction和pthrad_cond_signal函數,並且還在好幾個地方被其他函數調用過!如果直接簡單粗暴地NOP這些代碼,我擔心破壞原有正常的業務邏輯,所以就只能掛起其他線程了!這里直接使用看雪大佬YANG的腳本來掛起其他線程,然后再調試,整個ida再也沒有彈窗,清爽多了!
上次分析到:sub_6221C函數內部使用了base64碼表,通過ida調試也在內存發現了疑似X-Argus、X-Ladon、X-Tyhon的字符串,這里為了進一步確認,從函數頭開始逐行調試,整個邏輯清晰多了:

R1指向加密字符串的末尾,每次移動4字節;加密字節分別存放在R0、R1和R6中,分別通過不同的偏移從R4指向的碼表中取值!所以現在的關鍵就是確認這些碼表內偏移是怎么得到的了!繼續往上追溯:這幾個偏移都是從R5計算而來的,而R5又是通過下面的方式得到:從這里可以看出,R5或R11指向的內存區域並不是字符串,這里有點失望!
一般情況下:服務端為了確認接收到的數據沒被篡改,會讓客戶端將加密的校驗字段和原文一起發送;服務端接收后用同樣的加密算法計算原文,如果和客戶端發送的校驗字段一致,說明原文沒被篡改(數字證書就用到了這個原理);我原本的猜想:客戶端會選擇一下https包的原文字符串通過加密算法計算出校驗字段,然后把校驗字段和原文一起發送,所以在逐行調試時應該能找到原文字符串,結果大失所望,這里並不是!

估計是網上追溯的層次不夠,那就繼續追唄!在sub_6221C開始的地方發現R11是R3得到的,這個R3應該是上層函數調用的參數:

繼續往上一層函數追溯,發現這里直接跳轉過來了,所以R3又是由R5決定的!

已經追溯到偏移為0x6C41E的地方,暫時還沒找到https包的用來加密的原文,下一步打算根據棧回溯來挨個查找!

小結:
(1)這里確認找到了加密字段的生成代碼,但是還沒找到生成加密字段的原文是啥,需要繼續追蹤!
(2)信號量:主線程和子線程之間的通信方式,可能的反調試手段有:
-
- 子線程不停地讀取主線程status,一旦發現tracerpid不為0說明被調試了,可以發個kill或其他的信號,然后被調試器捕獲,就彈窗給使用人員看了
- 子線程和主線程通過信號量通信,一旦發現對方長時間不回復,說明“出事”了,所以要掛起其他線程,只留主線程
腳本:可掛起線程、指令級別地trace;
# -*- coding: utf-8 -*- import idaapi import idc import re import ida_dbg import ida_idd from idaapi import * from collections import OrderedDict import logging import time import datetime import os debughook = None def xx_hex(ea): return hex(ea).rstrip("L").lstrip("0x") def set_breakpoint(ea): #idc.SetReg(ea, "T", 1) #idc.MakeCode(ea)#ida7.5報錯:AttributeError: module 'idc' has no attribute 'MakeCode',新版本ida不兼容舊版本的api,鏈接有替代的接口 idc.create_insn(ea)#https://hex-rays.com/products/ida/support/ida74_idapython_no_bc695_porting_guide.shtml idc.add_bpt(ea) def my_get_reg_value(register): rv = ida_idd.regval_t() ida_dbg.get_reg_val(register, rv) current_addr = rv.ival return current_addr def suspend_other_thread(): current_thread = idc.get_current_thread() thread_count = idc.get_thread_qty() for i in range(0, thread_count): other_thread = idc.getn_thread(i) if other_thread != current_thread: idc.suspend_thread(other_thread) def resume_process(): current_thread = idc.get_current_thread() thread_count = idc.get_thread_qty() for i in range(0, thread_count): other_thread = idc.getn_thread(i) if other_thread != current_thread: idc.resume_thread(other_thread) idc.resume_thread(current_thread) idc.resume_process() class MyDbgHook(DBG_Hooks): """ Own debug hook class that implementd the callback functions """ def __init__(self, modules_info, skip_functions, end_ea): super(MyDbgHook, self).__init__() self.modules_info = modules_info self.skip_functions = skip_functions self.trace_step_into_count = 0 self.trace_step_into_size = 1 self.trace_total_size = 300000 self.trace_size = 0 self.trace_lr = 0 self.end_ea = end_ea self.bpt_trace = 0 self.Logger = None self.line_trace = 0 print("__init__") def start_line_trace(self): self.bpt_trace = 0 self.line_trace = 1 self.start_hook() def start_hook(self): self.hook() print("start_hook") def dbg_process_start(self, pid, tid, ea, name, base, size): print("Process started, pid=%d tid=%d name=%s" % (pid, tid, name)) def dbg_process_exit(self, pid, tid, ea, code): self.unhook() if self.Logger: self.Logger.log_close() print("Process exited pid=%d tid=%d ea=0x%x code=%d" % (pid, tid, ea, code)) def dbg_process_detach(self, pid, tid, ea): self.unhook() self.Logger.log_close() return 0 def dbg_bpt(self, tid, ea): print("Break point at 0x%x tid=%d" % (ea, tid)) if ea in self.end_ea: ida_dbg.enable_insn_trace(False) ida_dbg.enable_step_trace(False) ida_dbg.suspend_process() return 0 return 0 def dbg_trace(self, tid, ea): #print("Trace tid=%d ea=0x%x" % (tid, ea)) # return values: # 1 - do not log this trace event; # 0 - log it if self.line_trace: in_mine_so = False for module_info in self.modules_info: # print (module_info) so_base = module_info["base"] so_size = module_info["size"] if so_base <= ea <= (so_base + so_size): in_mine_so = True break self.trace_size += 1 if (not in_mine_so) or (ea in self.skip_functions): if (self.trace_lr != 0) and (self.trace_step_into_count < self.trace_step_into_size): self.trace_step_into_count += 1 return 0 if (self.trace_lr != 0) and (self.trace_step_into_count == self.trace_step_into_size): ida_dbg.enable_insn_trace(False) ida_dbg.enable_step_trace(False) ida_dbg.suspend_process() if self.trace_size > self.trace_total_size: self.trace_size = 0 ida_dbg.request_clear_trace() ida_dbg.run_requests() ida_dbg.request_run_to(self.trace_lr) ida_dbg.run_requests() self.trace_lr = 0 self.trace_step_into_count = 0 return 0 if self.trace_lr == 0: self.trace_lr = my_get_reg_value("LR") #arm thumb LR, arm64 X30:注意這里的返回寄存器根據不同的指令選不同的寄存器 return 0 def dbg_run_to(self, pid, tid=0, ea=0): # print("dbg_run_to 0x%x pid=%d" % (ea, pid)) if self.line_trace: ida_dbg.enable_insn_trace(True) ida_dbg.enable_step_trace(True) ida_dbg.request_continue_process() ida_dbg.run_requests() def unhook(): global debughook # Remove an existing debug hook try: if debughook: print("Removing previous hook ...") debughook.unhook() debughook.Logger.log_close() except: pass def starthook(): global debughook if debughook: debughook.start_line_trace() def main(): global debughook unhook() skip_functions = [] modules_info = [] start_ea = 0 end_ea = [] so_modules = ["libmetasec_ml.so"] for module in idc._get_modules(): module_name = os.path.basename(module.name) for so_module in so_modules: if re.search(so_module, module_name, re.IGNORECASE): print("modules_info append %08X %s %08X" % (module.base, module.name, module.size)) if module_name == "libmetasec_ml.so": modules_info.append({"base": module.base, "size": module.size, "name": module.name}) #start_ea = (module.base + 0x69d9c+1) #X-Gorgon相關函數的開頭 #end_ea = [((module.base + 0x6a44a+1))] #函數結尾 start_ea = (module.base + 0x6221C+1) #另外3個相關函數的開頭 end_ea = [((module.base + 0x62312+1))] #函數結尾 break if start_ea: set_breakpoint(start_ea) if end_ea: for ea in end_ea: set_breakpoint(ea) if skip_functions: print("skip_functions") for skip_function in skip_functions: print ("%08X" % skip_function) debughook = MyDbgHook(modules_info, skip_functions, end_ea) pass if __name__ == "__main__": main() pass
補充:
1、我這里的逆向方式有點“不走尋常路”:是先找加密代碼,再回溯輸入;因為arm有LR寄存器,所以函數調用時返回地址會先放入LR寄存器;如果存在函數嵌套調用,就會把LR的值push入棧,函數執行完后再pop到PC達到返回的目的(對比了一下,感覺還是x86更容易做棧回溯,因為有ebp棧幀;ebp+4就是返回地址,ebp+8就是參數),所以用ida回溯有兩種方式:
- LR回溯
- 根據棧回溯
2、“尋常路”的做法:參考第4條有個腳本,可以hook到native方法的注冊;運行后發現libmetasec_ml.so被某個java層的函數唯一注冊,對應的偏移是0x1094d;至於java層的哪個函數,感興趣的小伙伴建議自行嘗試一下,很容易找到的!可以用ida從0x1094d開始調式,看看java層的輸入是怎么一步一步到加密函數的,也就能夠確定java層的哪些輸入字符串被so層的函數加密了!
參考:
1、https://www.cnblogs.com/wblyuyang/archive/2012/11/13/2768923.html sigaction函數
2、https://www.cnblogs.com/coffee520/p/10770918.html APP加固反調試匯總
3、https://gtoad.github.io/2017/06/25/Android-Anti-Debug/ Android反調試技術整理與實踐
4、https://github.com/lasting-yang/frida_hook_libart/blob/master/hook_RegisterNatives.js registerNative的hook函數,可以查找所有的native函數注冊地址(注意:第15行也就是for循環結束那行一定要加個break,否則會找到錯誤的registerNativeMethod地址,導致hook不到native方法)
