Skip to content

Commit b3202b1

Browse files
Added support for automatic switch between fast and slow dumping.
Updated readme. Sorted classes in jar.
1 parent 55a0718 commit b3202b1

File tree

4 files changed

+105
-102
lines changed

4 files changed

+105
-102
lines changed

README.md

Lines changed: 58 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,74 @@
1-
# Java Forensics Toolkit
1+
# Easy and compact Java Forensics Toolkit
22

33
![broken java cup](BrokenJavaCup.jpg)
44

5-
A little *Java Forensics Toolkit* that can dump all loaded classes within a JVM
5+
# Java Forensics Toolkit
6+
7+
The **Java Forensics Toolkit** is a lightweight JVM agent that allows you to **inspect, snapshot, and export all loaded classes** from a running Java process.<br>
8+
Designed for **forensic analysis, debugging, and reverse engineering**, making it easier to discover unexpected or malicious code inside live JVMs.
9+
10+
## Features
11+
12+
- 📦 **Snapshot all loaded classes** – capture bytecode *after* any runtime transformations.
13+
- 🔍 **Class loader analysis** – see which classes exist in which class loader.
14+
- 🛡️ **Security investigations** – detect hidden, injected, or malicious classes.
15+
- 🧩 **Reverse engineering** – dumped classes can be decompiled with standard tools.
16+
- 🎯 **Flexible filters** – include/exclude classes with regex filters.
17+
-**Simple usage** – attach to any running JVM by PID with a single command.
18+
19+
## Installation
20+
21+
Download the latest release JAR from [here](https://github.com/BenjaminSoelberg/JavaForensicsToolkit/releases).
22+
23+
## Usage
24+
625
```
26+
java -jar JavaForensicsToolkit-<version>.jar
27+
28+
---------------------------------------------------------
29+
--> Java Forensics Toolkit v1.0.2 by Benjamin Sølberg <--
30+
---------------------------------------------------------
31+
https://github.com/BenjaminSoelberg/JavaForensicsToolkit
32+
733
usage: java -jar JavaForensicsToolkit.jar [-v] [-e] [-d destination.jar] [-f filter]... [-x] <pid>
834
935
options:
10-
-v verbose agent logging
11-
-e agent will log to stderr instead of stdout
12-
-d jar file destination of dumped classes
13-
Relative paths will be relative with respect to the target process.
14-
A jar file in temp will be generated if no destination was provided.
15-
-f regular expression class name filter
16-
Can be specified multiple times.
17-
-x exclude classes matching the filter
18-
pid process id of the target java process
36+
-v verbose agent logging
37+
-e agent will log to stderr instead of stdout
38+
-d jar file destination of dumped classes
39+
Relative paths will be relative with respect to the target process.
40+
A jar file in temp will be generated if no destination was provided.
41+
-f regular expression class name filter
42+
Can be specified multiple times.
43+
-x exclude classes matching the filter
44+
pid process id of the target java process
1945
2046
example:
2147
java -jar JavaForensicsToolkit.jar -d dump.jar -f java\\..* -f sun\\..* -f jdk\\..* -f com\\.sun\\..* -x 123456
2248
```
2349

24-
A small report with information about each class is generated and placed in the destination jar as well.
50+
## Example
51+
52+
Dump all non-JDK classes from a running process with PID 123456:
2553

26-
Here is an example:
2754
```
28-
$ java -jar JavaForensicsToolkit.jar -v 24576
29-
--------------------------------------------------------
30-
--> Java Forensics Toolkit v1.00 by Benjamin Sølberg <--
31-
--------------------------------------------------------
32-
https://github.com/BenjaminSoelberg/JavaForensicsToolkit
55+
java -jar JavaForensicsToolkit.jar -v -d dump.jar -f java\\..* -f sun\\..* -f jdk\\..* -f com\\.sun\\..* -x 123456
56+
```
3357

34-
Injecting agent into JVM with pid: 24576
35-
Dumping classes to: /tmp/dump-24651-9890604330559988075.jar
36-
--------------------------------------------------------
37-
--> Java Forensics Toolkit v1.00 by Benjamin Sølberg <--
38-
--------------------------------------------------------
39-
https://github.com/BenjaminSoelberg/JavaForensicsToolkit
58+
## Typical Use Cases
4059

41-
Agent loaded with options: -v -d /tmp/dump-24651-9890604330559988075.jar -f .* 24576
42-
43-
Querying classes...
44-
45-
Dumping started...
46-
Ignoring io/github/benjaminsoelberg/jft/Transformer
47-
Ignoring io/github/benjaminsoelberg/jft/ParserException
48-
Ignoring io/github/benjaminsoelberg/jft/Utils
49-
Ignoring io/github/benjaminsoelberg/jft/Options
50-
Ignoring io/github/benjaminsoelberg/jft/ClassInfo
51-
Ignoring io/github/benjaminsoelberg/jft/Report
52-
Ignoring io/github/benjaminsoelberg/jft/ClassDumper
53-
Ignoring io/github/benjaminsoelberg/jft/DummyRunner
54-
Dumping java.io.FileOutputStream$1
55-
Dumping java.util.Vector$Itr
56-
Dumping java.util.concurrent.ConcurrentLinkedQueue$CLQSpliterator
57-
Dumping java.util.zip.ZipOutputStream$XEntry
58-
...
59-
Dumping java.lang.Throwable
60-
Dumping java.lang.System
61-
Dumping java.lang.ClassLoader
62-
Dumping java.lang.Cloneable
63-
Dumping java.lang.Class
64-
Dumping java.lang.reflect.Type
65-
Dumping java.lang.reflect.GenericDeclaration
66-
Dumping java.lang.reflect.AnnotatedElement
67-
Dumping java.lang.String
68-
Dumping java.lang.CharSequence
69-
Dumping java.lang.Comparable
70-
Dumping java.io.Serializable
71-
Dumping java.lang.Object
72-
73-
Creating jar...
74-
Class info: java.io.FileOutputStream$1@null@0 726 bytes
75-
Class info: java.util.Vector$Itr@null@0 2492 bytes
76-
Class info: java.util.concurrent.ConcurrentLinkedQueue$CLQSpliterator@null@0 3608 bytes
77-
Class info: java.util.zip.ZipOutputStream$XEntry@null@0 563 bytes
78-
Class info: java.time.chrono.IsoChronology@null@0 11739 bytes
79-
Class info: java.time.chrono.AbstractChronology@null@0 15281 bytes
80-
Class info: java.time.chrono.Chronology@null@0 8639 bytes
81-
Class info: java.time.temporal.TemporalAdjusters@null@0 6423 bytes
82-
Class info: java.time.LocalDate@null@0 27720 bytes
83-
...
84-
Class info: java.time.chrono.ChronoLocalDate@null@0 9575 bytes
85-
Class info: java.time.zone.ZoneOffsetTransition@null@0 6109 bytes
86-
Class info: java.time.temporal.ValueRange@null@0 4463 bytes
87-
Class info: java.math.BigInteger@null@0 59485 bytes
88-
Class info: java.time.Duration@null@0 18086 bytes
89-
Class info: java.time.temporal.TemporalAmount@null@0 399 bytes
90-
Class info: java.lang.Comparable@null@0 235 bytes
91-
Class info: java.io.Serializable@null@0 113 bytes
92-
Class info: java.lang.Object@null@0 1944 bytes
93-
94-
Dumped classes, including report.txt, can be found in: /tmp/dump-24651-9890604330559988075.jar
95-
Done
96-
```
60+
- 🔐 **Malware hunting** – identify injected or malicious classes hidden inside a compromised JVM.
61+
- 🛠️ **Debugging classpath issues** – find out which versions of classes are actually loaded and by which class loaders.
62+
- 📊 **Security audits** – verify that only expected code is running inside production JVMs.
63+
- 🕵️ **Incident response** – capture a live snapshot of loaded classes for later offline analysis.
64+
- ⚙️ **Reverse engineering & research** – decompile transformed classes to understand runtime modifications (e.g. instrumentation, bytecode weaving, or AOP frameworks).
65+
66+
## How It Works
67+
68+
1) **Attach to target JVM**<br>The toolkit uses the standard com.sun.tools.attach API to connect to a running Java process by PID.
69+
2) **Load agent**<br>Once attached, it dynamically loads a lightweight Java agent into the target JVM without requiring a restart.
70+
3) **Enumerate classes**<br>The agent queries the JVM for all currently loaded classes and their associated class loaders.
71+
4) **Dump bytecode**<br>Each class is retrieved as it exists in memory after any transformations (e.g. instrumentation, weaving, or obfuscation).
72+
5) **Write output**<br>The classes are packaged into a JAR file (either user-specified or temporary) for convenient storage and analysis.
73+
74+
This approach ensures the dumped classes reflect the exact state of the JVM at runtime, providing a faithful snapshot for investigation.

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

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public class ClassDumper {
1717
// Public and non-final for testing purposes only.
1818
public static String TEST_AGENT_CMD_LINE = null;
1919

20+
public static final int DUMP_BATCH_SIZE = 500;
21+
2022
public static void agentmain(String cmdline, Instrumentation instrumentation) throws Exception {
2123
// We are unable to parse arguments to agentmain while running unit test, hence this inject-hook
2224
if (TEST_AGENT_CMD_LINE != null) {
@@ -31,27 +33,44 @@ public static void agentmain(String cmdline, Instrumentation instrumentation) th
3133

3234
report.println("Querying classes...");
3335
Class<?>[] classes = Arrays.stream(instrumentation.getAllLoadedClasses())
34-
.filter(clazz -> !clazz.isArray())
35-
.filter(clazz -> !clazz.isSynthetic())
3636
.filter(instrumentation::isModifiableClass)
3737
.filter(clazz -> options.getFilterPredicate().test(clazz.getName()))
38-
.toArray(Class[]::new);
38+
.toArray(Class<?>[]::new);
3939
report.println("");
4040

41-
if (classes.length == 0) {
42-
report.println("WARNING: No classes were found, bad filter ?%n");
43-
}
44-
4541
// 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-
Transformer dumper = new Transformer(report, Arrays.asList(classes));
42+
Transformer dumper = new Transformer(report, classes);
4743

48-
// Invoke the transformer and remove it when filtered classes are processed
49-
report.println("Dumping started...");
50-
instrumentation.addTransformer(dumper, true);
51-
try {
52-
instrumentation.retransformClasses(classes);
53-
} finally {
54-
instrumentation.removeTransformer(dumper);
44+
if (classes.length > 0) {
45+
report.println("%d classes found.%n", classes.length);
46+
instrumentation.addTransformer(dumper, true);
47+
48+
report.println("Dumping started...");
49+
50+
// Invoke the transformer and remove it when filtered classes are processed
51+
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+
}
69+
} finally {
70+
instrumentation.removeTransformer(dumper);
71+
}
72+
} else {
73+
report.println("WARNING: No classes found, bad filter ?%n");
5574
}
5675
report.println("");
5776

@@ -66,10 +85,10 @@ public static void agentmain(String cmdline, Instrumentation instrumentation) th
6685

6786
report.println("Creating jar...");
6887
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(destination))) {
69-
dumper.getClassInfos().forEach(classInfo -> {
88+
dumper.getClassInfos().stream().sorted().forEach(classInfo -> {
7089
try {
7190
report.dump(classInfo);
72-
writeZipEntry(jar, classInfo.getClassName() + ".class", classInfo.getBytecode());
91+
writeZipEntry(jar, classInfo.getNativeClassName() + ".class", classInfo.getBytecode());
7392
} catch (IOException e) {
7493
throw new RuntimeException(e);
7594
}
@@ -87,7 +106,7 @@ private static void writeZipEntry(JarOutputStream jar, String name, byte[] data)
87106
private static String getHeader() {
88107
return "" +
89108
"---------------------------------------------------------%n" +
90-
"--> Java Forensics Toolkit v1.0.1 by Benjamin Sølberg <--%n" +
109+
"--> Java Forensics Toolkit v1.0.2 by Benjamin Sølberg <--%n" +
91110
"---------------------------------------------------------%n" +
92111
"https://github.com/BenjaminSoelberg/JavaForensicsToolkit%n%n";
93112
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import java.security.ProtectionDomain;
44

5-
public class ClassInfo {
5+
public class ClassInfo implements Comparable<ClassInfo> {
66
private final String nativeClassName;
77
private final ClassLoader classLoader;
88
private final ProtectionDomain protectionDomain;
@@ -51,4 +51,9 @@ public byte[] getBytecode() {
5151
public String toString() {
5252
return String.format("%s@%s %d bytes", getClassName(), getClassLoaderName(), bytecode.length);
5353
}
54+
55+
@Override
56+
public int compareTo(ClassInfo o) {
57+
return getNativeClassName().compareTo(o.getNativeClassName());
58+
}
5459
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.lang.instrument.ClassFileTransformer;
44
import java.security.ProtectionDomain;
5+
import java.util.Arrays;
56
import java.util.Collection;
67
import java.util.List;
78
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -13,9 +14,9 @@ public class Transformer implements ClassFileTransformer {
1314
private final List<Class<?>> classes;
1415
private volatile Throwable lastException;
1516

16-
public Transformer(Report report, List<Class<?>> classes) {
17+
public Transformer(Report report, Class<?>[] classes) {
1718
this.report = report;
18-
this.classes = classes;
19+
this.classes = Arrays.asList(classes);
1920
}
2021

2122
@Override
@@ -28,7 +29,7 @@ public byte[] transform(ClassLoader loader, String nativeClassName, Class<?> cla
2829
try {
2930
// Ignore our own classes
3031
if (nativeClassName.startsWith(FILTER_JFT_CLASSES)) {
31-
report.println("Ignoring %s", nativeClassName);
32+
report.println("Ignoring %s", classBeingRedefined.getName());
3233
return null;
3334
}
3435

0 commit comments

Comments
 (0)