Skip to content

Commit fbae993

Browse files
trash
1 parent ec48f75 commit fbae993

File tree

8 files changed

+241
-122
lines changed

8 files changed

+241
-122
lines changed

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>
Lines changed: 60 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
package io.github.benjaminsoelberg.jft;
22

3-
import com.sun.tools.attach.VirtualMachine;
4-
53
import java.io.File;
64
import java.io.FileOutputStream;
75
import java.io.IOException;
86
import java.lang.instrument.Instrumentation;
9-
import java.net.URL;
10-
import java.nio.charset.StandardCharsets;
117
import java.util.Arrays;
128
import java.util.Comparator;
139
import java.util.jar.JarOutputStream;
@@ -18,66 +14,78 @@ public class ClassDumper {
1814
// Public and non-final for testing purposes only.
1915
public static String TEST_AGENT_CMD_LINE = null;
2016

21-
public static final int DUMP_BATCH_SIZE = 500;
17+
public static final int DUMP_BATCH_SIZE = 50;
2218

19+
@SuppressWarnings("ReassignedVariable")
2320
public static void agentmain(String cmdline, Instrumentation instrumentation) throws Exception {
21+
/* Stage 0: Override command line options if running a unit test */
2422
// We are unable to parse arguments to agentmain while running unit test, hence this inject-hook
2523
if (TEST_AGENT_CMD_LINE != null) {
2624
cmdline = TEST_AGENT_CMD_LINE;
2725
}
2826

27+
/* Stage 1: decode options */
2928
String[] args = Utils.decodeArgs(cmdline);
3029
Options options = new Options(args);
3130

32-
Report report = new Report(getHeader(), options.isVerbose(), options.isLogToStdErr());
31+
/* Stage 2: initialize report */
32+
Report report = new Report(Utils.getVersionHeader(), options.isVerbose(), options.isLogToStdErr());
3333
report.println("Agent loaded with options: %s%n", String.join(" ", args));
3434

35+
/* Stage 3: query all loaded classes */
3536
report.println("Querying classes...");
3637
Class<?>[] classes = getAllLoadedClasses(instrumentation, options);
3738
report.println("");
3839

39-
// 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
40+
/* Stage 4: initialize transformer */
41+
// The transformer could (as a side effect) be called with classes not in the list which is why we pass the filtered classes list
4042
Transformer dumper = new Transformer(report, classes);
4143

4244
if (classes.length > 0) {
45+
/* Stage 5: add transformer */
4346
report.println("%d classes found.%n", classes.length);
4447
instrumentation.addTransformer(dumper, true);
4548

46-
report.println("Dumping started...");
49+
/* Stage 6: dump all classes in filtered list */
50+
report.println("Dumping classes...");
4751
// Invoke the transformer and remove it when filtered classes are processed
4852
try {
4953
retransformClasses(instrumentation, classes, report);
5054
} finally {
5155
instrumentation.removeTransformer(dumper);
5256
}
53-
} else {
54-
report.println("WARNING: No classes found, bad filter ?%n");
55-
}
56-
report.println("");
57+
report.println("");
5758

58-
// Validate that no exceptions were generated during the dump process
59-
if (dumper.getLastException() != null) {
60-
report.println("WARNING: One or more transformer exceptions occurred while dumping classes.");
61-
report.dump(dumper.getLastException());
59+
/* Stage 7: dump class loader & class tree */
60+
report.println("Class loader & class tree...");
61+
//TODO: Print tree
62+
// ClassTree classTree1 = new ClassTree(dumper.getClassInfos());
63+
// classTree = classTree1.getRoot();
6264
report.println("");
65+
} else {
66+
report.println("WARNING: No classes found, bad filter ?%n");
6367
}
6468

65-
File destination = new File(options.getDestination());
66-
69+
/* Stage 8: create the jar */
6770
report.println("Creating jar...");
71+
File destination = new File(options.getDestination());
6872
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(destination))) {
69-
dumper.getClassInfos().entrySet().stream().sorted(Comparator.comparing(o -> o.getKey().getName())).forEach(entry -> {
70-
Class<?> clazz = entry.getKey();
71-
byte[] bytecode = entry.getValue();
72-
try {
73-
report.dump(clazz, bytecode);
74-
writeZipEntry(jar, Utils.toNativeClassName(clazz.getName()) + ".class", bytecode);
75-
} catch (IOException e) {
76-
throw new RuntimeException(e);
77-
}
78-
});
79-
writeZipEntry(jar, "report.txt", report.generate().getBytes(StandardCharsets.UTF_8));
73+
74+
ClassTree.Node root = dumper.getClassTree().getRoot();
75+
String base = Utils.toClassLoaderName(root.getLoader()) + "/";
76+
addNodeToJar(jar, report, root, base);
77+
78+
// Validate that no exceptions were generated during the dump process and if so display it last in the report
79+
if (dumper.getLastException() != null) {
80+
report.println("WARNING: One or more transformer exceptions occurred while dumping classes.");
81+
report.dump(dumper.getLastException());
82+
report.println("");
83+
}
84+
85+
writeZipEntry(jar, "report.txt", Utils.fromUtf8String(report.generate()));
8086
}
87+
88+
/* Stage 9: finalize the dump */
8189
report.println("%nDumped classes, including report.txt, can be found in: %s", options.getDestination());
8290
}
8391

