1. 程式人生 > >【天翼杯安卓題二】 愛加密脫殼實戰

【天翼杯安卓題二】 愛加密脫殼實戰

encode ask ner 符號 rom fse write 找到 ace

前言

這個apk使用愛加密加密,加密時間是2017.6月。這個題其實就是個脫殼題,脫完立馬見flag。(出題人也太懶了)

題目鏈接:https://gitee.com/hac425/blog_data/blob/master/app02.apk

殼介紹

愛加密的殼16年年底就已經開始通過 hook dvmResolveClass ,在調用具體方法時解密方法指令,然後將 DexFile結構體中的對應方法的 md->insns 指向 解密後的方法指令數據區,然後進入 真正的dvmResolveClass中執行指令,執行完後在重新加密指令,這樣就可以防止 dexhunter 等工具在內存中 dump dex 文件。

流程圖

技術分享圖片

圖片來源

脫殼

由上面可以知道,在dvmResolveClass函數執行的時候,代碼是已經還原好了的。這時我們去dump相應的指令就是正確的指令。於是修改 dvmResolveClass 的代碼,dump 方法的數據。
修改 dvmResolveClass 函數:


/*  add dump .....*/   
    
  char key_str[20] = "jiajiatest";
  int fd=open("/data/local/tmp/resolve_class_config",O_RDONLY,0666);
  if(fd!=-1){
    int len = read(fd,key_str,19);
    key_str[len-1] = ‘\x00‘;
    key_str[len] = ‘\x00‘;
    close(fd);
  }
  ALOGI("The key_str ---> %s----referrer->descriptor--->%s--", key_str, referrer->descriptor);
  
