admin管理员组

文章数量:1661522

文章目录

  • 前言
  • 签名验证
    • 1.1 签名机制
    • 1.2 签名验签
    • 1.3 签名绕过
  • 反反调试
    • 2.1 tracerPid检测
    • 2.2 进程名称检测
    • 2.3 关键文件检测
    • 2.4 调试端口检测
    • 2.5 ptrace值检测
    • 2.6 时间差异检测
    • 2.7 内置函数检测
    • 2.8 调试断点检测
  • 总结

前言

Android 的 APK 文件为了防止被篡改和重打包,经常会做签名校验来保证自身完整性,当程序被篡改后将提示用户或者直接退出运行。同时有些 APP 为了防止被攻击者动态调试和分析,还做了反调试机制。本文来学习记录下 Android 签名验证机制与反调试机制的实现原理和其对抗技术。

签名验证

Android 系统使用 JAR 包的签名机制对 APK 进行完整性保护,确保 APK 在不安全的网络传输时的完整性得到保护。但 Android 系统没有对数字签名的颁发者进行管理,任何人都可以生成数字签名,并使用该签名对 APK 包进行重新签名。如果 APP 本身不对自身的签名来源进行有效的完整性检查,攻击者可以篡改应用(插入恶意代码、木马、后门、广告等),重新签名并且二次发布,导致应用程序完整性被破坏。为了说明 APK 签名比对对软件安全的有效性,我们有必要了解一下 Android APK 的签名机制。

1.1 签名机制

对比一个没有签名的 APK 和一个签名好的 APK,我们会发现,签名好的 APK 包中多了一个叫做 META-INF 的文件夹。里面有三个文件,分别名为 MANIFEST.MF、CERT.SF 和 CERT.RSA,这些就是使用 signapk.jar 生成的签名文件。

签名文件作用
MANIFEST.MF保存了 apk 所有文件的摘要信息(SHA-1+Base64)
CERT.SF保存了对 MANIFEST.MF 文件再进行一次 SHA-1 并 Base64 加密的信息,并同时保存了 MANIFEST.MF 文件的摘要信息
CERT.RSA保存了公钥和所采用的加密算法等信息

其中 signapk.jar 是 Android 源码包中的一个签名工具,由于 Android 是个开源项目,所以可以直接找到 signapk.jar 的源码,路径为 /build/tools/signapk/SignApk.java。通过阅读 signapk 源码,我们可以理清签名 APK 包的整个过程。

1、 生成 MANIFEST.MF 文件:

程序遍历 update.apk 包中的所有文件(entry),对非文件夹非签名文件的文件,逐个生成 SHA1 的数字签名信息,再用 Base64 进行编码。具体代码见这个方法:

private static Manifest addDigestsToManifest(JarFile jar)

关键代码如下:

for (JarEntry entry: byName.values()) {
     String name = entry.getName();
     if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
         !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
         (stripPattern == null ||!stripPattern.matcher(name).matches())){
         InputStream data = jar.getInputStream(entry);
         while ((num = data.read(buffer)) > 0) {
         md.update(buffer, 0, num);
       }
       Attributes attr = null;
       if (input != null) attr = input.getAttributes(name);
       attr = attr != null ? new Attributes(attr) : new Attributes();
       attr.putValue("SHA1-Digest", base64.encode(md.digest()));
       output.getEntries().put(name, attr);
    }
}

之后将生成的签名写入 MANIFEST.MF 文件,关键代码如下:

Manifest manifest = addDigestsToManifest(inputJar);
je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);

这里简单介绍下 SHA1 数字签名。简单地说,它就是一种安全哈希算法,类似于 MD5 算法。它把任意长度的输入,通过散列算法变成固定长度的输出(这里我们称作“摘要信息”)。你不能仅通过这个摘要信息复原原来的信息。另外,它保证不同信息的摘要信息彼此不同。因此如果你改变了 apk 包中的文件,那么在 apk 安装校验时,改变后的文件摘要信息与 MANIFEST.MF 的检验信息不同,于是程序就不能成功安装。

2、 生成 CERT.SF 文件:

对前一步生成的 Manifest,使用 SHA1-RSA 算法,用私钥进行签名。关键代码如下:

Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(privateKey);
je = new JarEntry(CERT_SF_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureFile(manifest,
new SignatureOutputStream(outputJar, signature));

RSA 是一种非对称加密算法。用私钥通过 RSA 算法对摘要信息进行加密。在安装时只能使用公钥才能解密它。解密之后,将它与未加密的摘要信息进行对比,如果相符,则表明内容没有被异常修改。

3、 生成 CERT.RSA 文件:

生成 MANIFEST.MF 没有使用密钥信息,生成 CERT.SF 文件使用了私钥文件。那么我们可以很容易猜测到,CERT.RSA 文件的生成肯定和公钥相关。CERT.RSA 文件中保存了公钥、所采用的加密算法等信息。核心代码如下:

je = new JarEntry(CERT_RSA_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureBlock(signature, publicKey, outputJar);

其中 writeSignatureBlock 的代码如下:

private static void writeSignatureBlock(
      Signature signature, X509Certificate publicKey, OutputStream out)
         throws IOException, GeneralSecurityException {
             SignerInfo signerInfo = new SignerInfo(
             new X500Name(publicKey.getIssuerX500Principal().getName()),
                  publicKey.getSerialNumber(),
                  AlgorithmId.get("SHA1"),
                  AlgorithmId.get("RSA"),
                  signature.sign());

        PKCS7 pkcs7 = new PKCS7(
              new AlgorithmId[] { AlgorithmId.get("SHA1") },
              new ContentInfo(ContentInfo.DATA_OID, null),
              new X509Certificate[] { publicKey },
              new SignerInfo[] { signerInfo });

       pkcs7.encodeSignedData(out);
}

好了,分析完APK包的签名流程,我们可以清楚地意识到:

  1. Android 签名机制其实是对 APK 包完整性和发布机构唯一性的一种校验机制;
  2. Android 签名机制不能阻止 APK 包被修改,但修改后的再签名无法与原先的签名保持一致(拥有私钥的情况除外);
  3. APK 包加密的公钥就打包在 APK 包内,且不同的私钥对应不同的公钥,我们可以对比公钥来判断私钥是否一致;
  4. Android 并不要求所有应用程序的签名证书都由可信任 CA 的根证书签名,通过这点保证了其生态系统的开放性,所有人都可以用自己生成的证书对应用程序签名。

如果想修改一个已经发布的应用程序,哪怕是修改一张图片,都必须对其进行重新签名。但是,签原始应用的私钥一般是拿不到的(肯定在原始应用程序开发者的手上,且不可能公布出去),所以只能用另外一组公私钥对,生成一个新的证书,对重打包的应用进行签名,因此重打包的 apk 中所带证书的公钥肯定和原始应用不一样。同时,在手机上如果想安装一个应用程序,应用程序安装器会先检查相同包名的应用是否已经被安装过,如果已经安装过,会继续判断已经安装的应用和将要安装的应用,其所携带的数字证书中的公钥是否一致。如果相同,则继续安装;而如果不同,则会提示用户先卸载前面已安装的应用。

1.2 签名验签

在程序中获取 APK 的签名时,通过 signature 方法进行获取,如下:

packageInfo = manager.getPackageInfo(pkgname,PackageManager.GET_SIGNATURES);
signatures = packageInfo.signatures;
for (Signature signature : signatures) {
    builder.append(signature.toCharsString());
}
signature = builder.toString();

所以一般的程序就是在代码中通过判断 signature 的值,来判断 APK 是否被重新打包过。

APK 签名比对的应用场景大致有三种:

  1. 程序自检测:在程序运行时,自我进行签名比对,比对样本可以存放在 APK 包内,也可存放于云端。缺点是程序被破解时,自检测功能同样可能遭到破坏,使其失效;
  2. 可信赖的第三方检测:由可信赖的第三方程序负责 APK 的软件安全问题,对比样本由第三方收集,放在云端,这种方式适用于杀毒安全软件或者APP Market之类的软件下载市场,缺点是需要联网检测,在无网络情况下无法实现功能(不可能把大量的签名数据放在移动设备本地);
  3. 系统限定安装:这就涉及到改 Android 系统了,限定仅能安装某些证书的 APK,软件发布商需要向系统发布上申请证书,如果发现问题,能追踪到是哪个软件发布商的责任,适用于系统提供商或者终端产品生产商,缺点是过于封闭,不利于系统的开放性。

以上三种场景,虽然各有缺点,但缺点并不是不能克服的。例如,我们可以考虑程序自检测的功能用 native method的 方法实现等等。软件安全是一个复杂的课题,往往需要多种技术联合使用,才能更好的保障软件不被恶意破坏。

附上一个完整 APK 签名校验工具类:

public class SignCheckUtil {

    private Context context;
    private String cer = null;
    private String type = "SHA1";
    private String sha1RealCer = "签名SHA1值";
    private String md5RealCer = "签名MD5";
    private static final String TAG = "sign";

    public SignCheckUtil(Context context,String type) {
        this.context = context;
        this.type = type;
    }

    /**
     * 获取应用的签名
     *
     * @return
     */
    public String getCertificateSHA1Fingerprint() {
        String hexString = "";
        //获取包管理器
        PackageManager pm = context.getPackageManager();
        //获取当前要获取 SHA1 值的包名,也可以用其他的包名,但需要注意,
        //在用其他包名的前提是,此方法传递的参数 Context 应该是对应包的上下文。
        String packageName = context.getPackageName();
        //签名信息
        Signature[] signatures = null;

        try {
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
                PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNING_CERTIFICATES);
                SigningInfo signingInfo = packageInfo.signingInfo;
                signatures = signingInfo.getApkContentsSigners();
            } else {
                //获得包的所有内容信息类
                PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
                signatures = packageInfo.signatures;
            }
            byte[] cert = signatures[0].toByteArray();
            //将签名转换为字节数组流
            InputStream input = new ByteArrayInputStream(cert);
            //证书工厂类,这个类实现了出厂合格证算法的功能
            CertificateFactory cf = CertificateFactory.getInstance("X509");
            //X509 证书,X.509 是一种非常通用的证书格式
            X509Certificate c = null;
            c = (X509Certificate) cf.generateCertificate(input);
            //加密算法的类,这里的参数可以使 MD4,MD5 等加密算法
            MessageDigest md = MessageDigest.getInstance(type);
            //获得公钥
            byte[] publicKey = md.digest(c.getEncoded());
            //字节到十六进制的格式转换
            hexString = byte2HexFormatted(publicKey);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e1) {
            e1.printStackTrace();
        } catch (CertificateEncodingException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return hexString.trim();
    }

    //这里是将获取到得编码进行16 进制转换
    private String byte2HexFormatted(byte[] arr) {
        StringBuilder str = new StringBuilder(arr.length * 2);
        for (int i = 0; i < arr.length; i++) {
            String h = Integer.toHexString(arr[i]);
            int l = h.length();
            if (l == 1)
                h = "0" + h;
            if (l > 2)
                h = h.substring(l - 2, l);
            str.append(h.toUpperCase());
            if (i < (arr.length - 1))
                str.append(':');
        }
        return str.toString();
    }

    /**
     * 检测签名是否正确
     *
     * @return true 签名正常 false 签名不正常
     */
    public boolean check() {

        if (this.sha1RealCer != null || md5RealCer!= null) {
            cer = getCertificateSHA1Fingerprint();
            if ((TextUtils.equals(type,"SHA1") && this.cer.equals(this.sha1RealCer)) || (TextUtils.equals(type,"MD5") && this.cer.equals(this.md5RealCer))) {
                return true;
            }
        }
        return false;
    }
}

1.3 签名绕过

在讲签名绕过的方式前,需要先明确 DEX 校验和签名校验:

  1. 将 APK 以压缩包的形式打开删除原签名后,再签名,安装能够正常打开,但是用 IDE(即 apk 改之理,会自动反编译 dex)工具二次打包,却出现非正常情况的,如:闪退/弹出非正版提示框,则可以确定是dex文件的校验;
  2. 将 APK 以压缩包的形式打开删除原签名再签名,安装之后打开异常的,则基本可以断定是签名检验。如果在断网的情况下同样是会出现异常,则是本地的签名检验;如果首先出现的是提示网络没有连接,则是服务器端的签名校验。

针对给类签名校验方式的绕过:

签名校验方式介绍绕过方式
Java 层校验获取签名信息和验证的方法都写在 Android 的 Java 层1)Hook Java 层函数的返回值;2)反编译修改校验函数逻辑并二次打包 ;3)动态调试 APK 并篡改内存中校验函数的返回值
SO 层校验获取签名信息和验证的方法都写在 Android 的 So 层1) Hook SO 层函数的返回值;2)反汇编程序、修改校验函数逻辑并二次打包 ;3)动态调试 APK 并篡改内存中校验函数的返回值
服务器验证在 Android 的 Java 层获取签名信息,上传服务器在服务端进行签名然后返回验证结果1)拦截并篡改服务端的校验返回结果;2)反编译程序并篡改破坏校验过程

