Skip to content

Commit 2beb1dd

Browse files
committed
Replace bogus instructions that jump beyond the method code bounds
This kind of trick only works with -noverify so the replacement being NOP + RETURN is still 'wrong' but less wrong. ASM will not crash when reading the class, and the code where this occurs cannot be visited via normal control flow anyways. A dead code pass should remove this.
1 parent 9611e3c commit 2beb1dd

File tree

4 files changed

+98
-4
lines changed

4 files changed

+98
-4
lines changed

core/src/main/java/software/coley/cafedude/classfile/attribute/CodeAttribute.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package software.coley.cafedude.classfile.attribute;
22

3+
import jakarta.annotation.Nonnull;
4+
import jakarta.annotation.Nullable;
35
import software.coley.cafedude.classfile.behavior.AttributeHolder;
46
import software.coley.cafedude.classfile.behavior.CpAccessor;
57
import software.coley.cafedude.classfile.constant.CpClass;
@@ -8,8 +10,6 @@
810
import software.coley.cafedude.classfile.instruction.Instruction;
911
import software.coley.cafedude.io.AttributeContext;
1012

11-
import jakarta.annotation.Nonnull;
12-
import jakarta.annotation.Nullable;
1313
import java.util.Collections;
1414
import java.util.List;
1515
import java.util.Set;
@@ -50,6 +50,38 @@ public CodeAttribute(@Nonnull CpUtf8 name, int maxStack, int maxLocals, @Nonnull
5050
this.attributes = attributes;
5151
}
5252

53+
/**
54+
* A instance matching index-of since the standard {@link List#indexOf(Object)} uses object equality.
55+
*
56+
* @param instruction
57+
* A specific instruction in the code.
58+
*
59+
* @return Index in the instructions list where the code appears.
60+
*/
61+
public int indexOf(@Nonnull Instruction instruction) {
62+
// We have our own index-of because 'list.indexOf' uses 'item.equals(other)' which is not what we want.
63+
for (int i = 0; i < instructions.size(); i++)
64+
if (instructions.get(i) == instruction)
65+
return i;
66+
return -1;
67+
}
68+
69+
/**
70+
* @param instruction
71+
* A specific instruction in the code.
72+
*
73+
* @return Method bytecode offset of the instruction.
74+
*/
75+
public int computeOffsetOf(@Nonnull Instruction instruction) {
76+
int index = indexOf(instruction);
77+
if (index == 0) return 0;
78+
if (index < 0) return -1;
79+
int offset = 0;
80+
for (int i = 0; i < index; i++)
81+
offset += instructions.get(i).computeSize();
82+
return offset;
83+
}
84+
5385
/**
5486
* @return Instruction code data.
5587
*/

core/src/main/java/software/coley/cafedude/transform/IllegalStrippingTransformer.java

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@
5353
import software.coley.cafedude.classfile.constant.CpClass;
5454
import software.coley.cafedude.classfile.constant.CpEntry;
5555
import software.coley.cafedude.classfile.constant.CpUtf8;
56+
import software.coley.cafedude.classfile.instruction.BasicInstruction;
57+
import software.coley.cafedude.classfile.instruction.Instruction;
58+
import software.coley.cafedude.classfile.instruction.IntOperandInstruction;
59+
import software.coley.cafedude.classfile.instruction.LookupSwitchInstruction;
60+
import software.coley.cafedude.classfile.instruction.TableSwitchInstruction;
5661
import software.coley.cafedude.io.AttributeContext;
5762

5863
import java.util.Collection;
@@ -64,6 +69,8 @@
6469
import java.util.regex.Pattern;
6570
import java.util.stream.Collectors;
6671

