Skip to content

3.2. Match API

EpicPlayerA10 edited this page Sep 6, 2025 · 3 revisions

The Match API is a powerful pattern matching system that allows you to find and match specific bytecode patterns in Java methods. Instead of manually iterating through instructions and checking conditions, you can declaratively describe what you're looking for.

🎯 Basic Concepts

The Match API works by creating matchers that can identify specific instruction patterns. Each matcher can:

  • Match single instructions or sequences
  • Capture matched instructions for later use
  • Combine with other matchers using logical operations
  • Work with stack frame analysis

πŸ” Core Match Types

OpcodeMatch

Matches instructions by their opcodes.

// Match any LDC instruction
Match ldcMatch = OpcodeMatch.of(LDC);

// Match INVOKEVIRTUAL
Match invokeVirtualMatch = OpcodeMatch.of(INVOKEVIRTUAL);

Bytecode it matches:

ldc "Hello World"     # βœ“ Matches
ldc 42               # βœ“ Matches
invokevirtual ...    # βœ— Doesn't match

NumberMatch

Matches numeric constants (LDC, ICONST, BIPUSH, etc.).

// Match any number
Match anyNumber = NumberMatch.of();

// Match specific number
Match specificNumber = NumberMatch.of(42);

// Match long numbers
Match longNumber = NumberMatch.numLong();

Bytecode it matches:

ldc 42              # βœ“ Matches NumberMatch.of(42)
iconst_1            # βœ“ Matches NumberMatch.of()
bipush 100          # βœ“ Matches NumberMatch.of()
ldc 123L            # βœ“ Matches NumberMatch.numLong()
ldc "string"        # βœ— Doesn't match

StringMatch

Matches string constants.

// Match any string
Match anyString = StringMatch.of();

// Match specific string
Match specificString = StringMatch.of("Hello World");

Bytecode it matches:

ldc "Hello World"   # βœ“ Matches StringMatch.of("Hello World")
ldc "Other text"    # βœ“ Matches StringMatch.of()
ldc 42             # βœ— Doesn't match

MethodMatch

Matches method invocations with detailed filtering.

// Match any static method call
Match staticCall = MethodMatch.invokeStatic();

// Match specific method
Match printlnCall = MethodMatch.invokeVirtual()
    .owner("java/io/PrintStream")
    .name("println")
    .desc("(Ljava/lang/String;)V");

// Match by descriptor pattern
Match lookupMethod = MethodMatch.invokeStatic()
    .and(Match.of(ctx -> ((MethodInsnNode) ctx.insn()).desc.startsWith("(JJLjava/lang/Object;)")));

Bytecode it matches:

invokestatic java/lang/Math.max (II)I                    # βœ“ Matches invokeStatic()
invokevirtual java/io/PrintStream.println (Ljava/lang/String;)V  # βœ“ Matches println example
invokevirtual java/lang/Object.toString ()Ljava/lang/String;     # βœ— Doesn't match println

FieldMatch

Matches field access instructions.

// Match static field access
Match staticFieldGet = FieldMatch.getStatic();

// Match specific field
Match systemOut = FieldMatch.getStatic()
    .owner("java/lang/System")
    .name("out")
    .desc("Ljava/io/PrintStream;");

// Match field writes
Match fieldWrite = FieldMatch.putStatic().desc("J");

Bytecode it matches:

getstatic java/lang/System.out Ljava/io/PrintStream;  # βœ“ Matches getStatic() and systemOut
putstatic MyClass.field J                             # βœ“ Matches putStatic().desc("J")
getfield MyClass.instanceField I                      # βœ— Doesn't match getStatic()

πŸ”— Combining Matchers

SequenceMatch

Matches a sequence of instructions in order.

Match sequence = SequenceMatch.of(
    OpcodeMatch.of(ALOAD),
    OpcodeMatch.of(ARETURN)
);

Bytecode it matches:

aload_0        # βœ“ First instruction matches
areturn        # βœ“ Second instruction matches - complete sequence

AnyMatch

Matches if any of the provided matchers match (OR logic).

Match anyConvert = AnyMatch.of(
    MethodMatch.invokeVirtual().name("intValue").desc("()I"),
    MethodMatch.invokeVirtual().name("longValue").desc("()J"),
    MethodMatch.invokeVirtual().name("floatValue").desc("()F")
);

Bytecode it matches:

invokevirtual java/lang/Integer.intValue ()I     # βœ“ Matches first option
invokevirtual java/lang/Long.longValue ()J       # βœ“ Matches second option
invokevirtual java/lang/String.length ()I        # βœ— Doesn't match any

Chaining with .and()

Combines matchers with AND logic.

Match complexMatch = MethodMatch.invokeStatic()
    .name("valueOf")
    .and(FrameMatch.stack(0, NumberMatch.of()));

Bytecode it matches:

ldc 42                                          # Stack preparation
invokestatic java/lang/Integer.valueOf (I)Ljava/lang/Integer;  # βœ“ Matches both conditions

πŸ“Š Frame-based Matching

FrameMatch

Matches based on stack frame contents, enabling sophisticated pattern recognition.

// Match method call where top stack value is a number
Match methodWithNumber = MethodMatch.invokeVirtual()
    .name("println")
    .and(FrameMatch.stack(0, NumberMatch.of()));

// Complex stack analysis
Match complexFrame = FieldMatch.putStatic().desc("J")
    .and(FrameMatch.stack(0, MethodMatch.invokeInterface().desc("(J)J")
        .and(FrameMatch.stack(0, NumberMatch.numLong().capture("decrypt-key")))
        .and(FrameMatch.stack(1, MethodMatch.invokeStatic().capture("create-decrypter")))
    ));

Bytecode it matches:

# For methodWithNumber:
ldc 42                                                   # Pushes number to stack
getstatic java/lang/System.out Ljava/io/PrintStream;     # Stack: [42, PrintStream]
swap                                                     # Stack: [PrintStream, 42]
invokevirtual java/io/PrintStream.println (I)V           # βœ“ Matches - stack[0] is number

# For complexFrame (Zelix long decryption pattern):
ldc 5832394289974403481L                                  # Key 1
ldc -8943439614781261032L                                 # Key 2  
invokestatic SomeClass.createDecrypter (JJLjava/lang/Object;)LSomeDecrypter;  # Create decrypter
ldc 19597665297729L                                       # Decrypt key
invokeinterface SomeDecrypter.decrypt (J)J                # Decrypt method
putstatic SomeClass.field J                               # βœ“ Matches complex pattern

πŸ“¦ Capturing and Using Results

Capturing Matches

Use .capture("name") to save matched instructions for later use.

Match captureExample = MethodMatch.invokeStatic().capture("method-call")
    .and(FrameMatch.stack(0, StringMatch.of().capture("string-arg")))
    .and(FrameMatch.stack(1, NumberMatch.of().capture("number-arg")));

Using Captured Results

captureExample.findAllMatches(methodContext).forEach(matchContext -> {
    MethodInsnNode methodCall = (MethodInsnNode) matchContext.captures().get("method-call").insn();
    String stringArg = matchContext.captures().get("string-arg").insn().asString();
    int numberArg = matchContext.captures().get("number-arg").insn().asInteger();
    
    // Use the captured values...
});

🌍 Real-World Examples

Example 1: Zelix Try-Catch Removal

// Pattern: Methods that instantly return exceptions
private static final Match INSTANT_RETURN_EXCEPTION = SequenceMatch.of(
    OpcodeMatch.of(ALOAD),    // Load exception parameter
    OpcodeMatch.of(ARETURN)   // Return it immediately
);

// Pattern: Invoke method and throw result
private static final Match INVOKE_AND_RETURN = SequenceMatch.of(
    MethodMatch.invokeStatic().capture("invocation"),  // Call static method
    OpcodeMatch.of(ATHROW)                             // Throw returned exception
);

Bytecode patterns:

# INSTANT_RETURN_EXCEPTION matches:
aload_0        # Load exception parameter
areturn        # Return it

# INVOKE_AND_RETURN matches:
invokestatic SomeClass.wrapException (LException;)LException;
athrow         # Throw the result

Example 2: String Decryption Detection

// Superblaubeere string decryption pattern
private static final Match STRING_DECRYPT_BLOWFISH_MATCH = SequenceMatch.of(
    MethodMatch.invokeVirtual().owner("java/lang/String").name("getBytes"),
    MethodMatch.invokeVirtual().owner("java/security/MessageDigest").name("digest"),
    StringMatch.of("Blowfish"),
    MethodMatch.invokeSpecial().owner("javax/crypto/spec/SecretKeySpec").name("<init>")
);

// Usage pattern
Match STRING_DECRYPT_USAGE = MethodMatch.invokeStatic()
    .and(FrameMatch.stack(0, StringMatch.of().capture("key")))
    .and(FrameMatch.stack(1, StringMatch.of().capture("encrypted")));

Bytecode patterns:

# STRING_DECRYPT_BLOWFISH_MATCH detects this decryption method:
aload_1                                                          # Load key string
getstatic java/nio/charset/StandardCharsets.UTF_8               # Get charset
invokevirtual java/lang/String.getBytes (Ljava/nio/charset/Charset;)[B  # Convert to bytes
invokevirtual java/security/MessageDigest.digest ([B)[B         # Hash the key
ldc "Blowfish"                                                   # Algorithm name
invokespecial javax/crypto/spec/SecretKeySpec.<init> ([BLjava/lang/String;)V

# STRING_DECRYPT_USAGE finds calls like:
ldc "encrypted_data_here"                                        # Encrypted string
ldc "key_here"                                                   # Decryption key
invokestatic MyClass.decrypt (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;

Example 3: Number Pool Detection

// Detect number pool initialization
Match NUMBER_POOL_INIT = FieldMatch.putStatic().capture("field")
    .and(FrameMatch.stack(0, 
        OpcodeMatch.of(ANEWARRAY).capture("arrayType")
            .and(FrameMatch.stack(0, NumberMatch.of().capture("size")))
    ));

// Detect number pool access
Match NUMBER_POOL_ACCESS = OpcodeMatch.of(AALOAD)
    .and(FrameMatch.stack(0, NumberMatch.of().capture("index")))
    .and(FrameMatch.stack(1, FieldMatch.getStatic()));

Bytecode patterns:

# NUMBER_POOL_INIT matches:
bipush 100                                    # Array size
anewarray java/lang/Integer                   # Create Integer array
putstatic MyClass.numberPool [Ljava/lang/Integer;  # Store in field

# NUMBER_POOL_ACCESS matches:
getstatic MyClass.numberPool [Ljava/lang/Integer;  # Load number pool
iconst_5                                           # Array index
aaload                                             # Load array element

πŸ’‘ Best Practices

  1. Start Simple: Begin with basic matchers and combine them progressively
  2. Use Captures: Always capture important instructions you'll need later
  3. Frame Analysis: Use FrameMatch for complex stack-based patterns
  4. Test Patterns: Verify your matchers work on known bytecode examples
  5. Combine Logically: Use .and(), AnyMatch, and SequenceMatch appropriately
  6. Handle Edge Cases: Consider variations in bytecode generation

The Match API provides a declarative way to find complex bytecode patterns without manually parsing instructions, making transformer development much more maintainable and readable.

Clone this wiki locally