具体的对抗案例分析可参见:APK签名校验绕过,此处不展开叙述。

反反调试

反调试在代码保护中扮演着很重要的角色,虽然并不能完全阻止逆向行为,但是能在长期的攻防战中给破解人员不断的增加逆向难度。

2.1 tracerPid检测

APK 在被调试的状态下,Linux 会向/proc/<pid>/status文件中写入一些进程状态信息,其中最大的变化就是文件中的 TracerPid 字段被写入了调试进程的 pid,如下图所示:
所以可以通过检测/proc/<pid>/status文件中 TracerPid 的值是否为 0 来判断当前进程正在被调试,是的话则杀死进程。具体的 So 层检测示例代码如下:

#include <unistd.h>

...
void check_process_status(){
    int buffsize=1024;
    char filename[buffsize];    // 文件名
    char line[buffsize];        // 文件中的每一行
    int pid=getpid();           // 获取进程号
    sprintf(filename,"/proc/%d/status",pid);
    FILE *fp=fopen(filename,"r");
    if (fp != NULL){
        while (fgets(line,buffsize,fp)){
            if (strncmp(line,"TracerPid",9)==0){
                int status=atoi(&line[10]);
                if (status!=0){
                    fclose(fp);
                    kill(pid,SIGKILL);   // 杀死进程
                }
                break;
            }
        }
    }
    fclose(fp);
}

