Skip to content

Commit 4756733

Browse files
Added support for dumping class loader & class trees.
Added "turbo" dumping speed. Better handling of errors while dumping. Added more documentation in code.
1 parent ec48f75 commit 4756733

File tree

12 files changed

+334
-210
lines changed

12 files changed

+334
-210
lines changed

BrokenJavaCup.jpg

-66.4 KB
Binary file not shown.

JFT.jpg

92.6 KB
Loading

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Easy and compact Java Forensics Toolkit
22

3-
![broken java cup](BrokenJavaCup.jpg)
3+
![JFT](JFT.jpg)
44

55
# Java Forensics Toolkit
66

@@ -51,15 +51,15 @@ options:
5151
pid process id of the target java process
5252
5353
example:
54-
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 1337
5555
```
5656

5757
## Example
5858

59-
Dump all non-JDK classes from a running process with PID 123456:
59+
Dump all non-JDK classes loaders from a running process with PID 1337:
6060

6161
```
62-
java -jar JavaForensicsToolkit.jar -v -d dump.jar -f java\\..* -f sun\\..* -f jdk\\..* -f com\\.sun\\..* -x 123456
62+
java -jar JavaForensicsToolkit.jar -v -s -p -d dump.jar 1337
6363
```
6464

6565
## Typical Use Cases

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
<configuration>
4949
<archive>
5050
<manifestEntries>
51-
<Main-Class>io.github.benjaminsoelberg.jft.ClassDumper</Main-Class>
51+
<Main-Class>io.github.benjaminsoelberg.jft.Main</Main-Class>
5252
<Agent-Class>io.github.benjaminsoelberg.jft.ClassDumper</Agent-Class>
5353
<Can-Retransform-Classes>true</Can-Retransform-Classes>
5454
</manifestEntries>

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

Lines changed: 136 additions & 129 deletions
Large diffs are not rendered by default.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package io.github.benjaminsoelberg.jft;
2+
3+
import java.util.*;
4+
import java.util.stream.Collectors;
5+
6+
public final class ClassTree {
7+
private final Map<ClassLoader, Node> root = new HashMap<>();
8+
9+
public static class Node {
10+
private final ClassLoader loader;
11+
private final List<Node> children = new ArrayList<>();
12+
private final Map<Class<?>, byte[]> classes = new HashMap<>();
13+
14+
public Node(ClassLoader loader) {
15+
this.loader = loader;
16+
}
17+
18+
public void add(Node node) {
19+
children.add(node);
20+
}
21+
22+
public void add(Class<?> clazz, byte[] bytecode) {
23+
// "putIfAbsent" ensures uniqueness
24+
classes.putIfAbsent(clazz, bytecode);
25+
}
26+
27+
public ClassLoader getLoader() {
28+
return loader;
29+
}
30+
31+
public List<Node> getChildren() {
32+
return children;
33+
}
34+
35+
public Map<Class<?>, byte[]> getClasses() {
36+
return classes.entrySet()
37+
.stream()
38+
.sorted(Map.Entry.comparingByKey(Comparator.comparing(Class::getName)))
39+
.collect(Collectors.toMap(
40+
Map.Entry::getKey,
41+
Map.Entry::getValue,
42+
(a, b) -> a, // Dummy merge
43+
LinkedHashMap::new // Preserve order
44+
));
45+
}
46+
}
47+
48+
public ClassTree() {
49+
// Create explicit bootstrap root
50+
Node bootstrap = new Node(null);
51+
root.put(null, bootstrap);
52+
}
53+
54+
public synchronized void add(Class<?> clazz, byte[] bytecode) {
55+
ClassLoader loader = clazz.getClassLoader();
56+
Node node = ensureNode(loader);
57+
node.add(clazz, bytecode);
58+
}
59+
60+
private Node ensureNode(ClassLoader loader) {
61+
if (root.containsKey(loader)) {
62+
return root.get(loader);
63+
}
64+
65+
Node node = new Node(loader);
66+
root.put(loader, node);
67+
68+
if (loader != null) {
69+
Node parent = ensureNode(loader.getParent());
70+
parent.add(node);
71+
}
72+
73+
return node;
74+
}
75+
76+
public Node getRoot() {
77+
return root.get(null);
78+
}
79+
80+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.github.benjaminsoelberg.jft;
2+
3+
import com.sun.tools.attach.VirtualMachine;
4+
5+
import java.io.IOException;
6+
import java.net.URL;
7+
8+
//TODO: Test on windows (especially file separator)
9+
public class Main {
10+
11+
private static void showUsage() {
12+
System.out.println("usage: java -jar JavaForensicsToolkit.jar [-v] [-e] [-d destination.jar] [-s] [-p] [-f filter]... [-x] <pid>");
13+
System.out.println();
14+
System.out.println("options:");
15+
System.out.println("-v\tverbose agent logging");
16+
System.out.println("-e\tagent will log to stderr instead of stdout");
17+
System.out.println("-d\tjar file destination of dumped classes");
18+
System.out.println("\tRelative paths will be relative with respect to the target process.");
19+
System.out.println("\tA jar file in temp will be generated if no destination was provided.");
20+
System.out.println("-s\tignore system class loader (like java.lang.String)");
21+
System.out.println("-p\tignore platform class loader (like system extensions)");
22+
System.out.println("-f\tregular expression class name filter");
23+
System.out.println("\tCan be specified multiple times.");
24+
System.out.println("-x\texclude classes matching the filter");
25+
System.out.println("pid\tprocess id of the target java process");
26+
System.out.println();
27+
System.out.println("example:");
28+
System.out.println("java -jar JavaForensicsToolkit.jar -d dump.jar -f 'java\\\\..*' -f 'sun\\\\..*' -f 'jdk\\\\..*' -f 'com\\\\.sun\\\\..*' -x 1337");
29+
}
30+
31+
/**
32+
* Get the absolut file location of the jar embedding this class
33+
*
34+
* @return absolut file location of jar
35+
*/
36+
private static String getJarLocation() {
37+
URL url = Main.class.getProtectionDomain().getCodeSource().getLocation();
38+
String file = url.getFile();
39+
if (url.getProtocol().equals("file") && file.startsWith("/") && file.endsWith(".jar")) {
40+
return file;
41+
}
42+
return null;
43+
}
44+
45+
public static void main(String[] args) throws Exception {
46+
System.out.printf(Utils.getApplicationHeader() + "%n");
47+
String absolutJarLocation = getJarLocation();
48+
if (args.length < 1 || absolutJarLocation == null) {
49+
showUsage();
50+
System.exit(1);
51+
}
52+
53+
// We pre-parse the command line to be sure that it is syntactically correct prior to sending it to agentmain
54+
Options options = new Options(args);
55+
56+
String pid = options.getPid();
57+
System.out.println("Injecting agent into JVM with pid: " + pid);
58+
VirtualMachine vm = VirtualMachine.attach(pid);
59+
try {
60+
System.out.println("Dumping classes to: " + options.getDestination());
61+
String[] cmdLine = options.getArgs();
62+
vm.loadAgent(absolutJarLocation, Utils.encodeArgs(cmdLine));
63+
} finally {
64+
try {
65+
vm.detach();
66+
} catch (IOException ioe) {
67+
System.out.println("Unable to detach from process");
68+
}
69+
}
70+
71+
System.out.println("Done!");
72+
}
73+
74+
}

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

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

40-
public void dump(Class<?> clazz, byte[] bytecode) {
41-
println("Class info: %s via %s, %d bytes", clazz.getName(), Utils.toClassLoaderName(clazz), bytecode.length);
42-
}
43-
4440
public String generate() {
4541
return lines.stream().collect(Collectors.joining(System.lineSeparator()));
4642
}

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

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

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

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,41 @@ public static String[] decodeArgs(String args) {
7474
return Arrays.stream(args.split(" ")).map(Utils::fromHex).map(Utils::toUtf8String).collect(Collectors.toList()).toArray(new String[]{});
7575
}
7676

77-
public static String toClassLoaderName(Class<?> clazz) {
78-
ClassLoader classLoader = clazz.getClassLoader();
77+
@SuppressWarnings("ConstantConditions")
78+
public static String toClassLoaderName(ClassLoader classLoader) {
7979
if (classLoader == null) {
80-
return Utils.toObjectId(null);
80+
return toClassLoaderName(classLoader, "bootloader", false);
81+
} else if (classLoader == ClassLoader.getPlatformClassLoader()) {
82+
return toClassLoaderName(classLoader, "platform", false);
83+
} else if (classLoader == ClassLoader.getSystemClassLoader()) {
84+
return toClassLoaderName(classLoader, "app", false);
85+
} else {
86+
return toClassLoaderName(classLoader, "", true);
8187
}
88+
}
89+
90+
private static String toClassLoaderName(ClassLoader classLoader, String defaultName, boolean appendClassName) {
91+
String name = defaultName.trim();
92+
if (classLoader != null) {
93+
if (classLoader.getName() != null && !classLoader.getName().trim().isBlank()) {
94+
name = classLoader.getName().trim();
95+
}
96+
if (appendClassName) {
97+
if (!name.isBlank()) {
98+
name += "_";
99+
}
100+
name += toObjectId(classLoader);
101+
}
102+
}
103+
return "[" + name + "]";
104+
}
82105

83-
return String.format(
84-
"%s%s@%s",
85-
classLoader.getClass().getName(),
86-
classLoader.getName() != null ? (String.format("(%s)", classLoader.getName())) : "",
87-
Integer.toHexString(classLoader.hashCode()));
106+
@SuppressWarnings("ConcatenationWithEmptyString")
107+
public static String getApplicationHeader() {
108+
return "" +
109+
"---------------------------------------------------------%n" +
110+
"--> Java Forensics Toolkit v1.1.0 by Benjamin Sølberg <--%n" +
111+
"---------------------------------------------------------%n" +
112+
"https://github.com/BenjaminSoelberg/JavaForensicsToolkit%n";
88113
}
89114
}

0 commit comments

Comments
 (0)