72+
import static software.coley.cafedude.classfile.instruction.Opcodes.*;
73+
6774
/**
6875
* A transformer to remove illegal attributes and data from a class.
6976
*
@@ -103,15 +110,70 @@ public void transform() {
103110
clazz.getAttributes().removeIf(attribute -> !isValidWrapped(clazz, attribute));
104111
for (Field field : clazz.getFields())
105112
field.getAttributes().removeIf(attribute -> !isValidWrapped(field, attribute));
106-
for (Method method : clazz.getMethods())
113+
for (Method method : clazz.getMethods()) {
107114
method.getAttributes().removeIf(attribute -> !isValidWrapped(method, attribute));
115+
CodeAttribute code = method.getAttribute(CodeAttribute.class);
116+
if (code != null)
117+
removeInvalidInstructions(code);
118+
}
108119

109120
// Record filtered CP refs, the difference of the sets are the indices that were referenced
110121
// by removed attributes/data.
111122
Set<CpEntry> filteredCpAccesses = clazz.cpAccesses();
112123
cpAccesses.removeAll(filteredCpAccesses);
113124
}
114125

126+
protected void removeInvalidInstructions(@Nonnull CodeAttribute code) {
127+
List<Instruction> instructions = code.getInstructions();
128+
if (instructions.size() <= 1)
129+
return;
130+
int maxPc = code.computeOffsetOf(instructions.get(instructions.size() - 1));
131+
132+
// Remove junk try-catch entries with bogus offsets
133+
List<ExceptionTableEntry> exceptions = code.getExceptionTable();
134+
for (int i = exceptions.size() - 1; i >= 0; i--) {
135+
ExceptionTableEntry entry = exceptions.get(i);
136+
if (entry.getStartPc() > maxPc || entry.getEndPc() > maxPc || entry.getHandlerPc() > maxPc)
137+
exceptions.remove(i);
138+
}
139+
140+
// Remove any jump instructions that point to bogus offsets
141+
// - The code is already unverifiable, so when we stub out the instruction with
142+
// a 'return' the code is still unverifiable but fixes the ASM crash.
143+
// - The number of bytes if kept the same, so valid jump instructions elsewhere
144+
// in the method are not broken.
145+
// - This de-syncs the stack-frame entries for some reason I haven't looked into but
146+
// since the code that relies on these tricks already requires use of -noverify
147+
// I don't really feel like looking into it. Just use SKIP_FRAMES with ASM on these classes.
148+
for (int i = instructions.size() - 1; i >= 0; i--) {
149+
Instruction instruction = instructions.get(i);
150+
int op = instruction.getOpcode();
151+
if (((op >= IFEQ && op <= JSR) || (op == GOTO_W || op == JSR_W)) && instruction instanceof IntOperandInstruction jump) {
152+
int jumpOffset = jump.getOperand();
153+
if (jumpOffset > maxPc) {
154+
int size = instruction.computeSize();
155+
instructions.set(i, new BasicInstruction(RETURN));
156+
for (int j = 0; j < size - 1; j++)
157+
instructions.add(i, new BasicInstruction(NOP));
158+
}
159+
} else if (instruction instanceof TableSwitchInstruction tswitch) {
160+
if (tswitch.getDefault() > maxPc || tswitch.getOffsets().stream().anyMatch(o -> o > maxPc)) {
161+
int size = instruction.computeSize();
162+
instructions.set(i, new BasicInstruction(RETURN));
163+
for (int j = 0; j < size - 1; j++)
164+
instructions.add(i, new BasicInstruction(NOP));
165+
}
166+
} else if (instruction instanceof LookupSwitchInstruction lswitch) {
167+
if (lswitch.getDefault() > maxPc || lswitch.getOffsets().stream().anyMatch(o -> o > maxPc)) {
168+
int size = instruction.computeSize();
169+
instructions.set(i, new BasicInstruction(RETURN));
170+
for (int j = 0; j < size - 1; j++)
171+
instructions.add(i, new BasicInstruction(NOP));
172+
}
173+
}
174+
}
175+
}
176+
115177
private void removeInvalidBootstrapMethodAttribute() {
116178
// ASM will try to read this attribute if any CP entry exists for DYNAMIC or INVOKE_DYNAMIC.
117179
// If no methods actually refer to those CP entries, this attribute can be filled with garbage,

core/src/test/java/software/coley/cafedude/CrasherPatchingTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
* Test that asserts ASM-crashing obfuscated classes are written back without the offending attributes.
3030
*/
3131
public class CrasherPatchingTest {
32-
private static final Set<String> noverify_code = Set.of("sample35.class");
32+
private static final Set<String> noverify_code = Set.of("sample35.class", "sample36.class");
3333

3434
@ParameterizedTest
3535
@MethodSource("supply")
Binary file not shown.

0 commit comments

Comments
 (0)