2016年10月28日星期五

Android 逆向学习笔记 (六)- 安卓脱壳之 dvmDexFileOpenPartial

加壳

学习脱壳之前,首先先学习一下什么是加壳,加壳是在二进制的程序中植入一段代码,在运行的时候这段植入的代码会优先取得程序的控制权,做一些额外的工作(将原来的代码还原出来执行,以达到保护原来的代码直接暴露的问题)。大多数病毒就是基于此原理。

Android 作为新兴出现的平台,加壳方式不像 PC 端一样成熟,本次分析常见的 Dex 加壳和脱壳方式。在此之前,首先需要简单了解 Dex 文件的结构如下表所示。

字段名称偏移值长度描述
magic0x08'Magic'值,即魔数字段,格式如”dex/n035/0”,其中的035表示结构的版本。
checksum0x84校验码。
signature0xC20SHA-1签名。
file_size0x204Dex文件的总长度。
header_size0x244文件头长度,009版本=0x5C,035版本=0x70。
endian_tag0x284标识字节顺序的常量,根据这个常量可以判断文件是否交换了字节顺序,缺省情况下=0x78563412。
link_size0x2C4连接段的大小,如果为0就表示是静态连接。
link_off0x304连接段的开始位置,从本文件头开始算起。如果连接段的大小为0,这里也是0。
map_off0x344map数据基地址。
string_ids_size0x384字符串列表的字符串个数。
string_ids_off0x3C4字符串列表表基地址。
type_ids_size0x404类型列表里类型个数。
type_ids_off0x444类型列表基地址。
proto_ids_size0x484原型列表里原型个数。
proto_ids_off0x4C4原型列表基地址。
field_ids_size0x504字段列表里字段个数。
field_ids_off0x544字段列表基地址。
method_ids_size0x584方法列表里方法个数。
method_ids_off0x5C4方法列表基地址。
class_defs_size0x604类定义类表中类的个数。
class_defs_off0x644类定义列表基地址。
data_size0x684数据段的大小,必须以4字节对齐。
data_off0x6C4数据段基地址


可以看到 Dex 文件都是以 "dex/n035/0" 开头的,然后后面跟着一堆记录 Dex 文件基本信息的数据,这些不管了,下图有简单的展示,更加具体的 Dex 文件结构可以到给出的参考链接中查看。


举一个简单的加壳例子简单了解加壳过程,流程图如下:


可以看到此时原来的 app 被加密之后放入 “加密 Source DEX 数据” 段,在 “解壳 DEX Body” 段的后面,所以当程序执行时先执行到 “解壳 DEX Body” 部分后将 “加密 Source DEX 数据” 部分解密,然后再执行解密后的内容(即是原 app 的内容)。

加壳程序工作流程:                
1、加密源程序 APK 文件为解壳数据                
2、把解壳数据写入解壳程序 Dex 文件末尾,并在文件尾部添加解壳数据的大小。                
3、修改解壳程序 DEX 头中 checksum、 signature 和 file_size 头信息。                
4、修改源程序 AndroidMainfest.xml 文件并覆盖解壳程序 AndroidMainfest.xml 文件。
        

解壳DEX程序工作流程:                
1、读取 DEX 文件末尾数据获取借壳数据长度。                
2、从 DEX 文件读取解壳数据,解密解壳数据。以文件形式保存解密数据到 a.APK 文件                
3、通过 DexClassLoader 动态加载 a.apk。

脱壳

本次脱壳针对 classes.dex 的重加载,重加载时会动态调用到 libdvm.so 里面的 dvmDexFileOpenPartial 这个函数,这个时候只需要用 IDA 的动态调试并且下断点就行了。

android_server -> adb forward -> adb shell am start -D -n -> ida attach -> add breakpoint -> ddms -> jdb
IDA 动态调试不详细说了,另外提一下,因为这里先运行的是壳程序所以 adb 启动程序的时候需要按照 AndroidManifest.xml 里面显示的 :
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.ali.tg.testapp">
启动 com.ali.tg.testapp/.MainActivity。


在 dvmDexFileOpenPartial 下好断点之后一路点击 IDA 的绿色小箭头运行,直到在 dvmDexFileOpenPartial 这个地方停下来。



