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