jint JNI_OnLoad(JavaVM* vm, void* reserved){
    check_process_status();
    ...
}

至于破解反调试的方法:Frida Hook 篡改校验调试状态的函数的返回值,或者使用 IDA 反汇编 APK 并篡改程序逻辑后重新打包,具体参见:IDA动态调试破解AliCrackme与反调试对抗。

2.2 进程名称检测

根据上一种反调试方法我们知道可以通过检测 TracerPid 的值判断程序是否被调试,而 TracerPid 的值就是调试器的进程号,调试器的进程名则被存储在/proc/<pid>/cmdline文件中,这里的 pid 为调试器的 pid。所以可以检测/proc/<pid>/cmdline文件中的内容是否包含一些调试器的进程名,比如 android_server,来判断程序是否被调试。

校验代码示例如下:

void check_process_name(){
    int buffsize=1024;
    char filename[buffsize];
    char line[buffsize];
    char name[buffsize];
    char nameline[buffsize];
    int pid=getpid();
    sprintf(filename,"/proc/%d/status",pid);
    FILE *fp=fopen(filename,"r");
    if (fp!=NULL){
        while (fgets(line,buffsize,fp)){
            // 检测/proc/<pid>/status文件的某一行中是否包含TracerPid
            if (strstr(line,"TracerPid")!=NULL){  
                int status=atoi(&line[10]);
                if (status!=0){
                    sprintf(name,"/proc/%d/cmdline",status);
                    FILE *fpname=fopen(name,"r");
                    if (fpname!=NULL){
                        while (fgets(nameline,buffsize,fpname)!=NULL){
                            // 检测/proc/<pid>/cmdline文件的某一行是否包含android_server
                            if (strstr(nameline,"android_server")!=NULL){  
                                kill(pid,SIGKILL);
                            }
                        }
                    }
                    fclose(fpname);
                }
            }
        }
    }
    fclose(fp);
}

