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

Proguard 源码分析 (七) 混淆

 
阅读更多

本章我们讲Proguard非常重要的一个步骤:混淆Obfuscator

混淆的目的很明显,是为了混淆语义。

我们能轻车熟路的找到混淆的源头:

Obfuscator 的execute方法

我们来看一下就行混淆器给我们设置了怎样的访问者:

ClassVisitor memberInfoLinker =
            configuration.useUniqueClassMemberNames ?
                (ClassVisitor)new AllMemberVisitor(new MethodLinker()) :
                (ClassVisitor)new BottomClassFilter(new MethodLinker());

我们直接跟到最终的访问者,也就是MethodLinker

 

public void visitAnyClass(Clazz clazz)
    {
        // Collect all non-private members in this class hierarchy.
        clazz.hierarchyAccept(true, true, true, false,
            new AllMethodVisitor(
            new MemberAccessFilter(0, ClassConstants.INTERNAL_ACC_PRIVATE | ClassConstants.INTERNAL_ACC_STATIC,
            this)));

        // Clean up for the next class hierarchy.
        memberMap.clear();
    }

它的意思是说将自己作为AllMethodVisitor 所有方法中,MemberAccessFilter可接受的访问权限的访问者,

既然它的目的很明显是为了处理方法,我们直接跟到相关的调用:

public void visitAnyMember(Clazz clazz, Member member)
    {
        String name       = member.getName(clazz);
        String descriptor = member.getDescriptor(clazz);
        if (name.equals(ClassConstants.INTERNAL_METHOD_NAME_CLINIT) ||
            name.equals(ClassConstants.INTERNAL_METHOD_NAME_INIT))
        {
            return;
        }
        String key = name + ' ' + descriptor;
        Member otherMember = (Member)memberMap.get(key);

        if (otherMember == null)
        {
            Member thisLastMember = lastMember(member);
            memberMap.put(key, thisLastMember);
        } else {
            link(member, otherMember);
        }
    }

我们看到刚开始它会先取得方法的名称和签名,对于cinit 和init不做处理,注释便是最好的源码解析:

// Special cases: <clinit> and <init> are always kept unchanged.
        // We can ignore them here.

之后将通过签名来找到享元池中的方法Member otherMember = (Member)memberMap.get(key);

if (otherMember == null)
        {
            // Get the last method in the chain.
            Member thisLastMember = lastMember(member);

            // Store the new class method in the map.
            memberMap.put(key, thisLastMember);
        }
        else
        {
            // Link both members.
            link(member, otherMember);
        }

如果没有这个方法,便加入到享元池中,但是之前调用了lastMember

我们来看一下lastMember 方法:

 public static Member lastMember(Member member)
    {
        Member lastMember = member;
        while (lastMember.getVisitorInfo() != null &&
               lastMember.getVisitorInfo() instanceof Member)
        {
            lastMember = (Member)lastMember.getVisitorInfo();
        }

        return lastMember;
    }

也就是说如果它的visitor信息里面如果存在,则直接返回上一个的方法,这种写法很类似于职责链模式,但我只是猜测,只是有职责链的感觉,我们继续往下看,如果方法已经存在了呢?~

我们回到clazz.hierarchyAccept 的参数:

 public void hierarchyAccept(boolean      visitThisClass, //true
                                boolean      visitSuperClass, //true
                                boolean      visitInterfaces, //true
                                boolean      visitSubclasses,//false
                                ClassVisitor classVisitor);

这里面很可能存在一个方法visitThisClass,visitSuperClass,visitInterfaces都存在的情况

所以link(member, otherMember); 的目的就是为了合并到一个共同的职责链源头

 

好的我们继续混淆的过程:

NameMarker nameMarker = new NameMarker();
        ClassPoolVisitor classPoolvisitor =
            ClassSpecificationVisitorFactory.createClassPoolVisitor(configuration.keep,
                                                                    nameMarker,
                                                                    nameMarker,
                                                                    false,
                                                                    false,
                                                                    true);

前文好几次提到了ClassSpecificationVisitorFactory.createClassPoolVisitor 这个方法,我们不做深究我们直接看NameMarker

 

public void visitProgramClass(ProgramClass programClass)
    {
        keepClassName(programClass);

        // Make sure any outer class names are kept as well.
        programClass.attributesAccept(this);
    }

public void keepClassName(Clazz clazz)
    {
        ClassObfuscator.setNewClassName(clazz,
                                        clazz.getName());
    }

 static void setNewClassName(Clazz clazz, String name)
    {
        clazz.setVisitorInfo(name);
    }

