Complex systems involving polyglot programming introduce unique issues for security professionals because of the interactions between different languages. To better understand such a system and its security mechanisms, it is often required for security professionals to trace call flows through the language boundaries of polyglot code. Unfortunately, the language boundaries rarely involve a one-to-one mapping between the languages on either side, and often require some level of translation. This translation can complicate some of the simplest of manual call flow traces. However, it makes it even more difficult to develop reusable call flow tracing tools to aid security professionals.
This post discusses one solution to this problem for Java-based systems that use the JavaScript interpreter Rhino1 and demonstrates Rhino Tracker,2 a proof-of-concept demonstrating this solution. The output of Rhino Tracker is a call graph that captures the call flows through the Rhino interpreter without the runtime complexities that the Rhino interpreter introduces. Effectively, Rhino Tracker bridges the call flows from Java to Javascript to Java, creating call graphs that can be used to develop further analysis tools.
Designing Rhino Tracker
The problem of tracing call flows from Java to JavaScript to Java can be subdivided into two separate problems: 1) tracing code from JavaScript to Java and 2) tracing code from Java to JavaScript. Below is a discussion of these problems, how to solve them, and how to combine their output to obtain the desired call graph that eliminates the Rhino interpreter runtime complexities.
JavaScript to Java Code Tracing - First Attempt
To tackle the problem of tracing Java calls from JavaScript code through the Rhino interpreter, a first attempt was made making use of Rhino’s ability to compile JavaScript to Java class files. When generating a call graph for Java code, static analysis frameworks (e.g. SootUp3) can use a class file containing standard bytecode invocation statements of the Java methods in the JavaScript to replace calls to the Rhino interpreter. The class conversion functionality can be found in the compileToClassFiles
method of the org.mozilla.javascript.optimizer.ClassCompiler
class of Rhino. An example of how to call this method can be found in the stringToClassFile
method of the com.snc.secres.tool.dynamic.Tools
class found in the Rhino Tracker source code.4
Unfortunately, the class file compileToClassFiles
produces is not particularly useful for our purposes. For example, take the sample JavaScript code found below that makes a number of calls to Java standard library methods.
var now = java.time.LocalDateTime.now();
now.toString();
now.minusHours(48);
now.getDayOfYear();
var then = now.minusDays(10);
var thenInstant = then.toInstant(java.time.ZoneOffset.UTC);
var thenDate = new java.util.Date.from(thenInstant);
var nowDate = new java.util.Date();
nowDate.compareTo(thenDate);
thenDate.compareTo(nowDate);
When this JavaScript is run through the compileToClassFiles
method of Rhino and then decompiled into standard Java, the following code is produced.
Decompiled Class File of JavaScript Compiled Using Rhino
private static Object _c_script_0(test testVar, Context context, Scriptable global, Scriptable scriptable, Object[] objArr) {
ScriptRuntime.initScript(testVar, scriptable, context, global, false);
Object obj = Undefined.instance;
ScriptRuntime.setName(ScriptRuntime.bind(context, global, "now"), OptRuntime.callProp0(ScriptRuntime.getObjectProp(ScriptRuntime.getObjectProp(ScriptRuntime.name(context, global, "java"), "time", context, global), "LocalDateTime", context, global), "now", context, global), context, global, "now");
OptRuntime.callProp0(ScriptRuntime.name(context, global, "now"), "toString", context, global);
OptRuntime.call1(ScriptRuntime.getPropFunctionAndThis(ScriptRuntime.name(context, global, "now"), "minusHours", context, global), ScriptRuntime.lastStoredScriptable(context), _k0, context, global);
OptRuntime.callProp0(ScriptRuntime.name(context, global, "now"), "getDayOfYear", context, global);
...
The generated code is much more complex than expected, and does not directly contain invocations to any of the Java methods mentioned in the sample JavaScript code. Indeed, all Java method calls from JavaScript are wrapped in calls to the Rhino interpreter. The actual invocation target of these Java methods calls is not determined until evaluation at runtime. In fact, what the compileToClassFiles
method actually does when producing a class file is simply dump the AST it generates of the JavaScript code, the same AST that would be used to run the JavaScript code at runtime. Unfortunately, this AST, and in turn the class file, determines the invocation target of method calls just before a method is invoked at runtime.
As a result, the class files produced by Rhino cannot directly be used to solve the problem of tracing Java calls from JavaScript code through the Rhino interpreter. This is because static call graph generation algorithms (e.g. SootUp3) are not able to resolve runtime call stacks. These algorithms have a similar limitation for calls using the more well known Java reflection API. However, the class files do provide a concrete means to invoke the JavaScript code from Java. This allows for the capture of the Java methods being called from JavaScript at runtime.
JavaScript to Java Code Tracing - Instrumenting Rhino
Even though it is not possible to directly use the class files produced in the last section to construct a full call graph, it is possible to run the class files and capture the Java methods being invoked. These runtime method captures can then be used with static analysis tools (e.g. SootUp3) to construct a full call graph. While this call graph is not entirely generated through static analysis tooling, it still eliminates the Rhino interpreter runtime complexities while requiring minimal runtime analysis and overhead.
To instrument Rhino at runtime and capture the Java methods called from JavaScript, Rhino Tracker uses a combination of Byte Buddy’s7 method interceptor APIs, Byte Buddy’s advice APIs, and Java reflection APIs. These are all combined together into a Java Agent that uses the premain
entry method to initialize all instrumentation before the server runs.
Java Agent Stub Making Use of Byte Buddy’s API
public static void premain(String agentArgs, Instrumentation inst) {
AgentBuilder agentBuilder = new AgentBuilder.Default()
.with(new AgentBuilder.Listener.StreamWriting(loggingPrintStream).withTransformationsOnly())
.with(new AgentBuilder.InstallationListener.StreamWriting(loggingPrintStream));
...
The remaining subsections will contain discussions on how Rhino methods were identified as instrumentation targets, how to ensure the capture of only the methods invoked directly by the JavaScript being sampled, and how to disable the SecurityManager
during the capture.
Identifying Code to Instrument
In order to identify the Java methods being called from JavaScript, it must first be known what areas of Rhino hold enough context to describe these Java methods. This was manually determined by making use of Byte Buddy’s Advice API to hook and dump stack traces from known Java methods called from JavaScript code. The code below provides an example on how to hook the now
method of LocalDateTime
and have it dump a stack trace once it is finished executing.
Instrumenting now
Method of LocalDateTime
Using Advice API
agentBuilder
.type(ElementMatchers.named("java.time.LocalDateTime"))
.transform(new AgentBuilder.Transformer() {
...
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
TypeDescription typeDescription, ClassLoader classLoader,
JavaModule module, ProtectionDomain protectionDomain) {
return builder.visit(Advice.to(WhoCalledMeAdvice.class).on(ElementMatchers.named("now")));
}
...
Code Run Using Advice API on Exit of now
Method of LocalDateTime
public class WhoCalledMeAdvice {
@Advice.OnMethodExit
public static void exit() throws Exception {
Exception e = new RuntimeException("Foo bar");
StackTraceElement[] elements = e.getStackTrace();
Logging.warn(Arrays.toString(elements));
}
}
Using this technique on a number of different types of Java methods, it was clear from the stack traces that Rhino uses Java’s Reflection API to invoke Java methods from JavaScript. In fact, all calls using reflection take place in the methods of the org.mozilla.javascript.MemberBox
class. Unfortunately, while this class stores all the information needed to determine the Java methods being called, it does not contain any information about the JavaScript from which the calls originate. Such information is required to ensure the capture of only those methods invoked directly in the JavaScript being sampled. To obtain this information, Rhino Tracker hooks methods that are one or two steps back from the methods in MemberBox
on the call stack. These methods contain both access to a MemberBox
object describing the Java method that will be invoked and access to an object of the org.mozilla.javascript.Context
class that can be used to gain the origin information needed.
The methods in Rhino we hook are the call
methods of the org.mozilla.javascript.NativeJavaConstructor
, org.mozilla.javascript.FunctionObject
, and org.mozilla.javascript.NativeJavaMethod
classes, along with the constructSpecific
method of the org.mozilla.javascript.NativeJavaClass
. The code below illustrates how Rhino Tracker uses Byte Buddy’s Method Interceptor API to hook the call
method of FunctionObject
to record calls from JavaScript to Java. Further information on how these are implemented can be found in the com.snc.secres.tool.dynamic.Agent
and com.snc.secres.tool.dynamic.JavaMethodInterceptor
classes of Rhino Tracker’s source code.89
Instrumenting call
Method of FunctionObject
Using Method Interceptor API
agentBuilder
.type(ElementMatchers.named("org.mozilla.javascript.FunctionObject"))
.transform(new AgentBuilder.Transformer() {
...
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
TypeDescription typeDescription, ClassLoader classLoader,
JavaModule module, ProtectionDomain protectionDomain) {
return builder.method(ElementMatchers.named("call").and(ElementMatchers.takesArguments(5)))
.intercept(MethodDelegation.withDefaultConfiguration()
.filter(ElementMatchers.named("interceptFunctionObject"))
.to(JavaMethodInterceptor.class)
);
}
...
Code Run When The call
Method Of FunctionObject
Is Intercepted
public static Object interceptFunctionObject(@AllArguments Object[] allArguments,
@SuperCall Callable<Object> callableMethod,
@This Object thiz) throws Exception {
if(testCaptureEnabled(allArguments[0])) {
try {
Field f = thiz.getClass().getDeclaredField("member");
f.setAccessible(true);
Object ctor = f.get(thiz); //MemberBox
String result = Tools.memberBoxToString(ctor);
...
Restricting Captured Java Methods
Without any restrictions, the methodology mentioned above would capture all Java methods called from the Rhino interpreter during the duration the agent is active. For multi-threaded systems where more than one JavaScript is being run simultaneously, this approach would capture Java methods not called from the sampled JavaScript. Additionally, if the Rhino interpreter were to be called to process JavaScript from a Java method in the original JavaScript, the Java methods in this secondary JavaScript would also be captured. To ensure the capture of only those Java methods invoked directly in the JavaScript being sampled, Rhino Tracker’s agent adds the field _capture
to the Context
class. The code below demonstrates how to add a field to an existing class using Byte Buddy and how to initialize it.
Instrumenting Context
To Add Fields
agentBuilder
.type(ElementMatchers.named("org.mozilla.javascript.Context"))
.transform(new AgentBuilder.Transformer() {
...
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
TypeDescription typeDescription, ClassLoader classLoader,
JavaModule module, ProtectionDomain protectionDomain) {
Agent.getActiveAgent().orgClassLoader = classLoader;
builder = builder.defineField("_capture", boolean.class, Visibility.PUBLIC);
builder = builder.defineField("_capture_path", Path.class, Visibility.PUBLIC);
builder = builder.visit(Advice.to(ContextAdvice.class).on(ElementMatchers.isConstructor()));
return builder;
}
...
Initializing New Fields In The Context
Class
public class ContextAdvice {
@Advice.OnMethodExit
public static void exit(@Advice.FieldValue(value = "_capture", readOnly = false) boolean _capture,
@Advice.FieldValue(value = "_capture_path", readOnly = false) Path _capture_path)
throws Exception {
_capture = false;
_capture_path = null;
}
}
The added _capture
field of the Context
class is a boolean that indicates if the hooked Rhino methods when invoked should be capturing Java method calls. By default, this field is set to false
to indicate that capturing should not take place. However, when sampling a piece of JavaScript, Rhino Tracker sets the field to true
before running the script through the Rhino interpreter. As shown in the code for the call
method of FunctionObject
(here), all hooked methods of Rhino have access to the same Context
object created when the script is invoked. As such, the method testCaptureEnabled
can be used to determine if capturing is enabled by querying the _capture
field. Additionally, the method setCapture
can be used to disable the capture of Java methods just before the current Java method is invoked and restore the _capture
field to its previous value after the current Java method is invoked. Such a setup prevents the capture of any Java methods except those directly invoked in the JavaScript. All methods referenced here can be found in JavaMethodInterceptor
class of Rhino Tracker’s source code.9
Preventing Security Managers From Loading
Often in systems more complex than the sample server provided with Rhino Tracker, a Java SecurityManager
class may be registered. Security managers can cause issues when reading/writing to files or other actions that do not fall into the realm of normal server behavior for an instrumentation target. To prevent interference from security managers, Rhino Tracker stops the registration of new security managers by instrumenting the setSecurityManager
method of java.lang.System
and replacing the entire method with a no-op.
The process for instrumenting Java standard library methods is more complicated than other methods as these classes are loaded by the bootloader. This is especially true for System
as it is always one of the first classes loaded. To instrument the System
class, Rhino Tracker makes use of the Byte Buddy setup shown below.
Instrumenting System
To Prevent Security Manager Additions
private void sackSecurityManager(Instrumentation instrumentation,
PrintStream loggingPrintStream) throws IOException {
File temp = Files.createTempDirectory("tmp").toFile();
temp.deleteOnExit();
ClassInjector.UsingInstrumentation.of(temp, ClassInjector.UsingInstrumentation.Target.BOOTSTRAP,
instrumentation).inject(Collections.singletonMap(
new TypeDescription.ForLoadedType(SecurityManagerInterceptor.class),
ClassFileLocator.ForClassLoader.read(SecurityManagerInterceptor.class)
)
);
...
Java to JavaScript Code Tracing
To acquire a call graph that allows for tracing call flows from a given entry point method in Java through the Rhino interpreter’s handling of some JavaScript, Rhino Tracker requires a view of the Java call flows that can be manipulated to substitute in the data gathered at runtime. To gain this functionality and generate the call graph, Rhino Tracker makes use of the static analysis framework SootUp.3 SootUp is the new version of Soot.10 SootUp has much of the same functionality of Soot but has been rewritten to modernize and improve performance. It has a number of uses that aid in performing static analysis. These include manipulation of compiled or un-compiled Java code, code generation, CFG generation and manipulation, and call graph generation and manipulation.
The remaining subsections discuss how to initialize SootUp from compiled Java code, how to use SootUp to construct a call graph, how to limit the scope of the call graph, and how to modify the call graph to add additional data (e.g. the runtime generated data from the previous section).
Initializing SootUp
To initialize and use SootUp, a JavaView
must be created from the class path of the code that is to be analyzed. A JavaView
is the container for the code being analyzed by SootUp. It is responsible for the loading of code and mapping code back to files. Essentially, all data in SootUp maps back to a JavaView
in some way. A JavaView
is created from a list of AnalysisInputLocation
classes, which are various ways of specifying where to find the code that is to be analyzed. It is possible, but not required, to specify a list of BodyInterceptor
objects when creating a AnalysisInputLocation
object. A BodyInterceptor
processes the method bodies of classes when they are loaded by the JavaView
and transforms the loaded code such as to improve performance, human readability, and analyzability. Additionally, it is possible to provide a SourceType
when creating a AnalysisInputLocation
. The available source types are Application
and Library
where the former’s code is traversed during call graph generation, while the latter’s code is not.
The code below shows how a JavaView
is created in Rhino Tracker. The JavaView
created is actually a child type known as a MutableJavaView
. A MutableJavaView
allows for the addition of generated classes and methods to an already existent JavaView
.
Creating a JavaView in SootUp
public static MutableJavaView makeJavaView(String classPath) {
List<BodyInterceptor> bodyInterceptors = Collections.unmodifiableList(Arrays.asList(
new UnreachableCodeEliminator(),
new Aggregator(),
...
List<AnalysisInputLocation> inputLocations = new ArrayList<>();
inputLocations.add(new JavaClassPathAnalysisInputLocation(classPath, SourceType.Application,
bodyInterceptors));
return new MutableJavaView(inputLocations);
}
Constructing A Call Graph
Once a JavaView
is obtained, it is possible to construct a call graph. Constructing a call graph in SootUp requires two additional inputs, a call graph building algorithm and an entry point method. SootUp currently supports two types of call graph algorithms, Class Hierarchy Analysis (CHA) and Rapid Type Analysis (RTA). Both are similar in that they rely on the class hierarchy to resolve interface and super class method calls. However, RTA sacrifices some completeness for precision by only considering instantiated implementers of interfaces and super classes when resolving method calls. As such, RTA must be supplied with a entry point method that leads to the instantiation of all classes being analyzed. Typically this method is the main method of a Java application but any method may be given. CHA has no limitations on the entry point it is supplied with, but will ultimately be much less precise than RTA.
Rhino Tracker supports the use of both the CHA and RTA call graph algorithms. However, the use of RTA is the default. As shown in the sample code below, the construction of a call graph using either algorithm is similar. First, a MethodSignature
of the entry point method must be obtained from the JavaView
. These can be obtained in a number of ways but the easiest is by supplying a string representation and calling view.getIdentifierFactory().parseMethodSignature(sig)
. Here sig
is formatted as follows <com.snc.secres.sample.DummyMain: void main(java.lang.String[])>
. Next, a CallGraphAlgorithm
must be created and the initialize
method must be called and supplied the entry point method. At this point a GraphBasedCallGraph
is created which represents the Java call graph of the given entry point. This call graph can then be manipulated further to remove unwanted edges. This will be discussed later.
Constructing A Call Graph Using SootUp
MethodSignature entryMethodSignature = view.getIdentifierFactory().parseMethodSignature(
config.getEntryPointMethodSig()
);
MethodSignature mainMethodSignature = view.getIdentifierFactory().parseMethodSignature(
config.getMainMethodSig()
);
CallGraphAlgorithm cga = new RapidTypeAnalysisAlgorithm(view);
CallGraphWrapper callGraph = new CallGraphWrapper(
(GraphBasedCallGraph)cga.initialize(Collections.singletonList(mainMethodSignature))
);
callGraph.applyFilter(cgFilter, view);
While CHA appears to be more suited to constructing call graphs for APIs, for multi-entry programs, and other systems where a main method does not accurately capture call flow, RTA can be used. By creating a dummy main method that instantiates all classes used by a particular entry point, a sound and precise call graph can be constructed using RTA. The sample code provided with Rhino Tracker contains a DummyMain
class11 that performs such an operation. However, it is not required that this dummy main method be created ahead of time and included with the code being analyzed by SootUp. SootUp can be used to generate such a method during the analysis process if desired.
Limiting Call Graph Scope
As it stands, the call graph may contain paths to segments of code that are unrelated to the analysis being performed. These paths could increase analysis time, result in noise in the output data, or increase the manual effort involved in the analysis. As such, reducing the scope of the call graph further may be desired. To do this, we created CallGraphFilter
. CallGraphFilter
allows a user to specify specific class methods whose outgoing edges are to be removed from the call graph. This effectively makes these methods end nodes in the graph. Its use can be seen in the call graph construction above (here). For Rhino Tracker, the filter is defined in the yaml config file.
Example Of A Call Graph Filter Definition
filter_default_policy: deny
filter:
- type: class_path
pattern: 'com\.snc\.secres\.sample.*'
policy: allow
Adding Runtime Data To Call Graph
Once a initial call graph has been generated, the runtime dump of the Java methods called from the sampled JavaScript can be added to it. To do this Rhino Tracker first iterates through the call graph to locate any calls to a given sink. The sink should be the point at which Rhino is called to evaluate the sampled JavaScript. For the sample code, the sink was defined as <org.mozilla.javascript.Context: java.lang.Object evaluateString(org.mozilla.javascript.Scriptable, java.lang.String, java.lang.String, int, java.lang.Object)>
, but this can be changed based on the system being analyzed. Next, SootUp is used to generate a class with a single empty method. The empty method is used as a wrapper in the call graph for the Java method calls recorded at runtime from the sampled Java script. The code used to generate the class with an empty method is shown below.
Generating A Empty Method And Class Using SootUp
public static JavaSootClass makeClassWithEmptyMethod(MutableJavaView view, String fullClassName,
String methodName) {
JavaClassType classType = view.getIdentifierFactory().getClassType(fullClassName);
JavaSootMethod method = makeEmptyMethod(classType, methodName);
InMemoryJavaAnalysisInputLocation memInputLocation = new InMemoryJavaAnalysisInputLocation(
...
Finally, the call graph is modified to replace the calls to the sink method with calls to the generated empty method. Edges are then added to the call graph from the empty method to all Java methods recorded during the runtime sampling of the JavaScript.
Running The Sample Code Through Rhino Tracker
The Rhino Tracker source code and sample code can be found at the ServiceNow SecurityResearch GitHub repo.12 Download the source code and run the following from the a-rhino-of-a-problem
directory, which will assemble all the necessary JAR files:
gradle build
Rhino Tracker uses two config files to specify input for the two phases. The default config file for the first phase (i.e. dynamic instrumentation) is shown below.
Default Yaml Config File For Dynamic Instrumentation Phase
log_dir_path: 'work/dynamic_logs'
out_dir_path: 'work/java_call_traces'
js_file_path: 'sample/sample.test.js'
js_full_class_name: 'sample.test'
instance_url: 'http://127.0.0.1:8080'
connect_tries: 30
connect_timeout: 120000
time_between_connect_attempts: 10000
compiled_js_dir_path: 'work/compiled_javascript'
To run the dynamic instrumentation of the sample server using the default config file path, run the following command:
gradle runDynamic
This command will start the sample server with the instrumentation agent attached to it. Once all the instrumentation has been loaded, the agent will spawn a separate thread before exiting. The thread waits until it is able to make a connection to the sample server. Once it can connect, it submits the sample script to the server for processing and waits on the results before exiting. Once the script has finished running, the thread will dump the Java method calls to a file in the directory java_call_traces
. The file is named timestamp + '__' + js_full_class_name + '__.txt'
. The Rhino compiled JavaScript class file can also be found in the compiled_javascript
directory. The output should look like the list of methods below.
Methods Invoked From Sample JavaScript
<java.time.LocalDateTime: java.time.LocalDateTime now()>
<java.time.LocalDateTime: java.lang.String toString()>
<java.time.LocalDateTime: java.time.LocalDateTime minusHours(long)>
<java.time.LocalDateTime: int getDayOfYear()>
<java.time.LocalDateTime: java.time.LocalDateTime minusDays(long)>
<java.time.chrono.ChronoLocalDateTime: java.time.Instant toInstant(java.time.ZoneOffset)>
<java.util.Date: java.util.Date from(java.time.Instant)>
<java.util.Date: void <init>()>
<java.util.Date: int compareTo(java.util.Date)>
<java.util.Date: int compareTo(java.util.Date)>
The default config file for the second phase (i.e. static analysis and call graph building) is shown below.
Default Yaml Config File For Static Analysis Phase
call_graph_algo: rta
class_path: sample/build/libs/sample.jar
entry_point_method_sig: '<com.snc.secres.sample.RhinoServlet: void doPost(jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse)>'
main_method_sig: '<com.snc.secres.sample.DummyMain: void main(java.lang.String[])>'
output_dir_path: work/cg
runtime_trace_file_path: work/java_call_traces/2024-07-17_13-24-53__sample.test__.txt
sink_method_sig: '<org.mozilla.javascript.Context: java.lang.Object evaluateString(org.mozilla.javascript.Scriptable, java.lang.String, java.lang.String, int, java.lang.Object)>'
filter_default_policy: deny
filter:
- type: class_path
pattern: 'com\.snc\.secres\.sample.*'
policy: allow
To run the static analysis using the default config file path, run the following command:
gradle runStatic
This command will first initialize soot with the given class paths. Then it will build the call graph using the given algorithm, entry point, and main method information. Any filters will then be applied to the call graph. Next, the call graph is searched for occurrences of the given sink method. Calls to this method in the call graph are replaced with a call to a generated method containing outgoing edges to the methods in the runtime trace file. Finally, the call graph is dumped to a dot file in the output directory. More information about this process can be found in the previous section. The output should look like the dot file below.
Generated Dot File Representing Java to JavaScript to Java Call Graph
...
"<com.snc.secres.sample.RhinoServlet: void doPost(jakarta.servlet.http.HttpServletRequest,jakarta.servlet.http.HttpServletResponse)>" -> "<com.snc.secres.sample.RhinoServlet: java.lang.String setupAndDoScriptEval(java.lang.String)>";
"<com.snc.secres.sample.RhinoServlet: java.lang.String setupAndDoScriptEval(java.lang.String)>" -> "<com.snc.secres.sample.RhinoServlet: java.lang.Object doScriptEval(java.lang.String,org.mozilla.javascript.Scriptable,org.mozilla.javascript.Context)>";
"<com.snc.secres.sample.RhinoServlet: java.lang.Object doScriptEval(java.lang.String,org.mozilla.javascript.Scriptable,org.mozilla.javascript.Context)>" -> "<sample.test20240717132453: void runtimeSimulator()>";
"<sample.test20240717132453: void runtimeSimulator()>" -> "<java.time.chrono.ChronoLocalDateTime: java.time.Instant toInstant(java.time.ZoneOffset)>";
"<sample.test20240717132453: void runtimeSimulator()>" -> "<java.util.Date: void <init>()>";
...
Using Graphviz, the graph can be rendered into an image. Below is a portion of the rendered graph.
Rendered Call Graph Representing Java to JavaScript to Java
More information about how to run Rhino Tracker and other available options can be found in the readme file in the ServiceNow SecurityResearch GitHub repo.18
Conclusion
While Rhino Tracker requires both static and dynamic analysis to produce a call graph of a polyglot Rhino server environment, the dynamic data required is minimal and reusable. Rhino Tracker requires only the JavaScript call flows be gathered dynamically. Such an approach is still subject to the same limitations of completeness as all dynamic analysis approaches. However, a good fuzzing environment will likely be able to run through all code paths in a given JavaScript codebase. As such, so long as all available code paths are hit during runtime, a complete dump of the JavaScript call flows will be produced by Rhino Tracker. Moreover, a purely static analysis-based approach to the call graph generation would not be guaranteed to be complete. Using static analysis for call graph generation would require parsing Rhino’s abstract syntax tree (AST). As much of the call targets of the Rhino AST are determined at runtime from customizable mappings that are also runtime generated, there is no simple way to statically map calls through the Rhino interpreter without runtime generated data.
The hybrid analysis approach of Rhino Tracker allows it to create complete call graphs that capture the call flows through the Rhino interpreter without the runtime complexities that the Rhino interpreter introduces. This allows Rhino Tracker to bridge the call flows from Java to JavaScript to Java. Not only can the call graphs produced by Rhino Tracker be used to aid manual review of polyglot Java/JavaScript code, but they can also but built on to develop more complex analysis tools.
-
https://github.com/ServiceNow/SecurityResearch/tree/main/rhino-tracker ↩
-
https://github.com/ServiceNow/SecurityResearch/tree/main/rhino-tracker/tool/src/main/java/com/snc/secres/tool/dynamic/Tools.java ↩
-
https://github.com/ServiceNow/SecurityResearch/tree/main/rhino-tracker/sample/sample.test.js ↩
-
https://github.com/ServiceNow/SecurityResearch/tree/main/rhino-tracker/sample/2024-07-17_13-24-53__sample.test__.class ↩
-
https://github.com/ServiceNow/SecurityResearch/tree/main/rhino-tracker/tool/src/main/java/com/snc/secres/tool/dynamic/Agent.java ↩ ↩2
-
https://github.com/ServiceNow/SecurityResearch/tree/main/rhino-tracker/tool/src/main/java/com/snc/secres/tool/dynamic/JavaMethodInterceptor.java ↩ ↩2
-
https://github.com/ServiceNow/SecurityResearch/tree/main/rhino-tracker/sample/src/main/java/com/snc/secres/sample/DummyMain.java ↩
-
https://github.com/ServiceNow/SecurityResearch/tree/main/rhino-tracker/sample ↩
-
https://github.com/ServiceNow/SecurityResearch/tree/main/rhino-tracker/sample/dynamic_config.yaml ↩
-
https://github.com/ServiceNow/SecurityResearch/tree/main/rhino-tracker/sample/2024-07-17_13-24-53__sample.test__.txt ↩
-
https://github.com/ServiceNow/SecurityResearch/tree/main/rhino-tracker/sample/static_config.yaml ↩
-
https://github.com/ServiceNow/SecurityResearch/tree/main/rhino-tracker/sample/sample.test20240717132453.dot ↩
-
https://github.com/ServiceNow/SecurityResearch/tree/main/rhino-tracker/sample/sample.test20240717132453.pdf ↩
-
https://github.com/ServiceNow/SecurityResearch/tree/main/rhino-tracker/Readme.md ↩