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