• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2021 Code Intelligence GmbH
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 package com.code_intelligence.jazzer.sanitizers
16 
17 import com.code_intelligence.jazzer.api.HookType
18 import com.code_intelligence.jazzer.api.MethodHook
19 import com.code_intelligence.jazzer.api.MethodHooks
20 import java.io.BufferedInputStream
21 import java.io.ByteArrayOutputStream
22 import java.io.InputStream
23 import java.io.ObjectInputStream
24 import java.io.ObjectOutputStream
25 import java.io.ObjectStreamConstants
26 import java.lang.invoke.MethodHandle
27 import java.util.WeakHashMap
28 
29 /**
30  * Detects unsafe deserialization that leads to attacker-controlled method calls, in particular to [Object.finalize].
31  */
32 @Suppress("unused_parameter", "unused")
33 object Deserialization {
34 
35     private val OBJECT_INPUT_STREAM_HEADER =
36         ObjectStreamConstants.STREAM_MAGIC.toBytes() + ObjectStreamConstants.STREAM_VERSION.toBytes()
37 
38     /**
39      * Used to memoize the [InputStream] used to construct a given [ObjectInputStream].
40      * [ThreadLocal] is required because the map is not synchronized (and likely cheaper than
41      * synchronization).
42      * [WeakHashMap] ensures that we don't prevent the GC from cleaning up [ObjectInputStream] from
43      * previous fuzzing runs.
44      *
45      * Note: The [InputStream] values can all be assumed to be markable, i.e., their
46      * [InputStream.markSupported] returns true.
47      */
48     private var inputStreamForObjectInputStream: ThreadLocal<WeakHashMap<ObjectInputStream, InputStream>> =
<lambda>null49         ThreadLocal.withInitial {
50             WeakHashMap<ObjectInputStream, InputStream>()
51         }
52 
53     /**
54      * A serialized instance of our honeypot class.
55      */
<lambda>null56     private val SERIALIZED_JAZ_ZER_INSTANCE: ByteArray by lazy {
57         // We can't instantiate jaz.Zer directly, so we instantiate and serialize jaz.Ter and then
58         // patch the class name.
59         val baos = ByteArrayOutputStream()
60         ObjectOutputStream(baos).writeObject(jaz.Ter())
61         val serializedJazTerInstance = baos.toByteArray()
62         val posToPatch = serializedJazTerInstance.indexOf("jaz.Ter".toByteArray())
63         serializedJazTerInstance[posToPatch + "jaz.".length] = 'Z'.code.toByte()
64         serializedJazTerInstance
65     }
66 
67     /**
68      * Guides the fuzzer towards producing a valid header for an ObjectInputStream.
69      */
70     @MethodHook(
71         type = HookType.BEFORE,
72         targetClassName = "java.io.ObjectInputStream",
73         targetMethod = "<init>",
74         targetMethodDescriptor = "(Ljava/io/InputStream;)V"
75     )
76     @JvmStatic
objectInputStreamInitBeforeHooknull77     fun objectInputStreamInitBeforeHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) {
78         val originalInputStream = args[0] as? InputStream ?: return
79         val fixedInputStream = if (originalInputStream.markSupported())
80             originalInputStream
81         else
82             BufferedInputStream(originalInputStream)
83         args[0] = fixedInputStream
84         guideMarkableInputStreamTowardsEquality(fixedInputStream, OBJECT_INPUT_STREAM_HEADER, hookId)
85     }
86 
87     /**
88      * Memoizes the input stream used for creating the [ObjectInputStream] instance.
89      */
90     @MethodHook(
91         type = HookType.AFTER,
92         targetClassName = "java.io.ObjectInputStream",
93         targetMethod = "<init>",
94         targetMethodDescriptor = "(Ljava/io/InputStream;)V"
95     )
96     @JvmStatic
objectInputStreamInitAfterHooknull97     fun objectInputStreamInitAfterHook(
98         method: MethodHandle?,
99         objectInputStream: ObjectInputStream?,
100         args: Array<Any?>,
101         hookId: Int,
102         alwaysNull: Any?,
103     ) {
104         val inputStream = args[0] as? InputStream
105         check(inputStream?.markSupported() == true) {
106             "ObjectInputStream#<init> AFTER hook reached with null or non-markable input stream"
107         }
108         inputStreamForObjectInputStream.get()[objectInputStream] = inputStream
109     }
110 
111     /**
112      * Guides the fuzzer towards producing a valid serialized instance of our honeypot class.
113      */
114     @MethodHooks(
115         MethodHook(
116             type = HookType.BEFORE,
117             targetClassName = "java.io.ObjectInputStream",
118             targetMethod = "readObject"
119         ),
120         MethodHook(
121             type = HookType.BEFORE,
122             targetClassName = "java.io.ObjectInputStream",
123             targetMethod = "readObjectOverride"
124         ),
125         MethodHook(
126             type = HookType.BEFORE,
127             targetClassName = "java.io.ObjectInputStream",
128             targetMethod = "readUnshared"
129         ),
130     )
131     @JvmStatic
readObjectBeforeHooknull132     fun readObjectBeforeHook(
133         method: MethodHandle?,
134         objectInputStream: ObjectInputStream?,
135         args: Array<Any?>,
136         hookId: Int,
137     ) {
138         val inputStream = inputStreamForObjectInputStream.get()[objectInputStream]
139         if (inputStream?.markSupported() != true) return
140         guideMarkableInputStreamTowardsEquality(inputStream, SERIALIZED_JAZ_ZER_INSTANCE, hookId)
141     }
142 
143     /**
144      * Calls [Object.finalize] early if the returned object is [jaz.Zer]. A call to finalize is
145      * guaranteed to happen at some point, but calling it early means that we can accurately report
146      * the input that lead to its execution.
147      */
148     @MethodHooks(
149         MethodHook(type = HookType.AFTER, targetClassName = "java.io.ObjectInputStream", targetMethod = "readObject"),
150         MethodHook(type = HookType.AFTER, targetClassName = "java.io.ObjectInputStream", targetMethod = "readObjectOverride"),
151         MethodHook(type = HookType.AFTER, targetClassName = "java.io.ObjectInputStream", targetMethod = "readUnshared"),
152     )
153     @JvmStatic
readObjectAfterHooknull154     fun readObjectAfterHook(
155         method: MethodHandle?,
156         objectInputStream: ObjectInputStream?,
157         args: Array<Any?>,
158         hookId: Int,
159         deserializedObject: Any?,
160     ) {
161         if (deserializedObject?.javaClass?.name == HONEYPOT_CLASS_NAME) {
162             deserializedObject.javaClass.getDeclaredMethod("finalize").run {
163                 isAccessible = true
164                 invoke(deserializedObject)
165             }
166         }
167     }
168 }
169