Hidden File
This extended introductory tutorial demonstrates how to create a simple attack module to hide a file using JReFrameworker and provides a basic understanding of the underlying bytecode manipulations performed by the tool.
At the end of the tutorial you will have created a module with JReFrameworker to modify the behavior of the Java runtime’s java.io.File
class to return false if the file name is “secretFile” regardless if the file actually exists or not.
Creating a New Module
Each “module” consists of an Eclipse JReFrameworker project. A module consists of annotated Java source code for one or more Java classes, which define how the runtime environment should be modified. A module may also contain test code that uses the runtime APIs that will be modified. The test code can be used to execute and debug the module in the modified as well as original runtime environments.
To create a new attack module, navigate to File
> New
> Other...
> JReFrameworker
> JReFrameworker Runtime Project
.
Enter a new project name for the module.
Adding Test Logic
Next let’s add some test code that will interact with the java.io.File
API so that we can effectively test the modified runtime. The following Test
class contains a main method that creates a File
named “secretFile” and writes the string “blah” to the file. After the file is written, the existence of the file is printed to the console. Finally, the file is deleted.
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
public class Test {
public static void main(String[] args) throws IOException {
File testFile = new File("secretFile");
FileWriter fw = new FileWriter(testFile);
fw.write("blah");
fw.close();
System.out.println("Secret File Exists: " + testFile.exists());
testFile.delete();
}
}
In an unmodified runtime, the print out should return “true” assuming the file could be written. In the event that a file could not be written an exception will be thrown causing stack trace to be written to the output.
Our goal is to modify the runtime so that the print out reads “false” if the File
object’s name is “secretFile” regardless if the file actually exists on the file system, while maintaining the original functionality of the File.exists()
method in all other cases.
Prototyping Intended Behavior
Let’s prototype a class that has the behavior we intend to modify the runtime with by developing the class as if we had control of the source code to the File
class. Since we are designing special case of java.io.File
this a prime example of how Object Oriented languages can leveraged to make a subclass containing the desired behavior.
In this tutorial we will use the Eclipse New Class Wizard to create a subclass of java.io.File
. We create a class named HiddenFile
that extends java.io.File
in the package java.io
.
Now because the File
class does not have a default constructor, creating a subclass of File
causes a compile error if we do not also create a HiddenFile
constructor.
In this tutorial we use Eclipse to resolve the compile error by generating a HiddenFile
constructor. Optionally, we can also use Eclipse to resolve the warning that HiddenFile
does not declare a serialVersionUID
field.
Now that we have created a subclass of java.io.File
we can override the behavior of the File.exists()
method with our desired functionality. First we can leverage the inherited File.isFile()
and File.getName()
methods to check if the File
object is a file (and not a directory) and that the filename matches “secretFile”. If both conditions are true we can immediately return false. Since we wish for the functionality of HiddenFile.exists()
to behave normally in all other cases we can simply call File.exists()
using the super keyword to access the parent’s method implementation. After making these modifications we arrive at the following implementation for the HiddenFile
class.
package java.io;
public class HiddenFile extends File {
private static final long serialVersionUID = 1L;
public HiddenFile(String name) {
super(name);
}
@Override
public boolean exists(){
if(isFile() && getName().equals("secretFile")){
return false;
} else {
return super.exists();
}
}
}
Note that we use the source level annotation @Override
here to ensure that the exists method is actually overriding File.exists()
. Source methods such as @Override
do not get compiled into the resulting bytecode.
We can test the implementation of our prototype HiddenFile
class by modifying our Test
class code to instantiate a File
object of type HiddenFile
. If the test logic does not produce the desired result, we can take this opportunity to set breakpoints in the HiddenFile
class and debug it appropriately.
import java.io.File;
import java.io.FileWriter;
import java.io.HiddenFile;
import java.io.IOException;
public class Test {
public static void main(String[] args) throws IOException {
File testFile = new HiddenFile("secretFile");
FileWriter fw = new FileWriter(testFile);
fw.write("blah");
fw.close();
System.out.println("Secret File Exists: " + testFile.exists());
testFile.delete();
}
}
Important Note: The java.io
package is a restricted package. Running the test logic that includes a class extending a class in a restricted package will likely throw an exception depending on your current runtime security policy.
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.io
If need be, this issue can be avoided by moving the HiddenFile
class to a non-restricted package such as the default package or a new package such as modules.java.io
.
JReFrameworker Annotations
At this point we will take a short digression to examine the annotations provided by the JReFrameworker plugin. Annotations are used to guide the behavior of JReFrameworker during the bytecode rewriting steps desired in a module. There are two primary classes of annotations: define and merge annotations. For both classes of annotations there are three levels that annotations may be applied: type, method, and field. The following matrix defines each supported annotation type.
Define | Merge | |
---|---|---|
Type | @DefineType | @MergeType |
Method | @DefineMethod | @MergeMethod |
Field | @DefineField | N/A |
Annotation Type | Description |
---|---|
@DefineType | Inserts or overwrites the class or interface into the runtime with the fields and methods of the current type. |
@DefineMethod | Inserts or overwrites the method into the runtime type. The current type must also be annotated with @MergeType. |
@DefineField | Inserts or overwrites the field into the runtime type. The current type must also be annotated with @MergeType. |
@MergeType | Merges the fields and methods of the current type into the existing runtime type based on the field and method annotation types. |
@MergeMethod | Replaces the method in the existing runtime type. The original runtime method is renamed and made private. Calls using the super keyword to the original method are replaced with dynamic invocations to the renamed original method. |
Modifying the Runtime
As described in the previous section, annotations are used to guide the behavior of JReFrameworker during the bytecode rewriting steps desired in a module. This means that the steps of manipulating bytecode are completely abstracted away through the use of annotations. To convert our HiddenFile
prototype into a working module that rewrites the behavior of java.io.File
we must carefully choose the annotations we wish to apply.
First we should consider the type level annotations. We want to maintain the original functionality of java.io.File
, so we should not use the @DefineType annotation since it would overwrite the entire functionality of the original class. This leaves us with the @MergeType annotation, which allows us to replace parts of the API while maintaining the original functionality of the original class.
The HiddenFile
constructor and the serialVersionUID
field were necessary for compiling and testing the HiddenFile
functionality without warnings. The constructor and field are not needed for modifying the original File
class however, so we will not annotate either the constructor or the field. JReFrameworker will ignore fields and methods that are not annotated when merging two types.
This brings us to the HiddenFile.exists()
method. Using @DefineMethod would be unwise here because it would completely replace the File.exists()
method, which we are still depending on through our super.exists()
method call! Instead we should use the @MergeMethod annotation, which preserves the old File.exists()
method by renaming and making the method private. Any calls to the original File.exists()
method through super calls will automatically get replaced with calls to the renamed version of the original method.
After adding the appropriate annotation our HiddenFile
implementation should look like the following.
package java.io;
import jreframeworker.annotations.methods.MergeMethod;
import jreframeworker.annotations.types.MergeType;
@MergeType
public class HiddenFile extends File {
private static final long serialVersionUID = 1L;
public HiddenFile(String name) {
super(name);
}
@MergeMethod
@Override
public boolean exists(){
if(isFile() && getName().equals("secretFile")){
return false;
} else {
return super.exists();
}
}
}
For a merge, it is not important what package the subclass is in. JReFrameworker will merge the annotated methods and fields of a class to merge into the parent class. However, if we were defining (inserting or replacing) a runtime class the package would be important.
JReFrameworker implements a custom builder that automatically detects annotations and modifies the runtime appropriately. After pressing the save button you have effectively modified the runtime!
Important Note: Until incremental building support is implemented, you may have to perform a Build
> Clean
operation from within Eclipse to trigger a fresh build of the runtime.
Important Note: Note that the modified runtime is only a local version of the runtime. During development JReFrameworker does not replace your system’s version of runtime libraries (as this would negatively effect Eclipse and would be essentially attacking the attacker…). Steps to deploy the module to a victim’s machine will be covered in a later tutorial.
Understanding the Bytecode Manipulations
Let’s digress one more time for a minute to take a look at the bytecode manipulations that were made by JReFrameworker.
The modified version of the runtime library is placed in the runtimes
directory stored in the JReFrameworker project. Let’s decompile the modified rt.jar
runtime file using the free JAD decompiler. Alternatively you may choose to use the free Java Bytecode Viewer to view both the decompiled source and the raw bytecode instructions.
Inspecting the modified version of java.io.File
shows that the original File.exists()
method was renamed to jref_exists()
and made private as shown in the figure below. Note that the prefix used to renamed methods can be edited by changing the JReFrameworker preferences under Eclipse
> Preferences...
(or Window
> Preferences...
) > JReFrameworker
.
Inspecting the new version of java.io.File
reveals that the new File.exists()
method first calls File.isFile()
and then File.getName()
to check if the File
is a file (and not a directory) and that the filename equals “secretFile”. If both conditions are true, then the boolean value of false is returned immediately. Otherwise the value of File.jref_exists()
is returned.
Note that if we inspect at the bytecode level, we would find that special invocations through super calls are replaced with dynamic invocations and all instruction owners have been remapped from the subclass type to the base class type where applicable.
Testing the Modified Runtime
Before we begin testing, we should remember to revert our test logic back to the original test so we avoid calling any versions of the locally implemented HiddenFile
functionality.
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
public class Test {
public static void main(String[] args) throws IOException {
File testFile = new File("secretFile");
FileWriter fw = new FileWriter(testFile);
fw.write("blah");
fw.close();
System.out.println("Secret File Exists: " + testFile.exists());
testFile.delete();
}
}
Now that our test logic is using whatever implementation of java.io.File
exists in the runtime environment we can run it again as a standard Java application. It should return true as is the normal expectation of the runtime. To run Test
again with the modified runtime use the JReFrameworker Run or Debug launch profile as shown in the image below.
Running with either of these launch profiles runs Test
in the modified runtime (located at <project>/runtimes/rt.jar
). If everything was done correctly, the test program should return false! We will cover the steps to deploy the module’s bytecode manipulations on a victims machine in a later step.
At this point you can save and share your module with others by right clicking on the project and navigating to Export...
> General
> Archive File
and saving the project as an archive file.
You can download the module created during this tutorial at https://github.com/JReFrameworker/modules/tree/master/HiddenFile.
A video demonstration of developing this module can be seen at the 7 minute and 28 second mark in the video below.