一、漏洞描述
- 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中的一个旧问题的补丁绕过。
三、漏洞补丁
/**
* 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,不过这次全部是以字符串的形式写入,所以不会出现问题。