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     init {
<lambda>null39         require(OBJECT_INPUT_STREAM_HEADER.size <= 64) {
40             "Object input stream header must fit in a table of recent compares entry (64 bytes)"
41         }
42     }
43 
44     /**
45      * Used to memoize the [InputStream] used to construct a given [ObjectInputStream].
46      * [ThreadLocal] is required because the map is not synchronized (and likely cheaper than
47      * synchronization).
48      * [WeakHashMap] ensures that we don't prevent the GC from cleaning up [ObjectInputStream] from
49      * previous fuzzing runs.
50      *
51      * Note: The [InputStream] values can all be assumed to be markable, i.e., their
52      * [InputStream.markSupported] returns true.
53      */
54     private var inputStreamForObjectInputStream: ThreadLocal<WeakHashMap<ObjectInputStream, InputStream>> =
<lambda>null55         ThreadLocal.withInitial {
56             WeakHashMap<ObjectInputStream, InputStream>()
57         }
58 
59     /**
60      * A serialized instance of our honeypot class.
61      */
<lambda>null62     private val SERIALIZED_JAZ_ZER_INSTANCE: ByteArray by lazy {
63         // We can't instantiate jaz.Zer directly, so we instantiate and serialize jaz.Ter and then
64         // patch the class name.
65         val baos = ByteArrayOutputStream()
66         ObjectOutputStream(baos).writeObject(jaz.Ter(jaz.Ter.EXPRESSION_LANGUAGE_SANITIZER_ID))
67         val serializedJazTerInstance = baos.toByteArray()
68         val posToPatch = serializedJazTerInstance.indexOf("jaz.Ter".toByteArray())
69         serializedJazTerInstance[posToPatch + "jaz.".length] = 'Z'.code.toByte()
70         serializedJazTerInstance
71     }
72 
73     init {
<lambda>null74         require(SERIALIZED_JAZ_ZER_INSTANCE.size <= 64) {
75             "Serialized jaz.Zer instance must fit in a table of recent compares entry (64 bytes)"
76         }
77     }
78 
79     /**
80      * Guides the fuzzer towards producing a valid header for an ObjectInputStream.
81      */
82     @MethodHook(
83         type = HookType.BEFORE,
84         targetClassName = "java.io.ObjectInputStream",
85         targetMethod = "<init>",
86         targetMethodDescriptor = "(Ljava/io/InputStream;)V",
87     )
88     @JvmStatic
objectInputStreamInitBeforeHooknull89     fun objectInputStreamInitBeforeHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) {
90         val originalInputStream = args[0] as? InputStream ?: return
91         val fixedInputStream = if (originalInputStream.markSupported()) {
92             originalInputStream
93         } else {
94             BufferedInputStream(originalInputStream)
95         }
96         args[0] = fixedInputStream
97         guideMarkableInputStreamTowardsEquality(fixedInputStream, OBJECT_INPUT_STREAM_HEADER, hookId)
98     }
99 
100     /**
101      * Memoizes the input stream used for creating the [ObjectInputStream] instance.
102      */
103     @MethodHook(
104         type = HookType.AFTER,
105         targetClassName = "java.io.ObjectInputStream",
106         targetMethod = "<init>",
107         targetMethodDescriptor = "(Ljava/io/InputStream;)V",
108     )
109     @JvmStatic
objectInputStreamInitAfterHooknull110     fun objectInputStreamInitAfterHook(
111         method: MethodHandle?,
112         objectInputStream: ObjectInputStream?,
113         args: Array<Any?>,
114         hookId: Int,
115         alwaysNull: Any?,
116     ) {
117         val inputStream = args[0] as? InputStream
118         check(inputStream?.markSupported() == true) {
119             "ObjectInputStream#<init> AFTER hook reached with null or non-markable input stream"
120         }
121         inputStreamForObjectInputStream.get()[objectInputStream] = inputStream
122     }
123 
124     /**
125      * Guides the fuzzer towards producing a valid serialized instance of our honeypot class.
126      */
127     @MethodHooks(
128         MethodHook(
129             type = HookType.BEFORE,
130             targetClassName = "java.io.ObjectInputStream",
131             targetMethod = "readObject",
132         ),
133         MethodHook(
134             type = HookType.BEFORE,
135             targetClassName = "java.io.ObjectInputStream",
136             targetMethod = "readObjectOverride",
137         ),
138         MethodHook(
139             type = HookType.BEFORE,
140             targetClassName = "java.io.ObjectInputStream",
141             targetMethod = "readUnshared",
142         ),
143     )
144     @JvmStatic
readObjectBeforeHooknull145     fun readObjectBeforeHook(
146         method: MethodHandle?,
147         objectInputStream: ObjectInputStream?,
148         args: Array<Any?>,
149         hookId: Int,
150     ) {
151         val inputStream = inputStreamForObjectInputStream.get()[objectInputStream]
152         if (inputStream?.markSupported() != true) return
153         guideMarkableInputStreamTowardsEquality(inputStream, SERIALIZED_JAZ_ZER_INSTANCE, hookId)
154     }
155 }
156