若要绕过反调试,修改 android_server 的文件名即可。

2.3 关键文件检测

在使用 IDA 动态调试之前一般会先将 IDA Pro 目录下的 android_server 放入到 /data/local/tmp 目录下,所以可以检测 /data/local/tmp 目录是否包含一个名为 android_server 的文件。

在 native_lib.cpp 文件中添加一个 check_name() 方法,并在 JNI_OnLoad() 中调用:

void check_name(){
    char* root_path="/data/local/tmp";
    DIR* dir;
    dir=opendir(root_path);   // 打开目录
    int pid=getpid();
    if (dir!=NULL){
        dirent* currentDir;
        while ((currentDir=readdir(dir))!=NULL){
            if (strncmp(currentDir->d_name,"android_server",14)==0){
                kill(pid,SIGKILL);
            }
        }
        closedir(dir);
    }
}

若要绕过反调试,可以修改 android_server 的文件名,或者将 android_server 放在其他目录。

2.4 调试端口检测

android_server 的默认监听的端口号是 23946,所以可以通过检测这个端口号来起到一定的反调试作用。在 Linux 系统中,/proc/net/tcp文件会记录一些连接信息,在启动 android_server 以后,该文件中多了一行内容:

可以看到,/proc/net/tcp 文件中多了一个运行在 5D8A 端口上的连接信息,而 5D8A 正好是 23946 的十六进制,因此可以检测该文件中的端口号来达到反调试的效果。

在 native_lib.cpp 中添加一个 check_port() 方法,并在 JNI_OnLoad() 中调用:

void check_port(){
    int buffsize=1024;
    char filename[buffsize];
    char line[buffsize];
    int pid=getpid();
    sprintf(filename,"/proc/net/tcp");
    FILE *fp=fopen(filename,"r");
    if (fp!=NULL){
        while (fgets(line,buffsize,fp)){
            if (strncmp(line,"5D8A",4)){
                kill(pid,SIGKILL);
            }
        }
    }
    fclose(fp);
}

若要绕过反调试,只需要修改 android_server 的运行端口即可,也就是说在启动 android_server 时运行以下命令:

./android_server -p 24000

2.5 ptrace值检测

Linux 内核提供一个 ptrace 函数:

#include <sys/ptrace.h>
long ptrace(enum _ptrace_request request,pid_t pid,void* addr,void* data)

ptrace 可以允许进程 A 控制进程 B,并且进程 A 可以检查和修改进程 B 的内存和寄存器。但是一个进程最多只能被另一个进程跟踪,根据这个特点,可以让进程自己跟踪自己,参数 request 设置为 PTRACE_TRACEME,程序被自己附加调试后,其他的调试操作就会失败。

在 native-lib.cpp 中添加一个 ptrace_me() 方法,并在 JNI_OnLoad() 中调用:

#include <linux/ptrace.h>
#include <sys/ptrace.h>

void ptrace_me(){
    ptrace(PTRACE_TRACEME,0,NULL,NULL);
}

绕过的方法同样是反汇编程序并篡改校验逻辑后进行重打包。

2.6 时间差异检测

正常情况下,一段程序在两行代码之间的时间差是很短的,而对于调试程序来说,单步调试时两行代码之间的时间差会比较大,检测两条代码之间的时间差,可以大概率判断程序是否被调试。

在 C 语言中可以使用函数 gettimeofday() 来得到时间,其精度可以达到微秒。

#include<sys/time.h>
int gettimeofday(struct timeval *tv,struct timezone *tz )

gettimeofday() 会把当前时间存放在tv所指的结构体中,当地时区的信息则放到tz所指的结构体中。执行成功则返回0,失败则返回-1。

在 native-lib.cpp 中添加一个 check_time() 方法,并在 JNI_OnLoad() 方法中调用:

void check_time(){
    int pid=getpid();
    struct timeval start;
    struct timeval end;
    struct timezone tz;
    gettimeofday(&start,&tz);
    gettimeofday(&end,&tz);
    int timeoff=end.tv_sec-start.tv_sec;
    if (timeoff>1){
        kill(pid,SIGKILL);
    }
}

