ARMR Patch Rule
The ARMR Patch rule provides the user with the ability to change the behaviour of a class at runtime. While ARMR Patch rules can target any class loaded by the JVM, some ARMR Engine implementations may choose to restrict patching of a small number of primordial classes that are tightly coupled to the JVM, such as java.lang.String
, java.lang.Class
, java.lang.Object
. In all other cases, any class loaded by the JVM can be patched by an ARMR Patch rule.
Given (Conditions)
The Patch rule has one condition that is specified via the function
statement. This is used to identify the function to be patched. The function statement must contain the fully-qualified class name, method name, and method descriptor of the target function to be patched, specified using the internal notation of the underlying machine, such as the JVM or the CLR.
When (Event)
ARMR Patch rules are applied to targeted bytecode instructions at runtime. The Patch rule supports many different types of event statements, called location-specifiers. Each location-specifier identifies a bytecode instruction within the function where the patch should be applied.
Then (Action)
Unlike the other ARMR rules that have various declarative actions like detect, protect, deny, etc; a Patch rule must provide its intended action as a code block of supplied source-code. The code-block is specified by means of the code
keyword and terminated with the endcode
keyword. When the defined event conditions of a given Patch rule are triggered, the specified code statement will be compiled and executed at the specified patch location.
Runtime Notation
In order to target events, the function
and location-specifier need to be declared using the internal signature notation used by the machine running the instructions. In the case of Java, this is the Java Virtual Machine (JVM). An event is relative to the function of a particular namespace. Using Java as an example, consider the following line of code.
String.valueOf(8);
The invocation of the method valueOf()
on the String
class would actually appear differently written in the JVMs internal form. The following example shows how it would appear.
Part Name | Part | Description |
---|---|---|
Class | java/lang/String | The fully qualified name FQN of the class that contains the targetted method. |
Method | valueOf | The method name that needs to be targeted. Overloaded methods will have different arguments. |
Arguments | (I) | It is important to target the specific method by specifying the correct arguments, in the order they are expected. |
Return Type | Ljava/lang/String; | The return type is always declared at the end of the signature. |
Descriptor | (I)Ljava/lang/String; | The descriptor is a combination of the arguments and the return type. |
Java Types
Type | Internal | Example | Default Value | Size | Frame Slot Allocation |
---|---|---|---|---|---|
object | L<type> | Ljava/lang/String; | null | 16 bytes minimum | 1 |
boolean | Z | false | 1 bit | 1 | |
byte | B | 0 | 8 bit signed | 1 | |
char | C | \u0000 | 16 bit | 1 | |
double | D | 0.0d | 64 bit | 2 | |
float | F | 0.0f | 32 bit | 1 | |
int | I | 0 | 32 bit | 1 | |
long | J | 0L | 64 bit | 2 | |
short | S | 0 | 16 bit | 1 | |
void | V | N/A | |||
Single dimensional array | [<type> | [J | 1 | ||
Multidimensional array | [[<type> | [[java/lang/Object; | 1 |
More JVM Internal Form Examples
java/lang/String.toUpperCase()Ljava/lang/String;
java/lang/Class.forName(Ljava/lang/String;)Ljava/lang/Class;
java/io/File.setReadable(Z)Z
java/util/Hashtable.<init>(I)V
com/sun/crypto/provider/DESKey.getEncoded()[B
Function
The function is the main target of the Given (Condition) step. It identifies the exact method of the exact class that we would like to apply the patch to. As an example, if we wanted our ARMR Patch rule to target the constructor for java.net.URI(String str)
the function
would be written as follows.
function("java/net/URI.<init>(Ljava/lang/String;)V")
Location-Specifier
The location-specifier provides the When (Event) step. Once we have defined the Class and method we would like to patch in the function statement, we can use one of the location-specifier statements to declare a specific instruction within the function where the patch should be applied. Here is a complete list of all available location-specifiers statements.
entry() | instruction() | read() | write() | call() |
exit() | line() | readsite() | writesite() | callsite() |
error() | readreturn() | writereturn() | callreturn() |
Every Patch rule must specify a single location-specifier. Every location-specifier, except for entry()
and exit()
must take an argument. The differences for each location-specifier will be discussed in the following tables.
ENTRY / EXIT / ERROR
Location | Example | Description |
---|---|---|
entry() | Apply the patch at the start of the targeted function, before the first bytecode instruction is executed in the targeted function. | |
exit() | Apply the patch at the return instruction from the targeted function. There may be more than one return instruction in a method, and the patch will be applied at every return instruction. | |
error() | error("java/io/IOException") | Apply the patch to every exception which propagates from the targeted function. |
INSTRUCTION / LINE
Location | Example | Description |
---|---|---|
instruction() | instruction(391) | Apply the patch immediately before the bytecode instruction at the specified instruction offset of the target function instruction stream. |
line() | line(12) | Trigger the patch immediately before the instruction at the specified source code line number. |
READ / READSITE / READRETURN
Location | Example | Description |
---|---|---|
read() | read("java/io/File.path") | Apply the patch in place of the memory read instruction for the specified memory field. |
readsite() | readsite("java/io/File.path") | Apply the patch immediately before the memory read instruction for the specified memory field. |
readreturn() | readreturn("java/io/File.path") | Apply the patch immediately after the memory read instruction for the specified memory field. |
WRITE / WRITESITE / WRITERETURN
Location | Example | Description |
---|---|---|
write() | write("java/io/File.path") | Apply the patch in place of the memory write instruction for the specified memory field. |
writesite() | writesite("java/io/File.path") | Apply the patch immediately before the memory write instruction for the specified memory field. |
writereturn() | writereturn("java/io/File.path") | Apply the patch immediately after the memory write instruction for the specified memory field. |
CALL / CALLSITE / CALLRETURN
Location | Example | Description |
---|---|---|
call() | call(``"java/lang/String.valueOf(I)``Ljava/lang/String;") | Apply the patch in place of the invoke instruction for the specified method. |
callsite() | callsite(``"java/lang/String.valueOf(I)``Ljava/lang/String;") | Apply the patch immediately before the invoke instruction for the specified method. |
callreturn() | callreturn(``"java/lang/String.valueOf(I)``Ljava/lang/String;") | Apply the patch immediately after the invoke instruction for the specified method. |
Code
The code block contains the source code that will be compiled into the target function by the ARMR Engine. The type of source code needs to be declared by the use of the language parameter. If the type of source code is not supported by the underlying runtime, the ARMR Patch will not be linked. The source code in the code block can reference runtime classes by means of import declarations. For Java code blocks, this is done using the import
keyword. The optional import parameter takes an array of strings that represent the runtime classes to import. The example here illustrates the use of the code block. As shown, the language parameter is set to java and the import parameter has an import for java.io.IOException
.
app("Security Policy"):
requires(version: "ARMR/2.0")
patch("Example Patch"):
function("java/net/URI.<init>(Ljava/lang/String;)V")
entry()
code(language: java, import: ["java.io.IOException"]):
private static final String MSG = "The patch is working!";
public void patch(JavaFrame frame) {
frame.raiseException(new IOException(MSG));
}
endcode
endpatch
endapp
All text between the code
block’s opening and closing declarations will be interpreted as source code. ARMR language syntax should not be used within the code block. It is possible to create new methods, classes, static blocks, instance fields or static fields within the code block. It is important to note that in ARMR 2.0, source code written in one ARMR Patch is not shared with any another ARMR Patch.
ARMR Patch Methods
An ARMR Patch rule makes certain methods available to the patch developer that are tied to the ARMR Rule life-cycle. These methods can be overridden to provide customized behavior at each lifecycle event.
Method | Required | Description |
---|---|---|
public void load(); | optional | The load() method will be invoked once the link life-cycle event is triggered. Since this is a once-off event, the load method is useful for the initialization of the state. |
public void patch(JavaFrame frame); | mandatory | The patch() method will be invoked once the execute life-cycle event is triggered. This event can happen multiple times. Every patch must implement the patch() method. |
public void unload(); | optional | The unload() method will be invoked once the unlink life-cycle event is triggered. As the load() event, this is also a once-off event. |
ARMR Patch State
The ARMR Engine provides an efficient memory-store for patches within the same ARMR Mod to share memory state between them. The memory-store for a given ARMR Mod can be accessed via two built-in functions within the ARMR Engine.
Method | Description |
---|---|
saveValue(Object key, Object value) | Store an object into the shared cache with a unique key. |
restoreValue(Object key) | Retrieve an object stored into the shared cache by passing in the key. |
JavaFrame
The JavaFrame accessor provides access to the active frame of the patched function at the location where the patch is applied. Using the JavaFrame accessor, the current state and contents of the operand stack and local variables can be read and overwritten. The active JavaFrame accessor is provided to the patch developer as the single argument to the patch()
method. Please refer to the JavaFrame API for a detailed list of the accessors for reading and writing active frame state. For more information regarding frames, local variable array, and operand stack, please refer to Java Virtual Machine specification at Oracle’s “The Java Virtual Machine Specification”.
JavaField / JavaMethod
The JavaField and JavaMethod accessors are provided by the ARMR Engine for unrestricted access to any members of any class. They can be used by a Patch rule to access private members, overwrite final fields, and other similar operations. The following example highlights how to create a JavaField accessor, and how it can be used to read/write a private field. Use of the JavaMethod accessor follows the same convention. Please refer to the JavaField / JavaMethod API documentation for further information.
app("Security Policy"):
requires(version: "ARMR/2.0")
patch("Patch File.getCanonicalPath() Method"):
function("java/io/File.getCanonicalPath()Ljava/lang/String;")
error("java/io/IOException")
code(language: java, import: ["java.io.IOException"]):
private static JavaField detailMessageField;
public void load() {
detailMessageField = JavaField.load(
"java/lang/Throwable.detailMessage");
}
public void patch(JavaFrame frame) {
IOException ioe = (IOException) frame.loadObjectOperand(0);
String detailMessage = detailMessageField.readString(ioe);
detailMessageField.writeString(ioe,
"The IOException message has been changed!");
}
endcode
endpatch
endapp
Patch Rule Example
Consider the following Java source code.
package ie.example;
public class Utils {
public byte[] createByteArray(int length) {
return new byte[length];
}
}
The Java bytecode for the method createByteArray()
can be seen here.
public byte[] createByteArray(int);
descriptor: (I)[B
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: iload_1
1: newarray byte
3: areturn
LineNumberTable:
line 4: 0
Now consider the case where a source-code change was introduced to check whether the integer argument called length
is a positive integer and that it does not exceed a size of 100 before creating the byte[]
, throwing an IllegalStateException
if either of these conditions are not met. Here is what the new source-code would look like for the createByteArray()
method.
package ie.example;
public class Utils {
public byte[] createByteArray(int length) {
if (length < 0 || length > 100) {
throw new IllegalStateException("Length must be a positive integer and cannot exceed a size of 100");
}
return new byte[length];
}
}
To apply the same effect with an ARMR Patch rule is trivial. To do so, we can create an ARMR Patch rule
that targets the createByteArray()
method, at the entry location, to be applied before instruction 0 is executed. At this location, the ARMR Patch will have access to the length argument from the local variable array, and can perform the same check conditions, raising an IllegalStateException
if either of the conditions are not met. Below is an example ARMR Patch to provide this behavior.
-
Function:
"ie/example/Utils.createByteArray()[B"
-
Location Specifier:
entry()
app("Security Policy"):
requires(version: "ARMR/2.0")
patch("Example Patch")
function("ie/example/Util.createByteArray(I)[B")
entry()
code(language: java):
public void patch(JavaFrame frame) {
int length = frame.loadIntVariable(1);
if (length < 0 || length > 100) {
frame.raiseException(new IllegalStateException("Length must be a positive integer and cannot exceed a size of 100"));
}
}
endcode
endpatch
endapp
The above ARMR App declares a single ARMR Patch rule. The rule has the following statements.
-
function
- the signature of the method which contains the code to be patched
-
location-specifier
- the specific location within the function where the patch should be applied
-
code
- the code to be compiled into the target function at the specified location
Occurrences
In certain cases, there may be multiple locations of the same bytecode instruction with a target function
being patched. It is possible to select the exact instruction by using an optional parameter to the function statement called occurrences
. The occurrences parameter is a key:value pair with a key of occurrences and the value is an array of integers that represent each occurrence of the location specifier. Only the specified occurrence(s) will be patched. If an occurrence has been specified that is out of bounds, i.e. that occurrence does not exist, then it will be ignored and the ARMR Patch rule will apply where applicable. The occurrences parameter can be specified on the following location specifiers.
Location | Example | Description |
---|---|---|
read() | read("java/io/File.path", occurrences: [2]) | Apply the patch by replacing only the 2nd occurrence of the getfield bytecode instruction of the path field. |
readsite() | readsite("java/io/File.path", occurrences: [3, 5]) | Apply the patch immediately before the 3rd and 5th occurrence of the getfield bytecode instruction of the path field. |
readreturn() | readreturn("java/io/File.path", occurrences: [4, 6]) | Apply the patch immediately after the 4th and 6th occurrence of the getfield bytecode instruction of the path field. |
write() | write("java/io/File.path", occurrences: [1, 7]) | Apply the patch by replacing the 1st and 7th occurrence of the putfield bytecode instruction of the path field. |
writesite() | writesite("java/io/File.path", occurrences: [2]) | Apply the patch immediately before the 2nd occurrence of the putfield bytecode instruction of the path field. |
writereturn() | writereturn("java/io/File.path", occurrences: [4, 6]) | Apply the patch immediately after the 4th and 6th occurrence of the putfield bytecode instruction of the path field. |
call() | call("java/lang/String.valueOf(I) Ljava/lang/String;", occurrences: [2]) | Apply the patch by replacing only the 2nd occurrence of the invoke* bytecode instruction of the valueOf method. |
callsite() | callsite("java/lang/String.valueOf(I) Ljava/lang/String;", occurrences: [3, 5]) | Apply the patch immediately before the 3rd and 5th occurrence of the invoke* bytecode instruction of the valueOf method. |
callreturn() | callreturn("java/lang/String.valueOf(I) Ljava/lang/String;", occurrences: [4, 6]) | Apply the patch immediately after the 4th and 6th occurrence of the invoke* bytecode instruction of the valueOf method. |
Consider the following example.
package com.example;
class Person {
private int age;
public Person(int age) {
this.age = age;
}
@Override
public String toString() {
if (age == 0) {
return "This person has no age.";
}
return "This person is " + age + " years old.";
}
}
The following bytecode is for the toString()
method as shown in the Person class.
public java.lang.String toString();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field age:I
4: ifne 10
7: ldc #3 // String This person has no age.
9: areturn
10: new #4 // class java/lang/StringBuilder
13: dup
14: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
17: ldc #6 // String This person is
19: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: aload_0
23: getfield #2 // Field age:I
26: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
29: ldc #9 // String years old.
31: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
34: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
37: areturn
As shown here, the age
field is being read at two different locations. The getfield
bytecode instruction is called at instruction 1
and 23
. If the ARMR developer is interested in only targeting the second getfield
instruction, then they can use one of the read location specifiers, and pass in an occurrence of 2.
app("Security Policy"):
requires(version: "ARMR/2.0")
patch("Readsite patch")
function("com/example/Person.toString()Ljava/lang/String")
readsite("com/example/Person.age", occurrences: [2])
code(language: java):
public void patch(JavaFrame frame) {
// patch code here
}
endcode
endpatch
endapp
Whenever an ARMR developer specifies an occurrence that does not exist, that specific occurrence is ignored but others will still trigger the rule. For example, "occurrences: [2]
" and "occurrences: [2,3]
" would produce the same effect in the above case. However, if all the occurrences specified are out of bounds, then the patch code cannot be applied. For such cases, a link error message is generated in the CEF log file specifying the maximum event count and the set of configured occurrences of the patch. Consider the following readsite specifier.
readsite("com/example/Person.age", occurrences: [3])
Since there is no third occurrence of the getfield instruction for the age field, the patch cannot be applied. As a result, a log message will be printed to the CEF log file.
<14>1 2020-07-09T04:01:00.321Z win_system_1 java 18914 - - CEF:0|ARMR:ARMR|ARMR|2.2|rule 2|Link Rule|Very-High|rt=Jul 09 2020 04:01:00.307 +0000 dvchost=win_system_1 procid=18914 ruleType=patch securityFeature=patch outcome=failure reason=occurrences for patch [3] exceed the maximum occurrence count 2
Patch Execution Ordering And Greedy Location Specifiers
It is possible for plural ARMR Patch rules to target the same function code at the same location-specifier.
In such cases, all plural site and return patches will be applied sequentially in an undefined,
implementation-specific order.
However, when two or more ARMR Patch rules target a call()
, read()
, or write()
location-specifier in a target function, then only one of the patches will be applied with link errors recorded for the matching but unapplied patches.
The location-specifiers for which only one patch can be applied at a time are known as greedy location
specifiers. They are different from the site and return location specifiers as they consume (i.e. replace)
the targeted bytecode instruction.
As an example consider the following Java program.
package ie.example;
class Person {
private int age;
public Person(int age) {
this.age = age;
}
@Override
public String toString() {
String message;
if (age == 0) {
message = "This person has no age.";
} else {
message = "This person is " + age + " years old.";
}
return message;
}
}
The following ARMR Mod contains some ARMR Patch rules that all target the same field in the same function using the various write location specifiers. Two of the patches are write() patches, which is a greedy specifier. Only one of the write() patches will be applied; the other will be recorded with a link error.
app("Security Policy"):
requires(version: "ARMR/2.0")
patch("write :01"):
function("com/example/Person.toString()Ljava/lang/String")
write("com/example/Person.message")
code(language: java):
public void patch(JavaFrame frame) {
// patch code here
}
endcode
endpatch
patch("write :02"):
function("com/example/Person.toString()Ljava/lang/String")
write("com/example/Person.message")
code(language: java):
public void patch(JavaFrame frame) {
// patch code here
}
endcode
endpatch
patch("writesite"):
function("com/example/Person.toString()Ljava/lang/String")
writesite("com/example/Person.message")
code(language: java):
public void patch(JavaFrame frame) {
// patch code here
}
endcode
endpatch
patch("writereturn"):
function("com/example/Person.toString()Ljava/lang/String")
writereturn("com/example/Person.message")
code(language: java):
public void patch(JavaFrame frame) {
// patch code here
}
endcode
endpatch
endapp
In these cases, the first patch rule to trigger will greedily consume the memory write instruction and subsequent rules with an identical location specifier will be unable to be applied. Whenever there is a conflict of this sort, a link error notice will be printed to the ARMR Engine’s event file to notify the user that a rule was suppressed due to the conflict.
<14>1 2020-07-09T04:01:00.321Z win_system_1 java 18914 - - CEF:0|ARMR:ARMR|ARMR|2.3|patch person|Link Rule|Very-High|rt=Jul 09 2020 04:01:00.307 +0000 dvchost=win_system_1 procid=18914 ruleType=patch securityFeature=patch outcome=failure appVersion=1
Life-Cycle For ARMR Patch Rule
The linking conditions for an ARMR Patch rule are as follows. The method matching the function statement must first be found by the ARMR Engine. If the target function is never loaded into the JVM, then the patch will not be applied. As a result, the link event for that ARMR Patch rule will not occur. Similarly, if the target function is loaded into the JVM, but the location-specifier statement cannot be matched to one-or-more instructions in the target function, then again, the patch will not be applied and the link event for that ARMR Patch rule will not occur.
For an ARMR Patch rule to link, the target function must be found, and the location-specifier with the target function must also be found. When both of these cases are true (the target function is found
and one or more location-specifier(s) are found) then the ARMR Engine will link that ARMR Patch rule into the target function.
Linking events can occur at any time during JVM execution, but will always occur before the target function and location-specifier(s) begin executing for the first time. However linking does not necessary have to happen during the startup of the application. At any time the matching function/location-specifier is loaded into the JVM, the ARMR Engine will link the matching ARMR Patch rule.
The link state is also useful when debugging an ARMR Patch rule. If no link states are noted in the ARMR Engine event log when it was expected to present, this may indicate that there is an error in the signature specified in the function statement or location-specifier.
During the link state for an ARMR Patch rule, the Java code contained in the code block will be compiled. It is during this time that any compilation errors will be reported and logged in the ARMR Engine event log.
JavaFrame API
The 'this' Variable
Returns the this
instance for non-static functions.
Object loadThisVariable()
Raising Exceptions
When there is a deliberate intention to throw an Exception in the context of the running application,
an Exception needs to be raised. If an uncaught Exception is thrown from the patch(JavaFrame) method of an ARMR Patch, the ARMR Engine will consider the ARMR Patch rule to be broken, and immediately unlink (i.e, uncompile) the offending ARMR Patch rule from the target function.
void raiseException(Throwable throwable)
Returning Values
There are cases where an ARMR Patch will be required to return a value from the patched function, which will prevent any further bytecode instructions to be executed after the location at which the ARMR Patch was applied.
void returnVoid()
void returnFloat(float returnValue)
void returnBoolean(boolean returnValue)
void returnInt(int returnValue)
void returnDouble(double returnValue)
void returnLong(long returnValue)
void returnChar(char returnValue)
void returnByte(byte returnValue)
void returnShort(short returnValue)
void returnString(String returnValue)
void returnObject(Object returnValue)
Load Variables
The loadVariable methods are used to read values stored in a certain index in the local variable array. Please note that long and double take up two index slots.
void loadFloatVariable(int index)
void loadIntVariable(int index)
void loadDoubleVariable(int index)
void loadLongVariable(int index)
void loadBooleanVariable(int index)
void loadByteVariable(int index)
void loadShortVariable(int index);
void loadCharVariable(int index);
void loadStringVariable(int index);
void loadObjectVariable(int index);
Store Variables
The storeVariable methods are used to write values to a certain index in the local variable array. Please note that long and double take up two index slots.
void storeFloatVariable(int index, float newValue)
void storeIntVariable(int index, int newValue)
void storeDoubleVariable(int index, double newValue)
void storeLongVariable(int index, long newValue)
void storeBooleanVariable(int index, boolean newValue)
void storeByteVariable(int index, byte newValue)
void storeShortVariable(int index, short newValue);
void storeCharVariable(int index, char newValue);
void storeStringVariable(int index, String newValue);
void storeObjectVariable(int index, Object newValue);
Load Operand
The loadOperand methods are used to read values stored in a certain index in the operand stack. Please note that long and double take up two index slots.
float loadFloatOperand(int index)
int loadIntOperand(int index)
double loadDoubleOperand(int index)
long loadLongOperand(int index)
boolean loadBooleanOperand(int index)
byte loadByteOperand(int index)
short loadShortOperand(int index);
char loadCharOperand(int index);
String loadStringOperand(int index);
Object loadObjectOperand(int index);
Store Operand
The storeOperand methods are used to write values to a certain index in the operand stack. Please note that long and double take up two index slots.
void storeFloatOperand(int index, float newValue)
void storeIntOperand(int index, int newValue)
void storeDoubleOperand(int index, double newValue)
void storeLongOperand(int index, long newValue)
void storeBooleanOperand(int index, boolean newValue)
void storeByteOperand(int index, byte newValue)
void storeShortOperand(int index, short newValue);
void storeCharOperand(int index, char newValue);
void storeStringOperand(int index, String newValue);
void storeObjectOperand(int index, Object newValue);