可见:keepClassName 的目的就是为了往VisitorInfo 设置name 标签

public void visitAnyAttribute(Clazz clazz, Attribute attribute) {}

说明它对属性并没有任何的访问操作:

接着由于库文件没有必要混淆,所以要将库文件保持签名

 libraryClassPool.classesAccept(nameMarker);
        libraryClassPool.classesAccept(new AllMemberVisitor(nameMarker));

 

接着:

 // Mark attributes that have to be kept.
        AttributeVisitor attributeUsageMarker =
            new NonEmptyAttributeFilter(
            new AttributeUsageMarker());

        AttributeVisitor optionalAttributeUsageMarker =
            configuration.keepAttributes == null ? null :
                new AttributeNameFilter(new ListParser(new NameParser()).parse(configuration.keepAttributes),
                                        attributeUsageMarker);

        programClassPool.classesAccept(
            new AllAttributeVisitor(true,
            new RequiredAttributeFilter(attributeUsageMarker,
                                        optionalAttributeUsageMarker)));

这是对class的属性做标记,接着我们省略掉无关紧要的代码我们直接看混淆的主要访问者

 programClassPool.classesAccept(
            new ClassObfuscator(programClassPool,
                                classNameFactory,
                                packageNameFactory,
                                configuration.useMixedCaseClassNames,
                                configuration.keepPackageNames,
                                configuration.flattenPackageHierarchy,
                                configuration.repackageClasses,
                                configuration.allowAccessModification));

我们可以跟到对于混淆的名是通过:

String name    = programClass.getName();
        String newName = ClassObfuscator.newClassName(programClass);

来生成的而ClassObfuscator.newClassName

实际上是放回访问者的访问标志,而这个标记值是通过ClassObfuscator的

 public void visitProgramClass(ProgramClass programClass)
    {
        // Does this class still need a new name?
        newClassName = newClassName(programClass);
        if (newClassName == null)
        {
            // Make sure the outer class has a name, if it exists. The name will
            // be stored as the new class name, as a side effect, so we'll be
            // able to use it as a prefix.
            programClass.attributesAccept(this);

            // Figure out a package prefix. The package prefix may actually be
            // the an outer class prefix, if any, or it may be the fixed base
            // package, if classes are to be repackaged.
            String newPackagePrefix = newClassName != null ?
                newClassName + ClassConstants.INTERNAL_INNER_CLASS_SEPARATOR :
                newPackagePrefix(ClassUtil.internalPackagePrefix(programClass.getName()));

            // Come up with a new class name, numeric or ordinary.
            newClassName = newClassName != null && numericClassName ?
                generateUniqueNumericClassName(newPackagePrefix) :
                generateUniqueClassName(newPackagePrefix);

            setNewClassName(programClass, newClassName);
        }
    }

来设置的。而对于采用keep标记的那些值,在调用 newClassName(programClass);会放回keep访问者标记的值就不走if中的语句块,这样就达到了只针对非keep中的参数混淆的效果。我们来看一下它是如何给定名字的吧:

private String generateUniqueClassName(String newPackagePrefix)

 

 private String generateUniqueClassName(String      newPackagePrefix,
                                           NameFactory classNameFactory)
    {
        // Come up with class names until we get an original one.
        String newClassName;
        String newMixedCaseClassName;
        do
        {
            // Let the factory produce a class name.
            newClassName = newPackagePrefix +
                           classNameFactory.nextName();
           
            newMixedCaseClassName = mixedCaseClassName(newClassName);
        }
        while (classNamesToAvoid.contains(newMixedCaseClassName));

        // Explicitly make sure the name isn't used again if we have a
        // user-specified dictionary and we're not allowed to have mixed case
        // class names -- just to protect against problematic dictionaries.
        if (this.classNameFactory != null &&
            !useMixedCaseClassNames)
        {
            classNamesToAvoid.add(newMixedCaseClassName);
        }

        return newClassName;
    }
    {
        // Find the right name factory for this package.
        NameFactory classNameFactory =
            (NameFactory)packagePrefixClassNameFactoryMap.get(newPackagePrefix);
        if (classNameFactory == null)
        {
            // We haven't seen classes in this package before.
            // Create a new name factory for them.
            classNameFactory = new SimpleNameFactory(useMixedCaseClassNames);
            if (this.classNameFactory != null)
            {
                classNameFactory =
                    new DictionaryNameFactory(this.classNameFactory,
                                              classNameFactory);
            }

            packagePrefixClassNameFactoryMap.put(newPackagePrefix,
                                                 classNameFactory);
        }

        return generateUniqueClassName(newPackagePrefix, classNameFactory);
    }

 

 可见,是通过调用工厂来生成下一个匹配的名字,而且每一个包对应一个工厂,Proguard中有很多的命名工厂类的实现,我们直接用默认的命名工厂类:SimpleNameFactory

 private String name(int index)
    {
        // Which cache do we need?
        List cachedNames = generateMixedCaseNames ?
            cachedMixedCaseNames :
            cachedLowerCaseNames;

        // Do we have the name in the cache?
        if (index < cachedNames.size())
        {
            return (String)cachedNames.get(index);
        }

private String newName(int index)
    {
        // If we're allowed to generate mixed-case names, we can use twice as
        // many characters.
        int totalCharacterCount = generateMixedCaseNames ?
            2 * CHARACTER_COUNT :
            CHARACTER_COUNT;

int baseIndex = index / totalCharacterCount;
        int offset    = index % totalCharacterCount;

        char newChar = charAt(offset);

        String newName = baseIndex == 0 ?
            new String(new char[] { newChar }) :
            (name(baseIndex-1) + newChar);

        return newName;
    }

        // Create a new name and cache it.
        String name = newName(index);
        cachedNames.add(index, name);

        return name;
    }
