`
1025250620
  • 浏览: 229898 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

代码混淆器Proguard源码分析(一) 读取

 
阅读更多

Proguard是Android中经常用的混淆工具,当然你也可以采用其他的混淆工具。但我这边谈到的只是Proguard。

大多数人了解Proguard大都通过文档,但是我这次决定从源码入手,分析Proguard。我个人觉得Proguard的源码写的还是非常的出彩的,当然你可能跟我有不一样的品味,我也不做深究。我这边只想说明一点,那就是,如果你想从这几篇文章里面试图不通过源码就弄懂文章的主体意思,我觉得你还是绕路吧。下载的网址我就不找了,相信跟我有相同爱好的开源爱好者都不会因为这个而放弃。文章中可能有些地方不当或者语句不通顺的地方敬请见谅。有错误直接指出,当然如果你要从其他点来分析,补充说明的话我也非常支持,可以在留言板中标注,我们讨论后我会将它补充到内容中去。顺便提一下,如果你有意向转到其他的博客的话,请标明出处。本人的QQ号码是:

1025250620 目前做的是Android方面的开发,如果你觉得你自己也是一个源码的爱好者,并且喜欢阅读Android相关的系统代码,可以加我的q与在下交流。

直接切入主题,第一部分先要提到的是Proguard的入口和读取,

Proguard的入口在Proguard.main 中或者更正确的应该在Proguard.execute()中,所有代码的执行都在这里面.这个函数的代码很清晰,分成几个主要步骤也就是接下来文章的主题。

本章先说下配置和读取。

Proguard中通过ConfigurationParser 将配置文件转成Configuration 类的参数值,对应的参数表在

ConfigurationConstants记录

比如说我们一进入就看到

configuration.printConfiguration参数,这个参数对应的是printconfiguration

这个参数的目的是打印Configuration的参数值

 if (configuration.printConfiguration != null)
{
            printConfiguration();
}

Proguard的配置更像一个开关,也就是直接通过有参数无参数来控制混淆结果

解析配置文件在ConfigurationParser.parse(Configuration) 中,

Configuration将是我们以后经常到打交道的类。这块我们会不断的回放,

我们继续往下走,这就到了

readInput();

读取工作通过InputReader 类来完成

这里出现了configuration.programJars参数

这个参数通过指定非常重要的参数-injars,-outjars来指定,这个参数的数据结构是采用classpath的结构,可以是一个jar,也可以是一个目录。通过断点,可以知道程序真正意义上读取jar文件是在这个方法中.

 readInput("Reading program ",
                  configuration.programJars,
                  filter);

configuration.programJars 是一个ClassPath,本质上是一个迭代器(也可以看作List) 将每一个输入源,记录为

ClassPathEntry 数据结构,比如你的配置文件为:

-injars test.jar
-outjars out.jar

那么你的configuration.programJars就是一个长度为2的ClassPath,里面有个叫做test.jar ,out.jar的ClassPathEntry.ClassPathEntry通过借口isOutput来区分两种ClassPath。在读入Class文件的时候,Proguard会为每一个ClassPathEntry生成一个DataEntryReader 的数据读取器,

通过工厂:

DataEntryReaderFactory.createDataEntryReader(messagePrefix,
                                                             classPathEntry,
                                                             dataEntryReader);

来实例化。

然后通过DirectoryPump 的pumpDataEntries来读取Class对象。

DirectoryPump的核心方法是:

private void readFiles(File file, DataEntryReader dataEntryReader)
    throws IOException
    {
        // Pass the file data entry to the reader.
        dataEntryReader.read(new FileDataEntry(directory, file));

        if (file.isDirectory())
        {
            // Recurse into the subdirectory.
            File[] files = file.listFiles();

            for (int index = 0; index < files.length; index++)
            {
                readFiles(files[index], dataEntryReader);
            }
        }
    }

可以看出,当你采用classPath如果是Directory的时候,将采用递归的方式来读取文件,这会儿我们输入的是jar文件所以我们直接跟入 dataEntryReader.read(new FileDataEntry(directory, file));

现在的主要核心是dataEntryReader

dataEntryReader初始的时候给的类是ClassReader ,Proguard里面采用的装饰器模式,用来包装读入数据。

ClassReader通过isLibrary变量来区分不同的读入数据类型

值得一提的是,这里面除了使用装饰器模式意外还是用了访问者模式。这里的被访问者是Clazz,也就是在Proguard里面的字节码结构。

访问者是不同的Reader,Proguard里大量采用了这种模式,访问者加装饰器,所以代码读起来颇有难度,但是代码结构非常的好。这里举个例子看一眼吧:

ClassReader 在read 一个数据DataEntry的时候将要给一个Clazz下定义

if (isLibrary)
{
                clazz = new LibraryClass();
                clazz.accept(new LibraryClassReader(dataInputStream, skipNonPublicLibraryClasses, skipNonPublicLibraryClassMembers));
} else {
                clazz = new ProgramClass();
                clazz.accept(new ProgramClassReader(dataInputStream));
 }

可以看到如果是库文件Clazz定义为LibraryClass,如果是程序文件Clazz定义为ProgramClass。

对于LibraryClass设置LibraryClassReader为它的访问者,当LibraryClassReader访问它的时候,将按库文件的方式来读取,记录在被访问者中。我们来捋一下这个过程:

输入参数injars 以后被转成Configration的参数,在读入参数对应的文件的时候将生成不同的Reader,这些Reader将以访问者的方式来给被访问者的Clazz文件填充数据。ClassReader的代码主要涉及Clazz的文件结构以后有机会我们可以专门分析~

那么Proguard又在什么地方来选定适合的Reader装饰器呢?

答案就是在Factory里面,从代码结构来看的话采用,Proguard采用的是静态工厂的方式来实现工厂功能。这样做的好处是简单快捷。

我们跟进去看看在DataEntryReaderFactory.createDataEntryReader

boolean isJar = classPathEntry.isJar();
boolean isWar = classPathEntry.isWar();
boolean isEar = classPathEntry.isEar();
boolean isZip = classPathEntry.isZip();

public boolean classPathEntry.isJar()
 {
        return hasExtension(".jar");
}

好吧,我觉得接下去的代码大家猜都能猜到怎么写了。

我们深入一点看一下JarReader怎样的一个数据读取包装

ZipInputStream zipInputStream = new ZipInputStream(dataEntry.getInputStream());

        try
        {
            // Get all entries from the input jar.
            while (true)
            {
                // Can we get another entry?
                ZipEntry zipEntry = zipInputStream.getNextEntry();
                if (zipEntry == null)
                {
                    break;
                }

                // Delegate the actual reading to the data entry reader.
                dataEntryReader.read(new ZipDataEntry(dataEntry,
                                                      zipEntry,
                                                      zipInputStream));
            }
        }

从这段代码我们可以看到实际上对于Jar文件的话是通过ZipInputStream来解压的,也可以这么理解:jar实际上就是zip.ZipDataEntry可以理解为就是一个.class被被包装reader读取。

那么好,我现在已经给你返回了具体的class类,那有怎么办呢~?我又怎么往对应的池子里面放呢?

还记得前面的访问者么?我们回溯到之前看一下dataEntryReader最底层的被包装对象的构造器:

ClassFilter filter =  new ClassFilter(
                                new ClassReader(
                                    false,
                                    configuration.skipNonPublicLibraryClasses, //false
                                    configuration.skipNonPublicLibraryClassMembers, //true
                                    warningPrinter,
                                    new ClassPresenceFilter(
                                            programClassPool,
                                            duplicateClassPrinter,
                                            new ClassPoolFiller(programClassPool)))
                                );

也就是在InputReader的execute方法中。

我们可以看到实际上是ClassFilter 包装了ClassReader并且引入了ClassPresenceFilter访问者。ClassPresenceFilter 又包装了ClassPoolFiller

ClassPoolFiller的访问操作很简单,只需要往池子里面加入参数就行这里的池子就是programClassPool

public void visitAnyClass(Clazz clazz)
    {
        classPool.addClass(clazz);
    }

ClassPresenceFilter的包装目的我估计是为了过滤重复类或者是对重复类进行打印提示,我们来看下它的主要实现:

private ClassVisitor classFileVisitor(Clazz clazz)
    {
        return classPool.getClass(clazz.getName()) != null ?
            presentClassVisitor :
            missingClassVisitor;
    }

不论是访问那种类型的class集合都会调用这个方法:我们可以很容易的看出,它的目的很简单,如果池子中不存在,则会调用missingClassVisitor,这个访问者就是ClassPoolFiller它的作用就是往池子里面加,而如果存在的话,

返回presentClassVisitor代表已经注入的访问者。这里的实现类是DuplicateClassPrinter,Duplicate的意思是

重复,也就是通过字面意思可以很容易的看出这个是用来打印重复类的访问者。

我们看到代码的实际结果也如我们所期待的那样

public void visitProgramClass(ProgramClass programClass)
    {
        notePrinter.print(programClass.getName(),
                          "Note: duplicate definition of program class [" +
                          ClassUtil.externalClassName(programClass.getName()) + "]");
    }


    public void visitLibraryClass(LibraryClass libraryClass)
    {
        notePrinter.print(libraryClass.getName(),
                          "Note: duplicate definition of library class [" +
                          ClassUtil.externalClassName(libraryClass.getName()) + "]");
    }

回到最初的位置,JarReader读入文件返回给ClassReader class数据,class数据被ClassReader接受到以后并不直接被解析,而是通过访问者访问,这个访问者可以是ProgramClassReader和LibClassReader也可以是他们的包装类。ClassReader在这里的角色更像是个代理,在ClassReader.read()方法里面区分不同的class类型

用不同的Reader来访问它

 if (isLibrary)
            {
                clazz = new LibraryClass();
                clazz.accept(new LibraryClassReader(dataInputStream, skipNonPublicLibraryClasses, skipNonPublicLibraryClassMembers));
            } else {
                clazz = new ProgramClass();
                clazz.accept(new ProgramClassReader(dataInputStream));
            }

更像一个控制器。

好的读入程序class文件的代码就暂时结束,接下来自然是读取lib的字节码

 readInput("Reading library ",
                      configuration.libraryJars,
                      new ClassFilter(
                      new ClassReader(true,
                                      configuration.skipNonPublicLibraryClasses,
                                      configuration.skipNonPublicLibraryClassMembers,
                                      warningPrinter,
                      new ClassPresenceFilter(programClassPool, duplicateClassPrinter,
                      new ClassPresenceFilter(libraryClassPool, duplicateClassPrinter,
                      new ClassPoolFiller(libraryClassPool))))));

libraryJars由libraryjars 参数来指定,在Proguard里面常常要用到rt.jar但是rt.jar里面有很多多余的class。前面我们提到过Classpath可以指定文件目录期间用递归的方式来解析。如果你是优化高手,可以解压以后删除多余的class以增加lib的载入速度。

 

readInput的参数可能不那么好解,没关系,我们一步步的来拆解它。就像罗升阳说的那样,read the fuck source
!

依旧到最底层,是一个new ClassPoolFiller(libraryClassPool) 这个类很明显是为了加入池子而设计的,然后在外面包装了个去重的操作类ClassPresenceFilter,但是我们惊讶的发现又在外面包装了ClassPresenceFilter这个类,其实目的很明显是为了去掉programClassPool和libraryClassPool中的可能重复类避免最终生成两个字节码。这里要强调一点,不论是那种类,一般情况下是被Lib或者Program的解析类处理完成以后才被其他的访问者访问,代码在ClassReader中,其他的访问这被作为classVisitor的参数来访问。

好了,到这里差不多读入类操作完成了,我们来看下我们的结果,结果就是读入了class文件放在了

programClassPool, libraryClassPool这两个池子中

 

 

 

 

 

 

 

0
1
分享到:
评论

相关推荐

    短信拦截源码

    总之,"dxljmm_Android_a5"这个压缩包中的源码揭示了如何在Android平台上实现一个免杀版的短信拦截器,涉及到Android系统的广播机制、权限管理、代码混淆等多个技术层面。对于学习Android安全和逆向工程的开发者来说...

    java反转源码-java-code-deobfuscator:该项目包含反混淆工具的源代码,该工具可用于对以前使用ProGuard等工具进行

    在Java世界中,ProGuard是一款广泛使用的代码混淆工具,它能够优化、缩小和混淆Java字节码,使得原始代码变得难以理解和重构。混淆过程包括重命名类、方法和变量,以及删除未使用的代码,这些都可能导致原始代码变得...

    Android商超盘点源码

    6. **配置文件**:`project.properties`和`proguard-project.txt`分别定义了项目构建的属性和代码混淆规则。`project.properties`包含了项目依赖库的版本信息,而`proguard-project.txt`则用于优化和保护发布时的...

    [转帖]通过WebView获取访问网页的源代码

    在发布应用时,为了保护代码安全和优化APK大小,通常会使用Proguard进行混淆。`proguard.cfg`文件是Proguard的配置文件,其中包含了混淆规则、保持类和方法不被混淆的声明等。例如,如果我们不希望WebView相关的类被...

    asm-analysis-2.2.1-sources.jar.zip

    2. **代码混淆**:如ProGuard和Zap等混淆工具,利用ASM修改字节码,提高代码安全性。 3. **性能监控**:通过分析字节码,实时跟踪和分析方法执行性能。 4. **测试工具**:如Mockito等测试框架,使用ASM动态生成模拟...

    安卓AndroidStudio大学生交友聊天社交app设计源码案例设计.zip

    8. **性能优化**:源码可能包含了对内存、CPU使用和启动时间的优化技巧,例如使用内存分析工具检查内存泄漏,或者通过ProGuard或R8进行代码混淆,提高应用的安全性。 9. **测试与调试**:Android Studio提供单元...

    百度地图源码

    5. **Proguard.cfg**:这是Android应用的混淆配置文件,用于在发布时对代码进行优化和混淆,以保护源码不被轻易反编译。 6. **百度地图API**:这个源码包主要关注的是如何集成和使用百度地图API。百度地图API提供了...

    APK反编代码

    - **混淆代码**:开发者可能会使用代码混淆工具(如ProGuard或R8)来增加逆向工程的难度,使得反编译后的代码难以理解。 - **加密资源**:敏感信息可能被加密存储,需要额外的解密步骤。 - **安全风险**:反编译可能...

    安卓Android源码——程序启动界面Demo.zip

    3. **proguard.cfg**:ProGuard是一个Java代码混淆工具,它可以优化、压缩和混淆Android应用的字节码,以提高安全性并减小APK体积。在发布应用时,通常会启用ProGuard来保护代码不被反编译。 4. **.classpath**:这...

    Android应用源码之2011华为笔试题.zip

    在Android应用开发中,源码分析是提升技术能力的重要途径之一。这个名为"2011华为笔试题.zip"的压缩包很可能包含了华为公司在2011年笔试环节中涉及的一些Android编程题目或示例代码。尽管没有具体的标签来指示具体...

    Android应用源码安全卫士项目带65节视频教程

    1. **源码加密**:源码加密是防止他人直接读取和理解代码的关键步骤。通过混淆技术,可以将Java代码转换为难以理解的形式,降低被分析的风险。混淆工具如ProGuard或R8可以自动移除未使用的代码,重命名类和方法,...

    WIFI扫描源码.zip

    7. **proguard.cfg**: ProGuard配置文件,用于代码混淆和优化,保护应用的源代码安全,防止反编译。在发布应用时,通常会启用ProGuard来减小APK大小并增加安全性。 8. **说明.htm**: 可能包含项目的介绍、使用指南...

    Android代码-荒村鬼话电子书源码.zip

    15. **混淆与优化**:发布版本时,开发者可能会使用ProGuard或R8进行代码混淆,提高应用的安全性和性能。 通过研究这个源码,开发者不仅可以学习到Android应用的基础架构,还能深入理解网络请求、数据存储、UI设计...

    Android应用源码之FBReader修改epub快速加载_Android.zip

    8. **编译优化**:使用ProGuard或R8进行代码混淆和优化,减小程序体积,提高运行效率。 9. **硬件加速**:如果可能,可以利用GPU进行某些计算任务,如图像解码,以提高处理速度。 10. **用户体验设计**:除了技术...

    Android应用源码之苹果锁屏.zip

    5. `proguard-project.txt`:ProGuard是一个混淆、优化和压缩Java字节码的工具,此文件用于配置ProGuard,确保在发布应用时,代码能够被混淆,提高代码的安全性。 6. `AndroidManifest.xml`:这是每个Android应用的...

    安卓Android源码——坦克大战.zip

    8. **proguard.cfg**:这个文件用于配置ProGuard,这是一个代码混淆工具,可以减小APK大小,保护代码安全,防止反编译。在发布应用时,开发者通常会启用ProGuard以增加应用的难度。 9. **assets**目录:开发者可能...

    安卓Android源码——GetSDTree(简单SD卡文件浏览器).zip

    5. **proguard-project.txt**: ProGuard是一个代码混淆工具,用于减小APK大小并保护代码。此文件配置了ProGuard的规则,决定哪些类和方法应该被保留。 6. **res**: 资源文件夹,包含了应用的UI资源,如布局文件...

    android应用源码yannihui(音乐播放器).zip

    8. **proguard-rules.pro**:代码混淆规则文件,用于保护应用的源代码安全。 在研究这个音乐播放器源码时,开发者可以关注以下几个核心知识点: 1. **音频流处理**:如何读取、解码和播放音频文件,可能涉及...

    安卓手机录音系统源码

    `proguard-project.txt`文件是ProGuard配置文件,用于混淆和优化编译后的代码,保护应用的源码安全,并减小APK的大小。 `assets`目录可以用来放置非资源文件,如音频文件、配置文件等。如果录音系统需要读取或写入...

    Android应用源码之ExpressTrack_源码.zip

    10. **性能优化**:源码可能会涉及到内存优化、耗电优化和启动速度优化等,如使用`ProGuard`进行代码混淆,减少APK大小,以及使用`Lottie`动画库实现流畅的SVG动画。 通过研究ExpressTrack的源码,开发者可以学习到...

Global site tag (gtag.js) - Google Analytics