介绍
从安全角度来看,序列化和反序列化机制始终是高风险的操作。在大多数语言和框架中,如果攻击者能够反序列化任意输入(或者像我们多年前用 Rusty Joomla RCE(https://1day.dev/notes/Rusty-Joomla-Remote-Code-Execution)演示的那样直接破坏输入),其影响通常最为严重:远程代码执行。无需赘述,因为网上已经有很多优秀的资源解释了不安全反序列化问题的基本概念,我们想将注意力集中在一个有趣的 Android API 和类上:getSerializableExtra
和Serializable
。
getSerializableExtra
介绍
getSerializableExtra
(https://developer.android.com/reference/android/content/Intent#getSerializableExtra(java.lang.String))类中的 API 允许 通过接收 Intent 的额外参数Intent
(https://developer.android.com/reference/android/content/Intent#getSerializableExtra(java.lang.String,-java.lang.Class))检索 Serializable
(https://developer.android.com/reference/java/io/Serializable)对象,如果组件已导出并启用,则从攻击者的角度来看,它可能是一个有趣的攻击面。getSerializableExtra(String name)
在 Android API 级别 33(Android 13)中,已弃用 ,取而代之的是更安全的 类型getSerializableExtra(String name, Class<T> clazz)
。 Serializable
(https://developer.android.com/reference/java/io/Serializable)启用对象反序列化的类文档包含以下粗体文本:
警告:反序列化不受信任的数据本质上是危险的,应尽量避免。应仔细验证不受信任的数据。
由于我们已经知道反序列化任意输入对象的一般风险,因此深入研究的目的是了解getSerializableExtra
使用和不使用类型安全参数调用任意输入的实际后果。
getSerializableExtra
内部代码概述
第一步
getSerializableExtra
还有什么比真正从阅读我们感兴趣的 API 的源代码开始更好的呢?我们没什么可想的,所以这是在 Android 15 上使用 AOSP 的流程摘要: Intent::getSerializableExtra
=> Bundle::getSerializable
=> => BaseBundle::getSerializable
=> BaseBundle::getValue
=> ..
。
BaseBundle::getSerializable
负责从接收到的 Intent 中检索值(或者在这个级别最好将其定义为 Parcel
对象),并返回强制转换为 的对象Serializable
。此流程与其他参数类型的检索非常相似。如果您看到getString
、getCharSequence
或getDobule
方法,它们的行为方式类似:它们Object
从中检索泛型mMap.getKey()
,然后通过强制转换返回其类型(例如return (String) o)
)。
在这种情况下,情况略有不同:getValue
指定null
类,并在一些调用之后,getValueAt
调用 来检索序列化对象。mMap.valueAt
返回泛型Object
,然后将其与泛型转换T
(如果未指定类)一起返回给调用者。在这中间有一个非常奇怪的 if 条件,用于检查检索到的 是否object
是 的实例BiFunction<?, ?, ?>
。老实说,我无法通过代码审查手动确定这个条件,所以我在运行时尝试了一下,结果在getSerializable
调用 时确实触发了真正的路径。unwrapLazyValueFromMapLocked
堆栈跟踪非常有趣:android.os.BaseBundle.unwrapLazyValueFromMapLocked
=> android.os.Parcel$LazyValue.apply
=> android.os.Parcel.readValue
=>android.os.Parcel.readSerializableInternal
Parcel::readSerializableInternal
由于我们的主要兴趣在于如何处理和反序列化输入对象,因此我们可以直接关注似乎与我们的目标一致的最新方法:
方法参数:loader
和clazz
我们可以通过对整个方法进行高层次的概述来开始了解正在发生的事情。它接受两个参数:loader
和clazz
。如果没有指定任何类(在前面提到的方法中指定),则clazz
为空。相反, 参数会在和之间的堆栈跟踪中传递和定义一些内容:getSerializible``null``BaseBundle::getValue``loader``unwrapLazyValueFromMapLocked``android.os.Parcel.readSerializableInternal
大部分代码与 Parcel 对象的解组过程相关,为了专注于我们的主要工作,这些代码已被有意删除。loader
我们正在寻找的参数似乎源自Parcel::apply
[1] 方法。mLoader
在Parcel
上下文中,它是 类型的类成员ClassLoader
,并在构造函数中定义为最后一个参数。惰性打包机制是一种“新”的(几年前)引入的方法,用于根据前缀长度对 Parcel 进行惰性反序列化,在“ Android Parcels:优缺点 - Android 更安全的 Parcel 介绍(https://www.youtube.com/watch?v=qIzMKfOmIAA)Parcel::LazyValue
”讲座中已有详细解释 。
通过动态挂钩readSerializableInternal
使用frida,加载器(类型dalvik.system.PathClassLoader
)具有以下值:
dalvik.system.PathClassLoader[DexPathList[[zipfile"/data/app/~~pSOjjaFofZg9BArMhAPO3w==/com.example.serialized.receiver-xCRsymIZLPj1E9xRk7LQpw==/base.apk"],nativeLibraryDirectories=[/data/app/~~pSOjjaFofZg9BArMhAPO3w==/com.example.serialized.receiver-xCRsymIZLPj1E9xRk7LQpw==/lib/arm64, /system/lib64, /system_ext/lib64]]]
加载器的类型为dalvik.system.PathClassLoader
,用于解析传递的对象并包含以下路径(DexPathList
):
-
/data/app/~~pSOjjaFofZg9BArMhAPO3w==/com.example.serialized.receiver-xCRsymIZLPj1E9xRk7LQpw==/base.apk
-
/data/app/~~pSOjjaFofZg9BArMhAPO3w==/com.example.serialized.receiver-xCRsymIZLPj1E9xRk7LQpw==/lib/arm64
-
/system/lib64
-
/system_ext/lib64
前两条路径是特定于应用程序的,而后两条路径是特定于系统的。首先,非常明显的是:输入对象必须在应用程序或系统上下文中定义。
类解析
loader
现在我们对和参数有了更深入的理解clazz
,我们可以回到readSerializableInternal
上面显示的源代码。如果clazz
定义了 ,Class.forName
则使用 来与来自包裹的输入类名进行匹配,以返回Class
对象并使用 进行验证isAssignableFrom
(BadTypeParcelableException
如果不“匹配”,则抛出 )。由于我们感兴趣的getSerializable
是没有显式类型转换的表面,因此clazz
在这些情况下 为空,并执行以下代码:
createByteArray
使用( )从 parcel 中读取一个字节数组serializedData
,并将其初始化为一个 ByteArrayInputStream
(https://developer.android.com/reference/java/io/ByteArrayInputStream)( ),该( )bais
用于初始化 ( ) ,如果定义了 (我们的例子),则会使用不同的逻辑覆盖该方法。然而,其逻辑与文档(https://developer.android.com/reference/java/io/ObjectInputStream#resolveClass(java.io.ObjectStreamClass))中提到的 “原始”行为类似 :ObjectInputStream
(https://developer.android.com/reference/java/io/ObjectInputStream)ois
resolveClass
loader
resolveClass
(https://developer.android.com/reference/java/io/ObjectInputStream#resolveClass(java.io.ObjectStreamClass))
此方法的默认实现
ObjectInputStream
返回调用的结果Class.forName(desc.getName(), false, loader)
ObjectInputStream
这 ObjectInputStream
(https://developer.android.com/reference/java/io/ObjectInputStream#resolveClass(java.io.ObjectStreamClass))似乎是我们深入研究的下一个目标。它是一个 Java 类对象(https://docs.oracle.com/javase/8/docs/api/?java/io/ObjectInputStream.html),我们可以从其官方文档中提取一些有趣的语句:
ObjectInputStream ==反序列化先前使用 ObjectOutputStream 写入的原始数据和对象==。
== 方法
readObject
用于从流 == 中读取对象。应使用 Java 的安全转换来获取所需的类型。
==读取对象类似于运行新对象的构造函数==。
对象的默认反序列化机制==将每个字段的内容恢复为写入时的值和类型。
类通过实现 java.io.Serializable 或 java.io.Externalizable 接口来控制如何序列化。只有支持 java.io.Serializable 或 java.io.Externalizable 接口的对象才能从流中读取。
由于它并非 Android 特有的类,因此网上有很多资源对其做了详尽的介绍,尤其是 Matthias Kaiser 在 2016 年发表的一篇有趣的演讲:“Java 反序列化漏洞 - 被遗忘的 bug 类”(https://www.youtube.com/watch?v=9Bw1urhk8zw)。我们可以总结的关键概念是:在我们的例子中,resolveClass
方法ObjectInputStream
被重写,以便使用方法参数提供的“自定义”类加载器,并且反序列化过程实际上从 开始 ois.readObject
(https://cs.android.com/android/platform/superproject/main/+/main:libcore/ojluni/src/main/java/java/io/ObjectInputStream.java;l=420;drc=7f1a1070dbdd1bda00223be2f21936f63a8f3850)。
ObjectInputStream::readObject
最后,我们处于反序列化过程的核心,我们可以说我们处于使用该ObjectInputStream::readObject
方法的通用 Java 反序列化机制中。我的好奇心告诉我要更深入地研究 Java 对象序列化流协议(https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html)解析过程,但理性的部分提醒我要坚持客观(剧透:我部分做到了)。不过,如果你愿意,你可以从 ObjectInputStream::readObject
]https://cs.android.com/android/platform/superproject/main/+/main:libcore/ojluni/src/main/java/java/io/ObjectInputStream.java;l=420;drc=7f1a1070dbdd1bda00223be2f21936f63a8f3850)和 开始更深入地研究ObjectInputStream::readObject0
(https://cs.android.com/android/platform/superproject/main/+/main:libcore/ojluni/src/main/java/java/io/ObjectInputStream.java;l=1389;drc=7f1a1070dbdd1bda00223be2f21936f63a8f3850)。
反序列化总结
代码概述让我们得出一个非常简单的结论:输入对象使用通用 JavaObjectInputStream::readObject
机制进行反序列化,并且类加载器包含应用程序和系统java.io.Serialiazible
特定的路径。考虑到这一点,我们现在意识到我们处于一个常见的 Java 反序列化场景中,我们可以实例化实现或接口的系统或应用程序类java.io.Externalizable
。为了创建一个有效的场景,我们应该仅有的需要寻找一个有用的工具吗?
你需要的只是一个好的小工具,对吗?
实例化系统对象非常简单:导入相应的模块并从中创建对象。第三方库对象也是如此,您可以定期导入它们并使用导出的类。但是,如果我们想要从特定应用程序中获取特定类,该怎么办?在这种情况下,情况会略有不同。
应用程序特定的小工具
为了正确地将目标应用程序对象实例化到另一个应用程序中,可以使用动态代码加载和反射。首先,在识别目标对象后,需要提取相应的classesN.dex
文件并将其存储在攻击者应用程序的应用程序资源中(或以任何其他所需的方式)。可以使用 逆向工程目标应用程序来识别相应的 dex 文件jadx-gui
,其中文件名显示在逆向 Java 代码中。然后,apktool
可以使用 直接提取它(apktool --no-src d app.apk
)。
上面的代码展示了如何导入classes.dex
文件并DexClassLoader
从中实例化 [1]。返回的结果ClassLoader
可用于加载类 [2],然后通过方法 [3] 实例化对象。可以使用方法 [4] 和[5]Object.newInstance()
通过已加载的类访问和修改类字段。最后,只需将输入对象强制转换为[6] 即可满足逻辑要求。getDeclaredField``Field.set``Serializable``Intent.putExtra
当然,这不是实现此结果的唯一方法,更隐秘的内存解决方案或完全不同的替代方案(例如原始对象字节)也可能是可行的,但不符合本博文的兴趣。
内部反序列化过程
一旦通过 IPC 从目标应用程序接收到对象,反序列化的对象就只是一串字节(一堆需要解释的 0 和 1,就像计算机科学中的一切一样),而前面提到的 Java对象序列化流协议(https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html)ObjectInputStream::readFile0
规范负责执行此操作 。正如我们所说,我们不会深入探讨这个过程,但有一些有趣的事情值得我们关注:
如果字节流包含一个对象(TC_OBJECT
), readOrdinaryObject
(https://cs.android.com/android/platform/superproject/main/+/main:libcore/ojluni/src/main/java/java/io/ObjectInputStream.java;l=1895;drc=7f1a1070dbdd1bda00223be2f21936f63a8f3850)则调用 [2] ,经过一些验证步骤后,该对象将根据.newInstance
其类型通过 方法来实例化。这.isInstantiable
是一个很好的起点,可以帮助我们理解构造函数选择背后的逻辑:
如果我们搜索对cons
变量 [1] 的写入引用(来自 cs.android.com),我们可以在ObjectStreamClass
构造函数 2 中找到它的定义。 Externalizable
和Serializable
接口均通过无参数构造函数实例化(在 的情况下public
也是如此)4。然而,在 的情况下,返回的构造函数是第一个不可序列化的超类。protected``Serializable``Serializable
回到readOrdinaryObject
上面所示的内容,if/else 条件根据接收到的对象类类型调度解析方法。
#readSerialData
从已知的Serializable
接口开始,让我们从方法中看一下负责处理此类对象的精简版代码 ObjectInputStream::readSerialData
(https://cs.android.com/android/platform/superproject/main/+/main:libcore/ojluni/src/main/java/java/io/ObjectInputStream.java;l=2063;drc=60545d5caebd2d51949000994964458249a234c3):
该对象需要从超类反序列化到子类,因此通过 [1] 获取getClassDataLayout
并循环。在for
循环内部,我们可以识别出两个有趣的调用:.invokeReadObject
[1] 和[2]。如果这两个方法通过反射在序列化类中定义,.invokeReadObjectNoData
则负责调用相应的readObject
或readObjectNoData
方法[3]。
#readExternalData
该readExternalData
方法负责处理 Externalizable
(https://developer.android.com/reference/java/io/Externalizable)接口:
该方法不是调用readObject
或readObjectNoData
,readExternal
而是直接从 本身调用obj
。在这种情况下, 的readExternal
实现是强制的且特定于类的,而Serializable
只是一个标记接口。
#readRecord
该readRecord
方法负责解析记录类型。由于记录是不可变的、以数据为中心的类,因此它们不在我们的关注范围内,因此我们将跳过它的解析。
瞬态类和不可序列化的类
有些类通常与系统资源(套接字、流、线程等)或特定于操作系统和运行时相关,它们是不可序列化的,可以使用 transient
关键字声明。 transient
(https://www.w3schools.com/java/ref_keyword_transient.asp)可防止属性被反序列化,尤其用于防止通过不受保护的long
指针将 Java 反序列化升级为 C++ 内存损坏原语相关的问题( 一个类统治所有:Android 中的 0 天反序列化漏洞(https://www.usenix.org/system/files/conference/woot15/woot15-paper-peles.pdf)和 Android 反序列化漏洞:简史(https://securitylab.github.com/resources/android-deserialization-vulnerabilities/))。如果不可序列化的属性对象(例如未标记implements
接口Serializable
)未标记为transient
并且是类的一部分Serializable
,则仅当从发送方设置了不可序列化的属性时,它才会触发java.io.NotSerializableException
内部攻击Parcel::writeObject0
。否则,接收部分只会接收null
。
概念验证
设想
让我们构建一个概念验证应用程序,它接受一个可序列化的对象getIntent().getSerializible()
并将其强制转换为一个真正的通用类型(例如Activity
)。此外,该应用程序包含以下易受攻击的类,该类实现了一个readObject
允许写入任意内容的任意文件的功能。该类Serializable
从未在整个应用程序中使用过(您还可以注意到它的不可序列化ComponentName
属性):
接收器导出的活动包含以下代码:
漏洞利用
按照之前“应用程序专用小工具”一章中的描述,我们可以提取classesN.dex
目标对象(com.example.serialized.receiver.CustomTargetClass
)的定义位置,并将其导入到我们的应用程序中。使用jadx-gui
和的组合可以轻松完成此任务apktool
。从中jadx-gui
我们可以看到该类的com.example.serialized.receiver.CustomTargetClass
定义位置classes4.dex
(从下面的注释“加载自”):
然后,apktool --no-src d app.apk
我们可以提取classes4.dex
文件并导入到目标应用程序(内部res/raw
)。之后,我们可以动态加载类并设置filename
和content
为任意值:
结果是……
结论
在这篇博文中,我们深入研究了关键和常见getSerializable
API 的反序列化机制,从源代码的角度展示了它的内部结构,并证明了其潜在的安全影响。
原文始发于微信公众号(安全视安):【翻译】<推荐阅读> Android 反序列化深入探究
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论