我们看到它的命名规则非常简单~~就是采用26个字母顺序使用,然后依次扩展.

接下来我们来看下如何将这些规则应用到字节码库中:      

我们看到访问者:

public void visitProgramClass(ProgramClass programClass)
    {
        // Rename this class.
        programClass.thisClassConstantAccept(this);

        // Rename the class members.
        programClass.fieldsAccept(this);
        programClass.methodsAccept(this);
    }

 

ClassRenamer 它对常量的访问操作是:

public void visitClassConstant(Clazz clazz, ClassConstant classConstant)
    {
        // Update the Class entry if required.
        String newName = ClassObfuscator.newClassName(clazz);
        if (newName != null)
        {
            // Refer to a new Utf8 entry.
            classConstant.u2nameIndex =
                new ConstantPoolEditor((ProgramClass)clazz).addUtf8Constant(newName);
        }
    }

很容易看出,实际上它是将常量池对应的索引数据替换成它的新数据,

我们看下它对member的操作吧:

 public void visitProgramMember(ProgramClass  programClass,
                                     ProgramMember programMember)
    {
        // Has the class member name changed?
        String name    = programMember.getName(programClass);
        String newName = MemberObfuscator.newMemberName(programMember);
        if (newName != null &&
            !newName.equals(name))
        {
            programMember.u2nameIndex =
                new ConstantPoolEditor(programClass).addUtf8Constant(newName);
        }
    }

还是一样的道理,先对member定义个一个新的名字,然后替换掉常量中的数据。

 

 

 

 

 

 

 

 

分享到:
评论

