某vmp殼原理分析筆記----ELF文件的加載,鏈接,IDAPYTHON

某vmp殼原理分析筆記

分析的樣本爲某數字公司最新免費殼子。之前的殼子已經被很多大佬分析了,這篇筆記的主要目的是比較詳細的分析下該vmp殼子的原理,數字殼子主要分爲反調試,linker,虛擬機三部分。筆記結構如下:

  • 反調試

    • 時間反調試
    • rtld_db_dlactivity反調試
    • traceid反調試
    • 端口反調試
  • linker部分:用來加載第二個so

    • 裝載
    • 創建soinfo
    • 鏈接
    • dump second so
  • 虛擬機部分:解釋執行保護的代碼

    • dump dex
    • onCreate分析
    • 虛擬機入口

準備部分

因爲懶得hook系統函數,每次過反調試就要手工更改寄存器的值,所以用IDAPython來模擬手工調試,下面是一些輔助函數,其中addBrk根據模塊名和偏移地址下斷點,fn_f7, fn_f8, fn_f9分別模擬f7, f8, f9.

def getModuleBase(moduleName):
    base = GetFirstModule()
    while (base != None) and (GetModuleName(base).find(moduleName) == -1):
        base = GetNextModule(base)
    if base == None:
        print "failed to find module: " + moduleName
        return None
    else:
        return base
 
def addBrk(moduleName, functin_offset):
    base = getModuleBase(moduleName)
    AddBpt(base + functin_offset)
 
 
def fn_f7():
    idaapi.step_into()
    GetDebuggerEvent(WFNE_SUSP | WFNE_SUSP, -1)
 
def fn_f8():
    idaapi.step_over()
    GetDebuggerEvent(WFNE_SUSP | WFNE_CONT, -1)
 
def fn_f9():
    idaapi.continue_process()
    GetDebuggerEvent(WFNE_SUSP | WFNE_CONT, -1)
 
def delBrk(moduleName, function_offset):
    base = getModuleBase(moduleName)
    DelBpt(base + function_offset)


時間反調試反調試

反調試的第一處就是時間反調試,在反調試的開始調用time獲取時間,在結束再調用一次time,當差值大於3就會觸發反調試。因爲使用腳本過反調試,所以兩次時間差肯定是小於3的,所以只是用腳本簡單的輸出時間就可以了。0x19FC是time在libjiagu.so的plt段地址。

#add breakpoint at time
addBrk("libjiagu.so", 0x19FC)
fn_f9()
delBrk("libjiagu.so", 0x19FC)
print("first call time")
lr = GetRegValue("LR")
AddBpt(lr)
fn_f9()
r0 = GetRegValue("R0")
DelBpt(lr)
print("first get time: %x" % (r0))

rtld_db_dlactivity反調試

rtld_db_dlactivity函數:這個函數默認情況下爲空函數,這裏的值應該爲0,而當有調試器時,這裏會被改爲斷點指令,所以可以被用來做反調試。libjiagu.so的sub_1E9C就是用來查找rtld_db_dlactivity函數的地址的。sub_1E9C的代碼就不貼了。過這個反調試我們就可以在sub_1E9C返回後,獲取到rtld_db_dlactivity的地址,然後將其修改爲nop指令0x46C0。

#add breakpoint at sub_1E9C
addBrk("libjiagu.so", 0x1E9c)
fn_f9()
delBrk("libjiagu.so", 0x1E9c)
print("call sub_1E9C")
#add breakpoint at return from sub_1E9C
lr = GetRegValue("LR")
AddBpt(lr)
fn_f9()
DelBpt(lr)
#get rtld_db_dlactivity address
r0 = GetRegValue("R0")
print("rtld_db_dlactivity address is: %x" % (r0 - 1))
#set rtld_db_dlactivity nop
PatchDword(r0 - 1, 0x46c0)

tracepid反調試

tracepid反調試:當我們使用Ptrace方式跟蹤一個進程時,目標進程會記錄自己被誰跟蹤。過tracepid反調試可以在調用strtol後,將其返回值修改爲0.

#add breakpoint at strtol
addBrk("libjiagu.so", 0x1A80)
fn_f9()
delBrk("libjiagu.so", 0x1A80)
print("call strtol")
lr = GetRegValue("LR")
#add breakpoint at return from strtol
AddBpt(lr)
fn_f9()
DelBpt(lr)
r0 = GetRegValue("R0")
print("get trace id: %x" % (r0))
SetRegValue(0, "R0")

端口反調試

檢測IDA的默認端口23946是否被佔用,通過跟蹤,可以發現sub_6DD0()函數判斷23946端口是否被佔用。原理就是查看/proc/net/tcp,過端口反調試,就可以通過更改sub_6DD0的返回值。

#add breakpoint at strtol
addBrk("libjiagu.so", 0x1A80)
fn_f9()
delBrk("libjiagu.so", 0x1A80)
print("call strtol")
lr = GetRegValue("LR")
#add breakpoint at return from strtol
AddBpt(lr)
fn_f9()
DelBpt(lr)
r0 = GetRegValue("R0")
print("get trace id: %x" % (r0))
SetRegValue(0, "R0")

時間反調試

在反調試的最後還會調用一次time,計算差值,因爲使用腳本過得,所以時間差值肯定小於3,這裏只是簡單輸出一下時間

#add break point at time in  sub_6EF8
addBrk("libjiagu.so", 0x19FC)
fn_f9()
delBrk("libjiagu.so", 0x19FC)
print("second call time")
lr = GetRegValue("LR")
AddBpt(lr)
fn_f9()
DelBpt(lr)
r0 = GetRegValue("R0")
print("second get time: %x" % (r0))
#SetRegValue(r0 + 2, "R0")

Linker部分

這部分的主要作用是手工實現加載鏈接第二個so,並且調用第二個的so的JNI_Onload方法。因爲篇幅問題,只列出一些核心的步驟,具體的調試的方法是在case 31的BLX LR處下斷點,跟蹤sub_7BBC函數,就可以分析linker部分。既然是手工實現linker,那linker的主要功能就必不可少:裝載和鏈接。下面就對應libjiagu.so中的函數來分析具體的裝載與鏈接過程。分析的起點爲sub_3DBC, sub_3DBC中調用了sub_4780,sub_3D60,sub_33F8,分別對應裝載,創建soinfo,鏈接三個過程。首先定義兩個後面會用到的數據結構,這兩個數據結構的來源是通過動態調試分析出的,是後續很多函數的參數類型,和libjiagu.so的真實實現可能會有出入。

struct EncryptSoInfo {
void *encryptSo;             //指向壓縮加密的so
int sizeOfEncryptSo;         //壓縮加密so的大小
void *uncompressedSo;        //解壓後的so位置
int sizeOfUncompressSo;      //解壓後的so大小
int num;                     //表示某種類型,在後面會用到一次,具體含義不知道
char name[4];                //第二個so的名字
char data[124];
 
};
 
//類似於標準linker中的ELFReader
struct MElfReader {
void * soinfo
Byte[52] header;             // offset 4
int num_program_header;      //段表的個數 offset 56
ProgreamHeaderTable *pht;    //段表 offset 60
int unkonw;                  // offset 64
void *load_base;               //segment 起始地址 offset 68
int size; //offset 72
void *load_bias;         //offset 76
};

裝載

這一步驟libjiagu.so的實現和標準linker的實現十分相似。先說下標準linker的做法,創建ElfReader對象,通過 ElfReader 對象的 Load 方法將 SO 文件裝載到內存。

//標準linker的裝載
bool ElfReader::Load() {
  return ReadElfHeader() &&      //讀取elf header
         VerifyElfHeader() &&    //驗證elf header
         ReadProgramHeader() &&  //讀取program header
         ReserveAddressSpace() &&  //分配空間
         LoadSegments() &&         //按照program header的指示裝載segments
         FindPhdr();               //找到裝載後的phdr
}

下面看下libjiagu.so的實現,只是少了驗證elf header的環節。

int __fastcall sub_4780(MELFReader *a1, EncryptSoInfo *a2)
{
  Byte *v2; // r5@1
  Byte *v3; // r4@1
  int result; // r0@3
 
  v2 = a2;
  v3 = a1;
  if ( a2 && sub_40B0(a1, a2)   //讀取elf header
          && sub_41A8(v3, v2)   //讀取program header
          && sub_4424((int)v3, (int)v2) && //分配空間
           sub_4448(v3, v2) )    //按照program header的指示裝載segments
    result = sub_46CC(v3);      //找到裝載後的phdr
  else
    result = 0;
  return result;
}