然后 Hex-View 设置和 R0 同步,可以看到熟悉的 “dex\n035\0”,开始说到的 Dex 头,然后再设置成和 R1 同步,可以看到 R1 的值。


为什么要着重强调这两个值呢,因为通过函数原型  dvmDexFileOpenPartial(addr, len, &pDvmDex) 可以知道, addr(R0) 保存的是原来 apk classes.dex 的文件起始地址,len(R1) 保存的是原来 apk classes.dex 的文件长度。

然后依次 dump,可以将原 classes.dex 恢复出来,这里有一个 dump 的小脚本,使用 shift + F2 执行。

auto fp, dex_addr, end_addr;
fp = fopen("classes.dex", "wb");
end_addr = r0 + r1;
for (dex_addr = r0; dex_addr < end_addr; dex_addr++)
    fputc(Byte(dex_addr), fp);

然后原 Dex 就 dump 出来了,然后直接反编译成 smali 文件查看即可。这里有一个小问题需要提一下:

有时断 dexFileParse 会有更好的效果,有的壳判断系统版本,>=4.0则使用dvmRawDexFileOpenArray 函数,<4 的话才用 dvmDexFileOpenPartial。

reference

Read More

2016年10月25日星期二

Android 逆向学习笔记 (五)- ARM 和 Thumb 指令集学习

学习安卓逆向,ARM 和 Thumb 是两个绕不开的内容。Thumb 可以看作是 ARM 指令集压缩形式的子集,是针对指令长度问题而提出的,它具有 16 位的指令长度(ARM:32 位),但操作数、指令地址这两项 Thumb 均和 ARM 相同,为 32 位,所以如果想要区分当前指令是 ARM 还是 Thumb 通过指令长度就可以知道。

Thumb 不是一个完整的指令体系结构,必须和 ARM 指令集结合才能正常工作。安卓逆向总是由 CODE32 声明的 ARM 指令集开始,执行的过程中可以通过 BX 指令切换到 CODE16 声明的 Thumb 指令集。

ARM 程序分析(I)

源码:

#include 
#include 

void (*func)();
  
void sub() {
  void *p = dlopen("libxxx.so", RTLD_NOW);
  if (!p) {
    return;
  }
  else {
    // myfn是libxxx.so的一个函数,作用是打印"call myfn...!"
    func = (void (*)())dlsym(p,"myfn");
    func();
  }
}

int main()
{
  printf("RTLD_NOW=%d\n", RTLD_NOW);
  sub();
  return 0;
}

反汇编之后:



sub:
1.    84a0:  4808        ldr  r0, [pc, #32]  ; (84c4 )
; r0 = [pc + 32 + 4] = [0x84a0 + 0x24] = [0x84c4] = 0x48
; 这里之所以多 +4 是因为 Thumb 涉及 PC 时需要 +4,而 ARM 计算涉及 PC 时需要 +8

2.    84a2:  2100        movs  r1, #0
; r1 = 0

3.    84a4:  b510        push  {r4, lr}
; r4 和 lr 入栈,r4 属于局部变量而 lr 保存了 sub 函数的返回地址

4.    84a6:  4c08        ldr  r4, [pc, #32]  ; (84c8 )
; r4 = [pc + 32 + 4] = [0x84a6 + 0x24] = [0x8aca] = [0x84c8] = 0x0c26
; 这里的 [0x8aca] 不属于标准起始,必须对齐到 (0, 4, 8, c),所以 [0x84ca] 变成 [0x84c8]

5.    84a8:  4478        add  r0, pc
; r0 = r0 + pc = 0x48 + 0x84a8 + 4 = 0x84f4 -> [0x84f4] = 'libxxx.so' 

6.    84aa:  447c        add  r4, pc
; r4 = r4 + pc = 0x0c26 + 0x84aa + 4 = 0x90d4

7.    84ac:  f7ff efc8   blx  8440 
; 执行 dlopen 函数,可以看到此时的指令长度为 (0x84ac ~ 0x84b0) = 4 bytes = 32 bit 为 ARM 指令
; 而之前指令长度可以看到尾 2 bytes = 16 bit,为 Thumb 指令

8.    84b0:  b138        cbz  r0, 84c2 
; r0 不为 0 则跳转执行 0x84c2 位置的 printf 函数,此时 r0 为上一条指令 dlopen 的返回结果

9.    84b2:  4906        ldr  r1, [pc, #24]  ; (84cc )
; r1 = [0x84b2 + 24 + 4] = [0x84b2 + 0x1c] = [0x84ce] = [0x84cc] = 0x46

10.   84b4:  4479        add  r1, pc
; r1 = 0x46 + 0x84b4 + 4= 0x84fe -> [0x84fe] = 'myfn'

11.   84b6:  f7ff efca   blx  844c 
; 切换到 ARM 指令执行 dlsym

12.   84ba:  4905        ldr  r1, [pc, #20]  ; (84d0 )
; r1 = [pc + 20 + 4] = [0x84ba + 0x18] = [0x84d2] = [0x84d0] = 0xfffffffc

13.   84bc:  5863        ldr  r3, [r4, r1]
; r3 = [r4 + r1] = [0x90d4 + 0xfffffffc] = [0x90d4 - 0x04] = [0x90d0] = 0x90f0
; 图中没有给出 [0x90d0] 的内容,原文中截图截少了,这里的 0x90f0 即是 myfn 函数的地址,下面传入 r0 并执行

14.   84be:  6018        str  r0, [r3, #0]
15.   84c0:  4780        blx  r0
16.   84c2:  bd10        pop  {r4, pc}

ARM 程序分析(II)

#include <stdio.h>
#include <string.h>
int main(int argc,char *argv[])
{
  char name[]="helloworld";
  int  keys[]={0xb,0x1f,0x19,0x19,0x49,0xb,0xb,0xb,0x31,0x53};
  char Thekeys[11];
  int i;
  for(i=0;i<10;i++)
  {
    keys[i]^=7;
    keys[i]=keys[i]/6;
    keys[i]+=22;
    keys[i]-=24;
    keys[i]^=name[i];
  }
  for(i=0;i<10;i++)
  {
    Thekeys[i]=keys[i];
  }
  Thekeys[i]=0;
  if(!strcmp(Thekeys,argv[1]))
    printf("Good Work,you have Successed!");
  else
    printf("NO,you are failed!");
  return 0;
}

反编译之后,流程图下所示,可以看到起点是 start 函数,但是没有发现名称为 main 的函数。那怎么确定改名之后的 main 函数呢?可以通过搜索 “__libc_init” ,定位到调用位置,代码类似如下:
.text:0000847C start
.text:0000847C
.text:0000847C var_14          = -0x14
.text:0000847C var_10          = -0x10
.text:0000847C var_C           = -0xC
.text:0000847C var_8           = -8
.text:0000847C
.text:0000847C                 LDR     R12, =(_GLOBAL_OFFSET_TABLE_ - 0x8498)
.text:00008480                 STMFD   SP!, {R11,LR}
.text:00008484                 LDR     R3, =(off_AF98 - 0xAFCC)
.text:00008488                 ADD     R11, SP, #4
.text:0000848C                 SUB     SP, SP, #0x10
.text:00008490                 ADD     R12, PC, R12 ; _GLOBAL_OFFSET_TABLE_
.text:00008494                 LDR     R3, [R12,R3] ; unk_AE90
.text:00008498                 STR     R3, [R11,#var_14]
.text:0000849C                 LDR     R3, =(off_AF9C - 0xAFCC)
.text:000084A0                 ADD     R0, R11, #4
.text:000084A4                 LDR     R3, [R12,R3] ; unk_AE88
.text:000084A8                 STR     R3, [R11,#var_10]
.text:000084AC                 LDR     R3, =(off_AFA0 - 0xAFCC)
.text:000084B0                 MOV     R1, #0
.text:000084B4                 LDR     R3, [R12,R3] ; unk_AE80
.text:000084B8                 STR     R3, [R11,#var_C]
.text:000084BC                 LDR     R3, =(off_AFA4 - 0xAFCC)
.text:000084C0                 LDR     R3, [R12,R3] ; unk_AE98
.text:000084C4                 STR     R3, [R11,#var_8]
.text:000084C8                 LDR     R3, =(off_AFA8 - 0xAFCC)
.text:000084CC                 LDR     R2, [R12,R3] ; sub_850C
.text:000084D0                 SUB     R3, R11, #-var_14
.text:000084D4                 BL      __libc_init
.text:000084D8                 SUB     SP, R11, #4
.text:000084DC                 LDMFD   SP!, {R11,PC}
.text:000084DC ; End of function start

根据 __libc_init 函数的定义,可以知道调用时保存在寄存器 R2 中的地址即使 main 函数的地址,此时根据 “LDR R2, [R12, R3] ; sub_850C” 这一行即可知道 main 函数为 sub_850C,跟进 F5 看看。

int __fastcall sub_850C(int a1, int a2)
{
  int *v2; // r3@1
  char *v3; // r0@1
  int v4; // t1@2
  signed int *v5; // r3@3
  char *v6; // r2@3
  signed int v7; // t1@4
  const char *v8; // r1@5
  int result; // r0@7
  int v10; // [sp+0h] [bp-60h]@3
  int v11; // [sp+4h] [bp-5Ch]@1
  signed int v12; // [sp+8h] [bp-58h]@1
  signed int v13; // [sp+Ch] [bp-54h]@1
  signed int v14; // [sp+10h] [bp-50h]@1
  signed int v15; // [sp+14h] [bp-4Ch]@1
  signed int v16; // [sp+18h] [bp-48h]@1
  signed int v17; // [sp+1Ch] [bp-44h]@1
  signed int v18; // [sp+20h] [bp-40h]@1
  signed int v19; // [sp+24h] [bp-3Ch]@1
  signed int v20; // [sp+28h] [bp-38h]@1
  int v21; // [sp+2Ch] [bp-34h]@1
  signed int v22; // [sp+30h] [bp-30h]@1
  __int16 v23; // [sp+34h] [bp-2Ch]@1
  char v24; // [sp+36h] [bp-2Ah]@1
  char v25; // [sp+37h] [bp-29h]@3
  char v26; // [sp+38h] [bp-28h]@5
  char v27; // [sp+42h] [bp-1Eh]@5
  int v28; // [sp+44h] [bp-1Ch]@1

  v21 = 1819043176;
  v22 = 1919907695;
  v23 = 'dl';
  v24 = 0;
  v11 = 11;
  v12 = 31;
  v13 = 25;
  v14 = 25;
  v15 = 73;
  v16 = 11;
  v17 = 11;
  v18 = 11;
  v2 = &v11;
  v28 = _stack_chk_guard;
  v19 = 49;
  v20 = 83;
  v3 = (char *)&v20 + 3;
  do
  {
    v4 = (unsigned __int8)(v3++)[1];
    *v2 = ((*v2 ^ 7) / 6 - 2) ^ v4;
    ++v2;
  }
  while ( v2 != &v21 );
  v5 = &v10;
  v6 = &v25;
  do
  {
    v7 = v5[1];
    ++v5;
    (v6++)[1] = v7;
  }
  while ( v5 != &v20 );
  v8 = *(const char **)(a2 + 4);
  v27 = 0;
  if ( !strcmp(&v26, v8) )
    printf("Good Work,you have Successed!");
  else
    printf("NO,you are failed!");
  result = 0;
  if ( v28 != _stack_chk_guard )
    _stack_chk_fail(0);
  return result;
}

可以看到做了一系列变换之后进行判断,不涉及其他位置的函数了,不详细分析下去了。

参考链接:
IDA 如何识别 ARM 的 main 函数
ARM 指令和 Thumb 指令的区别 
关于__stack_chk_guard_ptr的理解
ARM 汇编版 CRACKME 分析

Read More

2016年10月11日星期二

ANDROID 逆向实例(二)- XDCTF Mobile 100

刚刚接触安卓逆向不久看见 XDCTF 有部分安卓逆向的题目,拿过来看看,试试手,第一次分析 CTF 相关的题目确实用了不少时间,不过同时也学到了不少东西。

常规手段还原出 smali 文件(这里偷了个懒,直接用的 apktool),没有混淆也没有 so 加固,只需要理出 smali 处理流程即可。根据 AndroidManifest.xml 找到 com.example.ring.myapplication.MainActivity, 故从对应的 MainActivity.smali 作为起点分析。

进行了一系列的初始化操作,然后理出比较重要的代码:进入 p 函数、绑定类 d 为点击事件。

.method protected onCreate(Landroid/os/Bundle;)V
    ...
    invoke-direct {p0}, Lcom/example/ring/myapplication/MainActivity;->p()V
    ...
    new-instance v1, Lcom/example/ring/myapplication/d;
    invoke-direct {v1, p0}, Lcom/example/ring/myapplication/d;->(Lcom/example/ring/myapplication/MainActivity;)V
    invoke-virtual {v0, v1}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V
.end method

跟进一下 p() 函数:

.method private p()V
    ...
    const-string v1, "url.png"
    invoke-virtual {v0, v1}, Landroid/content/res/AssetManager;->open(Ljava/lang/String;)Ljava/io/InputStream;
    ...
    invoke-virtual {v0, v2, v3, v1}, Ljava/io/InputStream;->read([BII)I
    ...
    const/16 v1, 0x90
    const/4 v3, 0x0
    const/16 v4, 0x10
    invoke-static {v2, v1, v0, v3, v4}, Ljava/lang/System;->arraycopy(Ljava/lang/Object;ILjava/lang/Object;II)V
    ...
    iput-object v1, p0, Lcom/example/ring/myapplication/MainActivity;->v:Ljava/lang/String;
    ...
.end method

可以看到这里打开了 asset 目录下的 “url.png”,然后读取了 0x90 ~ 0xa0 位置的内容保存到 MainActivity->v 里,下面看下 d 类:

.method public onClick(Landroid/view/View;)V
    ...
    invoke-virtual {v0}, Landroid/widget/EditText;->getText()Landroid/text/Editable;
    move-result-object v0
    invoke-virtual {v0}, Ljava/lang/Object;->toString()Ljava/lang/String;
    move-result-object v0 
    ...
    # 下行是用来获取 MainActivity->v 的
    invoke-static {v2}, Lcom/example/ring/myapplication/MainActivity;->a(Lcom/example/ring/myapplication/MainActivity;)Ljava/lang/String;
    move-result-object v2
    invoke-static {v1, v2, v0}, Lcom/example/ring/myapplication/MainActivity;->a(Lcom/example/ring/myapplication/MainActivity;Ljava/lang/String;Ljava/lang/String;)Z
    move-result v0
    ...
    if-eqz v0, :cond_0
    ...
    const-string v2, "Congratulations!"

好的,发现成功条件了,当 MainActivity->a(String,String) 返回结果为真时程序成功,看下这里的两个 String,一个是 MainActivity->v(上面说过,其实是 url.png 的 0x90 ~ 0xa0 位置), 一个是用户输入的字符串(即为 Flag),下面先看一下 MainActivity->v :


xxd 可以看到 MainActivity->v 即是 "this_is_the_key.",然后跟进 MainActivity->a(String,String) 函数,看后续流程,由下面的代码可以看出,当用户输入的内容经过转换后和 array_0 位置的内容相同为成功条件。

.method private a(Ljava/lang/String;Ljava/lang/String;)Z
    ...
    new-instance v0, Lcom/example/ring/myapplication/c;
    invoke-direct {v0}, Lcom/example/ring/myapplication/c;->()V
    invoke-virtual {v0, p1, p2}, Lcom/example/ring/myapplication/c;->a(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    ...
    const/16 v1, 0x20
    new-array v1, v1, [B
    fill-array-data v1, :array_0
    new-instance v2, Ljava/lang/String;    
    invoke-direct {v2, v1}, Ljava/lang/String;->([B)V      # 将 array_0 位置的内容转化成字符串
    invoke-virtual {v0, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)
    move-result v0
    return v0
.end method

在比较之前,函数初始化了类 c,然后执行 c->a(String,String) ,这里有点复杂,偷个懒使用 jeb 还原 java 代码看,这里。



可以看到做的主要的事情有:进入 c->a(String) 函数将 bytes 分为两个一组,然后组内交换顺序,那么现在的 MainActivity->v 就变成了 "htsii__sht_eek.y";另外初始化了类 a,并执行了 a->a(byte[]) 和 a->b(byte[]) 操作,看下类 a 的代码。


可以看出这里其实是以 MainActivity->v 作为了 AES 加密 Key,加密了用户输入的字符串这个操作。所以最后的比较即是:AES 密文和 array_0 位置数据的比较,写了个解密代码,运行出来了 Flag。

import java.io.*;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.Cipher;

class test  
{
 public static void main (String[] args) throws java.lang.Exception
 { 
         String key = "htsii__sht_eek.y";
         SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
         Cipher cipher = Cipher.getInstance("AES");// 创建密码器  
         byte[] bye = {21,-93,-68,-94,86,117,-19,-68,-92,33,50,118,16,13,1,-15,-13,3,4,103,-18,81,30,68,54,-93,44,-23,93,98,5,59};
         /*
            byte[] byteContent = content.getBytes("utf-8");  
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);// 初始化   
            byte[] result = cipher.doFinal(byteContent);  
            String str2 = new String(result,"UTF-8");
            System.out.println(str2); // 加密   
            */
            
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            byte[] result2 = cipher.doFinal(bye);
            String str3 = new String(result2, "utf-8");
            System.out.println(str3);
 }
}


# LCTF{1t's_rea1ly_an_ea3y_ap4}

* 逆向的 apk 上传到了 Github
Read More

2016年10月10日星期一

Android 逆向学习笔记 (四)- 签名校验突破

有的时候在篡改 apk 并且重新打包之后不能直接运行,此时部分情况是因为存在软件的签名校验机制。在前面几篇的学习中可以知道,软件打包之后需要重新签名才可以安装到安卓机器上,此时重新签名一般用的都是公开的  platform.x509.pem 和 platform.pk8 , 有的软件为达到防篡改的目的会对新老签名进行比较,如果存在差异则确定被篡改,程序终止。

除了签名校验之外,还有一种情况是 classes.dex 的 MD5 检验,也可以发现被篡改的 classes.dex。怎么区分这两种不同的校验呢?拿到 apk 之后对其重新签名,如果仍然可以正常安装运行的即说明是 classes.dex 的 MD5 校验,反之则是签名校验。

一般在程序内进行的校验有两种途径,在线比对和离线比对,逆向者在意的是比对的这个过程及其参数。在线比对和离线比对的区别是:在断网情况下仍旧闪退的是离线比对,断网后先提示没有网络连接再闪退的是在线比对。

既然校验的过程是在本地程序进行的,那么在反编译得到代码之后,只要修改一下比较时的对应条件就可以简单的绕过校验了,整个校验过程有点掩耳盗铃的意思。

Signature · smali

获取当前 apk 签名的 Java 代码如下:

PackageInfo packageInfo = getPackageManager().getPackageInfo(
                    "xx.xxx.xx", PackageManager.GET_SIGNATURES);
Signature[] signs = packageInfo.signatures;

对应的 smali 代码为:

Landroid/content/pm/PackageInfo;->signatures:[Landroid/content/pm/Signature

所以对付签名校验,只要通过这段关键代码找到对应的调用位置,不断跟进,确定最终判断的位置然后修改比较条件即可。

另外有的时候会进入 native 层判断,这个时候搜索关键字 signature 然后判断跟进即可。

MD5 · smali

这类首先会在 smali 层获取当前 apk 的目录,然后再传入 native 层面进行比较,典型代码如下所示:

iget-object v1, v0,Landroid/content/pm/ApplicationInfo;->sourceDir:Ljava/lang/String;
const-string v1, "/sdcard/buyudaren.apk" # 本行目的是 bypass 校验,路径为原版 apk 路径,覆盖 v1
invoke-static {v1}, Lorg/cocos2dx/lib/Cocos2dxHelper;->nativeSetApkPath(Ljava/lang/String;)V

简单添加中间一行即可绕过 MD5 的判断,因为此时进入 native 层的是原版 apk 的路径,所以校验的也是原版 apk,此时当然没有问题。可以看到,看似复杂的进入 native 层面有的时候只是掩耳盗铃的一种方式。

如果要进入 native 层面去 Bypass 检查,在把 .so 文件拖入 IDA Pro 后搜索 “classes.dex” 即可,跟进可以发现 MD5 字符串或是进行比对的特征值。

另外有时存在 .so 和 classes.dex 文件相互验证的情况,这个时候就对应的搜索 “classes.dex” 和 “xxx.so” 两个特征字符串,找到程序运行中校验的文件路径,然后对应地修改成原版文件的路径即可。

REFERENCE



* 另外说一下,有的时候两个程序的 AndroidManifest.xml 中定义了 SharedUserId 并进行交互,这时必须两个程序的签名相同才可以正常安装。
Read More