Skip to content

Match API

EpicPlayerA10 edited this page Jan 17, 2025 · 3 revisions

The Match API is a powerful tool for matching bytecode instruction patterns. It provides an easy-to-use API to match and transform complex bytecode patterns. No need to write complex visitor patterns or manually iterate over instructions. The API is designed to be simple and expressive, allowing you to focus on the logic you want to implement.

🔧 Basic Usage

The most basic example of using Match API looks like this:

Match match = OpcodeMatch.of(ALOAD);
boolean matches = match.matches(insnContext); // Returns true if instruction is ALOAD

It matches bytecode like:

aload 0  # Matches
aload 1  # Matches
aload 2  # Matches
astore 0 # Does not match - different opcode

📦 Built-in Matches

The API provides several built-in matches:

OpcodeMatch

Matches instruction by its opcode:

Match match = OpcodeMatch.of(ALOAD);

Matches bytecode like:

aload 0  # Matches
aload 1  # Matches
aload 2  # Matches
astore 0 # Does not match - different opcode

NumberMatch

Matches numeric instructions (ICONST, BIPUSH, SIPUSH, LDC):

Match match = NumberMatch.numInteger(); // Matches any integer
Match match = NumberMatch.num(5); // Matches number 5

Matches bytecode like:

iconst_5  # Matches both
bipush 5  # Matches both
sipush 5  # Matches both
ldc 5     # Matches both
iconst_1  # Matches only numInteger()
ldc 3.14  # Does not match - not an integer

MethodMatch

Matches method invocations:

Match match = MethodMatch.invokeVirtual() // Any method invocation
    .owner("java/lang/String") // Match owner class
    .name("length") // Match method name
    .desc("()I"); // Match method descriptor

Matches bytecode like:

invokevirtual java/lang/String.length ()I  # Matches
invokevirtual java/lang/String.isEmpty ()Z # Matches only invoke() - different method
invokestatic java/lang/Math.abs (I)I      # Does not match - different class

FieldMatch

Matches field accesses:

Match match = FieldMatch.getStatic() // Static field access
    .owner("java/lang/System") // Match owner class
    .name("out") // Match field name
    .desc("Ljava/io/PrintStream;"); // Match field descriptor

Matches bytecode like:

getstatic java/lang/System.out Ljava/io/PrintStream;  # Matches
putstatic java/lang/System.out Ljava/io/PrintStream;  # Does not match - put instead of get
getstatic java/lang/System.err Ljava/io/PrintStream;  # Does not match - different field name

SequenceMatch

Warning

SequenceMatch should not be used for complex pattern matching. Instead, it is recommended to use FrameMatch as it is more flexible and resilient to slight changes in obfuscation.

Matches sequence of instructions:

Match match = SequenceMatch.of(
    OpcodeMatch.of(ALOAD),
    MethodMatch.invoke(),
    OpcodeMatch.of(POP)
);

Matches bytecode like:

aload 0                                        # First instruction matches
invokevirtual java/lang/Object.toString ()V   # Second instruction matches  
pop                                           # Third instruction matches
# Above sequence matches

aload 0       # First instruction matches
pop           # Does not match - missing invoke in between

⛓️ Chaining Matches

Matches can be chained using logical operators:

Match match = OpcodeMatch.of(ALOAD)
    .and(MethodMatch.invoke().owner("java/lang/String"))
    .or(FieldMatch.getStatic().owner("java/lang/System"));

🎭 Stack values matching using FrameMatch

The Match API supports matching values on the stack! For example, to get System.out.println message:

Match match = MethodMatch.invokeVirtual()
    .owner("java/io/PrintStream")
    .name("println")
    .desc("(Ljava/lang/String;)V")
    .and(FrameMatch.stack(0, OpcodeMatch.of(LDC).capture("println-msg")));

MatchContext result = match.matchResult(insnContext);
LdcInsnNode ldcInsnNode = (LdcInsnNode) result.captures().get("println-msg").insn();

System.out.println("println message: " + ldcInsnNode.cst);

Matches bytecode like:

getstatic java/lang/System.out Ljava/io/PrintStream;
ldc "Hello World"
invokevirtual java/io/PrintStream.println (Ljava/lang/String;)V  # Matches

As well as this:

getstatic java/lang/System.out Ljava/io/PrintStream;
ldc "Hello World"
dup
dup
pop
pop
invokevirtual java/io/PrintStream.println (Ljava/lang/String;)V  # Matches

🎯 Capturing Matches

You can capture specific instructions for later use:

Match match = MethodMatch.invoke().capture("method-call");

MatchContext result = match.matchResult(insnContext);
// We can now access the captured instruction
MethodInsnNode methodCall = (MethodInsnNode)result.captures().get("method-call").insn();

🎨 Examples

Remove all System.out.println calls

Match match = MethodMatch.invoke()
    .owner("java/io/PrintStream")
    .name("println");

Matches bytecode like:

getstatic java/lang/System.out Ljava/io/PrintStream;
ldc "Hello World"
invokevirtual java/io/PrintStream.println (Ljava/lang/String;)V  # Matches

Match sequence ending with return

Match match = SequenceMatch.of(
    NumberMatch.num(0),
    OpcodeMatch.of(IRETURN)
);

Matches bytecode like:

iconst_0  # First instruction matches
ireturn   # Second instruction matches

bipush 0  # First instruction matches
ireturn   # Second instruction matches

iconst_1  # Does not match - different number
ireturn   

Other Examples

You can see other examples inside transformers. Here are some examples: