Skip to content

Commit ec48f75

Browse files
Added "ignore system classes" and "ignore platform classes" options.
Added presorting of classes. Added better error suppression of duplicated class dumps caused by "invalid byte code" while fast dumping (probably due to kotlin bytecode shenanigans)
1 parent 00a8bc2 commit ec48f75

File tree

8 files changed

+113
-109
lines changed

8 files changed

+113
-109
lines changed

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,38 @@ Designed for **forensic analysis, debugging, and reverse engineering**, making i
2020

2121
Download the latest release JAR from [here](https://github.com/BenjaminSoelberg/JavaForensicsToolkit/releases).
2222

23+
## build
24+
```
25+
mvn clean package
26+
```
27+
2328
## Usage
2429

2530
```
2631
java -jar JavaForensicsToolkit-<version>.jar
2732
2833
---------------------------------------------------------
29-
--> Java Forensics Toolkit v1.0.2 by Benjamin Sølberg <--
34+
--> Java Forensics Toolkit v1.1.0 by Benjamin Sølberg <--
3035
---------------------------------------------------------
3136
https://github.com/BenjaminSoelberg/JavaForensicsToolkit
3237
33-
usage: java -jar JavaForensicsToolkit.jar [-v] [-e] [-d destination.jar] [-f filter]... [-x] <pid>
38+
usage: java -jar JavaForensicsToolkit.jar [-v] [-e] [-d destination.jar] [-s] [-p] [-f filter]... [-x] <pid>
3439
3540
options:
3641
-v verbose agent logging
3742
-e agent will log to stderr instead of stdout
3843
-d jar file destination of dumped classes
3944
Relative paths will be relative with respect to the target process.
4045
A jar file in temp will be generated if no destination was provided.
46+
-s ignore system class loader (like java.lang.String)
47+
-p ignore platform class loader (like system extensions)
4148
-f regular expression class name filter
4249
Can be specified multiple times.
4350
-x exclude classes matching the filter
4451
pid process id of the target java process
4552
4653
example:
47-
java -jar JavaForensicsToolkit.jar -d dump.jar -f java\\..* -f sun\\..* -f jdk\\..* -f com\\.sun\\..* -x 123456
54+
java -jar JavaForensicsToolkit.jar -d dump.jar -f 'java\\..*' -f 'sun\\..*' -f 'jdk\\..*' -f 'com\\.sun\\..*' -x 123456
4855
```
4956

5057
## Example

src/main/java/io/github/benjaminsoelberg/jft/ClassDumper.java

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.net.URL;
1010
import java.nio.charset.StandardCharsets;
1111
import java.util.Arrays;
12+
import java.util.Comparator;
1213
import java.util.jar.JarOutputStream;
1314
import java.util.zip.ZipEntry;
1415

@@ -32,10 +33,7 @@ public static void agentmain(String cmdline, Instrumentation instrumentation) th
3233
report.println("Agent loaded with options: %s%n", String.join(" ", args));
3334

3435
report.println("Querying classes...");
35-
Class<?>[] classes = Arrays.stream(instrumentation.getAllLoadedClasses())
36-
.filter(instrumentation::isModifiableClass)
37-
.filter(clazz -> options.getFilterPredicate().test(clazz.getName()))
38-
.toArray(Class<?>[]::new);
36+
Class<?>[] classes = getAllLoadedClasses(instrumentation, options);
3937
report.println("");
4038

4139
// The transformer could (as a side effect by the JVM) be called with classes not in the list which is why we pass the filtered classes to it as well
@@ -46,26 +44,9 @@ public static void agentmain(String cmdline, Instrumentation instrumentation) th
4644
instrumentation.addTransformer(dumper, true);
4745

4846
report.println("Dumping started...");
49-
5047
// Invoke the transformer and remove it when filtered classes are processed
5148
try {
52-
for (int from = 0; from < classes.length; from += DUMP_BATCH_SIZE) {
53-
int to = Math.min(from + DUMP_BATCH_SIZE, classes.length);
54-
Class<?>[] batch = Arrays.copyOfRange(classes, from, to);
55-
try {
56-
// Transform the full batch in one go, and if this throws an exception, no classes have been retransformed.
57-
instrumentation.retransformClasses(batch);
58-
} catch (Throwable ignored) {
59-
// Transform classes one-by-one if batch transformation failed
60-
for (Class<?> clazz : batch) {
61-
try {
62-
instrumentation.retransformClasses(clazz);
63-
} catch (Throwable th) {
64-
report.println("Failed to dump %s", clazz.getName());
65-
}
66-
}
67-
}
68-
}
49+
retransformClasses(instrumentation, classes, report);
6950
} finally {
7051
instrumentation.removeTransformer(dumper);
7152
}
@@ -76,7 +57,7 @@ public static void agentmain(String cmdline, Instrumentation instrumentation) th
7657

7758
// Validate that no exceptions were generated during the dump process
7859
if (dumper.getLastException() != null) {
79-
report.println("WARNING: One or more exceptions occurred while dumping classes.");
60+
report.println("WARNING: One or more transformer exceptions occurred while dumping classes.");
8061
report.dump(dumper.getLastException());
8162
report.println("");
8263
}
@@ -85,10 +66,12 @@ public static void agentmain(String cmdline, Instrumentation instrumentation) th
8566

8667
report.println("Creating jar...");
8768
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(destination))) {
88-
dumper.getClassInfos().stream().sorted().forEach(classInfo -> {
69+
dumper.getClassInfos().entrySet().stream().sorted(Comparator.comparing(o -> o.getKey().getName())).forEach(entry -> {
70+
Class<?> clazz = entry.getKey();
71+
byte[] bytecode = entry.getValue();
8972
try {
90-
report.dump(classInfo);
91-
writeZipEntry(jar, classInfo.getNativeClassName() + ".class", classInfo.getBytecode());
73+
report.dump(clazz, bytecode);
74+
writeZipEntry(jar, Utils.toNativeClassName(clazz.getName()) + ".class", bytecode);
9275
} catch (IOException e) {
9376
throw new RuntimeException(e);
9477
}
@@ -98,35 +81,69 @@ public static void agentmain(String cmdline, Instrumentation instrumentation) th
9881
report.println("%nDumped classes, including report.txt, can be found in: %s", options.getDestination());
9982
}
10083

84+
private static Class<?>[] getAllLoadedClasses(Instrumentation instrumentation, Options options) {
85+
return Arrays.stream(instrumentation.getAllLoadedClasses())
86+
.filter(instrumentation::isModifiableClass)
87+
.filter(clazz -> options.getFilterPredicate().test(clazz.getName()))
88+
.filter(clazz -> !options.isIgnoreSystemClassloader() || clazz.getClassLoader() != null)
89+
.filter(clazz -> !options.isIgnorePlatformClassloader() || clazz.getClassLoader() != ClassLoader.getPlatformClassLoader())
90+
.sorted(Comparator.comparing(Class::getName))
91+
.toArray(Class<?>[]::new);
92+
}
93+
94+
private static void retransformClasses(Instrumentation instrumentation, Class<?>[] classes, Report report) {
95+
for (int from = 0; from < classes.length; from += DUMP_BATCH_SIZE) {
96+
int to = Math.min(from + DUMP_BATCH_SIZE, classes.length);
97+
Class<?>[] batch = Arrays.copyOfRange(classes, from, to);
98+
try {
99+
// Transform the full batch in one go, and if this throws an exception some classes may have been dumped but we deduplicate later on.
100+
instrumentation.retransformClasses(batch);
101+
} catch (Throwable ignored) {
102+
// Transform classes one-by-one if batch transformation failed
103+
for (Class<?> clazz : batch) {
104+
try {
105+
instrumentation.retransformClasses(clazz);
106+
} catch (Throwable th) {
107+
report.println("Failed to dump %s", clazz.getName());
108+
}
109+
}
110+
}
111+
}
112+
}
113+
114+
101115
private static void writeZipEntry(JarOutputStream jar, String name, byte[] data) throws IOException {
102116
jar.putNextEntry(new ZipEntry(name));
103117
jar.write(data);
104118
}
105119

120+
@SuppressWarnings("ConcatenationWithEmptyString")
106121
private static String getHeader() {
107122
return "" +
108123
"---------------------------------------------------------%n" +
109-
"--> Java Forensics Toolkit v1.0.2 by Benjamin Sølberg <--%n" +
124+
"--> Java Forensics Toolkit v1.1.0 by Benjamin Sølberg <--%n" +
110125
"---------------------------------------------------------%n" +
111126
"https://github.com/BenjaminSoelberg/JavaForensicsToolkit%n%n";
112127
}
113128

114129
private static void showUsage() {
115-
System.out.println("usage: java -jar JavaForensicsToolkit.jar [-v] [-e] [-d destination.jar] [-f filter]... [-x] <pid>");
130+
System.out.println("usage: java -jar JavaForensicsToolkit.jar [-v] [-e] [-d destination.jar] [-s] [-p] [-f filter]... [-x] <pid>");
116131
System.out.println();
117132
System.out.println("options:");
118133
System.out.println("-v\tverbose agent logging");
119134
System.out.println("-e\tagent will log to stderr instead of stdout");
120135
System.out.println("-d\tjar file destination of dumped classes");
121136
System.out.println("\tRelative paths will be relative with respect to the target process.");
122137
System.out.println("\tA jar file in temp will be generated if no destination was provided.");
138+
System.out.println("-s\tignore system class loader (like java.lang.String)");
139+
System.out.println("-p\tignore platform class loader (like system extensions)");
123140
System.out.println("-f\tregular expression class name filter");
124141
System.out.println("\tCan be specified multiple times.");
125142
System.out.println("-x\texclude classes matching the filter");
126143
System.out.println("pid\tprocess id of the target java process");
127144
System.out.println();
128145
System.out.println("example:");
129-
System.out.println("java -jar JavaForensicsToolkit.jar -d dump.jar -f java\\\\..* -f sun\\\\..* -f jdk\\\\..* -f com\\\\.sun\\\\..* -x 123456");
146+
System.out.println("java -jar JavaForensicsToolkit.jar -d dump.jar -f 'java\\\\..*' -f 'sun\\\\..*' -f 'jdk\\\\..*' -f 'com\\\\.sun\\\\..*' -x 123456");
130147
}
131148

132149
/**

src/main/java/io/github/benjaminsoelberg/jft/ClassInfo.java

Lines changed: 0 additions & 59 deletions
This file was deleted.

src/main/java/io/github/benjaminsoelberg/jft/Options.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ public class Options {
1111
public static final String VERBOSE_OPTION = "-v";
1212
public static final String LOG_TO_STD_ERR_OPTION = "-e";
1313
public static final String DESTINATION_OPTION = "-d";
14+
public static final String IGNORE_SYSTEM_CLASS_LOADER_OPTION = "-s";
15+
public static final String IGNORE_PLATFORM_CLASS_LOADER_OPTION = "-p";
1416
public static final String FILTER_OPTION = "-f";
1517
public static final String INVERTED_FILTER_OPTION = "-x";
1618
private final ArrayList<Pattern> filter = new ArrayList<>();
1719
private boolean verbose;
1820
private boolean logToStdErr;
1921
private String destination;
22+
private boolean ignoreSystemClassloader;
23+
private boolean ignorePlatformClassloader;
2024
private boolean invertedFilter;
2125
private String pid;
2226

@@ -40,13 +44,19 @@ public Options(String[] args) throws ParserException {
4044
case FILTER_OPTION:
4145
filter.add(Pattern.compile(iterator.next()));
4246
break;
47+
case IGNORE_SYSTEM_CLASS_LOADER_OPTION:
48+
ignoreSystemClassloader = true;
49+
break;
50+
case IGNORE_PLATFORM_CLASS_LOADER_OPTION:
51+
ignorePlatformClassloader = true;
52+
break;
4353
case INVERTED_FILTER_OPTION:
4454
invertedFilter = true;
4555
break;
4656
default:
4757
throw new ParserException(String.format("Unknown option [%s]", token));
4858
}
49-
} catch (NoSuchElementException nsee) {
59+
} catch (NoSuchElementException ignored) {
5060
throw new ParserException(String.format("Too few arguments for [%s]", token));
5161
}
5262
} else {
@@ -102,6 +112,12 @@ public String[] getArgs() {
102112
args.add(DESTINATION_OPTION);
103113
args.add(destination);
104114
}
115+
if (ignoreSystemClassloader) {
116+
args.add(IGNORE_SYSTEM_CLASS_LOADER_OPTION);
117+
}
118+
if (ignorePlatformClassloader) {
119+
args.add(IGNORE_PLATFORM_CLASS_LOADER_OPTION);
120+
}
105121
for (Pattern p : filter) {
106122
args.add(FILTER_OPTION);
107123
args.add(p.pattern());
@@ -135,6 +151,14 @@ public boolean isInvertedFilter() {
135151
return invertedFilter;
136152
}
137153

154+
public boolean isIgnoreSystemClassloader() {
155+
return ignoreSystemClassloader;
156+
}
157+
158+
public boolean isIgnorePlatformClassloader() {
159+
return ignorePlatformClassloader;
160+
}
161+
138162
public String getDestination() {
139163
return destination;
140164
}

src/main/java/io/github/benjaminsoelberg/jft/Report.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ public void dump(Throwable throwable) {
3737
add(Utils.toString(throwable));
3838
}
3939

40-
public void dump(ClassInfo classInfo) {
41-
println("Class info: " + classInfo.toString());
40+
public void dump(Class<?> clazz, byte[] bytecode) {
41+
println("Class info: %s via %s, %d bytes", clazz.getName(), Utils.toClassLoaderName(clazz), bytecode.length);
4242
}
4343

4444
public String generate() {

src/main/java/io/github/benjaminsoelberg/jft/Transformer.java

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,41 @@
33
import java.lang.instrument.ClassFileTransformer;
44
import java.security.ProtectionDomain;
55
import java.util.Arrays;
6-
import java.util.Collection;
76
import java.util.List;
8-
import java.util.concurrent.ConcurrentLinkedQueue;
7+
import java.util.concurrent.ConcurrentHashMap;
98

109
public class Transformer implements ClassFileTransformer {
1110
private static final String FILTER_JFT_CLASSES = Utils.toNativeClassName(Transformer.class.getPackageName()) + "/";
12-
private final ConcurrentLinkedQueue<ClassInfo> classInfos = new ConcurrentLinkedQueue<>();
11+
private final ConcurrentHashMap<Class<?>, byte[]> classInfos = new ConcurrentHashMap<>();
1312
private final Report report;
14-
private final List<Class<?>> classes;
13+
private final List<Class<?>> expectedClasses;
1514
private volatile Throwable lastException;
1615

17-
public Transformer(Report report, Class<?>[] classes) {
16+
public Transformer(Report report, Class<?>[] expectedClasses) {
1817
this.report = report;
19-
this.classes = Arrays.asList(classes);
18+
this.expectedClasses = Arrays.asList(expectedClasses);
2019
}
2120

2221
@Override
2322
public byte[] transform(ClassLoader loader, String nativeClassName, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
24-
// Ignore initial class load as we only want to dump classes that was previously accepted by the filter
25-
if (classBeingRedefined == null) {
26-
return null;
27-
}
28-
2923
try {
24+
// Ignore initial class load as we only want to dump classes that was previously accepted by the filter
25+
if (nativeClassName == null || classBeingRedefined == null || classfileBuffer == null) {
26+
return null;
27+
}
28+
3029
// Ignore our own classes
3130
if (nativeClassName.startsWith(FILTER_JFT_CLASSES)) {
3231
report.println("Ignoring %s", classBeingRedefined.getName());
3332
return null;
3433
}
3534

3635
// Save the class info if it previously passed the filtering
37-
if (classes.contains(classBeingRedefined)) {
38-
report.println("Dumping %s", Utils.toJavaClassName(nativeClassName));
39-
classInfos.add(new ClassInfo(nativeClassName, loader, protectionDomain, classfileBuffer));
36+
if (expectedClasses.contains(classBeingRedefined)) {
37+
classInfos.computeIfAbsent(classBeingRedefined, clazz -> {
38+
report.println("Dumping %s", Utils.toJavaClassName(nativeClassName));
39+
return classfileBuffer;
40+
});
4041
}
4142
} catch (Throwable throwable) {
4243
// Keep latest exception for later retrieval
@@ -51,7 +52,7 @@ public Throwable getLastException() {
5152
return lastException;
5253
}
5354

54-
public Collection<ClassInfo> getClassInfos() {
55+
public ConcurrentHashMap<Class<?>, byte[]> getClassInfos() {
5556
return classInfos;
5657
}
5758
}

0 commit comments

Comments
 (0)