相关推荐

    proguard混淆器

    `proguard-base-5.2.1-sources.jar` 是ProGuard 5.2.1版本的源码包,开发者可以通过查看源码了解其内部工作原理,学习如何配置和使用ProGuard。 `proguard-base-5.2.1.jar` 是ProGuard 5.2.1的二进制库文件,包含了...

    如何混淆Android项目代码(ProGuard)防止反编译.rar

    为了对抗这种威胁,开发者通常会采用代码混淆技术,其中ProGuard是Android官方推荐的混淆工具。本教程将深入探讨如何使用ProGuard来混淆Android项目代码,以防止反编译。 一、ProGuard介绍 ProGuard是一款免费的...

    开发J2ME程序的混淆器proguard3.4

    4. **分析日志**:ProGuard在混淆过程中会产生详细的日志,帮助开发者了解哪些类和方法被处理,以及可能出现的问题。 **ProGuard 3.4的特点** - **版本改进**:ProGuard 3.4是该工具的一个稳定版本,相比于之前的...

    利用混淆器ProGuard混淆java类,防止反编译

    对于Android项目,DexGuard是ProGuard的一个增强版,专门针对Dalvik和ART虚拟机进行了优化,提供了额外的安全特性,如资源混淆和防动态分析。 7. **注意事项** 混淆可能导致运行时错误,特别是当混淆破坏了依赖...

    【android开发】混淆打包proguard模板

    ProGuard是一款强大的Java字节码混淆、优化、预校验和分析工具,它可以为我们的Android项目提供必要的保护,防止恶意逆向工程分析。本文将深入探讨"【android开发】混淆打包proguard模板"的相关知识点。 一、...

    Proguard最新版6.0.3

    7. **日志和映射文件**:ProGuard会在编译后生成`.mapping`文件,记录混淆前后的类和方法映射关系,有助于调试和分析混淆效果。 8. **处理依赖库**:在处理依赖库时,ProGuard可以自动处理jar文件和aar模块,确保...

    对java jar包实现混淆加密

    总结,Java jar包的混淆加密是提升代码安全的重要手段,通过ProGuard等混淆工具和加密技术,可以有效降低源代码被逆向工程分析的风险。然而,安全是一个持续的过程,开发者应始终保持警惕,及时更新和优化保护策略。

    proguard使用

    混淆可能导致运行时错误,因此在测试阶段,可以开启 ProGuard 的日志记录,以便于分析问题。添加以下规则: ```properties -printmapping mapping.txt # 输出映射文件 -verbose # 输出详细日志 ``` 映射文件 `...

    java 混淆、加密js代码

    Java混淆和加密JavaScript代码是保护Web应用程序源代码安全的重要手段,尤其是在公开发布或者与第三方共享时,能够防止恶意用户分析和篡改代码。本压缩包包含的资源专注于这个主题,让我们详细了解一下相关知识点。 ...

    代码混淆—关于android被反编译的两种解决方案

    ProGuard是Android Studio内置的一个免费混淆器,它可以删除未使用的类、字段和方法,优化代码,以及混淆剩余的代码。启用ProGuard后,你需要在项目的`proguard-rules.pro`文件中配置规则,例如保留特定库或自定义...

    代码混淆器,专业混淆

    1. **选择混淆器**:根据编程语言和需求选择合适的混淆器,例如Java有ProGuard,JavaScript有UglifyJS或Terser等。 2. **配置混淆规则**:设置混淆策略,如保留特定类、方法或变量不混淆。 3. **运行混淆器**:执行...

    android 混淆 去除第三方jar

    8. **动态混淆**:对于一些敏感逻辑,可以考虑使用动态混淆,即将混淆的代码在运行时动态加载,这样即使有人获取到APK,也无法直接分析混淆后的代码。 混淆第三方jar包是一个细致的过程,需要平衡安全性和应用性能...

    混淆压缩

    在编程领域,特别是移动应用开发(如Android或iOS)和JavaScript开发中,混淆经常被用于对源代码进行变形处理,使其变得难以阅读和理解,从而防止恶意用户逆向工程分析。本文将深入探讨混淆压缩的原理、目的以及实际...

    java混淆代码的使用

    - 配置混淆规则:编写proguard-rules.pro文件,定义需要保留的类、方法和变量,以及混淆策略。 - 执行混淆:在构建脚本中添加混淆步骤,如在Gradle中使用`minifyEnabled true`开启混淆,并指定混淆配置文件。 - ...

    android源码混淆避免反编译定义.pdf

    经验丰富的开发者仍然可以通过分析混淆后的字节码和日志来理解和重构代码逻辑。此外,XML布局文件和资源文件通常不会被混淆,因此它们仍然是潜在的安全风险点。 为了增强保护,可以采取以下策略: 1. 使用更复杂的...

    Android四款小游戏源码分析_安卓源码.zip

    10. **调试与优化**:源码可能包含了一些调试工具和性能优化策略,如使用Systrace进行性能分析,或者使用ProGuard进行代码混淆和优化。 通过深入研究这四款小游戏的源码,开发者不仅可以提升自己的Android编程技能...

    module 混淆打包

    首先,混淆是Android开发中一个重要的工具,主要用于减少APK的大小、提高运行效率并保护源码不被轻易逆向分析。混淆工具主要是ProGuard,它通过重命名类、方法和变量名,使得原始代码变得难以理解和调试。在模块化...

    Android代码-混淆详解

    首先,我们来了解一下**Proguard**,它是Android开发中的一个静态代码分析工具,主要用于优化、压缩、混淆和预览APK。Proguard的主要作用有以下几点: 1. **代码优化**:通过删除未使用的类、方法和字段,以及优化...

    dhroid ioc模块对 加密混淆问题

    在Android开发中,为了保护应用的安全性和防止代码被逆向工程分析,开发者通常会采用加密和混淆技术。本文将深入探讨“dhroid IOC模块”在处理加密混淆问题上的策略和方法,结合`proguard-project.txt`文件,我们将...

    JAVA混淆编译工具

    - **主要功能**:一个简单的类文件分析器和混淆器。 - **许可证类型**:免费 - **适用场景**:适合于需要快速混淆处理的场合。 6. **BloatFree** - **开发者/公司**:Riggs Hill Software - **主要功能**:...

Global site tag (gtag.js) - Google Analytics