load ELF Header

裝載的第一步就是加載ELF Header,這一過程比較簡單,直接看代碼

int __fastcall sub_40B0(MELFReader *a1, EncryptSoInfo *a2)
{
  Byte *v2; // r4@1
  const void *base; // r1@1
  int v4; // r4@3
  Byte *v6; // r5@5
  _DWORD *v7; // r0@5
  _DWORD *v8; // r6@5
 
  v2 = a2;
  base = (const void *)*((_DWORD *)a2 + 2); //a2->uncompressedSo
  if ( base
    && *((_DWORD *)v2 + 3) > 0x34u   //a2->int sizeOfUncompressSo
    && (v6 = a1 + 4,
     memcpy(a1 + 4, base, 0x34u),  //拷貝加密的後的ELF Header
     v7 = sub_6534(*((_DWORD *)v2 + 40)), //新建解密對象v7
     (v8 = v7) != 0)
    && sub_58F0((int)v7, (int)v6, 52)     //初始化解密對象
    && (v4 = (*(int (__fastcall **)(_DWORD *))(*v8 + 12))(v8)) != 0 //解密加密後的so
    )
  {
    (*(void (__fastcall **)(_DWORD *))(*v8 + 4))(v8); //析構解密對象
  }
  else
  {
    v4 = 0;
  }
  return v4;
}

load program header

這一過程也比較簡單,利用上一步讀取的program header,得到program header數量,然後分配空間,拷貝加密後到的program header table到分配的空間,然後解密。

int __fastcall sub_41A8(MELFReader *a1, EncryptSoInfo *a2)
{
  Byte *v2; // r5@1
  Byte *v3; // r4@1
  size_t v4; // r0@2
  size_t v5; // r6@3
  void *v6; // r0@3
  _DWORD *v7; // r0@4
  _DWORD *v8; // r5@4
  int v9; // r4@6
 
  v2 = a2;
  v3 = a1;
  if ( a2
    && (v4 = *((_WORD *)a1 + 24), //program header 數量
    *((_DWORD *)v3 + 14) = v4, ((v4 - 1) & 0xFFFF) <= 0x7FF)
    && (v5 = 32 * v4, *((_DWORD *)v3 + 16) = 32 * v4, v6 = calloc(v4, 0x20u), // 分配空間
     (*((_DWORD *)v3 + 15) = v6) != 0)
    && (memcpy(v6, (const void *)(*((_DWORD *)v2 + 2) + *((_DWORD *)v3 + 8)), v5), //拷貝段表
        v7 = sub_6534(*((_DWORD *)v2 + 41)), 
        (v8 = v7) != 0)
    && sub_58F0((int)v7, *((_DWORD *)v3 + 15), *((_DWORD *)v3 + 16))
    && (v9 = (*(int (__fastcall **)(_DWORD *))(*v8 + 12))(v8)) != 0 ) //解密段表
  {
    (*(void (__fastcall **)(_DWORD *))(*v8 + 4))(v8);
  }
  else
  {
    v9 = 0;
  }
  return v9;
}

計算加載所需的空間並分配空間

ELF文件中所有類型爲LOAD的segment都需加載到內存,這一步驟主要是計算加載所需要的空間並分配空間。這裏需要說明一下loadbias,因爲so可以指定加載基址,但指定的基址可能不是頁對齊,所以實際加載的基址可能會和指定基址有個差值,loadbias用來保存這個差值。但一般so都是可以加載到任意地址的,所以loadbias的值一般就是so實際加載的基址。

signed int __fastcall sub_43A8(MELFReader *a1)
{
  Byte *v1; // r4@1
  int size; // r0@1
  signed int result; // r0@2
  _BYTE *v4; // r5@3
  _BYTE *v5; // r0@3
  int v6; // r5@4
  void *addr; // [sp+Ch] [bp-14h]@1
 
  v1 = a1;
  //計算加載所需的空間並且將加載的基址保存在addr中
  size = sub_4264(*((_DWORD *)a1 + 15), *((_DWORD *)a1 + 14), (unsigned int *)&addr, 0);
  *((_DWORD *)v1 + 18) = size;
  //mmap分配空間
  if ( size && (v4 = addr, v5 = mmap(addr, size, 0, 34, -1, 0), v5 != (_BYTE *)-1) )
  {
    v6 = v5 - v4;   //計算 loadbias
    *((_DWORD *)v1 + 17) = v5;
    result = 1;
    *((_DWORD *)v1 + 19) = v6;
  }
  else
  {
    result = 0;
  }
  return result;
}
sub_4264的原理,因爲篇幅原因就不貼代碼了,就是循環段表,讀取每個需要load的段,然後找到加載的最小虛擬地址,和最大的虛擬地址,頁對齊後,取差值就是load size。

