一、漏洞描述
- 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(前面的0只是占位符,实际的值在第二个writeInt写入),一个String对象,但是这里面我们发现无论第一个int的值是不是0,都会写入一个String,只不过如果stackTrace的length为0的话写入的是空字符串。但是如果了解writeString实现的读者会知道,writeString固定会使用writeInt写入字符串的长度,即使长度是0也一样,而读取的时候如果前面的int读取是0,就不执行readString了,没有考虑到writeString时候使用writeInt自带写入的长度问题。这里面就明显和序列化的代码有所不同了,所以这里是一个序列化与反序列化不匹配的漏洞。
- 这类漏洞的利用可以参考Bundle风水的利用,使用AccountManagerService中的一个旧问题的补丁绕过。
三、漏洞补丁
- 下面是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,不过这次全部是以字符串的形式写入,所以不会出现问题。