android签名验证与反调试机制的对抗技术(代码片段)

Tr0e Tr0e     2023-01-23     273

关键词:

前言

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查看详情  

ida动态调试破解alicrackme与反调试对抗(代码片段)

文章目录前言APK破解(上)1.1静态分析1.2动态分析反调试对抗2.1Ptrace2.2全局调试2.3反反调试APK破解(下)3.1重新编译3.2动态调试总结前言在前面的文章中IDA动态调试破解EXE文件与分析APK流程介绍了IDA对APK进行动态调试分析的简单流程&#... 查看详情

破解“贪吃蛇大作战”的签名信息服务端验证机制

...玩了一把5分钟限时赛:长度69224,击杀1456。将原包重新签名,安装到手机上,一直提示网络无法连接,原包没有问题。这里很明显是将签名信息上传到了服务器端,在服务器端进行了签名校验,校验失败则断开与此客户端的连... 查看详情

javascript加密代码反调试

JavaScript奇技淫巧:加密JS代码反调试JS代码混淆加密,已被很多人使用,因为它真的很有用、很实用,可以用于保护代码、防护分析、复制、盗用,还可以用于小游戏过审、APP加固等方面。混淆加密后的JS代码,可能被他人分析... 查看详情

javascript加密代码反调试

JavaScript奇技淫巧:加密JS代码反调试JS代码混淆加密,已被很多人使用,因为它真的很有用、很实用,可以用于保护代码、防护分析、复制、盗用,还可以用于小游戏过审、APP加固等方面。混淆加密后的JS代码,可能被他人分析... 查看详情

一种基于tls的高级反调试技术(代码片段)

...越引起人们的重视。在反盗版技术中,起最大作用的当属反调试技术。然而传统的反调试技术都存在一个弱点:他们都在程序真正开始执行之后才采取反调试手段。实际上在反调试代码被执行前,调试器有大量的时间来影响程序... 查看详情

windows下反(反)调试技术汇总(代码片段)

反调试技术,恶意代码用它识别是否被调试,或者让调试器失效。恶意代码编写者意识到分析人员经常使用调试器来观察恶意代码的操作,因此他们使用反调试技术尽可能地延长恶意代码的分析时间。为了阻止调试器的分析,当... 查看详情

android开发中的sslpinning

一、SSL在日常的安全渗透过程中,我们经常会遇到瓶颈无处下手,这时候如果攻击者从APP进行突破,往往会有很多惊喜。但是目前市场上的APP都会为防止别人恶意盗取和恶意篡改进行一些保护措施,比如模拟器检测、root检测、AP... 查看详情

《逆向工程核心原理》学习笔记:反调试技术(代码片段)

目录前言一、反调试技术概况二、静态反调试技术1、PEB2、NtQueryInformationProcess()(1)ProcessDebugPort(0x7)(2)ProcessDebugObjectHandle(0x1E)(3)ProcessDebugFlags(0x1F)(4)例子(5)破解之法3、NtQuerySystemIn... 查看详情

过反调试

...二者本为一体破解技术就不要我多介绍了,下面我来介绍反调试技术也就是所谓的防破解技术反调试技术可以简单通俗的理解为:防止OD分析软件的技术,也就是反调试技术那么反调试技术又有几种呢?下面我介绍几种常用反调... 查看详情

《逆向工程核心原理》学习笔记:反调试技术(代码片段)

目录前言一、反调试技术概况二、静态反调试技术1、PEB2、NtQueryInformationProcess()(1)ProcessDebugPort(0x7)(2)ProcessDebugObjectHandle(0x1E)(3)ProcessDebugFlags(0x1F)(4)例子 查看详情

《逆向工程核心原理》学习笔记:反调试技术(代码片段)

目录前言一、反调试技术概况二、静态反调试技术1、PEB2、NtQueryInformationProcess()(1)ProcessDebugPort(0x7)(2)ProcessDebugObjectHandle(0x1E)(3)ProcessDebugFlags(0x1F)(4)例子 查看详情

学习:反调试之检测类名与标题名(代码片段)

反调试之检测类名与标题名:通过FindWindow函数来进行反调试HWNDFindWindowA(LPCSTRlpClassName,LPCSTRlpWindowName);返回值类型:类型:HWND如果函数成功,则返回值是具有指定类名和窗口名的窗口的句柄。如果函数失败,则返回值为NULL。要获... 查看详情

android反调试实践(代码片段)

(一)xposed检测1.每一个被hook的进程,都会将xposed的相关库文件和jar文件加载到相应的进程空间中,如图:这里看到进程空间中加载了1)app_process32_xposed2)libxposed_art.so3)XResourcesSuperClass.dex既然能... 查看详情

修改android手机内核,绕过反调试(代码片段)

本文博客链接:http://blog.csdn.net/qq1084283172/article/details/570864860x1.手机设备环境Modelnumber:Nexus5OSVersion:Android4.4.4KTU84PKernelVersion:3.4.0-gd59db4e0x2.Android内核提取查找Android设备的boot分区文件。高通芯片的设备可 查看详情

反调试手法之createprocess反调试(代码片段)

              反调试手法之CreateProcess反调试在学习Win32创建进程的时候.我们发现了有一个进程信息结构体.STARTUPINFO.这个结构体可以实现反调试.具体CreateProcess可以参考上一篇博客.:  https://www.cnblogs.com/iBi... 查看详情

利用异常实现反调试(代码片段)

0×01介绍一些文章已经介绍过通过检测异常来对抗调试器的技术。这个思想很简单:根据设计本意,调试器会处理特定的异常。如果一个异常包裹在try块中,只有当没有附加调试器的时候,异常处理程序才会执行。因此,可... 查看详情

反调试技术常用api,用来对付检测od和自动退出程序

在调试一些病毒程序的时候,可能会碰到一些反调试技术,也就是说,被调试的程序可以检测到自己是否被调试器附加了,如果探知自己正在被调试,肯定是有人试图反汇编啦之类的方法破解自己。为了了解如何破解反调试技术... 查看详情