若要绕过反调试,只需按下 F9 执行两次 gettimeofday 函数即可。

2.7 内置函数检测

Android 的 android.os.Debug 类提供了 isDebuggerConnected() 方法,用于检测是否有调试器挂载到程序上。修改 MainActivity.java 中的代码:

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        if (!Debug.isDebuggerConnected()){
            System.loadLibrary("native-lib");
        }else {
            Process.killProcess(Process.myPid());
        }
    }
    ...
}

破解方法自然是 Frida hook 篡改返回值最为方便……

2.8 调试断点检测

IDA 等调试器在动态调试时会向断点地址插入breakpoint 断点指令,而把原来的指令暂时备份到别处,所以可以通过扫描代码中是否有 breakpoint 汇编指令即可。

一般来说,Android 的汇编代码有 ARM 和 Thumb,因此需要都检测一下:

- Arm:0x010x000x9f0xef
- Thumb16:0x010xde
- Thumb32:0xf00xf70x000xa0

在 native-lib.cpp 中添加 check_break_point 方法,并在 JNI_OnLoad 中调用:

#include <elf.h>

...

unsigned int getLibAddr(){
    unsigned int ret=0;
    char name[]="libnative-lib.so";
    char buf[4096],*tmp;
    int pid=getpid();
    FILE *fp;
    sprintf(buf,"/proc/%d/maps",pid);
    fp=fopen(buf,"r");
    if (fp!=NULL){
        while (fgets(buf,sizeof(buf),fp)!=NULL){
            if (strstr(buf,name)){
                tmp=strtok(buf,"-");
                ret=strtoul(tmp,NULL,16);
                break;
            }
        }
    }
    fclose(fp);
    return ret;
}
bool check_break_point(){
    unsigned int base,offset,pheader;
    Elf32_Ehdr *elfhdr;   // ELF_Header
    Elf32_Phdr *elfphdr;  // Program_Header
    base=getLibAddr();
    if (base == 0){
        return false;
    }
    elfhdr=(Elf32_Ehdr*) base;
    pheader=base+elfhdr->e_phoff;     // e_phoff:程序头偏移

    for (int i=0;i<elfhdr->e_phnum;i++){    // e_phnum:程序头表中元素的个数
        elfphdr=(Elf32_Phdr*)(pheader+i*sizeof(Elf32_Phdr));
        if (!(elfphdr->p_flags & 1)){
            continue;
        }
        offset=base+elfphdr->p_vaddr;      // p_vaddr:段的数据映射到虚拟地址空间中的位置
        offset+=sizeof(Elf32_Ehdr)+sizeof(Elf32_Phdr)*elfhdr->e_phnum;

        char *p=(char *)offset;
        for (int j = 0; j < elfphdr->p_memsz; ++j) {    // p_memsz:段在虚拟地址空间中的长度
            if (*p == 0x01 && *(p+1) == 0xde){          // Thumb16
                return true;
            } else if (*p == 0xf0 && *(p+1) == 0xf7 && *(p+2) == 0x00 && *(p+3) == 0xa0){    // Thumb32
                return true;
            } else if (*p == 0x01 && *(p+1) == 0x00 && *(p+2) == 0x9f && *(p+3) == 0xef){    // ARM
                return true;
            }
            p++;
        }
    }
    return false;
}

jint JNI_OnLoad(JavaVM* vm, void* reserved){
    if (check_break_point()){
        int pid=getpid();
        kill(pid,SIGKILL);
    }
    ...
}

破解方法: Frida hook So 层函数篡改返回值……

总结

综上可见对抗 APP 的签名验证、反调试机制,有几种通用性较强的对抗方式:

  1. 使用 Frida 对 Java/SO 层校验逻辑函数的判断结果进行 Hook 并篡改;
  2. 使用 AndroidKiller 反编译 APP 程序并篡改校验函数的逻辑并进行二次打包;
  3. 使用 IDA Pro 进行动态调试分析,对反汇编后的程序进行篡改后二次打包。

道高一尺魔高一丈,客户端安全加固手段往往只能增加 APP 被破解的难度,而非保证 APP 无法被破解。

本文标签: 机制技术android