CVE-2021-39676 AndroidFuture反序列化漏洞分析


一、漏洞描述

  • In writeThrowable of AndroidFuture.java, there is a possible parcel serialization/deserialization mismatch due to improper input validation. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is not needed for exploitation.Product: AndroidVersions: Android-11Android ID: A-197228210
  • 这个漏洞Google没有在AOSP网站上披露补丁diff,但是这个漏洞所在的位置是开源的部分,可以自己分析。

二、漏洞分析

  • 根据披露信息我们知道,漏洞位于AndroidFuture.java中,完整路径是frameworks/base/core/java/com/android/internal/infra/AndroidFuture.java,然后他这里面提到的函数名字writeThrowable已经是Android 12里面的名字了,而Android 12这里是没有漏洞的,因为进行了重构,所以我们在Android 11的代码中找到相同功能的函数,其实它在Android 11里面叫parcelException。
/**
 * Alternative to {@link Parcel#writeException} that stores the stack trace, in a
 * way consistent with the binder IPC exception propagation behavior.
 */
private static void parcelException(Parcel p, @Nullable Throwable t) {
    p.writeBoolean(t == null);
    if (t == null) {
        return;
    }
    p.writeInt(Parcel.getExceptionCode(t));
    p.writeString(t.getClass().getName());
    p.writeString(t.getMessage());
    p.writeStackTrace(t);
    parcelException(p, t.getCause());
}
/**
 * @see #parcelException
 */
private static @Nullable Throwable unparcelException(Parcel p) {
    if (p.readBoolean()) {
        return null;
    }
    int exCode = p.readInt();
    String cls = p.readString();
    String msg = p.readString();
    String stackTrace = p.readInt() > 0 ? p.readString() : "\t<stack trace unavailable>";
    msg += "\n" + stackTrace;
    Exception ex = p.createExceptionOrNull(exCode, msg);
    if (ex == null) {
        ex = new RuntimeException(cls + ": " + msg);
    }
    ex.setStackTrace(EMPTY_STACK_TRACE);
    Throwable cause = unparcelException(p);
    if (cause != null) {
        ex.initCause(ex);
    }
    return ex;
}
  • 本身AndroidFuture是一个Parcelable的类型,自身有writeToParcel函数的重载,而在writeToParcel中的特定分支会调用parcelException函数,写如一个Throwable对象。我们知道Throwable其实是异常对象的父类,所以他这里的业务逻辑是在异常分支中讲异常内容传递给调用者。
  • parcelException和unparcelException成对出现,自然也好理解。parcelException中首先写入的是异常code,为int类型,然后是异常类目和异常信息,是两个String对象,最后调用了writeStackTrace来写入Throwable对象自身,如果该Throwable对象包含cause,也就是一个多层的异常对象,那么需要递归调用整个流程。
  • 在unparcelException中我们可以看到,首先也是读取int和两个String对象,这没有问题,然后代码中根据下一个读取int的值是否大于0来决定是读取一个String还是不读取,可以看出来这里面大于0的情况就是存在stack trace,那么便读取它,如果不存在则使用占位符\t<stack trace unavailable>,后面就还是递归调用的流程。
  • 那么这里面有什么问题呢?看起来序列化和反序列化的流程确实不太一致?这里我们需要关注一下writeStackTrace的实现。
/** @hide */
public void writeStackTrace(@NonNull Throwable e) {
    final int sizePosition = dataPosition();
    writeInt(0); // Header size will be filled in later
    StackTraceElement[] stackTrace = e.getStackTrace();
    final int truncatedSize = Math.min(stackTrace.length, 5);
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < truncatedSize; i++) {
        sb.append("\tat ").append(stackTrace[i]).append('\n');
    }
    writeString(sb.toString());
    final int payloadPosition = dataPosition();
    setDataPosition(sizePosition);
    // Write stack trace header size. Used in native side to skip the header
    writeInt(payloadPosition - sizePosition);
    setDataPosition(payloadPosition);
}
  • 从writeStackTrace函数中我们可以看到,里面实际上是写入了一个int,一个String对象,但是最后又写入了一个int,这里面就明显和序列化的代码有所不同了,所以这里是一个序列化与反序列化不匹配的漏洞。
  • 更为重要的是,由于递归写入/读取的存在,实际上我们可以进行精确的布局,充分利用递归的特性来搞事情。

三、漏洞补丁

  • 下面是Android 12中的代码
/**
 * Alternative to {@link Parcel#writeException} that stores the stack trace, in a
 * way consistent with the binder IPC exception propagation behavior.
 */
private static void writeThrowable(@NonNull Parcel parcel, @Nullable Throwable throwable) {
    boolean hasThrowable = throwable != null;
    parcel.writeBoolean(hasThrowable);
    if (!hasThrowable) {
        return;
    }
    boolean isFrameworkParcelable = throwable instanceof Parcelable
            && throwable.getClass().getClassLoader() == Parcelable.class.getClassLoader();
    parcel.writeBoolean(isFrameworkParcelable);
    if (isFrameworkParcelable) {
        parcel.writeParcelable((Parcelable) throwable,
                Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
        return;
    }
    parcel.writeString(throwable.getClass().getName());
    parcel.writeString(throwable.getMessage());
    StackTraceElement[] stackTrace = throwable.getStackTrace();
    StringBuilder stackTraceBuilder = new StringBuilder();
    int truncatedStackTraceLength = Math.min(stackTrace != null ? stackTrace.length : 0, 5);
    for (int i = 0; i < truncatedStackTraceLength; i++) {
        if (i > 0) {
            stackTraceBuilder.append('\n');
        }
        stackTraceBuilder.append("\tat ").append(stackTrace[i]);
    }
    parcel.writeString(stackTraceBuilder.toString());
    writeThrowable(parcel, throwable.getCause());
}
/**
 * @see #writeThrowable
 */
private static @Nullable Throwable readThrowable(@NonNull Parcel parcel) {
    final boolean hasThrowable = parcel.readBoolean();
    if (!hasThrowable) {
        return null;
    }
    boolean isFrameworkParcelable = parcel.readBoolean();
    if (isFrameworkParcelable) {
        return parcel.readParcelable(Parcelable.class.getClassLoader());
    }
    String className = parcel.readString();
    String message = parcel.readString();
    String stackTrace = parcel.readString();
    String messageWithStackTrace = message + '\n' + stackTrace;
    Throwable throwable;
    try {
        Class<?> clazz = Class.forName(className, true, Parcelable.class.getClassLoader());
        if (Throwable.class.isAssignableFrom(clazz)) {
            Constructor<?> constructor = clazz.getConstructor(String.class);
            throwable = (Throwable) constructor.newInstance(messageWithStackTrace);
        } else {
            android.util.EventLog.writeEvent(0x534e4554, "186530450", -1, "");
            throwable = new RuntimeException(className + ": " + messageWithStackTrace);
        }
    } catch (Throwable t) {
        throwable = new RuntimeException(className + ": " + messageWithStackTrace);
        throwable.addSuppressed(t);
    }
    throwable.setStackTrace(EMPTY_STACK_TRACE);
    Throwable cause = readThrowable(parcel);
    if (cause != null) {
        throwable.initCause(cause);
    }
    return throwable;
}
  • 我们可以看到在Android 12中对这块代码完全进行了重写,而且也改名成为了writeThrowable和readThrowable,这两个函数的流程就是匹配的。如果该异常对象能被Framework所识别,就会直接使用writeParcelable进行写入,如果是自定义的异常类,则不会按照异常对象的方式写入,而是像原来一样写入异常类型、信息和stack trace,不过这次全部是以字符串的形式写入,所以不会出现问题。