load segment

遍歷 program header table,找到類型爲 PT_LOAD 的 segment:

  1. 計算 segment 在內存空間中的起始地址 segstart 和結束地址 seg_end,seg_start 等於虛擬偏移加上基址load_bias,同時由於 mmap 的要求,都要對齊到頁邊界得到 seg_page_start 和 seg_page_end。
  2. 計算 segment 在文件中的頁對齊後的起始地址 file_page_start 和長度 file_length。
  3. 使用mprotect和memcpy將段映射到內存,指定映射地址爲 seg_page_start,長度爲 file_length,文件偏移爲 file_page_start。

  4. signed int __fastcall sub_4448(MELFReader *a1, EncryptSoInfo *a2)
    {
    Byte *v2; // r6@1
    unsigned int v3; // r2@3
    int v4; // r4@4
    int v5; // r5@7
    int v6; // r3@7
    int v7; // r5@7
    int v8; // r12@8
    int v9; // r2@8
    int v10; // r10@8
    unsigned int v11; // r11@8
    unsigned int v12; // r12@8
    bool v13; // cf@8
    bool v14; // zf@8
    unsigned int v15; // r8@8
    void *v16; // r7@8
    int v17; // r12@8
    int v18; // r10@8
    void *v19; // r0@16
    signed int v20; // r2@19
    size_t n; // [sp+4h] [bp-34h]@10
    unsigned int v23; // [sp+8h] [bp-30h]@2
    Byte *v24; // [sp+Ch] [bp-2Ch]@1
     
    v24 = a2;
    v2 = a1;
    if ( a2 )
    {
     v23 = *((_DWORD *)a2 + 3);                  // 解壓後的so的起始地址
     if ( *((_DWORD *)a2 + 3) )
     {
       v3 = *((_DWORD *)a1 + 14);                // segment 數量
       if ( !v3 )
         return 1;
       v4 = 0;
       while ( 1 )
       {
         while ( 1 )                             // 找到類型爲LOAD的段
         {
           v5 = *((_DWORD *)v2 + 15);            // 段表起始地址
           v6 = *(_DWORD *)(v5 + 32 * v4);       // 迭代讀取段表
           v7 = v5 + 32 * v4;
           if ( v6 == 1 )
             break;
           if ( v3 <= ++v4 )
             return 1;
         }
         v8 = *(_DWORD *)(v7 + 4);               // offset segment在文件的偏移
         v9 = *(_DWORD *)(v7 + 16);              // filesz segment在so中大小
         v10 = *((_DWORD *)v2 + 19) + *(_DWORD *)(v7 + 8);// segment 在虛擬內存的結束地址
         v11 = v8 & 0xFFFFF000;
         v12 = v8 + v9;
         v13 = v23 >= v12;
         v14 = v23 == v12;
         v15 = (*(_DWORD *)(v7 + 20) + 4095 + v10) & 0xFFFFF000;
         v16 = (void *)(v10 & 0xFFFFF000);
         v17 = v12 - v11;
         v18 = v10 + v9;
         if ( v14 || !v13 )
           break;
         n = v17;
         if ( mprotect(v16, v15 - (_DWORD)v16, 3) == -1 )
           break;
         if ( n )
           memcpy(v16, (const void *)(*((_DWORD *)v24 + 2) + v11), n);
         if ( *(_DWORD *)(v7 + 24) & 2 && v18 & 0xFFF )
           memset((void *)v18, 0, 4096 - (v18 & 0xFFF));
         v19 = (void *)((v18 + 4095) & 0xFFFFF000);
         if ( v15 > (unsigned int)v19 )
           memset(v19, 0, v15 - (_DWORD)v19);
         v20 = *(_DWORD *)(v7 + 24) & 1 ? 4 : 0;
         if ( mprotect(v16, v15 - (_DWORD)v16, *(_DWORD *)(v7 + 24) & 2 | (*(_DWORD *)(v7 + 24) << 29 >> 31) | v20) == -1 )
           break;
         v3 = *((_DWORD *)v2 + 14);
         if ( v3 <= ++v4 )
           return 1;
       }
     }
    }
    return 0;
    }

    至此第二個so已經完成了裝載。

    創建soinfo

    完成裝載後,就需要創建soinfo結構,並利用MELFReader設置一些soinfo參數。

  5. char *__fastcall sub_3D60(EncryptSoInfo *a1)
    {
      const char *v1; // r4@1
      char *result; // r0@2
      char *v3; // r5@2
     
      v1 = (const char *)(a1 + 20);
      if ( strlen((const char *)a1 + 20) > 0x7F )
      {
        result = 0;
      }
      else
      {
        result = (char *)operator new(0x128u); //創建soinfo
        v3 = result;
        if ( result )
        {
          memset(result, 0, 0x128u);
          strncpy(v3, v1, 0x7Fu);
          result = v3;
        }
      }
      return result;
    }
     
    sub_3DBC(EncryptSoInfo *a1)
    {
      //節選
      soinfo = sub_3D60(*(Byte **)v1);
      v3 = (int)soinfo;
      if ( !soinfo )
        goto LABEL_16;
      v4 = nmemb_56;
      v5 = nmemb_68;
      v6 = nmemb_72;
      v7 = nmemb_76;
      *((_DWORD *)soinfo + 33) = nmemb_56;          // 設置 soinfo->phnum;
      *((_DWORD *)soinfo + 35) = v5;                // 設置 soinfo->loadstart
      *((_DWORD *)soinfo + 36) = v6;                // 設置 soinfo->size; 
      *((_DWORD *)soinfo + 62) = v7;                // 設置 soinfo->loadbias
      //
    }

     

