OpenRASP agent源码分析

news2024/9/22 11:37:04

目录

前言

准备

源码分析

1. manifest

 2. agent分析

3. agent卸载逻辑 

总结


前言

笔者在很早前写了(231条消息) OpenRASP Java应用自我保护使用_fenglllle的博客-CSDN博客

实际上很多商业版的rasp工具都是基于OpenRASP的灵感来的,主要就是对核心的Java类通过Javaagent技术,对特定的方法注入字节码,做参数验证。核心技术就是Javaagent,那么分析OpenRASP的agent实现原理,即可明白主流的rasp实现逻辑。 在OpenRASP上优化部分实现逻辑就可以形成一个商业产品。

准备

准备百度的admin端,执行./rasp-cloud -d

登录后拿到

appid、appsecret

在Java的靶机上配置agent和agent使用的参数

注意百度自己的一键注入代码jar,注入的agent在其他所有agent之前,agent的加载是有顺序的。

源码分析

1. manifest

agent是mainfest定义的入口,可以在pom插件和java包内部看到

pom插件 

jar包

 2. agent分析

 以jvm参数方式为例,动态注入也差不多,rasp一般使用都是jvm参数启动时注入

com.baidu.openrasp.Agent

    public static void premain(String agentArg, Instrumentation inst) {
        init(START_MODE_NORMAL, START_ACTION_INSTALL, inst);
    }

    public static synchronized void init(String mode, String action, Instrumentation inst) {
        try {
            //添加jar文件到jdk的跟路径下,优先加载
            JarFileHelper.addJarToBootstrap(inst);
            //读取一些agent的数据
            readVersion();
            //核心代码
            ModuleLoader.load(mode, action, inst);
        } catch (Throwable e) {
            System.err.println("[OpenRASP] Failed to initialize, will continue without security protection.");
            e.printStackTrace();
        }
    }

 ModuleLoader.load(mode, action, inst);

    /**
     * 加载所有 RASP 模块
     *
     * @param mode 启动模式
     * @param inst {@link java.lang.instrument.Instrumentation}
     */
    public static synchronized void load(String mode, String action, Instrumentation inst) throws Throwable {
        if (Module.START_ACTION_INSTALL.equals(action)) {
            if (instance == null) {
                try {
                    //安装
                    instance = new ModuleLoader(mode, inst);
                } catch (Throwable t) {
                    instance = null;
                    throw t;
                }
            } else {
                System.out.println("[OpenRASP] The OpenRASP has bean initialized and cannot be initialized again");
            }
        } else if (Module.START_ACTION_UNINSTALL.equals(action)) {
            //卸载
            release(mode);
        } else {
            throw new IllegalStateException("[OpenRASP] Can not support the action: " + action);
        }
    }

看看new ModuleLoader逻辑

    /**
     * 构造所有模块
     *
     * @param mode 启动模式
     * @param inst {@link java.lang.instrument.Instrumentation}
     */
    private ModuleLoader(String mode, Instrumentation inst) throws Throwable {
        // JBoss参数,实际上就是设置一些系统变量
        if (Module.START_MODE_NORMAL == mode) {
            setStartupOptionForJboss();
        }
        //构造对象,加载另外的jar engine.jar 初始化com.baidu.openrasp.EngineBoot
        engineContainer = new ModuleContainer(ENGINE_JAR);
        //核心逻辑
        engineContainer.start(mode, inst);
    }

