-
Notifications
You must be signed in to change notification settings - Fork 73
3.2. Match API
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.
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
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 matchMatches 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 matchMatches 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 matchMatches 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 printlnMatches 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()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 sequenceMatches 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 anyCombines 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 conditionsMatches 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 patternUse .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")));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...
});// 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// 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;// 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- Start Simple: Begin with basic matchers and combine them progressively
- Use Captures: Always capture important instructions you'll need later
-
Frame Analysis: Use
FrameMatchfor complex stack-based patterns - Test Patterns: Verify your matchers work on known bytecode examples
-
Combine Logically: Use
.and(),AnyMatch, andSequenceMatchappropriately - 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.