@@ -98,7 +106,16 @@ private static void retransformClasses(Instrumentation instrumentation, Class<?>
98106
try {
99107
// Transform the full batch in one go, and if this throws an exception some classes may have been dumped but we deduplicate later on.
100108
instrumentation.retransformClasses(batch);
109+
} catch (ClassFormatError cfe) {
110+
System.out.println(cfe.getMessage());
111+
System.out.println(cfe.getLocalizedMessage());
112+
System.out.println(cfe.getCause());
113+
Arrays.stream(cfe.getSuppressed()).forEach(throwable -> throwable.printStackTrace());
114+
cfe.printStackTrace();
115+
System.exit(1);
101116
} catch (Throwable ignored) {
117+
ignored.printStackTrace();
118+
102119
// Transform classes one-by-one if batch transformation failed
103120
for (Class<?> clazz : batch) {
104121
try {
@@ -111,82 +128,24 @@ private static void retransformClasses(Instrumentation instrumentation, Class<?>
111128
}
112129
}
113130

114-
115-
private static void writeZipEntry(JarOutputStream jar, String name, byte[] data) throws IOException {
116-
jar.putNextEntry(new ZipEntry(name));
117-
jar.write(data);
118-
}
119-
120-
@SuppressWarnings("ConcatenationWithEmptyString")
121-
private static String getHeader() {
122-
return "" +
123-
"---------------------------------------------------------%n" +
124-
"--> Java Forensics Toolkit v1.1.0 by Benjamin Sølberg <--%n" +
125-
"---------------------------------------------------------%n" +
126-
"https://github.com/BenjaminSoelberg/JavaForensicsToolkit%n%n";
127-
}
128-
129-
private static void showUsage() {
130-
System.out.println("usage: java -jar JavaForensicsToolkit.jar [-v] [-e] [-d destination.jar] [-s] [-p] [-f filter]... [-x] <pid>");
131-
System.out.println();
132-
System.out.println("options:");
133-
System.out.println("-v\tverbose agent logging");
134-
System.out.println("-e\tagent will log to stderr instead of stdout");
135-
System.out.println("-d\tjar file destination of dumped classes");
136-
System.out.println("\tRelative paths will be relative with respect to the target process.");
137-
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)");
140-
System.out.println("-f\tregular expression class name filter");
141-
System.out.println("\tCan be specified multiple times.");
142-
System.out.println("-x\texclude classes matching the filter");
143-
System.out.println("pid\tprocess id of the target java process");
144-
System.out.println();
145-
System.out.println("example:");
146-
System.out.println("java -jar JavaForensicsToolkit.jar -d dump.jar -f 'java\\\\..*' -f 'sun\\\\..*' -f 'jdk\\\\..*' -f 'com\\\\.sun\\\\..*' -x 123456");
147-
}
148-
149-
/**
150-
* Get the absolut file location of the jar embedding this class
151-
*
152-
* @return absolut file location of jar
153-
*/
154-
private static String getJarLocation() {
155-
URL url = ClassDumper.class.getProtectionDomain().getCodeSource().getLocation();
156-
String file = url.getFile();
157-
if (url.getProtocol().equals("file") && file.startsWith("/") && file.endsWith(".jar")) {
158-
return file;
159-
}
160-
return null;
161-
}
162-
163-
public static void main(String[] args) throws Exception {
164-
System.out.printf(getHeader());
165-
String absolutJarLocation = getJarLocation();
166-
if (args.length < 1 || absolutJarLocation == null) {
167-
showUsage();
168-
System.exit(1);
169-
}
170-
171-
// We pre-parse the command line to be sure that it is syntactically correct prior to sending it to agentmain
172-
Options options = new Options(args);
173-
174-
String pid = options.getPid();
175-
System.out.println("Injecting agent into JVM with pid: " + pid);
176-
VirtualMachine vm = VirtualMachine.attach(pid);
177-
try {
178-
System.out.println("Dumping classes to: " + options.getDestination());
179-
String[] cmdLine = options.getArgs();
180-
vm.loadAgent(absolutJarLocation, Utils.encodeArgs(cmdLine));
181-
} finally {
131+
private static void addNodeToJar(JarOutputStream jar, Report report, ClassTree.Node node, String base) {
132+
node.getClasses().forEach((clazz, bytecode) -> {
182133
try {
183-
vm.detach();
184-
} catch (IOException ioe) {
185-
System.out.println("Unable to detach from process");
134+
report.dump(clazz, bytecode);
135+
writeZipEntry(jar, base + Utils.toNativeClassName(clazz.getName()) + ".class", bytecode);
136+
} catch (IOException e) {
137+
throw new RuntimeException(String.format("Failed to add %s with size %d to jar", clazz.getName(), bytecode.length), e);
186138
}
139+
});
140+
141+
for (ClassTree.Node child : node.getChildren()) {
142+
addNodeToJar(jar, report, child, base + Utils.toClassLoaderName(child.getLoader()) + "/");
187143
}
144+
}
188145

189-
System.out.println("Done");
146+
private static void writeZipEntry(JarOutputStream jar, String name, byte[] data) throws IOException {
147+
jar.putNextEntry(new ZipEntry(name));
148+
jar.write(data);
190149
}
191150

192151
}
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: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
public class Main {
9+
10+
private static void showUsage() {
11+
System.out.println("usage: java -jar JavaForensicsToolkit.jar [-v] [-e] [-d destination.jar] [-s] [-p] [-f filter]... [-x] <pid>");
12+
System.out.println();
13+
System.out.println("options:");
14+
System.out.println("-v\tverbose agent logging");
15+
System.out.println("-e\tagent will log to stderr instead of stdout");
16+
System.out.println("-d\tjar file destination of dumped classes");
17+
System.out.println("\tRelative paths will be relative with respect to the target process.");
18+
System.out.println("\tA jar file in temp will be generated if no destination was provided.");
19+
System.out.println("-s\tignore system class loader (like java.lang.String)");
20+
System.out.println("-p\tignore platform class loader (like system extensions)");
21+
System.out.println("-f\tregular expression class name filter");
22+
System.out.println("\tCan be specified multiple times.");
23+
System.out.println("-x\texclude classes matching the filter");
24+
System.out.println("pid\tprocess id of the target java process");
25+
System.out.println();
26+
System.out.println("example:");
27+
System.out.println("java -jar JavaForensicsToolkit.jar -d dump.jar -f 'java\\\\..*' -f 'sun\\\\..*' -f 'jdk\\\\..*' -f 'com\\\\.sun\\\\..*' -x 123456");
28+
}
29+
30+
/**
31+
* Get the absolut file location of the jar embedding this class
32+
*
33+
* @return absolut file location of jar
34+
*/
35+
private static String getJarLocation() {
36+
URL url = Main.class.getProtectionDomain().getCodeSource().getLocation();
37+
String file = url.getFile();
38+
if (url.getProtocol().equals("file") && file.startsWith("/") && file.endsWith(".jar")) {
39+
return file;
40+
}
41+
return null;
42+
}
43+
44+
public static void main(String[] args) throws Exception {
45+
System.out.printf(Utils.getVersionHeader());
46+
String absolutJarLocation = getJarLocation();
47+
if (args.length < 1 || absolutJarLocation == null) {
48+
showUsage();
49+
System.exit(1);
50+
}
51+
52+
// We pre-parse the command line to be sure that it is syntactically correct prior to sending it to agentmain
53+
Options options = new Options(args);
54+
55+
String pid = options.getPid();
56+
System.out.println("Injecting agent into JVM with pid: " + pid);
57+
VirtualMachine vm = VirtualMachine.attach(pid);
58+
try {
59+
System.out.println("Dumping classes to: " + options.getDestination());
60+
String[] cmdLine = options.getArgs();
61+
vm.loadAgent(absolutJarLocation, Utils.encodeArgs(cmdLine));
62+
} finally {
63+
try {
64+
vm.detach();
65+
} catch (IOException ioe) {
66+
System.out.println("Unable to detach from process");
67+
}
68+
}
69+
70+
System.out.println("Done");
71+
}
72+
73+
}

0 commit comments

Comments
 (0)