new ModuleContainer(ENGINE_JAR);

    public ModuleContainer(String jarName) throws Throwable {
        try {
            //engine jar,openrasp有2个jar,单独加载,方便卸载,agent常用手段
            File originFile = new File(baseDirectory + File.separator + jarName);
            JarFile jarFile = new JarFile(originFile);
            Attributes attributes = jarFile.getManifest().getMainAttributes();
            jarFile.close();
            this.moduleName = attributes.getValue("Rasp-Module-Name");
            // com.baidu.openrasp.EngineBoot
            String moduleEnterClassName = attributes.getValue("Rasp-Module-Class");
            //用classloader加载jar
            if (moduleName != null && moduleEnterClassName != null
                    && !moduleName.equals("") && !moduleEnterClassName.equals("")) {
                Class moduleClass;
                if (ClassLoader.getSystemClassLoader() instanceof URLClassLoader) {
                    Method method = Class.forName("java.net.URLClassLoader").getDeclaredMethod("addURL", URL.class);
                    method.setAccessible(true);
                    method.invoke(moduleClassLoader, originFile.toURI().toURL());
                    method.invoke(ClassLoader.getSystemClassLoader(), originFile.toURI().toURL());
                    moduleClass = moduleClassLoader.loadClass(moduleEnterClassName);
                    //com.baidu.openrasp.EngineBoot对象
                    module = (Module) moduleClass.newInstance();
                } else if (ModuleLoader.isCustomClassloader()) {
                    moduleClassLoader = ClassLoader.getSystemClassLoader();
                    Method method = moduleClassLoader.getClass().getDeclaredMethod("appendToClassPathForInstrumentation", String.class);
                    method.setAccessible(true);
                    try {
                        method.invoke(moduleClassLoader, originFile.getCanonicalPath());
                    } catch (Exception e) {
                        method.invoke(moduleClassLoader, originFile.getAbsolutePath());
                    }
                    moduleClass = moduleClassLoader.loadClass(moduleEnterClassName);
                    //初始化com.baidu.openrasp.EngineBoot
                    module = (Module) moduleClass.newInstance();
                } else {
                    throw new Exception("[OpenRASP] Failed to initialize module jar: " + jarName);
                }
            }
        } catch (Throwable t) {
            System.err.println("[OpenRASP] Failed to initialize module jar: " + jarName);
            throw t;
        }
    }

engineContainer.start(mode, inst); 

    public void start(String mode, Instrumentation inst) throws Exception {
        //帅气的时刻,打印logo
        System.out.println("\n\n" +
                "   ____                   ____  ___   _____ ____ \n" +
                "  / __ \\____  ___  ____  / __ \\/   | / ___// __ \\\n" +
                " / / / / __ \\/ _ \\/ __ \\/ /_/ / /| | \\__ \\/ /_/ /\n" +
                "/ /_/ / /_/ /  __/ / / / _, _/ ___ |___/ / ____/ \n" +
                "\\____/ .___/\\___/_/ /_/_/ |_/_/  |_/____/_/      \n" +
                "    /_/                                          \n\n");
        try {
            //载入v8,调用c,实际上可以使用火狐引擎替代,百度的官方文档的图就是rhino
            Loader.load();
        } catch (Exception e) {
            System.out.println("[OpenRASP] Failed to load native library, please refer to https://rasp.baidu.com/doc/install/software.html#faq-v8-load for possible solutions.");
            e.printStackTrace();
            return;
        }
        if (!loadConfig()) {
            return;
        }
        //缓存rasp的build信息
        Agent.readVersion();
        BuildRASPModel.initRaspInfo(Agent.projectVersion, Agent.buildTime, Agent.gitCommit);
        // 初始化插件系统 调用v8,支持动态更新插件和js
        if (!JS.Initialize()) {
            return;
        }
        //核心代码,加入检查点,就是哪些类的哪些方法需要check
        CheckerManager.init();
        //字节码替换
        initTransformer(inst);
        if (CloudUtils.checkCloudControlEnter()) {
            CrashReporter.install(Config.getConfig().getCloudAddress() + "/v1/agent/crash/report",
                    Config.getConfig().getCloudAppId(), Config.getConfig().getCloudAppSecret(),
                    CloudCacheModel.getInstance().getRaspId());
        }
        deleteTmpDir();
        String message = "[OpenRASP] Engine Initialized [" + Agent.projectVersion + " (build: GitCommit="
                + Agent.gitCommit + " date=" + Agent.buildTime + ")]";
        System.out.println(message);
        Logger.getLogger(EngineBoot.class.getName()).info(message);
    }