鏈接

自定義linker最重要的一步就是鏈接,鏈接主要步驟是:

  1. 定位 dynamic segment
  2. 解析 dynamic section
  3. 加載該so所依賴的so
  4. 重定位
    這一部分是由sub_33F8函數完成的,因爲這個函數很長,所以節選主要步驟貼出來
signed int __fastcall sub_33F8(soinfo *a1)
{
    //遍歷program header找到類型爲Dynamic的program header,從而定位到Dynamic segment
    sub_45F4(*((_DWORD *)a1 + 32), *((_DWORD *)v2 + 33), *((_DWORD *)v2 + 62), (_DWORD *)v2 + 37, &v38);
    v3 = *((_DWORD *)v2 + 37); //dynamic segment
 
    //解析dynamic segment
    v4 = *(_DWORD *)v3; //符號類型
 
    if ( *(_DWORD *)v3 ) {
        v5 = (int *)(v3 + 8);
        while ( 1 ) {
            switch ( v4 ) {
 
                //根據符號類型做重定位
            }
        }
    }
 
    LABEL_7:
    if ( *(_DWORD *)v3 )
    {
      v11 = 0;
      do
      {
        if ( v10 == 1 )
        {
          v13 = (const char *)(*((_DWORD *)v2 + 39) + *(_DWORD *)(v3 + 4));
          if ( strlen(v13) > 0x80 )
            goto LABEL_21;
          if ( *((_DWORD *)v2 + 72) <= v11 )
            goto LABEL_21;
          v14 = 136 * v11++;
          strncpy((char *)(*((_DWORD *)v2 + 73) + v14 + 4), v13, 0x7Fu);
          v15 = dlopen(v13, 0);  //加載所需要的so
          if ( !v15 )
            goto LABEL_21;
          *(_DWORD *)(*((_DWORD *)v2 + 73) + v14) = v15;
          *(_DWORD *)(*((_DWORD *)v2 + 73) + v14 + 132) = 0;
        }
        v12 = *(_DWORD *)(v3 + 8);
        v3 += 8;
        v10 = v12;
      }
      while ( v12 );
    }
 
}

dump second so

清楚第二個so的加載流程後,就可以dump出第二個so,具體做法是在sub_3DBC處下斷點,dump內存,並且解密ELF header 和 program header table。解密算法是libjiagu.so中的sub_6868.

