• 混淆技术研究笔记(八)扩展yGuard实现签名


    logo
    前面铺垫了这么多,终于开始实现签名反篡改的功能了。

    下载 yGuard 源码(https://github.com/yWorks/yGuard),然后先修改一处错误,在 settings.gradle 中定义的项目名是错的(和github上的名字不一样,git clone 下载会使用 github 定义的名字yGuard,估计作者本地建的项目名是 yguard),将里面的 rootProject.name = 'yguard' 改成 rootProject.name = 'yGuard' 即可。

    一开始的想法是要参考 实现,所以连代码都挨着 AdjustSection 类,写在了 ObfuscatorTask 内部:
    在这里插入图片描述
    假设我们允许配置一个 元素,在 ObfuscatorTask 创建变量和对应的 createSign 方法:
    在这里插入图片描述
    接下来先把 signSection 安排到我们在 混淆技术研究笔记(六)如何基于yGuard实现? 中指定的地方,当时指定了一个地方获取要签名的类:

    for (AdjustSection as : adjustSections)
    {
      as.createEntries(inFilesList);
    }
    
    • 1
    • 2
    • 3
    • 4

    一个读取混淆前后名字和内容的地方:

    if (inName.endsWith(CLASS_EXT))
    {
      if (fileFilter == null || fileFilter.accepts(inName)){
        // Write the obfuscated version of the class to the output Jar
        ClassFile cf = ClassFile.create(inStream);
        fireObfuscatingClass(Conversion.toJavaClass(cf.getName()));
        cf.remap(classTree, replaceClassNameStrings, log);
        String outName = createClassFileName(inName, cf) + CLASS_EXT;
        //省略部分代码
        updateManifest(i, inName, outName, digests);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    还有一个能写入到jar包中的地方:

    // write the entry itself
    outJar.addFile(entry.getName(), (byte[]) array[1]);
    
    • 1
    • 2

    下面详细介绍这部分实现。

    1. 实现配置签名指定的类

    虽然想参考下面的方法:

    for (AdjustSection as : adjustSections)
    {
      as.createEntries(inFilesList);
    }
    
    • 1
    • 2
    • 3
    • 4

    但是因为 获取的是资源文件,不是类,直接 copy 来用不合适,既然是获取类,就直接参考 下面的 ,找到 的实现:

    public class ClassSection extends PatternMatchedClassesSection implements Mappable {
    }
    
    • 1
    • 2

    从这里发现只要继承 PatternMatchedClassesSection 就能使用 addEntries( Collection entries, ZipFileSet zf)获取要签名的类,因此修改前面定义的 SignSection

    public static class SignSection extends PatternMatchedClassesSection {
      @Override
      public void addEntries( final Collection entries, final String matchedClass ) {
        
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    继承后必须实现抽象方法 addEntries,这个方法在PatternMatchedClassesSection 中被调用了,追踪public void addEntries( Collection entries, ZipFileSet zf)方法调用的位置发现在 ExposeSection (对应 标签)中存在下面的代码:
    在这里插入图片描述
    这里按顺序把 ,,等元素中的配置添加到了外部的 entries 中,这个变量存储了 中混淆的全部配置。追踪 ExposeSection#createEntries 方法发现在 ObfuscatorTask#execute 中执行的,那么我们第一个获取签名类的地方就在这里。
    在这里插入图片描述
    SignSection 中添加和 expose.createEntries 一样的方法,并且copy里面的部分代码:

    public static class SignSection extends PatternMatchedClassesSection {
      protected Project project;
      private Collection entries = new ArrayList( 20 );
    
      public SignSection( final Project project ) {
        this.project = project;
      }
    
      @Override
      public void addEntries( final Collection entries, final String matchedClass ) {
        entries.add(matchedClass);
      }
    
      public Collection createEntries( Collection srcJars ) throws IOException {
        for ( Iterator it = srcJars.iterator(); it.hasNext(); ) {
          File file = (File) it.next();
          ZipFileSet zipFile = new ZipFileSet();
          zipFile.setProject(project);
          zipFile.setSrc(file);
          addEntries(entries, zipFile);
        }
        return entries;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    由于参考代码中 zipFile.setProject(project); 这里需要 Project 类,因此修改了构造方法,增加了 Project 参数,同时修改 createSign 方法:

    public SignSection createSign() {
      if(signSection == null) {
        signSection = new SignSection(getProject());
      } else {
        throw new BuildException("Only one sign element allowed!");
      }
      return signSection;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    继续参考前面的 expose 调用的位置,增加 signSection 代码:

    if (expose != null){
      rules = expose.createEntries(inFilesList);
    } else {
      rules = new ArrayList(20);
    }
    //增加下面代码
    if(signSection != null) {
      signSection.createEntries(inFilesList);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    先不要继续其他的逻辑,完成一个功能后先测试验证是否正确,没问题后在继续。

    2. 发个小版本测试(1)的功能

    yGuard 中gradle.properties当前指定的版本如下:

    VERSION_MAJOR=4.0
    VERSION_MINOR=1-SNAPSHOT
    
    • 1
    • 2

    既然是快照版就不改版本号了,在 build.gradlepublishing 下面的 repositories 中添加 mavenLocal() 发布到本地:

    publishing {
      repositories {
        mavenLocal()
        maven {
          url 'https://oss.sonatype.org/service/local/staging/deploy/maven2'
          credentials {
            username SONATYPE_NEXUS_USERNAME
            password SONATYPE_NEXUS_PASSWORD
          }
        }
      }
      //省略其他
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    还要注意本文开头修改的 settings.gradle 配置。

    刷新 Gradle 配置,然后点击 publishToMavenLocal
    在这里插入图片描述

    如果jdk8编译出错可以刷新gradle配置试试

    将 yguard-module-parent 中 module-yguard 的依赖改为快照版:

    <dependency>
        <groupId>com.yworksgroupId>
        <artifactId>yguardartifactId>
        <version>4.0.1-SNAPSHOTversion>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    signSection.createEntries(inFilesList); 行添加断点,在 标签下面添加

    <rename logfile="${project.build.directory}/yguard.log.xml"
            replaceClassNameStrings="true">
      <sign>
          <patternset>
              <include name="org.example.a."/>
              <include name="org.example.b."/>
              <include name="org.example.c.util.FileUtil"/>
          patternset>
      sign>
      
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    右键debug运行package打包,代码运行过断点这里后可以看到包含的类:
    在这里插入图片描述
    这里说明我们已经获取到混淆前要进行签名的类了,我们继续进行后续的操作。

    3. 记录混淆后的类信息

    后面两处关键的地方都在 GuardDB中,这个对象在 ObfuscatorTask 中初始化和调用的,从这里先把 SignSection 设置到 GuardDB 中:
    在这里插入图片描述
    在使用前set进去:
    在这里插入图片描述
    接下来就可以在 GuardDB 中使用了:
    在这里插入图片描述
    这里增加了406~410行的代码,需要给 signSection 添加两个方法:

    public static class SignSection extends PatternMatchedClassesSection {
      protected Project project;
      private Collection entries = new ArrayList<>(20);
      private List<byte[]> bytes = new ArrayList<>(20);
      
      //省略其他无关方法
      
      public boolean contains(String name) {
        return entries.contains(name);
      }
    
      public void addBytes(byte[] bytes) {
        this.bytes.add(bytes);
      }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这样就拿到了所有混淆后的数据,接下来就是计算签名并写入到jar包中。

    4. 签名并写入jar包

    混淆技术研究笔记(四)反篡改介绍 中我们使用 hutool 的工具类实现了私钥加密的方法,我们这里直接用,首先添加 hutool 的依赖,在 build.gradle 中添加依赖:

    dependencies {
        annotation project(':annotation')
        implementation project(':annotation')
        implementation 'org.ow2.asm:asm:9.2'
        implementation 'org.apache.ant:ant:1.10.12'
        implementation 'cn.hutool:hutool-all:5.7.22'
        testImplementation 'junit:junit:4.13-beta-3'
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    新增加的 implementation 'cn.hutool:hutool-all:5.7.22'。刷新 gradle,然后在 SignSection 中实现签名和写入 jar 的方法,先增加一个属性用于设置写入的文件名:

    private String name = "sign";
    
    public void setName( final String name ) {
      this.name = name;
    }
    
    public String getName() {
      return name;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    然后是根据一定算法计算签名:

    public byte[] sign() {
      if(entries.size() != bytes.size()) {
        throw new BuildException("Number of entries and bytes must be equal!");
      }
      List<String> signs = new ArrayList<>(bytes.size());
      MD5 md5 = new MD5();
      for (final byte[] data : bytes) {
        //计算md5
        signs.add(md5.digestHex(data));
      }
      //排序,避免读取顺序影响
      Collections.sort(signs);
      //拼一串
      StringBuffer sb = new StringBuffer();
      for (final String sign : signs) {
        sb.append(sign);
      }
      //私钥签名
      return encryptHex(sb.toString()).getBytes(StandardCharsets.UTF_8);
    }
    
    private String encryptHex(String str) {
      String home = System.getProperty("user.home");
      String privateKeyPath = home + File.separator + ".yguard" + File.separator + "license-keys.pri";
      RSA rsa = new RSA(FileUtil.readBytes(privateKeyPath), null);
      byte[] bytes = rsa.encrypt(str, StandardCharsets.UTF_8, KeyType.PrivateKey);
      return HexUtil.encodeHexStr(bytes);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    注意这里会使用私钥,私钥在 混淆技术研究笔记(四)反篡改介绍 中有示例代码可以生成。

    修改 GuardDB 调用上面的方法:
    在这里插入图片描述
    接下来是测试功能。

    5. 测试完整功能

    打包到本地maven仓库,发现前面中文注释由于编码问题有乱码,移除后重新发布。

    在 module-yguard 中debug运行,在写入jar的地方断点看看效果。
    在这里插入图片描述
    发现 inName 包含 .class 后缀导致无法匹配,因此这里去掉最后的 .class 后缀再进行匹配:

    if(signSection != null && signSection.contains(inName.substring(0, inName.length() - 6))) {
     signSection.addBytes((byte[]) objects[1]);
    }
    
    • 1
    • 2
    • 3

    改完发布再次测试到签名时,又发现了新问题:
    在这里插入图片描述
    此时要签名的文件有5个,但是bytes只有2个,说明我们需要的文件还没获取全。

    这就涉及到一个顺序问题了,我们目前的实现会在所有 配置的 jar 包上执行一遍,我们实现的又是多模块混淆,因此想要签名获取所有的文件,就只能在最后一个文件中写入签名信息,只有最后一个的时候是全的,因此我们需要 能指定要给哪个 jar 包添加签名,还要修改对应 jar 包为最后一个 (也可以默认写入最后一个 配置的 jar 包,但是这种隐藏的方式还要特别强调才不容易出错,不如更明确的指定出来)。
    在这里插入图片描述
    观察这里的代码可以看到 out[i] 代表了当前处理的那个 jar 包,而且是生成的 jar,这里是 File 类型,因此我们可以在 中指定 jar 文件,改动如下:

    public static class SignSection extends PatternMatchedClassesSection {
        //省略其他
        private File jar;
    
        public File getJar() {
          return jar;
        }
    
        public void setJar( final File jar ) {
          this.jar = jar;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    修改 GuardDB 中写入的地方,要匹配文件名:

    if(signSection != null && signSection.getJar().equals(out[i])) {
      outJar.addFile(signSection.getName(), signSection.sign());
    }
    
    • 1
    • 2
    • 3

    发布后,修改 配置如下:

    <sign name="sign.txt" jar="..\module-a\target\module-a-${project.version}.jar">
        <patternset>
            <include name="org.example.a."/>
            <include name="org.example.b."/>
            <include name="org.example.c.util.FileUtil"/>
        patternset>
    sign>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里指定 jar 为最后一个 out 值:

    <inoutpair in="..\module-b\target\module-b-${project.version}.jar"
               out="..\module-b\target\module-b-${project.version}.jar"/>
    <inoutpair in="..\module-c\target\module-c-${project.version}.jar"
               out="..\module-c\target\module-c-${project.version}.jar"/>
    <inoutpair in="..\module-a\target\module-a-${project.version}.jar"
               out="..\module-a\target\module-a-${project.version}.jar"/>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果一切正常,就会写入到 module-a 中,再次 DEBUG:
    在这里插入图片描述
    这次一切正常,执行完成后,打开 module-a 的 jar 包查看:
    在这里插入图片描述
    签名成功的写入到了 jar 包中,签名的功能到这里就实现完成了。

    有了签名后,想要起作用,还需要在运行时对 jar 包内容的进行反篡改校验,这部分内容在 混淆技术研究笔记(四)反篡改介绍 有介绍,需要依赖具体的运行环境才能测试,这里就不具体实现了。

    整个系列的主要内容和过程已经呈现出来了,后面还会有一篇最后的总结,会从前面几篇提取一些内容摘抄出来,只要看过前面这八篇,第九篇总结也没必要看。因为第九篇只能在微信公众号查看(搜索 MyBatis),并且最后一篇是付费文章。如果你觉得这个系列对你有帮助,可以多多转发,也可以付费支持。

  • 相关阅读:
    CocosCreator-常见问题和解决-持续更新
    更新至2022年ESG评级评分数据合集(含华证、盟浪、wind、彭博、润灵环球、商道融绿、和讯网、富时罗素数据)
    2023 年 的 DBA 有哪些变化?
    docker 基本操作
    PLGA10K-PEG2K-GA/疏水性嵌段聚丙交酯PLGA10K-乙交酯PEG2K-聚乙二醇GA
    批量循环查询
    python入门-安装及环境配置(简单好用)
    Codeforces Round #831 (Div. 1 + Div. 2)——A、B、C、D、E
    Netty入门——组件(Channel)二
    Mysql ProxySQL的学习
  • 原文地址:https://blog.csdn.net/isea533/article/details/134028093