loadConfig()
    private boolean loadConfig() throws Exception {
        //处理日志相关
        LogConfig.ConfigFileAppender();
        //单机模式下动态添加获取删除syslog
        if (!CloudUtils.checkCloudControlEnter()) {
            LogConfig.syslogManager();
        } else {
            System.out.println("[OpenRASP] RASP ID: " + CloudCacheModel.getInstance().getRaspId());
        }
        return true;
    }

    private void init() {
        this.configFileDir = baseDirectory + File.separator + CONFIG_DIR_NAME;
        String configFilePath = this.configFileDir + File.separator + CONFIG_FILE_NAME;
        try {
            //载入靶机的配置文件,里面配置了appid appsecret等
            loadConfigFromFile(new File(configFilePath), true);
            if (!getCloudSwitch()) {
                try {
                    FileScanMonitor.addMonitor(
                            baseDirectory, instance);
                } catch (JNotifyException e) {
                    throw new ConfigLoadException("add listener on " + baseDirectory + " failed because:" + e.getMessage());
                }
                //支持动态刷新文件
                addConfigFileMonitor();
            }
        } catch (FileNotFoundException e) {
            handleException("Could not find openrasp.yml, using default settings: " + e.getMessage(), e);
        } catch (JNotifyException e) {
            handleException("add listener on " + configFileDir + " failed because:" + e.getMessage(), e);
        } catch (Exception e) {
            handleException("cannot load properties file: " + e.getMessage(), e);
        }
        String configValidMsg = checkMajorConfig();
        if (configValidMsg != null) {
            LogTool.error(ErrorType.CONFIG_ERROR, configValidMsg);
            throw new ConfigLoadException(configValidMsg);
        }
    }