#sub_6868
def decryptso(data, size):
    result = bytearray(data)
    for i in range(size):
        result[i] ^= 0x50
    return str(result)
 
 
def dump_so(start_address, size):
    fp = open("E:\\dump.so", "wb")
 
    #read elf header and decrypt header
    data = idaapi.dbg_read_memory(start_address, 52)
    header = decryptso(data, 52)
    fp.write(header)
 
    #read elf program header tables
    phnum = 9
    data = idaapi.dbg_read_memory(start_address + 52,  phnum * 32)
    pht = decryptso(data, phnum * 32)
    fp.write(pht)
 
    #read other part
    data = data = idaapi.dbg_read_memory(start_address + 52 + phnum * 32,  size - phnum * 32 - 52)
    fp.write(data)
 
    fp.close()

進入JNI_Onload

在加載完第二個so後,libjiagu.so通過sub_3F7C(soinfo info, char fucname),來找到第二個so的JNI_Onload入口,並在case 35中跳轉到第二個so的JNI_Onload.

 

虛擬機部分

這一部分就正式開始執行源apk中的代碼了。

dump dex

源dex要被執行,肯定需要加載,所以在libart.so的OpenMemory函數下斷點就可以dump出dex,使用IDAPython dump dex

#add break at OpenMemory
addBrk("libart.so", OpenMemory_offset)
fn_f9()
r0 = GetRegValue("R0")
r1 = GetRegValue("R1")
print("orgin dex at: %x" % (r0))
delBrk("libart.so", OpenMemory_offset)
#dump dex
data = idaapi.dbg_read_memory(r0, r1)
fp = open('d:\\dump.dex', 'wb')
fp.write(data)
fp.close()

用jeb打開dump出來的dex可以發現 oncreate函數native化了。

 

找到onCreate函數

native化通過靜態修改dex中onCreate函數的DexMethod結構就可以,但要想正確執行,必須在執行時動態註冊,通過攔截JNI註冊函數RegisterNative可以找到onCreate函數的地址

def hook_RegisterNative():
    #add break at RegisterNatives
    addBrk("libart.so", RegisterNative_offset)
    fn_f9()
    delBrk("libart.so", RegisterNative_offset)
    #JNINativeMethod method[]
    r2 = GetRegValue("R2")
    #nMethods
    r3 = GetRegValue("R3")
    for index in range(r3):
        name = get_string(Dword(r2))
        address = Dword(r2 + 8)
        print("native function %s address is %x" % (name, address))
        r2 = r2 + 12

 

然後在oncreate函數下斷點

onCreate函數分析

通過上一步可以找到onCreate在第二個so中的地址,f5之後代碼如下:

int __fastcall onCreate(JNIEnv *a1, int a2, int a3, int a4)
{
  int v5; // [sp+1Ch] [bp-Ch]@1
  int v6; // [sp+20h] [bp-8h]@1
  int v7; // [sp+24h] [bp-4h]@1
 
  v7 = a4;
  v6 = a3;
  v5 = a2;
  return sub_D930(0, a1, &v5);
}

直接調用了sub_D930,進入sub_D930分析,這個函數比較長,大致分析了一下流程:

  1. jni的一些初始化工作,FindClass,GetMethodID之類的工作
  2. 利用java.lang.Thread.getStackTrace獲取到調用當前方法的類的類名以及函數名
  3. 通過上一步獲取的類名以及方法名獲取被保護方法的DexCode結構
  4. 調用自己的虛擬機執行代碼

其中第二步的核心代碼如下:

sub_D930()
{
 
  //節選
  v54 = sub_66BD4(v122, (int)&v127);
  if ( v127 & 1 )
    j_j_j__ZdlPv(*((void **)v50 + 5));
  if ( v54 && (v55 = *(_DWORD *)(v54 + 4), (*(_DWORD *)(v54 + 8) - v55) >> 2 > v4) )
  {
    v56 = v55 + 4 * v4;                         // **v56爲Dexprotoid, *(*v56+4)爲DexMethodID, *(*v56+12)爲codeoff
    v114 = (int)jni_env;
    v123 = *(_DWORD *)v56;                      // DexProtoId
    v107 = *(_DWORD **)v54;                     // **v54爲dex在內存的地址
    DexCode = (_WORD *)(**(_DWORD **)v54 + *(_DWORD *)(*(_DWORD *)v56 + 12));// 獲取DexCode地址
    ((void (*)(void))(*jni_env)->PushLocalFrame)();
    v58 = j_j_j__Znwj(0x20u);
    v59 = *DexCode;
    *(_DWORD *)v58 = v114;                      // jni_env
    *(_DWORD *)(v58 + 4) = v59;                 // 使用的寄存器個數
    *(_DWORD *)(v58 + 8) = DexCode;
    *(_DWORD *)(v58 + 12) = 0;
 
    //進入虛擬機
    sub_3FE5C()
 
  }

核心爲sub_66BD4函數,具體原理沒分析。獲取到了onCreate的DexCode後,進入虛擬機執行加密後的DexCode。

虛擬機入口

虛擬機的入口在sub_3FE5C中調用的sub_3FF5C,進入虛擬機入口後,就如下圖

 

 

具體原理就是:

  1. 取出加密後的指令
  2. 根據加密後指令,還原操作數,並算出一個分支數
  3. 跳轉到具體分支解釋執行。

具體分支數的計算算法,參考文章中說的很詳細了,就不重複了。至此殼子的基本原理分析完了,接下來還要學習下davik虛擬機是如何解釋執行指令的。逆向新手,有錯誤歡迎指正。
參考文章:
https://bbs.pediy.com/thread-223796.htm

debug.py

import idautils
import idc
import idaapi
import time

#jni_Onload_offset = 0x1D6B50
jni_Onload_offset = 0x1D545E
RegisterNative_offset = 0x1B6610
OpenMemory_offset = 0xFC278 

def getModuleBase(moduleName):
    base = GetFirstModule()
    while (base != None) and (GetModuleName(base).find(moduleName) == -1):
        base = GetNextModule(base)
    if base == None:
        print "failed to find module: " + moduleName 
        return None
    else:
        return base

def addBrk(moduleName, functin_offset):
    base = getModuleBase(moduleName)
    AddBpt(base + functin_offset)


def fn_f7():
    idaapi.step_into()
    GetDebuggerEvent(WFNE_SUSP | WFNE_SUSP, -1) 

def fn_f8():
    idaapi.step_over()
    GetDebuggerEvent(WFNE_SUSP | WFNE_CONT, -1) 

def fn_f9():
    idaapi.continue_process()
    GetDebuggerEvent(WFNE_SUSP | WFNE_CONT, -1) 

def delBrk(moduleName, function_offset):
    base = getModuleBase(moduleName)
    DelBpt(base + function_offset)

def antiDebug():
    addBrk("libart.so", jni_Onload_offset)
    fn_f9()
    delBrk("libart.so", jni_Onload_offset)
    #into libjiagu.so
    fn_f7()

    #add breakpoint at time
    addBrk("libjiagu.so", 0x19FC)
    fn_f9()
    delBrk("libjiagu.so", 0x19FC)
    print("first call time")
    lr = GetRegValue("LR")
    AddBpt(lr)
    fn_f9()
    r0 = GetRegValue("R0")
    DelBpt(lr)
    print("first get time: %x" % (r0))

    #add breakpoint at sub_1E9C 
    addBrk("libjiagu.so", 0x1E9c)
    fn_f9()
    delBrk("libjiagu.so", 0x1E9c)
    print("call sub_1E9C")
    #add breakpoint at return from sub_1E9C
    lr = GetRegValue("LR")
    AddBpt(lr)
    fn_f9()
    DelBpt(lr)
    #get rtld_db_dlactivity address
    r0 = GetRegValue("R0")
    print("rtld_db_dlactivity address is: %x" % (r0 - 1))
    #set rtld_db_dlactivity nop
    PatchDword(r0 - 1, 0x46c0)

    #add breakpoint at strtol
    addBrk("libjiagu.so", 0x1A80)
    fn_f9()
    delBrk("libjiagu.so", 0x1A80)
    print("call strtol")
    lr = GetRegValue("LR")
    #add breakpoint at return from strtol
    AddBpt(lr)
    fn_f9()
    DelBpt(lr)
    r0 = GetRegValue("R0")
    print("get trace id: %x" % (r0))
    SetRegValue(0, "R0")

    #add break point at sub_6DD0
    addBrk("libjiagu.so", 0x6DD0)
    fn_f9()
    lr = GetRegValue("LR")
    print("call sub_6DD0")
    delBrk("libjiagu.so", 0x6DD0)
    AddBpt(lr)
    fn_f9()
    SetRegValue(0, "R0")
    DelBpt(lr)

    #add break point at time in  sub_6EF8
    addBrk("libjiagu.so", 0x19FC)
    fn_f9()
    delBrk("libjiagu.so", 0x19FC)
    print("second call time")
    lr = GetRegValue("LR")
    AddBpt(lr)
    fn_f9()
    DelBpt(lr)
    r0 = GetRegValue("R0")
    print("second get time: %x" % (r0))
    #SetRegValue(r0 + 2, "R0")


def get_string(addr):
  out = ""
  while True:
    if Byte(addr) != 0:
      out += chr(Byte(addr))
    else:
      break
    addr += 1
  return out


def hook_RegisterNative():
    #add break at RegisterNatives
    addBrk("libart.so", RegisterNative_offset)
    fn_f9()
    delBrk("libart.so", RegisterNative_offset)
    #JNINativeMethod method[]
    r2 = GetRegValue("R2")
    #nMethods
    r3 = GetRegValue("R3")
    for index in range(r3):
        name = get_string(Dword(r2))
        address = Dword(r2 + 8)
        #AddBpt(address - 1)
        print("native function %s address is %x" % (name, address))
        r2 = r2 + 12


def dump_dex(start_address, size):
    data = idaapi.dbg_read_memory(start_address, size)
    fp = open('E:\\dump.dex', 'wb')
    fp.write(data)
    fp.close()

#sub_6868
def decryptso(data, size):
    result = bytearray(data)
    for i in range(size):
        result[i] ^= 0x50
    return str(result)


def dump_so(start_address, size):
    fp = open("E:\\dump.so", "wb")

    #read elf header and decrypt header
    data = idaapi.dbg_read_memory(start_address, 52)
    header = decryptso(data, 52)
    fp.write(header)

    #read elf program header tables
    phnum = 9
    data = idaapi.dbg_read_memory(start_address + 52,  phnum * 32)
    pht = decryptso(data, phnum * 32)
    fp.write(pht)

    #read other part
    data = data = idaapi.dbg_read_memory(start_address + 52 + phnum * 32,  size - phnum * 32 - 52)
    fp.write(data)
    
    fp.close()


def main():

    antiDebug()

    #add break at sub_273c
    addBrk("libjiagu.so", 0x273C)
    fn_f9()
    base = GetRegValue("R0")
    print("second so uncompress at: %x" % (base))
    delBrk("libjiagu.so", 0x273C)
    

    #add break at sub_3DBC
    addBrk("libjiagu.so", 0x3DBC)
    fn_f9()
    r0 = GetRegValue("R0")
    print("encrypted so at: %x" % Dword(r0))
    print("encrypted so size: %x" % Dword(r0 + 4))
    print("uncompressed so at: %x" % Dword(r0 + 8))
    print("uncompressed so size: %x" % Dword(r0 + 12)) 
    delBrk("libjiagu.so", 0x3DBC)

    #dump_so(Dword(r0 + 8), Dword(r0 + 12))
    
    
    #add beak at case35
    addBrk("libjiagu.so", 0xAB84)
    fn_f9()
    delBrk("libjiagu.so", 0xAB84)
    lr = GetRegValue("LR")
    print("step into second so JNI_Onload: %x" % (lr - 1))

    fn_f7()

    hook_RegisterNative()  

    #add break at OpenMemory
    addBrk("libart.so", OpenMemory_offset)
    fn_f9()
    r0 = GetRegValue("R0")
    r1 = GetRegValue("R1")
    print("orgin dex at: %x" % (r0))
    delBrk("libart.so", OpenMemory_offset)
    #dump_dex(r0, r1)
    
    hook_RegisterNative()

    hook_RegisterNative()

    hook_RegisterNative()

    hook_RegisterNative()
   



main()


 

[進行中] 看雪20週年慶典12月28日上海舉辦,LV四級(中級)以上會員免費參與!,同時在校學生免費參加:學生報名鏈接!

最後於  2018-4-8 21:02 被glider菜鳥編輯 ,原因: 添加附件

上傳的附件:

發佈了265 篇原創文章 · 獲贊 164 · 訪問量 218萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章