美文网首页程序员
如何使用自定义类加载器防止代码被反编译破解

如何使用自定义类加载器防止代码被反编译破解

作者: linyb极客之路 | 来源:发表于2020-10-30 23:45 被阅读0次

    前言

    最近和朋友聊天,他接了个外包项目,他问我有没有办法让自己的源码不被反编译破解,我就跟他说可以对代码进行混淆和加密。今天我们就来聊聊如何通过对代码进行加密实现代码防反编译,至于混淆因为可以直接利用proguard-maven-plugin进行配置实现,相对比较简单,就不在本文论述

    代码防编译整体套路

    1、编写加密工具类

    @Slf4j
    public class EncryptUtils {
    
        private static String secretKey = "test123456lyb-geek"+System.currentTimeMillis();
    
        private EncryptUtils(){}
    
    
        public static void encrypt(String classFileSrcPath,String classFileDestPath) {
            System.out.println(secretKey);
            FileInputStream fis = null;
            FileOutputStream fos = null;
    
            try {
                fis = new FileInputStream(classFileSrcPath);
                fos = new FileOutputStream(classFileDestPath);
                int len;
                String[] arrs = secretKey.split("lyb-geek");
                long key = Long.valueOf(arrs[1]);
                System.out.println("key:"+key);
                while((len = fis.read())!=-1){
                  byte data = (byte)(len + key + secretKey.length());
                  fos.write(data);
                }
            } catch (Exception e) {
               log.error("encrypt fail:"+e.getMessage(),e);
            }finally {
                if(fis != null){
                    try {
                        fis.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
    
                if(fos != null){
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    
        }
    
    }
    

    2、对需要防止被反编译代码加密

     public static void main(String[] args) {
            String classFileSrcPath = classFileSrcPath("UserServiceImpl");
            System.out.println("classFileSrcPath:--->"+classFileSrcPath);
            String classFileDestDir = ServiceGenerate.class.getClassLoader().getResource("META-INF/services/").getPath();
            System.out.println("classFileDestDir:--->"+classFileDestDir);
            String classFileDestPath = classFileDestDir + "com.github.lybgeek.user.service.impl.UserServiceImpl.lyb";
            EncryptUtils.encrypt(classFileSrcPath,classFileDestPath);
        }
    

    3、对加密代码进行反编译验证

    打开反编译工具jd-gui,把加密的代码拖入jd-gui

    image.png

    打不开,至少说明不能用jd-gui来反编译加密过的代码。

    我们打开正常的编译的class文件,其内容形如下


    image.png

    从内容我们大概还是能看出一些东西,比如包名啥的。而打开加密后的文件,其内容如下


    image.png
    内容宛若天书

    思考一:代码都被加密了,那jvm如何识别?

    答案:既然有加密,自然可以通过解密来使用。那这个解密得存放在什么地方进行解密?

    如果对类加载有一定了解的朋友,就会知道java的class文件是通过类加载器把class加载入jvm内存中,因此我们可以考虑把解密放在类加载器中。常用的类加载有启动类加载器、扩展类加载器、系统类加载。我们正常classpath路径下的类都是通过系统类加载器进行加载。而不巧这三个jdk提供的加载器没法满足我们的需求。因此我们只能自己实现我们的类加载器。其自定义加载器代码如下

    @Slf4j
    public class CustomClassLoader extends ClassLoader{
    
        /**
         * 授权码
         */
        private String secretKey;
    
        private String SECRETKEY_PREFIX = "lyb-geek";
    
    
        /**
         * class文件的根目录
         */
        private String classRootDir = "META-INF/services/";
    
        public CustomClassLoader(String secretKey) {
            this.secretKey = secretKey;
        }
    
    
        public String getClassRootDir() {
            return classRootDir;
        }
    
        public void setClassRootDir(String classRootDir) {
            this.classRootDir = classRootDir;
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
    
            Class<?> clz = findLoadedClass(name);
            //先查询有没有加载过这个类。如果已经加载,则直接返回加载好的类。如果没有,则加载新的类。
            if(clz != null){
                return clz;
            }else{
                ClassLoader parent = this.getParent();
                clz = getaClass(name, clz, parent);
    
                if(clz != null){
                    return clz;
                }else{
                    clz = getaClass(name);
                }
    
            }
    
            return clz;
    
        }
    
        private Class<?> getaClass(String name) throws ClassNotFoundException {
            Class<?> clz;
            byte[] classData = getClassData(name);
            if(classData == null){
                throw new ClassNotFoundException();
            }else{
                clz = defineClass(name, classData, 0,classData.length);
            }
            return clz;
        }
    
        private Class<?> getaClass(String name, Class<?> clz, ClassLoader parent) {
            try {
                //委派给父类加载
                clz = parent.loadClass(name);
            } catch (Exception e) {
               //log.warn("parent load class fail:"+ e.getMessage(),e);
            }
            return clz;
        }
    
        private byte[] getClassData(String classname){
            if(StringUtils.isEmpty(secretKey) || !secretKey.contains(SECRETKEY_PREFIX) || secretKey.split(SECRETKEY_PREFIX).length != 2){
                throw new RuntimeException("secretKey is illegal");
            }
            String path = CustomClassLoader.class.getClassLoader().getResource("META-INF/services/").getPath() +"/"+ classname+".lyb";
            InputStream is = null;
            ByteArrayOutputStream bas = null;
            try{
                is  = new FileInputStream(path);
                bas = new ByteArrayOutputStream();
                int len;
                //解密
                String[] arrs = secretKey.split(SECRETKEY_PREFIX);
                long key = Long.valueOf(arrs[1]);
              //  System.out.println("key:"+key);
                while((len = is.read())!=-1){
                    byte data = (byte)(len - key - secretKey.length());
                    bas.write(data);
                }
                return bas.toByteArray();
            }catch(Exception e){
                e.printStackTrace();
                return null;
            }finally{
                try {
                    if(is!=null){
                        is.close();
                    }
                } catch (IOException e) {
                    log.error("encrypt fail:"+e.getMessage(),e);
                }
                try {
                    if(bas!=null){
                        bas.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
        }
    
    
    }
    

    通过如下方式进行调用

     public static void main(String[] args) throws Exception{
            CustomClassLoader customClassLoader = new CustomClassLoader("test123456lyb-geek1603895713759");
            Class clz = customClassLoader.loadClass("com.github.lybgeek.user.service.impl.UserServiceImpl");
            if(clz != null){
                Method method = clz.getMethod("list", User.class);
                method.invoke(clz.newInstance(),new User());
            }
        }
    

    思考二:通过自定义加载器加载过的类如何整合进行spring?

    答案: 通过spring提供的扩展点进行ioc容器注入

    1、编写bean定义,并注册注册bean定义

    @Component
    public class ServiceBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
            Object secretKey = YmlUtils.getValue("lyb-geek.secretKey");
            if(ObjectUtils.isEmpty(secretKey)){
                throw new RuntimeException("secretKey can not be null,you maybe need to config in application.yml with key lyb-geek.secretKey");
            }
            registerBean(beanFactory,secretKey.toString());
    
    //        setClassLoader(beanFactory,secretKey.toString());
        }
    
        /**
         * 如果项目中引入了>spring-boot-devtools,则默认加载器为org.springframework.boot.devtools.restart.classloader.RestartClassLoader
         * 此时如果使用自定加载器,则需把bean的类加载器变更为AppClassLoader
         * @param beanFactory
         */
        private void setClassLoader(ConfigurableListableBeanFactory beanFactory,String secretKey) {
    
            CustomClassLoader customClassLoader = new CustomClassLoader(secretKey);
            beanFactory.setBeanClassLoader(customClassLoader.getParent());
        }
    
        private void registerBean(ConfigurableListableBeanFactory beanFactory,String secretKey){
            DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) beanFactory;
            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(UserService.class);
            GenericBeanDefinition definition = (GenericBeanDefinition) builder.getRawBeanDefinition();
            definition.getPropertyValues().add("serviceClz",UserService.class);
            definition.getPropertyValues().add("serviceImplClzName", "com.github.lybgeek.user.service.impl.UserServiceImpl");
            definition.getPropertyValues().add("secretKey", secretKey);
            definition.setBeanClass(ServiceFactoryBean.class);
            definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
            String beanId = StringUtils.uncapitalize(UserService.class.getSimpleName());
            defaultListableBeanFactory.registerBeanDefinition(beanId, definition);
        }
    
    }
    
    

    2、如果是接口注入,还需通过FactoryBean进行狸猫换太子

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class ServiceFactoryBean <T> implements FactoryBean<T> , ApplicationContextAware, InitializingBean {
    
        private ApplicationContext applicationContext;
    
        private Class<T> serviceClz;
    
        private String serviceImplClzName;
    
        private String secretKey;
    
        private Object targetObj;
    
        @Override
        public T getObject() throws Exception {
            return (T) targetObj;
        }
    
        @Override
        public Class<?> getObjectType() {
            return serviceClz;
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
            targetObj = ServiceFactory.create(secretKey,serviceImplClzName,applicationContext);
    
        }
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
        }
    }
    

    3、验证是否整合成功

    验证示例代码

    @RestController
    @RequestMapping("/user")
    public class UserController {
    
    
        @Autowired
        private UserService userService;
    
        @PostMapping(value = "/save")
        public User save(User user){
            User newUser = userService.save(user);
            return newUser;
        }
        }
    
    image.png

    能够正常输出,说明整合成功

    总结

    上述的例子只是提供一个思路,并不能完全杜绝代码被反编译。因为如果真想进行反编译,其实可以先通过反编译你自定义的类加载器,然后通过解密方式,去逆推加密算法,从而还原加密类。要杜绝代码被反编译的思路有如下

    • 提高反编译的成本,比如对自定义类加载再次加密,编写复杂的加密算法

    • 编写让人没有欲望反编译的代码,比如写一堆垃圾代码

    demo链接

    https://github.com/lyb-geek/springboot-learning/tree/master/springboot-code-authorization

    相关文章

      网友评论

        本文标题:如何使用自定义类加载器防止代码被反编译破解

        本文链接:https://www.haomeiwen.com/subject/vmejvktx.html