JS.Initialize(),JS引擎初始化,js文件监听 

    public synchronized static boolean Initialize() {
        try {
            //v8初始化检查
            if (!V8.Initialize()) {
                throw new Exception("[OpenRASP] Failed to initialize V8 worker threads");
            }
            //log
            V8.SetLogger(new com.baidu.openrasp.v8.Logger() {
                @Override
                public void log(String msg) {
                    pluginLog(msg);
                }
            });
            //
            V8.SetStackGetter(new com.baidu.openrasp.v8.StackGetter() {
                @Override
                public byte[] get() {
                    try {
                        ByteArrayOutputStream stack = new ByteArrayOutputStream();
                        JsonStream.serialize(StackTrace.getParamStackTraceArray(), stack);
                        stack.write(0);
                        return stack.getByteArray();
                    } catch (Exception e) {
                        return null;
                    }
                }
            });
            Context.setKeys();
            //是否云端控制,比如js更新,插件更新
            if (!CloudUtils.checkCloudControlEnter()) {
                //更新插件
                UpdatePlugin();
                //js文件监听器
                InitFileWatcher();
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            LOGGER.error(e);
            return false;
        }
    }


    public synchronized static boolean UpdatePlugin(List<String[]> scripts) {
        boolean rst = V8.CreateSnapshot(pluginConfig, scripts.toArray(), BuildRASPModel.getRaspVersion());
        if (rst) {
            try {
                //执行js
                String jsonString = V8.ExecuteScript("JSON.stringify(RASP.algorithmConfig || {})",
                        "get-algorithm-config.js");
                Config.getConfig().setConfig(ConfigItem.ALGORITHM_CONFIG, jsonString, true);
            } catch (Exception e) {
                LogTool.error(ErrorType.PLUGIN_ERROR, e.getMessage(), e);
            }
            Config.commonLRUCache.clear();
        }
        return rst;
    }
CheckerManager.init();
    public synchronized static void init() throws Exception {
        for (Type type : Type.values()) {
            checkers.put(type, type.checker);
        }
    }

检查的类是写在代码里,意味着新增类需要重启,不过一般是增加参数更新规则,新增类频率比较低

​​​​​​​​​​​​​​​​​​​​​

自定义转换器,retransform

    private void initTransformer(Instrumentation inst) throws UnmodifiableClassException {
        transformer = new CustomClassTransformer(inst);
        transformer.retransform();
    }

这里很关键

    public CustomClassTransformer(Instrumentation inst) {
        this.inst = inst;
        inst.addTransformer(this, true);
        //定义了哪些类需要类替换
        addAnnotationHook();
    }

 addAnnotationHook();

    private void addAnnotationHook() {
        //com.baidu.openrasp.hook HookAnnotation注解的类
        Set<Class> classesSet = AnnotationScanner.getClassWithAnnotation(SCAN_ANNOTATION_PACKAGE, HookAnnotation.class);
        for (Class clazz : classesSet) {
            try {
                Object object = clazz.newInstance();
                if (object instanceof AbstractClassHook) {
                    addHook((AbstractClassHook) object, clazz.getName());
                }
            } catch (Exception e) {
                LogTool.error(ErrorType.HOOK_ERROR, "add hook failed: " + e.getMessage(), e);
            }
        }
    }

addHook((AbstractClassHook) object, clazz.getName());//实际上是缓存需要替换的类,还原的时候可以使用

关键还是transform方法

 

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain domain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if (loader != null) {
            DependencyFinder.addJarPath(domain);
        }
        if (loader != null && jspClassLoaderNames.contains(loader.getClass().getName())) {
            jspClassLoaderCache.put(className.replace("/", "."), new SoftReference<ClassLoader>(loader));
        }
        //关键点,使用javassist字节码增强
        for (final AbstractClassHook hook : hooks) {
            if (hook.isClassMatched(className)) {
                CtClass ctClass = null;
                try {
                    ClassPool classPool = new ClassPool();
                    addLoader(classPool, loader);
                    ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
                    if (loader == null) {
                        hook.setLoadedByBootstrapLoader(true);
                    }
                    //字节码增强了,包括Tomcat,jdk等类
                    classfileBuffer = hook.transformClass(ctClass);
                    if (classfileBuffer != null) {
                        checkNecessaryHookType(hook.getType());
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (ctClass != null) {
                        ctClass.detach();
                    }
                }
            }
        }
        serverDetector.detectServer(className, loader, domain);
        return classfileBuffer;
    }

定义了一系列的埋点类替换

 

以com.baidu.openrasp.hook.file.FileHook为例

 增加了对File的list方法调用前,对参数进行检查

    protected void hookMethod(CtClass ctClass) throws IOException, CannotCompileException, NotFoundException {
        String src = getInvokeStaticSrc(FileHook.class, "checkListFiles", "$0", File.class);
        insertBefore(ctClass, "list", "()[Ljava/lang/String;", src);
    }

 

openrasp的架构图和开源代码不对应,开源代码用的v8引擎 ,这里画的图是Rhino,是原生Java代码,不需要JNI

3. agent卸载逻辑 

release(mode)

    public static synchronized void release(String mode) {
        try {
            if (engineContainer != null) {
                System.out.println("[OpenRASP] Start to release OpenRASP");

                engineContainer.release(mode);
                engineContainer = null;
            } else {
                System.out.println("[OpenRASP] Engine is initialized, skipped");
            }
        } catch (Throwable throwable) {
            // ignore
        }
    }

 engineContainer.release(mode);

    public void release(String mode) {
        //任务线程停止
        CloudManager.stop();
        //CPU 监控线程停掉
        CpuMonitorManager.release();
        if (transformer != null) {
            transformer.release(); //类替换还原
        }
        JS.Dispose(); //JS检查还原
        CheckerManager.release(); //java 检查还原
        String message = "[OpenRASP] Engine Released [" + Agent.projectVersion + " (build: GitCommit="
                + Agent.gitCommit + " date=" + Agent.buildTime + ")]";
        System.out.println(message);
    }

    public synchronized static void Dispose() {
        if (watchId != null) {
            boolean oldValue = HookHandler.enableHook.getAndSet(false);
            FileScanMonitor.removeMonitor(watchId);
            watchId = null;
            HookHandler.enableHook.set(oldValue);
        }
    }

4. 执行检查的过程 

以Tomcat Xss为例,其他同理,因为定义了Tomcat的请求的hook字节码拦截,在启动的源码已经分析过了

因为是字节码拦截,所以可以定义拦截级别,可以阻断请求

 

具体使用的方式,在启动的时候就定义了,比如http请求就有xss的风险,具体的拦截效率可以极大程度提升应用程序的性能,比如不存在xss的情况,比如没有前端页面,就不配置xss拦截

 

 最终执行各个checker

checker检查,另外以FileHook代码注入检查为例

这个里面也会调用checker代码,然后检查参数

 

部分检查手段是JS检查

 

 实际上逻辑很清晰了

字节码注入检查点

执行检查,根据逻辑,可以通过插件调用checker,也可以直接检查返回

执行checker的时候,可以通过JS引擎检查参数等(js引擎的js文件可以动态更新规则)

 

总结

OpenRASP的源码就分析完成了,实际上代码并不复杂,但是思想很不错,在实际的使用过程中

要考虑性能影响,

要考虑是否阻断拦截对业务的影响

要考虑各个部件的使用逻辑,可以自定义,并且支持动态更新,毕竟字节码替换风险还是有的,所以一般更新检查的数据,所以引入了js引擎,通过js更新

最终还是要考虑实际需求,比如内网环境,毕竟开销很大。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/61408.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

堆(二叉堆)-优先队列-数据结构和算法(Java)

文章目录1 概述1.1 定义1.2 二叉堆表示法2 API3 堆相关算法3.1 上浮&#xff08;由下至上的堆有序化&#xff09;3.2 下沉&#xff08;由上至下的堆有序化&#xff09;3.3 插入元素3.4 删除最大元素4 实现5 性能和分析5.1 调整数组的大小5.2 元素的不可变性6 简单测试6 后记1 概…

2006-2020年全国31省人口老龄化水平

2006-2020年全国31省人口老龄化 1、时间为2006-2020年 2、来源&#xff1a;人口与就业年鉴 3、数据缺失情况说明&#xff1a; 其中2010年存在缺失&#xff0c;采用线性插值法进行填补&#xff0c;内含原始数据、线性插值 4、计算说明&#xff1a;以城镇地区老年抚养比衡量…

uImage的制作过程详解

1、uImage镜像介绍 参考博客&#xff1a;《vmlinuz/vmlinux、Image、zImage与uImage的区别》&#xff1b; 2、uImage镜像的制作 2.1、mkimage工具介绍 参考博客&#xff1a;《uImage的制作工具mkimage详解(源码编译、使用方法、添加的头解析、uImage的制作)》&#xff1b; 2.2…

软路由搭建:工控机(3865U)安装esxi并在esxi上创建iStoreOS做主路由(网卡直通)

一、硬件介绍 1、工控机&#xff08;3865U&#xff09; CPU&#xff1a;3865U 内存&#xff1a;8G 硬盘&#xff1a;120G 网卡&#xff1a;六口网卡 2、无线路由器&#xff08;荣耀路由器pro2&#xff09; 3、主机 下载资料、制作启动盘、系统设置 4、U盘 至少8G以上 …

ConcurrentHashMap 1.7与1.8的区别

ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于&#xff1a;put和 get 两次Hash到达指定的HashEntry&#xff0c;第一次hash到达Segment,第二次到达Segment里面的Entry,然后在遍历entry链表 从1.7到1.8版本&#xff0c;由于HashEntry从链表 变成了红黑树所以 concurr…

Python Gui之tkinter(下)

6.Radiobutton单按按钮 Radiobutton控件用于选择同一组单选按钮中的一个。Radiobutton可以显示文本&#xff0c;也可以显示图像。 7.Checkbutton复选按钮 Checkbutton控件用于选择多个按钮的情况。Checkbutton可以显示文本&#xff0c;也可以显示图像。 经典的Gui类的写法&a…

关于liunx 宝塔运行php项目

文章目录前言一、申请liunx服务器安装宝塔环境二、安装php看你自己安装需要的版本三.php文件创建四.数据库创建五.访问项目就可以了前言 自己研究学习&#xff0c;大佬勿喷 一、申请liunx服务器安装宝塔环境 我是线上安装的都一样看个人习惯爱好吧 等待安装完成提示地址和账…

Java基础—重新抛出异常

重新抛出异常 在catch块内处理完后&#xff0c;可以重新抛出异常&#xff0c;异常可以是原来的&#xff0c;也可以是新建的&#xff0c;如下所示&#xff1a; try{ //可能触发异常的代码 }catch(NumberFormatException e){ System.out.println("not valid numbe…

电子印章结构以及规范讲解

前言 为了确保电子印章的完整性、不可伪造性&#xff0c;以及合法用户才能使用&#xff0c;需要定义一个安全的电子印章数据格式&#xff0c;通过数字签名&#xff0c;将印章图像数据与签章者等印章属性进行安全绑定&#xff0c;形成安全电子印章 电子印章&#xff1a;一种由…

MVVM与Vue响应式的实现

Vue的响应式实现原理 MVVM M&#xff1a;模型 》data中的数据 V&#xff1a;视图 》模板 VM&#xff1a;视图模型 》Vue实例对象 ViewModel是一个中间的桥梁将视图View与模型Model连接起来&#xff0c;ViewModel内部通过数据绑定&#xff0c;实现数据变化&#xff0c;视图发…

链接装载(一)虚拟地址与物理地址

文章目录一、基本概念二、一个基本问题三、程序的执行四、从堆中分配的数据的逻辑地址一、基本概念 当我们写出一个程序&#xff0c;即便是最基本的 Hello World&#xff0c;都需要经过 预处理、编译、汇编、链接才能生成最终的可执行文件。 预处理&#xff1a; 预处理过程主…

spring ioc的循环依赖问题

spring ioc的循环依赖问题什么是循环依赖spring中循环依赖的场景通过构造函数注入时的循环依赖通过setter或Autowired注入时的循环依赖循环依赖的处理机制原型bean循环依赖单例bean通过构造函数注入循环依赖单例bean通过setter或者Autowired注入的循环依赖三级缓存对象的创建分…

Metasploit 操作及内网 Pivot图文教程

目录 一、metasploit 简介 二、 基本使用 三、 使用 encoders 四、pivot 技术 一、metasploit 简介 Metasploit 是一款开源的安全漏洞检测工具&#xff0c;集成了丰富的渗透测试工具&#xff0c;深受安 全工作者喜爱。官方网站&#xff1a;www.metasploit.com 本案例将以图…

OS-调度

调度 多个程序在并发的情况下执行&#xff0c;最大化CPU利用率&#xff0c;同时要保证一定的公平性 调度的时机 五种情况&#xff1a; Running -> Waiting&#xff1a;例如等待I/ORunning -> Ready: interupt&#xff0c;计时器到时间了Running -> TerminatedWait…

我把Idea给改了,看看有没有你常用的功能,没有,你告诉我,我来改

改造的目标 时隔2个多月的研发&#xff0c;11月25日&#xff0c;终于把Idea插件BG-BOOM的1.1.0版本搞上线了&#xff0c;本次更新勇哥也是百忙之中挤时间&#xff0c;加班加点开发为粉丝&#xff0c;目的也主要是帮助大家提升开发效率&#xff0c;有更多摸鱼和内卷时间&#x…

[附源码]Python计算机毕业设计SSM晋中学院教室管理系统(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

QT中怎么设置定时器/周期任务/定时触发任务

Qt中定时器的使用有两种方法&#xff0c;一种是使用QObject类提供的定时器&#xff0c;还有一种就是使用QTimer类。 其精确度一般依赖于操作系统和硬件&#xff0c;但一般支持20ms。下面将分别介绍两种方法来使用定时器。 QObject类提供的定时器 QObject中的定时器的使用&am…

Makefile 详解

文章目录1.什么是Makefile2.Makefile文件命名规则3.编写Makefile4.Makefile 的工作原理5.Makefile中的变量6.模式匹配7.函数1.什么是Makefile 一个工程中的源文件不计其数&#xff0c;按期类型、功能、模块分别放在若干个文件中&#xff0c;MakeFile文件定义了一系列的规则来制…

HIN应用调研总结

文章目录1. 代码安全iDev: enhancing social coding security by cross-platform user identification between GitHub and stack overflow【A】2. API推荐Group preference based API recommendation via heterogeneous information network【A】3.Andorid恶意软件检测Out-of-…

SSM甜品店系统计算机毕业论文java毕业设计选题源代码

&#x1f496;&#x1f496;更多项目资源&#xff0c;最下方联系我们✨✨✨✨✨✨ 目录 Java项目介绍 资料获取 Java项目介绍 计算机毕业设计java毕设之SSM甜品店系统-IT实战营_哔哩哔哩_bilibili项目资料网址: http://itzygogogo.com软件下载地址:http://itzygogogo.com/i…