diff options
Diffstat (limited to 'sanitizers/src/main/java/com/code_intelligence/jazzer')
13 files changed, 937 insertions, 48 deletions
diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel index 65480653..1b156f9e 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel @@ -1,17 +1,34 @@ load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") +java_library( + name = "regex_roadblocks", + srcs = ["RegexRoadblocks.java"], + deps = [ + "//agent:jazzer_api_compile_only", + "//agent/src/main/java/com/code_intelligence/jazzer/runtime:unsafe_provider", + "//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils:reflection_utils", + ], +) + kt_jvm_library( name = "sanitizers", srcs = [ "Deserialization.kt", "ExpressionLanguageInjection.kt", + "LdapInjection.kt", "NamingContextLookup.kt", + "OsCommandInjection.kt", "ReflectiveCall.kt", + "RegexInjection.kt", + "SqlInjection.kt", "Utils.kt", ], visibility = ["//sanitizers:__pkg__"], + runtime_deps = [ + ":regex_roadblocks", + ], deps = [ "//agent:jazzer_api_compile_only", - "//sanitizers/src/main/java/jaz", + "@maven//:com_github_jsqlparser_jsqlparser", ], ) diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt index f6401dfd..55691c1a 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt @@ -29,7 +29,7 @@ import java.util.WeakHashMap /** * Detects unsafe deserialization that leads to attacker-controlled method calls, in particular to [Object.finalize]. */ -@Suppress("unused_parameter") +@Suppress("unused_parameter", "unused") object Deserialization { private val OBJECT_INPUT_STREAM_HEADER = diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt index 9b1e8ca6..1dc1d5f0 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt @@ -24,7 +24,7 @@ import java.lang.invoke.MethodHandle /** * Detects injectable inputs to an expression language interpreter which may lead to remote code execution. */ -@Suppress("unused_parameter") +@Suppress("unused_parameter", "unused") object ExpressionLanguageInjection { /** @@ -44,6 +44,16 @@ object ExpressionLanguageInjection { targetClassName = "javax.el.ExpressionFactory", targetMethod = "createMethodExpression", ), + MethodHook( + type = HookType.BEFORE, + targetClassName = "jakarta.el.ExpressionFactory", + targetMethod = "createValueExpression", + ), + MethodHook( + type = HookType.BEFORE, + targetClassName = "jakarta.el.ExpressionFactory", + targetMethod = "createMethodExpression", + ), ) @JvmStatic fun hookElExpressionFactory( @@ -52,10 +62,8 @@ object ExpressionLanguageInjection { arguments: Array<Any>, hookId: Int ) { - if (arguments[1] is String) { - val expression = arguments[1] as String - Jazzer.guideTowardsContainment(expression, EXPRESSION_LANGUAGE_ATTACK, hookId) - } + val expression = arguments[1] as? String ?: return + Jazzer.guideTowardsContainment(expression, EXPRESSION_LANGUAGE_ATTACK, hookId) } // With default configurations the argument to diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt new file mode 100644 index 00000000..1afd614e --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/LdapInjection.kt @@ -0,0 +1,123 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.sanitizers + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical +import com.code_intelligence.jazzer.api.HookType +import com.code_intelligence.jazzer.api.Jazzer +import com.code_intelligence.jazzer.api.MethodHook +import com.code_intelligence.jazzer.api.MethodHooks +import java.lang.Exception +import java.lang.invoke.MethodHandle +import javax.naming.NamingException +import javax.naming.directory.InvalidSearchFilterException + +/** + * Detects LDAP DN and search filter injections. + * + * Untrusted input has to be escaped in such a way that queries remain valid otherwise an injection + * could be possible. This sanitizer guides the fuzzer to inject insecure characters. If an exception + * is raised during execution the fuzzer was able to inject an invalid pattern, otherwise all input + * was escaped correctly. + * + * Only the search methods are hooked, other methods are not used in injection attacks. Furthermore, + * only string parameters are checked, [javax.naming.Name] already validates inputs according to RFC2253. + * + * [javax.naming.directory.InitialDirContext] creates an initial context through the context factory + * stated in [javax.naming.Context.INITIAL_CONTEXT_FACTORY]. Other method calls are delegated to the + * initial context factory of type [javax.naming.directory.DirContext]. This is also the case for + * subclass [javax.naming.ldap.InitialLdapContext]. + */ +@Suppress("unused_parameter", "unused") +object LdapInjection { + + // Characters to escape in DNs + private const val NAME_CHARACTERS = "\\+<>,;\"=" + + // Characters to escape in search filter queries + private const val FILTER_CHARACTERS = "*()\\\u0000" + + @MethodHooks( + // Single object lookup, possible DN injection + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.directory.DirContext", + targetMethod = "search", + targetMethodDescriptor = "(Ljava/lang/String;Ljavax/naming.directory/Attributes;)Ljavax/naming/NamingEnumeration;", + additionalClassesToHook = ["javax.naming.directory.InitialDirContext"] + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.directory.DirContext", + targetMethod = "search", + targetMethodDescriptor = "(Ljava/lang/String;Ljavax/naming.directory/Attributes;[Ljava/lang/Sting;)Ljavax/naming/NamingEnumeration;", + additionalClassesToHook = ["javax.naming.directory.InitialDirContext"] + ), + + // Object search, possible DN and search filter injection + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.directory.DirContext", + targetMethod = "search", + targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/String;Ljavax/naming/directory/SearchControls;)Ljavax/naming/NamingEnumeration;", + additionalClassesToHook = ["javax.naming.directory.InitialDirContext"] + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.directory.DirContext", + targetMethod = "search", + targetMethodDescriptor = "(Ljavax/naming/Name;Ljava/lang/String;[Ljava.lang.Object;Ljavax/naming/directory/SearchControls;)Ljavax/naming/NamingEnumeration;", + additionalClassesToHook = ["javax.naming.directory.InitialDirContext"] + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.directory.DirContext", + targetMethod = "search", + targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;Ljavax/naming/directory/SearchControls;)Ljavax/naming/NamingEnumeration;", + additionalClassesToHook = ["javax.naming.directory.InitialDirContext"] + ) + ) + @JvmStatic + fun searchLdapContext(method: MethodHandle, thisObject: Any?, args: Array<Any>, hookId: Int): Any? { + try { + return method.invokeWithArguments(thisObject, *args).also { + (args[0] as? String)?.let { name -> + Jazzer.guideTowardsEquality(name, NAME_CHARACTERS, hookId) + } + (args[1] as? String)?.let { filter -> + Jazzer.guideTowardsEquality(filter, FILTER_CHARACTERS, 31 * hookId) + } + } + } catch (e: Exception) { + when (e) { + is InvalidSearchFilterException -> + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueCritical( + """LDAP Injection +Search filters based on untrusted data must be escape as specified in RFC 4515.""" + ) + ) + is NamingException -> + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueCritical( + """LDAP Injection +Distinguished Names based on untrusted data must be escaped as specified in RFC 2253.""" + ) + ) + } + throw e + } + } +} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt index 2d4fb9cf..56e12f03 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt @@ -22,6 +22,7 @@ import com.code_intelligence.jazzer.api.MethodHooks import java.lang.invoke.MethodHandle import javax.naming.CommunicationException +@Suppress("unused") object NamingContextLookup { // The particular URL g.co is used here since it is: @@ -31,6 +32,7 @@ object NamingContextLookup { private const val LDAP_MARKER = "ldap://g.co/" private const val RMI_MARKER = "rmi://g.co/" + @Suppress("UNUSED_PARAMETER") @MethodHooks( MethodHook( type = HookType.REPLACE, @@ -40,46 +42,10 @@ object NamingContextLookup { ), MethodHook( type = HookType.REPLACE, - targetClassName = "javax.naming.InitialContext", - targetMethod = "lookup", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.naming.InitialDirContext", - targetMethod = "lookup", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.naming.InitialLdapContext", - targetMethod = "lookup", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), - MethodHook( - type = HookType.REPLACE, targetClassName = "javax.naming.Context", targetMethod = "lookupLink", targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", ), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.naming.InitialContext", - targetMethod = "lookupLink", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.naming.InitialDirContext", - targetMethod = "lookupLink", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), - MethodHook( - type = HookType.REPLACE, - targetClassName = "javax.naming.InitialLdapContext", - targetMethod = "lookupLink", - targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", - ), ) @JvmStatic fun lookupHook(method: MethodHandle?, thisObject: Any?, args: Array<Any?>, hookId: Int): Any { diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt new file mode 100644 index 00000000..d3adc207 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/OsCommandInjection.kt @@ -0,0 +1,61 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.sanitizers + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical +import com.code_intelligence.jazzer.api.HookType +import com.code_intelligence.jazzer.api.Jazzer +import com.code_intelligence.jazzer.api.MethodHook +import java.lang.invoke.MethodHandle + +/** + * Detects unsafe execution of OS commands using [ProcessBuilder]. + * Executing OS commands based on attacker-controlled data could lead to arbitrary could execution. + * + * All public methods providing the command to execute end up in [java.lang.ProcessImpl.start], + * so calls to this method are hooked. + * Only the first entry of the given command array is analyzed. It states the executable and must + * not include attacker provided data. + */ +@Suppress("unused_parameter", "unused") +object OsCommandInjection { + + // Short and probably non-existing command name + private const val COMMAND = "jazze" + + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.lang.ProcessImpl", + targetMethod = "start", + additionalClassesToHook = ["java.lang.ProcessBuilder"] + ) + @JvmStatic + fun processImplStartHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + // Calling ProcessBuilder already checks if command array is empty + @Suppress("UNCHECKED_CAST") + (args[0] as? Array<String>)?.first().let { cmd -> + if (cmd == COMMAND) { + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueCritical( + """OS Command Injection +Executing OS commands with attacker-controlled data can lead to remote code execution.""" + ) + ) + } else { + Jazzer.guideTowardsEquality(cmd, COMMAND, hookId) + } + } + } +} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt index 7842d879..0fcabe36 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt @@ -14,21 +14,59 @@ package com.code_intelligence.jazzer.sanitizers +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh import com.code_intelligence.jazzer.api.HookType import com.code_intelligence.jazzer.api.Jazzer import com.code_intelligence.jazzer.api.MethodHook +import com.code_intelligence.jazzer.api.MethodHooks import java.lang.invoke.MethodHandle /** - * Detects unsafe reflective calls that lead to attacker-controlled method calls. + * Detects unsafe calls that lead to attacker-controlled class loading. + * + * Guide the fuzzer to load honeypot class via [Class.forName] or [ClassLoader.loadClass]. */ -@Suppress("unused_parameter") +@Suppress("unused_parameter", "unused") object ReflectiveCall { - @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Class", targetMethod = "forName") + @MethodHooks( + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Class", targetMethod = "forName", targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Class;"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Class", targetMethod = "forName", targetMethodDescriptor = "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.ClassLoader", targetMethod = "loadClass", targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Class;"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.ClassLoader", targetMethod = "loadClass", targetMethodDescriptor = "(Ljava/lang/String;Z)Ljava/lang/Class;"), + ) @JvmStatic - fun classForNameHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + fun loadClassHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { val className = args[0] as? String ?: return Jazzer.guideTowardsEquality(className, HONEYPOT_CLASS_NAME, hookId) } + + @MethodHooks( + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Class", targetMethod = "forName", targetMethodDescriptor = "(Ljava/lang/Module;Ljava/lang/String;)Ljava/lang/Class;"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.ClassLoader", targetMethod = "loadClass", targetMethodDescriptor = "(Ljava/lang/Module;Ljava/lang/String;)Ljava/lang/Class;"), + ) + @JvmStatic + fun loadClassWithModuleHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + val className = args[1] as? String ?: return + Jazzer.guideTowardsEquality(className, HONEYPOT_CLASS_NAME, hookId) + } + + @MethodHooks( + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Runtime", targetMethod = "load"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Runtime", targetMethod = "loadLibrary"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System", targetMethod = "load"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System", targetMethod = "loadLibrary"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.System", targetMethod = "mapLibraryName"), + MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.ClassLoader", targetMethod = "findLibrary"), + ) + @JvmStatic + fun loadLibraryHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + val libraryName = args[0] as? String ?: return + if (libraryName == HONEYPOT_LIBRARY_NAME) { + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueHigh("load arbitrary library") + ) + } + Jazzer.guideTowardsEquality(libraryName, HONEYPOT_LIBRARY_NAME, hookId) + } } diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt new file mode 100644 index 00000000..def5f6e3 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexInjection.kt @@ -0,0 +1,160 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.sanitizers + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueLow +import com.code_intelligence.jazzer.api.HookType +import com.code_intelligence.jazzer.api.Jazzer +import com.code_intelligence.jazzer.api.MethodHook +import com.code_intelligence.jazzer.api.MethodHooks +import java.lang.invoke.MethodHandle +import java.util.regex.Pattern +import java.util.regex.PatternSyntaxException + +@Suppress("unused_parameter", "unused") +object RegexInjection { + /** + * Part of an OOM "exploit" for [java.util.regex.Pattern.compile] with the + * [java.util.regex.Pattern.CANON_EQ] flag, formed by three consecutive combining marks, in this + * case grave accents: ◌̀. + * See [compileWithFlagsHook] for details. + */ + private const val CANON_EQ_ALMOST_EXPLOIT = "\u0300\u0300\u0300" + + /** + * When injected into a regex pattern, helps the fuzzer break out of quotes and character + * classes in order to cause a [PatternSyntaxException]. + */ + private const val FORCE_PATTERN_SYNTAX_EXCEPTION_PATTERN = "\\E]\\E]]]]]]" + + @MethodHook( + type = HookType.REPLACE, + targetClassName = "java.util.regex.Pattern", + targetMethod = "compile", + targetMethodDescriptor = "(Ljava/lang/String;I)Ljava/util/regex/Pattern;" + ) + @JvmStatic + fun compileWithFlagsHook(method: MethodHandle, alwaysNull: Any?, args: Array<Any?>, hookId: Int): Any? { + val pattern = args[0] as String? + val hasCanonEqFlag = ((args[1] as Int) and Pattern.CANON_EQ) != 0 + return hookInternal(method, pattern, hasCanonEqFlag, hookId, *args) + } + + @MethodHooks( + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.util.regex.Pattern", + targetMethod = "compile", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/util/regex/Pattern;" + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.util.regex.Pattern", + targetMethod = "matches", + targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/CharSequence;)Z" + ), + ) + @JvmStatic + fun patternHook(method: MethodHandle, alwaysNull: Any?, args: Array<Any?>, hookId: Int): Any? { + return hookInternal(method, args[0] as String?, false, hookId, *args) + } + + @MethodHooks( + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.lang.String", + targetMethod = "matches", + targetMethodDescriptor = "(Ljava/lang/String;)Z", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.lang.String", + targetMethod = "replaceAll", + targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.lang.String", + targetMethod = "replaceFirst", + targetMethodDescriptor = "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.lang.String", + targetMethod = "split", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/String;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "java.lang.String", + targetMethod = "split", + targetMethodDescriptor = "(Ljava/lang/String;I)Ljava/lang/String;", + ), + ) + @JvmStatic + fun stringHook(method: MethodHandle, thisObject: Any?, args: Array<Any?>, hookId: Int): Any? { + return hookInternal(method, args[0] as String?, false, hookId, thisObject, *args) + } + + private fun hookInternal( + method: MethodHandle, + pattern: String?, + hasCanonEqFlag: Boolean, + hookId: Int, + vararg args: Any? + ): Any? { + if (hasCanonEqFlag && pattern != null) { + // With CANON_EQ enabled, Pattern.compile allocates an array with a size that is + // (super-)exponential in the number of consecutive Unicode combining marks. We use a mild case + // of this as a magic string based on which we trigger a finding. + // Note: The fuzzer might trigger an OutOfMemoryError or NegativeArraySizeException (if the size + // of the array overflows an int) by chance before it correctly emits this "exploit". In that + // case, we report the original exception instead. + if (pattern.contains(CANON_EQ_ALMOST_EXPLOIT)) { + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueLow( + """Regular Expression Injection with CANON_EQ +When java.util.regex.Pattern.compile is used with the Pattern.CANON_EQ flag, +every injection into the regular expression pattern can cause arbitrarily large +memory allocations, even when wrapped with Pattern.quote(...).""" + ) + ) + } else { + Jazzer.guideTowardsContainment(pattern, CANON_EQ_ALMOST_EXPLOIT, hookId) + } + } + try { + return method.invokeWithArguments(*args).also { + // Only submit a fuzzer hint if no exception has been thrown. + if (!hasCanonEqFlag && pattern != null) { + Jazzer.guideTowardsContainment(pattern, FORCE_PATTERN_SYNTAX_EXCEPTION_PATTERN, hookId) + } + } + } catch (e: Exception) { + if (e is PatternSyntaxException) { + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueLow( + """Regular Expression Injection +Regular expression patterns that contain unescaped untrusted input can consume +arbitrary amounts of CPU time. To properly escape the input, wrap it with +Pattern.quote(...).""", + e + ) + ) + } + throw e + } + } +} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java new file mode 100644 index 00000000..1043ac02 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/RegexRoadblocks.java @@ -0,0 +1,322 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.sanitizers; + +import static com.code_intelligence.jazzer.sanitizers.utils.ReflectionUtils.INVALID_OFFSET; +import static com.code_intelligence.jazzer.sanitizers.utils.ReflectionUtils.field; +import static com.code_intelligence.jazzer.sanitizers.utils.ReflectionUtils.nestedClass; +import static com.code_intelligence.jazzer.sanitizers.utils.ReflectionUtils.offset; + +import com.code_intelligence.jazzer.api.HookType; +import com.code_intelligence.jazzer.api.Jazzer; +import com.code_intelligence.jazzer.api.MethodHook; +import com.code_intelligence.jazzer.runtime.UnsafeProvider; +import java.lang.invoke.MethodHandle; +import java.util.WeakHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import sun.misc.Unsafe; + +/** + * The hooks in this class extend the reach of Jazzer's string compare instrumentation to literals + * (both strings and characters) that are part of regular expression patterns. + * <p> + * Internally, the Java standard library represents a compiled regular expression as a graph of + * instances of Pattern$Node instances, each of which represents a single unit of the full + * expression and provides a `match` function that takes a {@link Matcher}, a {@link CharSequence} + * to match against and an index into the sequence. With a hook on this method for every subclass of + * Pattern$Node, the contents of the node can be inspected and an appropriate string comparison + * between the relevant part of the input string and the literal string can be reported. + */ +public final class RegexRoadblocks { + // The number of characters preceding one that failed a character predicate to include in the + // reported string comparison. + private static final int CHARACTER_COMPARE_CONTEXT_LENGTH = 10; + + private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe(); + private static final Class<?> SLICE_NODE = nestedClass(Pattern.class, "SliceNode"); + private static final long SLICE_NODE_BUFFER_OFFSET = + offset(UNSAFE, field(SLICE_NODE, "buffer", int[].class)); + private static final Class<?> CHAR_PREDICATE = nestedClass(Pattern.class, "CharPredicate"); + private static final Class<?> CHAR_PROPERTY = nestedClass(Pattern.class, "CharProperty"); + private static final long CHAR_PROPERTY_PREDICATE_OFFSET = offset( + UNSAFE, field(CHAR_PROPERTY, "predicate", nestedClass(Pattern.class, "CharPredicate"))); + private static final Class<?> BIT_CLASS = nestedClass(Pattern.class, "BitClass"); + private static final long BIT_CLASS_BITS_OFFSET = + offset(UNSAFE, field(BIT_CLASS, "bits", boolean[].class)); + + // Weakly map CharPredicate instances to characters that satisfy the predicate. Since + // CharPredicate instances are usually lambdas, we collect their solutions by hooking the + // functions constructing them rather than extracting the solutions via reflection. + // Note: Java 8 uses anonymous subclasses of CharProperty instead of lambdas implementing + // CharPredicate, hence CharProperty instances are used as keys instead in that case. + private static final ThreadLocal<WeakHashMap<Object, Character>> PREDICATE_SOLUTIONS = + ThreadLocal.withInitial(WeakHashMap::new); + + // Do not act on instrumented regexes used by Jazzer internally, e.g. by ClassGraph. + private static boolean HOOK_DISABLED = true; + + static { + Jazzer.onFuzzTargetReady(() -> HOOK_DISABLED = UNSAFE == null); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$Node", + targetMethod = "match", + targetMethodDescriptor = "(Ljava/util/regex/Matcher;ILjava/lang/CharSequence;)Z", + additionalClassesToHook = + { + "java.util.regex.Matcher", + "java.util.regex.Pattern$BackRef", + "java.util.regex.Pattern$Behind", + "java.util.regex.Pattern$BehindS", + "java.util.regex.Pattern$BmpCharProperty", + "java.util.regex.Pattern$BmpCharPropertyGreedy", + "java.util.regex.Pattern$BnM", + "java.util.regex.Pattern$BnMS", + "java.util.regex.Pattern$Bound", + "java.util.regex.Pattern$Branch", + "java.util.regex.Pattern$BranchConn", + "java.util.regex.Pattern$CharProperty", + "java.util.regex.Pattern$CharPropertyGreedy", + "java.util.regex.Pattern$CIBackRef", + "java.util.regex.Pattern$Caret", + "java.util.regex.Pattern$Curly", + "java.util.regex.Pattern$Conditional", + "java.util.regex.Pattern$First", + "java.util.regex.Pattern$GraphemeBound", + "java.util.regex.Pattern$GroupCurly", + "java.util.regex.Pattern$GroupHead", + "java.util.regex.Pattern$GroupRef", + "java.util.regex.Pattern$LastMatch", + "java.util.regex.Pattern$LazyLoop", + "java.util.regex.Pattern$LineEnding", + "java.util.regex.Pattern$Loop", + "java.util.regex.Pattern$Neg", + "java.util.regex.Pattern$NFCCharProperty", + "java.util.regex.Pattern$NotBehind", + "java.util.regex.Pattern$NotBehindS", + "java.util.regex.Pattern$Pos", + "java.util.regex.Pattern$Ques", + "java.util.regex.Pattern$Slice", + "java.util.regex.Pattern$SliceI", + "java.util.regex.Pattern$SliceIS", + "java.util.regex.Pattern$SliceS", + "java.util.regex.Pattern$SliceU", + "java.util.regex.Pattern$Start", + "java.util.regex.Pattern$StartS", + "java.util.regex.Pattern$UnixCaret", + "java.util.regex.Pattern$UnixDollar", + "java.util.regex.Pattern$XGrapheme", + }) + public static void + nodeMatchHook(MethodHandle method, Object node, Object[] args, int hookId, Boolean matched) { + if (HOOK_DISABLED || matched || node == null) + return; + Matcher matcher = (Matcher) args[0]; + if (matcher == null) + return; + int i = (int) args[1]; + CharSequence seq = (CharSequence) args[2]; + if (seq == null) + return; + + if (SLICE_NODE != null && SLICE_NODE.isInstance(node)) { + // The node encodes a match against a fixed string literal. Extract the literal and report a + // comparison between it and the subsequence of seq starting at i. + if (SLICE_NODE_BUFFER_OFFSET == INVALID_OFFSET) + return; + int currentLength = limitedLength(matcher.regionEnd() - i); + String current = seq.subSequence(i, i + currentLength).toString(); + + // All the subclasses of SliceNode store the literal in an int[], which we have to truncate to + // a char[]. + int[] buffer = (int[]) UNSAFE.getObject(node, SLICE_NODE_BUFFER_OFFSET); + char[] charBuffer = new char[limitedLength(buffer.length)]; + for (int j = 0; j < charBuffer.length; j++) { + charBuffer[j] = (char) buffer[j]; + } + String target = new String(charBuffer); + + Jazzer.guideTowardsEquality(current, target, perRegexId(hookId, matcher)); + } else if (CHAR_PROPERTY != null && CHAR_PROPERTY.isInstance(node)) { + // The node encodes a match against a class of characters, which may be hard to guess unicode + // characters. We rely on further hooks to track the relation between these nodes and + // characters satisfying their match function since the nodes themselves encode this + // information in lambdas, which are difficult to dissect via reflection. If we know a + // matching character, report a one-character (plus context) string comparison. + Object solutionKey; + if (CHAR_PROPERTY_PREDICATE_OFFSET == INVALID_OFFSET) { + if (CHAR_PREDICATE == null) { + // We are likely running against JDK 8, which directly construct subclasses of + // CharProperty rather than using lambdas implementing CharPredicate. + solutionKey = node; + } else { + return; + } + } else { + solutionKey = UNSAFE.getObject(node, CHAR_PROPERTY_PREDICATE_OFFSET); + } + if (solutionKey == null) + return; + Character solution = predicateSolution(solutionKey); + if (solution == null) + return; + // We report a string comparison rather than an integer comparison for two reasons: + // 1. If the characters are four byte codepoints, they will be coded on six bytes (a surrogate + // pair) in CESU-8, which is the encoding assumed for the fuzzer input, whereas ASCII + // characters will be coded on a single byte. By using the string compare hook, we do not + // have to worry about the encoding at this point. + // 2. The same character can appear multiple times in both the pattern and the matched string, + // which makes it harder for the fuzzer to determine the correct position to mutate the + // current character into the matching character. By providing a short section of the + // input string preceding the incorrect character, we increase the chance of a hit. + String context = + seq.subSequence(Math.max(0, i - CHARACTER_COMPARE_CONTEXT_LENGTH), i).toString(); + String current = seq.subSequence(i, Math.min(i + 1, matcher.regionEnd())).toString(); + String target = Character.toString(solution); + Jazzer.guideTowardsEquality(context + current, context + target, perRegexId(hookId, matcher)); + } + } + + // This and all following hooks track the relation between a CharPredicate or CharProperty + // instance and a character that matches it. We use an after hook on the factory methods so that + // we have access to the parameters and the created instance at the same time. + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "Single", + targetMethodDescriptor = "(I)Ljava/util/regex/Pattern$BmpCharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "SingleI", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "SingleS", + targetMethodDescriptor = "(I)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "SingleU", + targetMethodDescriptor = "(I)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + public static void + singleHook(MethodHandle method, Object node, Object[] args, int hookId, Object predicate) { + if (HOOK_DISABLED || predicate == null) + return; + PREDICATE_SOLUTIONS.get().put(predicate, (char) (int) args[0]); + } + + // Java 8 uses classes extending CharProperty instead of lambdas implementing CharPredicate to + // match single characters, so also hook those. + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$Single", + targetMethod = "<init>", additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$SingleI", + targetMethod = "<init>", additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$SingleS", + targetMethod = "<init>", additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$SingleU", + targetMethod = "<init>", additionalClassesToHook = {"java.util.regex.Pattern"}) + public static void + java8SingleHook( + MethodHandle method, Object property, Object[] args, int hookId, Object alwaysNull) { + if (HOOK_DISABLED || property == null) + return; + PREDICATE_SOLUTIONS.get().put(property, (char) (int) args[0]); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "Range", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "CIRange", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "CIRangeU", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + // Java 8 uses anonymous classes extending CharProperty instead of lambdas implementing + // CharPredicate to match single characters, so also hook those. + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "rangeFor", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharProperty;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "caseInsensitiveRangeFor", + targetMethodDescriptor = "(II)Ljava/util/regex/Pattern$CharProperty;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + public static void + rangeHook(MethodHandle method, Object node, Object[] args, int hookId, Object predicate) { + if (HOOK_DISABLED || predicate == null) + return; + PREDICATE_SOLUTIONS.get().put(predicate, (char) (int) args[0]); + } + + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern$CharPredicate", + targetMethod = "union", + targetMethodDescriptor = + "(Ljava/util/regex/Pattern$CharPredicate;)Ljava/util/regex/Pattern$CharPredicate;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + // Java 8 uses anonymous classes extending CharProperty instead of lambdas implementing + // CharPredicate to match single characters, so also hook union for those. Even though the classes + // of the parameters will be different, the actual implementation of the hook is the same in this + // case. + @MethodHook(type = HookType.AFTER, targetClassName = "java.util.regex.Pattern", + targetMethod = "union", + targetMethodDescriptor = + "(Ljava/util/regex/Pattern$CharProperty;Ljava/util/regex/Pattern$CharProperty;)Ljava/util/regex/Pattern$CharProperty;", + additionalClassesToHook = {"java.util.regex.Pattern"}) + public static void + unionHook( + MethodHandle method, Object thisObject, Object[] args, int hookId, Object unionPredicate) { + if (HOOK_DISABLED || unionPredicate == null) + return; + Character solution = predicateSolution(thisObject); + if (solution == null) + solution = predicateSolution(args[0]); + if (solution == null) + return; + PREDICATE_SOLUTIONS.get().put(unionPredicate, solution); + } + + private static Character predicateSolution(Object charPredicate) { + return PREDICATE_SOLUTIONS.get().computeIfAbsent(charPredicate, unused -> { + if (BIT_CLASS != null && BIT_CLASS.isInstance(charPredicate)) { + // BitClass instances have an empty bits array at construction time, so we scan their + // constants lazily when needed. + boolean[] bits = (boolean[]) UNSAFE.getObject(charPredicate, BIT_CLASS_BITS_OFFSET); + for (int i = 0; i < bits.length; i++) { + if (bits[i]) { + PREDICATE_SOLUTIONS.get().put(charPredicate, (char) i); + return (char) i; + } + } + } + return null; + }); + } + + // Limits a length to the maximum length libFuzzer will read up to in a callback. + private static int limitedLength(int length) { + return Math.min(length, 64); + } + + // hookId only takes one distinct value per Node subclass. In order to get different regex matches + // to be tracked similar to different instances of string compares, we mix in the hash of the + // underlying pattern. We expect patterns to be static almost always, so that this should not fill + // up the value profile map too quickly. + private static int perRegexId(int hookId, Matcher matcher) { + return hookId ^ matcher.pattern().toString().hashCode(); + } +} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.kt new file mode 100644 index 00000000..f317bcc8 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/SqlInjection.kt @@ -0,0 +1,113 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.sanitizers + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh +import com.code_intelligence.jazzer.api.HookType +import com.code_intelligence.jazzer.api.Jazzer +import com.code_intelligence.jazzer.api.MethodHook +import com.code_intelligence.jazzer.api.MethodHooks +import net.sf.jsqlparser.JSQLParserException +import net.sf.jsqlparser.parser.CCJSqlParserUtil +import java.lang.invoke.MethodHandle + +/** + * Detects SQL injections. + * + * Untrusted input has to be escaped in such a way that queries remain valid otherwise an injection + * could be possible. This sanitizer guides the fuzzer to inject insecure characters. If an exception + * is raised during execution the fuzzer was able to inject an invalid pattern, otherwise all input + * was escaped correctly. + * + * Two types of methods are hooked: + * 1. Methods that take an SQL query as the first argument (e.g. [java.sql.Statement.execute]). + * 2. Methods that don't take any arguments and execute an already prepared statement + * (e.g. [java.sql.PreparedStatement.execute]). + * For 1. we validate the syntax of the query using <a href="https://github.com/JSQLParser/JSqlParser">jsqlparser</a> + * and if both the syntax is invalid and the query execution throws an exception we report an SQL injection. + * Since we can't reliably validate SQL queries in arbitrary dialects this hook is expected to produce some + * amount of false positives. + * For 2. we can't validate the query syntax and therefore only rethrow any exceptions. + */ +@Suppress("unused_parameter", "unused") +object SqlInjection { + + // Characters that should be escaped in user input. + // See https://dev.mysql.com/doc/refman/8.0/en/string-literals.html + private const val CHARACTERS_TO_ESCAPE = "'\"\b\n\r\t\\%_" + + private val SQL_SYNTAX_ERROR_EXCEPTIONS = listOf( + "java.sql.SQLException", + "java.sql.SQLNonTransientException", + "java.sql.SQLSyntaxErrorException", + "org.h2.jdbc.JdbcSQLSyntaxErrorException", + ) + + @MethodHooks( + MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "execute"), + MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeBatch"), + MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeLargeBatch"), + MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeLargeUpdate"), + MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeQuery"), + MethodHook(type = HookType.REPLACE, targetClassName = "java.sql.Statement", targetMethod = "executeUpdate"), + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.persistence.EntityManager", + targetMethod = "createNativeQuery" + ) + ) + @JvmStatic + fun checkSqlExecute(method: MethodHandle, thisObject: Any?, arguments: Array<Any>, hookId: Int): Any { + var hasValidSqlQuery = false + + if (arguments.isNotEmpty() && arguments[0] is String) { + val query = arguments[0] as String + hasValidSqlQuery = isValidSql(query) + Jazzer.guideTowardsContainment(query, CHARACTERS_TO_ESCAPE, hookId) + } + return try { + method.invokeWithArguments(thisObject, *arguments) + } catch (throwable: Throwable) { + // If we already validated the query string and know it's correct, + // The exception is likely thrown by a non-existent table or something + // that we don't want to report. + if (!hasValidSqlQuery && SQL_SYNTAX_ERROR_EXCEPTIONS.contains(throwable.javaClass.name)) { + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueHigh( + """ + SQL Injection + Injected query: ${arguments[0]} + """.trimIndent(), + throwable + ) + ) + } + throw throwable + } + } + + private fun isValidSql(sql: String): Boolean = + try { + CCJSqlParserUtil.parseStatements(sql) + true + } catch (e: JSQLParserException) { + false + } catch (t: Throwable) { + // Catch any unexpected exceptions so that we don't disturb the + // instrumented application. + t.printStackTrace() + true + } +} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Utils.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Utils.kt index 3166773b..219490d8 100644 --- a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Utils.kt +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Utils.kt @@ -21,6 +21,7 @@ import java.io.InputStream * jaz.Zer is a honeypot class: All of its methods report a finding when called. */ const val HONEYPOT_CLASS_NAME = "jaz.Zer" +const val HONEYPOT_LIBRARY_NAME = "jazzer_honeypot" internal fun Short.toBytes(): ByteArray { return byteArrayOf( @@ -43,9 +44,20 @@ internal fun ByteArray.indexOf(needle: ByteArray): Int { } internal fun guideMarkableInputStreamTowardsEquality(stream: InputStream, target: ByteArray, id: Int) { + fun readBytes(stream: InputStream, size: Int): ByteArray { + val current = ByteArray(size) + var n = 0 + while (n < size) { + val count = stream.read(current, n, size - n) + if (count < 0) break + n += count + } + return current + } + check(stream.markSupported()) stream.mark(target.size) - val current = stream.readNBytes(target.size) + val current = readBytes(stream, target.size) stream.reset() Jazzer.guideTowardsEquality(current, target, id) } diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/BUILD.bazel b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/BUILD.bazel new file mode 100644 index 00000000..c7258447 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/BUILD.bazel @@ -0,0 +1,7 @@ +java_library( + name = "reflection_utils", + srcs = ["ReflectionUtils.java"], + visibility = [ + "//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers:__pkg__", + ], +) diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/ReflectionUtils.java b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/ReflectionUtils.java new file mode 100644 index 00000000..fd6ac72f --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/utils/ReflectionUtils.java @@ -0,0 +1,62 @@ +// Copyright 2022 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.sanitizers.utils; + +import java.lang.reflect.Field; +import sun.misc.Unsafe; + +public final class ReflectionUtils { + public static final long INVALID_OFFSET = Long.MIN_VALUE; + + private static final boolean JAZZER_REFLECTION_DEBUG = + "1".equals(System.getenv("JAZZER_REFLECTION_DEBUG")); + + public static Class<?> clazz(String className) { + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + if (JAZZER_REFLECTION_DEBUG) + e.printStackTrace(); + return null; + } + } + + public static Class<?> nestedClass(Class<?> parentClass, String nestedClassName) { + return clazz(parentClass.getName() + "$" + nestedClassName); + } + + public static Field field(Class<?> clazz, String name, Class<?> type) { + if (clazz == null) + return null; + try { + Field field = clazz.getDeclaredField(name); + if (!field.getType().equals(type)) { + throw new NoSuchFieldException( + "Expected " + name + " to be of type " + type + " (is: " + field.getType() + ")"); + } + return field; + } catch (NoSuchFieldException e) { + if (JAZZER_REFLECTION_DEBUG) + e.printStackTrace(); + return null; + } + } + + public static long offset(Unsafe unsafe, Field field) { + if (unsafe == null || field == null) + return INVALID_OFFSET; + return unsafe.objectFieldOffset(field); + } +} |