if(strstr(referrer->descriptor, key_str)){
       char task_name[] = "task_name";
      char *logbuf = new char[1024];
      char path[50] = {0};
      sprintf(path, "/data/local/tmp/%s_dump_%d", key_str, getpid());
      FILE *fpw = fopen(path, "awb+");
      for(int i=0; i < referrer->directMethodCount; i++){
        Method* md = &referrer->directMethods[i];
        const char* mName_d = md->name;
        const u2 insSize_d = md->insSize;
        const u2* insns_d = md->insns;
        const u2 methodldx_d = md->methodIndex;
        u4 insns_d_size = dvmGetMethodInsnsSize(md);
// ALOGI("hacklh_md---->%p, i-->%d, directMethodCount-->%d", md, i,referrer->directMethodCount);
        sprintf(logbuf,"-------------- (KL)resolving [class=%s, method=%s, methodIndex=%u, insSize=%u, insns_d=%x, codeSize=%d] in pid: %d(name: %s)",referrer->descriptor,mName_d,methodldx_d,insSize_d,(u4)insns_d, insns_d_size,getpid() , task_name);
        LOGD("%s",logbuf);
        if(fpw != NULL){ 
          fwrite(logbuf,1,strlen(logbuf),fpw);
          fflush(fpw);
          fwrite((u1*)insns_d,1,insns_d_size*2, fpw);
          fflush(fpw);
        }else{
          LOGD("——(KL)open %s fail!", path);
        }
      }
      for(int i=0; i < referrer->virtualMethodCount; i++){
        Method* mv = &referrer->virtualMethods[i];
        const char* mName_v = mv->name;
        const u2 insSize_v = mv->insSize;
        const u2* insns_v = mv->insns;
        const u2 methodIdx_v = mv->methodIndex;
        u4 insns_v_size = dvmGetMethodInsnsSize(mv);
        sprintf(logbuf,"-------------- (KL)resolving [class=%s, method=%s, methodIndex=%u, insSize=%u, insns_d=%x, codeSize=%d] in pid: %d(name: %s)",referrer->descriptor,mName_v,methodIdx_v,insSize_v,(u4)insns_v, insns_v_size,getpid() , task_name);
        LOGD("%s",logbuf);
        if(fpw != NULL){
          fwrite(logbuf,1,strlen(logbuf),fpw);
          fflush(fpw);
          fwrite((u1*)insns_v,1,insns_v_size*2, fpw);
          fflush(fpw);
        }else{
          LOGD("%s","——(KL)open file fail!");
        }
      }
      if(fpw != NULL){
        fclose(fpw);
      }
      delete logbuf;
/*  add end .....*/

dump之後我們需要把指令patch到dex對應位置上去,patch的方式有很多種,我選擇使用ida腳本對他進行patch。我覺得ida就是一個各種文件格式的loader,我們可以在ida中修改文件的內容,然後可以讓ida把修改應用到文件中,以完成patch。 因此在IDA中patch代碼十分的方便,而且也很方便的查看patch後的結果。patch代碼的流程是:


讀取dump的方法指令--->定位相應方法指令數據區在ida中的位置---->patch


代碼如下:

#! /usr/bin/python
# -*- coding: utf8 -*-

# 該腳本用於在ida中使用dump下來的method指令對 dex 進行Patch
import re
from dex_parser import dex

#存儲 存放dump數據的字典
data_array = []
#用來避免多次patch
patched = []
file_data = ""

def parse_meta_data(data=""):
    # print data
    ret = {}
    tokens = re.findall("\[class=(.*?),.*?method=(.*?),.*?codeSize=(.*?)\]",data)
    # print tokens

    ret[‘class_name‘] = tokens[0][0][1:].replace(‘/‘,‘.‘).replace(‘;‘,‘‘)
    ret[‘method‘] = tokens[0][1]
    ret[‘code_size‘] = int(tokens[0][2]) * 2 #dex文件格式定義,總大小為 codeSize*2
    # print ret
    return ret

#註釋,用於給ida執行
# def patch_byte(a, b):
#     print hex(b),

def patch(dest, src, size):
    print "dest::{}, src::{}, size::{}".format(dest, src, size)
    for i in range(size):
        patch_byte(dest + i, int(file_data[ src + i].encode(‘hex‘), 16))

    print "\n"

def parse_dump_data(filename):
    global file_data
    with open(filename, "rb") as fp:
        file_data = fp.read()

    #使用正則表達式把說明dump數據的元數據加載到內存
    all_item = re.findall("-------------- \(KL\)resolving(.*?) in pid:.*?\(name: task_name\)", file_data)
    offset = 0
    for meta_data in all_item:
        try:
            #使用字典組織數據
            #{‘class_name‘: ‘com.example.jiajiatest.MainActivity‘, ‘code_size‘: 306, ‘method‘: ‘add‘, ‘data_offset‘: 7175}

            ret = parse_meta_data(meta_data)
            data_addr = file_data.find(‘(name: task_name)‘, offset) + 17
            ret[‘data_offset‘] = data_addr
            data_array.append(ret)
            offset = data_addr
        except Exception as e:
            raise e

    return data_array

def get_method_addr(method_data, signature_str):
    for md_name in method_data:
        if signature_str in md_name:
            return method_data[md_name]
    return -1

def patch_dex(dump_data_file, dex_file):
    dump_data = parse_dump_data(dump_data_file)
    dex_obj = dex.dex_parser(dex_file)
    method_data = dex_obj.get_class_data()

    for item in dump_data:
        signature_str = "{}::{}".format(item[‘class_name‘], item[‘method‘])
        if  signature_str not in patched:

            #獲取要patch的目標地址
            addr = get_method_addr(method_data, signature_str)
            if addr == -1:
                print "{} can‘t get insns addr".format(signature_str)
                continue
            #do patch
            print "patch " + signature_str,
            patch(addr,item[‘data_offset‘],item[‘code_size‘])
            patched.append(signature_str)

    # print patched
    # for i in patched:
    #     print i

import pprint

patch_dex("F:\code_workplace\ida_script\jiajiatest_dump_20406","F:\code_workplace\ida_script\classes.dex" )
if __name__ == ‘__main__‘:
    print "comming main"
    # parse_dump_data("F:\code_workplace\ida_script\jiajiatest_dump_20406")
    # patch_dex("F:\code_workplace\ida_script\jiajiatest_dump_20406","F:\code_workplace\ida_script\classes.dex" )
    # dex_obj = dex.dex_parser("F:\code_workplace\ida_script\classes.dex")
    # class_data = dex_obj.get_class_data()
    # pprint.pprint(class_data)



    # parse_meta_data("-------------- (KL)resolving [class=Lcom/example/jiajiatest/HttpRunner;, method=makeImgHttpGET, methodIndex=13, insSize=2, insns_d=6daf04d8, codeSize=270] in pid: 20406(name: task_name)")

patch前後對比:

patch前

技術分享圖片
patch後

技術分享圖片

這時已經可以看到程序的主體邏輯了。然後查看字符串就可以拿到flag.........

我幹的傻事

  • 代碼循環條件忘記寫了,導致越界,一打開應用就報錯。

  • 文件打開失敗,貌似是權限問題,我直接暴力把 /data/local/tmp 改成 777

總結

分析安卓底層代碼的錯誤,要關註 logcat 日誌,找到出問題的代碼點,然後把庫的帶符號版本放到ida中分析
分析bug, 要看代碼的關鍵邏輯, 判斷條件等。

最後

要多實踐,如有問題請在下面評論。

【天翼杯安卓題二】 愛加密脫殼實戰