├── media
├── Main_Window.png
├── Bad_Unboxing_Demo.mp4
├── bad_unboxing_logo.png
└── confirm_execution.png
├── BadUnboxing
├── src
│ └── main
│ │ ├── resources
│ │ ├── icon.png
│ │ └── logback.xml
│ │ └── java
│ │ └── com
│ │ └── lauriewired
│ │ ├── ui
│ │ ├── NoIconTreeCell.java
│ │ ├── TextAreaOutputStream.java
│ │ ├── CustomProgressBar.java
│ │ ├── DirectoryTreeModel.java
│ │ ├── SyntaxUtility.java
│ │ └── AnalysisWindow.java
│ │ ├── analyzer
│ │ ├── ApkAnalysisDetails.java
│ │ ├── DynamicDexLoaderDetection.java
│ │ ├── JadxUtils.java
│ │ ├── ReflectionRemover.java
│ │ ├── IdentifierRenamer.java
│ │ ├── CodeReplacerUtils.java
│ │ └── UnpackerGenerator.java
│ │ └── BadUnboxing.java
└── pom.xml
├── .gitignore
├── README.md
└── LICENSE
/media/Main_Window.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LaurieWired/BadUnboxing/HEAD/media/Main_Window.png
--------------------------------------------------------------------------------
/media/Bad_Unboxing_Demo.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LaurieWired/BadUnboxing/HEAD/media/Bad_Unboxing_Demo.mp4
--------------------------------------------------------------------------------
/media/bad_unboxing_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LaurieWired/BadUnboxing/HEAD/media/bad_unboxing_logo.png
--------------------------------------------------------------------------------
/media/confirm_execution.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LaurieWired/BadUnboxing/HEAD/media/confirm_execution.png
--------------------------------------------------------------------------------
/BadUnboxing/src/main/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LaurieWired/BadUnboxing/HEAD/BadUnboxing/src/main/resources/icon.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled class files
2 | *.class
3 |
4 | # Log files
5 | *.log
6 |
7 | # Maven specific files
8 | target/
9 | pom.xml.tag
10 | pom.xml.releaseBackup
11 | pom.xml.versionsBackup
12 | pom.xml.next
13 | release.properties
14 |
15 | # VS Code specific files
16 | .vscode/
--------------------------------------------------------------------------------
/BadUnboxing/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/BadUnboxing/src/main/java/com/lauriewired/ui/NoIconTreeCell.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired.ui;
2 |
3 | import java.awt.Component;
4 |
5 | import javax.swing.JTree;
6 | import javax.swing.tree.DefaultTreeCellRenderer;
7 |
8 | public class NoIconTreeCell extends DefaultTreeCellRenderer {
9 | @Override
10 | public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
11 | super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
12 | setIcon(null); // Remove the icon
13 | return this;
14 | }
15 | }
--------------------------------------------------------------------------------
/BadUnboxing/src/main/java/com/lauriewired/analyzer/ApkAnalysisDetails.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired.analyzer;
2 |
3 | import java.io.File;
4 |
5 | public class ApkAnalysisDetails {
6 | private File baseDir;
7 | private String fullQualifiedClassName;
8 | private int recognizedImports;
9 |
10 | public ApkAnalysisDetails(File baseDir, String fullQualifiedClassName, int recognizedImports) {
11 | this.baseDir = baseDir;
12 | this.fullQualifiedClassName = fullQualifiedClassName;
13 | this.recognizedImports = recognizedImports;
14 | }
15 |
16 | public File getBaseDir() {
17 | return baseDir;
18 | }
19 |
20 | public String getFullyQualifiedClassName() {
21 | return fullQualifiedClassName;
22 | }
23 |
24 | public int getRecognizedImports() {
25 | return recognizedImports;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/BadUnboxing/src/main/java/com/lauriewired/ui/TextAreaOutputStream.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired.ui;
2 |
3 | import java.io.IOException;
4 | import java.io.OutputStream;
5 | import javax.swing.JTextArea;
6 |
7 | public class TextAreaOutputStream extends OutputStream {
8 | private JTextArea textArea;
9 |
10 | public TextAreaOutputStream(JTextArea textArea) {
11 | this.textArea = textArea;
12 | }
13 |
14 | @Override
15 | public void write(int b) throws IOException {
16 | textArea.append(String.valueOf((char) b));
17 | textArea.setCaretPosition(textArea.getDocument().getLength()); // Auto scroll to bottom
18 | }
19 |
20 | @Override
21 | public void write(byte[] b, int off, int len) throws IOException {
22 | textArea.append(new String(b, off, len));
23 | textArea.setCaretPosition(textArea.getDocument().getLength()); // Auto scroll to bottom
24 | }
25 | }
--------------------------------------------------------------------------------
/BadUnboxing/src/main/java/com/lauriewired/ui/CustomProgressBar.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired.ui;
2 |
3 | import java.awt.Color;
4 | import java.awt.Font;
5 | import java.awt.FontMetrics;
6 | import java.awt.Graphics;
7 |
8 | import javax.swing.JProgressBar;
9 |
10 | public class CustomProgressBar extends JProgressBar {
11 | private Color textColor;
12 |
13 | public CustomProgressBar() {
14 | super();
15 | this.textColor = Color.WHITE; // Default text color
16 | setFont(getFont().deriveFont(Font.BOLD, getFont().getSize() + 1));
17 | }
18 |
19 | public void setTextColor(Color textColor) {
20 | this.textColor = textColor;
21 | repaint(); // Repaint to apply the new color
22 | }
23 |
24 | @Override
25 | protected void paintComponent(Graphics g) {
26 | super.paintComponent(g);
27 | if (isStringPainted()) {
28 | String progressString = getString();
29 | FontMetrics fontMetrics = g.getFontMetrics();
30 | int stringWidth = fontMetrics.stringWidth(progressString);
31 | int stringHeight = fontMetrics.getAscent();
32 | int x = (getWidth() - stringWidth) / 2;
33 | int y = (getHeight() + stringHeight) / 2 - 3;
34 | g.setColor(textColor);
35 | g.drawString(progressString, x, y);
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/BadUnboxing/src/main/java/com/lauriewired/ui/DirectoryTreeModel.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired.ui;
2 |
3 | import javax.swing.tree.DefaultMutableTreeNode;
4 | import javax.swing.tree.DefaultTreeModel;
5 | import java.io.File;
6 |
7 | public class DirectoryTreeModel {
8 |
9 | public static DefaultTreeModel buildTreeModel(File root) {
10 | DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(new FileNode(root));
11 | buildTreeNodes(rootNode, root);
12 | return new DefaultTreeModel(rootNode);
13 | }
14 |
15 | private static void buildTreeNodes(DefaultMutableTreeNode parentNode, File file) {
16 | if (file.isDirectory()) {
17 | for (File child : file.listFiles()) {
18 | DefaultMutableTreeNode childNode = new DefaultMutableTreeNode(new FileNode(child));
19 | if (child.isDirectory()) {
20 | parentNode.add(childNode);
21 | buildTreeNodes(childNode, child);
22 | } else {
23 | parentNode.add(new DefaultMutableTreeNode(new FileNode(child)));
24 | }
25 | }
26 | } else {
27 | parentNode.add(new DefaultMutableTreeNode(new FileNode(file)));
28 | }
29 | }
30 | }
31 |
32 | class FileNode {
33 | private final File file;
34 |
35 | public FileNode(File file) {
36 | this.file = file;
37 | }
38 |
39 | public File getFile() {
40 | return file;
41 | }
42 |
43 | @Override
44 | public String toString() {
45 | return file.getName(); // Display only the name
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/BadUnboxing/src/main/java/com/lauriewired/analyzer/DynamicDexLoaderDetection.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired.analyzer;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Arrays;
5 | import java.util.HashSet;
6 | import java.util.List;
7 | import java.util.Set;
8 |
9 | import org.slf4j.Logger;
10 | import org.slf4j.LoggerFactory;
11 |
12 | import jadx.api.JadxDecompiler;
13 | import jadx.api.JavaClass;
14 |
15 | public class DynamicDexLoaderDetection {
16 | private static final Logger logger = LoggerFactory.getLogger(DynamicDexLoaderDetection.class);
17 |
18 | private static final Set dynamicDexLoadingKeywords = new HashSet<>(Arrays.asList(
19 | "DexClassLoader", "PathClassLoader", "InMemoryDexClassLoader", "BaseDexClassLoader", "loadDex", "OpenMemory"
20 | ));
21 |
22 | public static List getJavaDexLoadingDetails(JadxDecompiler jadx) {
23 | List details = new ArrayList<>();
24 | for (JavaClass cls : jadx.getClasses()) {
25 | String classCode = cls.getCode();
26 | for (String keyword : dynamicDexLoadingKeywords) {
27 | if (classCode.contains(keyword)) {
28 | String detail = String.format("Found keyword '%s' in class '%s'", keyword, cls.getFullName());
29 | logger.info(detail);
30 | details.add(detail);
31 | }
32 | }
33 | }
34 | return details;
35 | }
36 |
37 | public static boolean hasNativeDexLoading() {
38 | // TODO
39 |
40 | // Save the jadx output directory and find the native libs (files ending in .so)
41 |
42 | return false;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/BadUnboxing/src/main/java/com/lauriewired/ui/SyntaxUtility.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired.ui;
2 |
3 | import java.awt.Color;
4 |
5 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
6 | import org.fife.ui.rsyntaxtextarea.SyntaxScheme;
7 | import org.fife.ui.rsyntaxtextarea.Token;
8 |
9 | public class SyntaxUtility {
10 | public static void applyCustomTheme(RSyntaxTextArea textArea) {
11 | SyntaxScheme scheme = textArea.getSyntaxScheme();
12 |
13 | // Common color for keywords, return statements, and boolean literals
14 | Color keywordColor = Color.decode("#569CD6");
15 |
16 | // Define a color for operators (braces, parentheses, brackets)
17 | Color operatorColor = Color.WHITE; // Set operators to white
18 |
19 | // Define a more suitable shade of green for comments
20 | Color commentColor = Color.decode("#57A64A");
21 |
22 | scheme.getStyle(Token.RESERVED_WORD).foreground = keywordColor;
23 | scheme.getStyle(Token.DATA_TYPE).foreground = Color.decode("#4EC9B0");
24 | scheme.getStyle(Token.FUNCTION).foreground = Color.decode("#DCDCAA");
25 | scheme.getStyle(Token.LITERAL_NUMBER_DECIMAL_INT).foreground = Color.decode("#B5CEA8");
26 | scheme.getStyle(Token.LITERAL_STRING_DOUBLE_QUOTE).foreground = Color.decode("#CE9178");
27 | scheme.getStyle(Token.COMMENT_MULTILINE).foreground = commentColor;
28 | scheme.getStyle(Token.COMMENT_DOCUMENTATION).foreground = commentColor;
29 | scheme.getStyle(Token.COMMENT_EOL).foreground = commentColor; // Single-line comments
30 |
31 | scheme.getStyle(Token.OPERATOR).foreground = operatorColor; // Operators to white
32 | scheme.getStyle(Token.SEPARATOR).foreground = operatorColor;
33 | scheme.getStyle(Token.RESERVED_WORD_2).foreground = keywordColor;
34 | scheme.getStyle(Token.LITERAL_BOOLEAN).foreground = keywordColor; // Boolean literals
35 |
36 | scheme.getStyle(Token.IDENTIFIER).foreground = Color.decode("#9CDCFE");
37 |
38 | // Set the background color of the text area itself
39 | textArea.setBackground(Color.decode("#1E1E1E"));
40 | textArea.setCurrentLineHighlightColor(Color.decode("#264F78"));
41 | textArea.setFadeCurrentLineHighlight(true);
42 |
43 | // Apply the scheme to the text area
44 | textArea.revalidate();
45 | textArea.repaint();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](https://www.gnu.org/licenses/agpl-3.0)
3 | [](https://github.com/LaurieWired/BadUnboxing/releases)
4 | [](https://github.com/LaurieWired/BadUnboxing/stargazers)
5 | [](https://github.com/LaurieWired/BadUnboxing/network/members)
6 | [](https://github.com/LaurieWired/BadUnboxing/graphs/contributors)
7 | [](https://twitter.com/lauriewired)
8 |
9 | # Description
10 |
11 | BADUnboxing is an automated Android unpacker. It works by locating and decompiling code inside the APK that is relevant to the unpacking process.
12 |
13 | Once Bad Unboxing detects packing, it automatically generates a new Java application based on the decompiled code. This new application can be executed to drop dynamic unpacked artifacts to disk.
14 |
15 | https://github.com/LaurieWired/BadUnboxing/assets/123765654/b023fbad-a4a3-49fd-9572-61222c08c816
16 |
17 |
18 |
19 | # Installation
20 |
21 | A precompiled JAR file, as well as helper startup scripts are provided in the [Releases Page](https://github.com/LaurieWired/BadUnboxing/releases/)
22 |
23 |
24 | # Usage
25 |
26 | ### Check out the **[Wiki](https://github.com/LaurieWired/BadUnboxing/wiki)** for more details.
27 |
28 | ### Official Tutorial Video: [Bad Unboxing: Automated Android Unpacking](https://www.youtube.com/watch?v=8GbV3RWVo4A)
29 |
30 |
31 | # Contribute
32 | - Make a pull request
33 | - Add a new Unpacking Module
34 | - Add an Example to our Wiki
35 | - Report an error/issue
36 | - Suggest an improvement
37 | - Share with others or give a star!
38 |
39 | Your contributions are greatly appreciated and will help make BADUnboxing an even more powerful and versatile tool for the Android Reverse Engineering community.
40 |
41 | # Screenshots
42 |
43 | ### Basic BadUnboxing View
44 | 
45 |
46 | ### Confirm Generated Unpacker Execution
47 | 
48 |
--------------------------------------------------------------------------------
/BadUnboxing/pom.xml:
--------------------------------------------------------------------------------
1 |
4 | 4.0.0
5 |
6 | com.lauriewired
7 | BadUnboxing
8 | 1.0-SNAPSHOT
9 | jar
10 |
11 | BadUnboxing
12 | http://maven.apache.org
13 |
14 |
15 |
16 | google
17 | https://maven.google.com
18 |
19 |
20 |
21 |
22 |
23 | io.github.skylot
24 | jadx-core
25 | 1.5.0
26 |
27 |
28 |
29 | io.github.skylot
30 | jadx-dex-input
31 | 1.5.0
32 |
33 |
34 |
35 | xpp3
36 | xpp3
37 | 1.1.4c
38 |
39 |
40 |
41 | org.slf4j
42 | slf4j-api
43 | 2.0.13
44 |
45 |
46 |
47 | ch.qos.logback
48 | logback-classic
49 | 1.5.6
50 |
51 |
52 |
53 | com.fifesoft
54 | rsyntaxtextarea
55 | 3.4.0
56 |
57 |
58 |
59 | com.github.weisj
60 | darklaf-core
61 | 3.0.2
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | org.apache.maven.plugins
70 | maven-compiler-plugin
71 | 3.8.1
72 |
73 | 11
74 | 11
75 | UTF-8
76 |
77 |
78 |
79 |
80 |
81 | org.apache.maven.plugins
82 | maven-jar-plugin
83 | 3.2.0
84 |
85 |
86 |
87 | true
88 | com.lauriewired.BadUnboxing
89 |
90 |
91 |
92 |
93 |
94 | maven-assembly-plugin
95 | 3.3.0
96 |
97 |
98 |
99 | com.lauriewired.BadUnboxing
100 |
101 |
102 |
103 | jar-with-dependencies
104 |
105 |
106 |
107 |
108 | make-assembly
109 | package
110 |
111 | single
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
--------------------------------------------------------------------------------
/BadUnboxing/src/main/java/com/lauriewired/analyzer/JadxUtils.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired.analyzer;
2 |
3 | import java.io.File;
4 | import java.io.StringReader;
5 | import java.util.HashSet;
6 | import java.util.Set;
7 |
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 | import org.xmlpull.v1.XmlPullParser;
11 | import org.xmlpull.v1.XmlPullParserFactory;
12 |
13 | import jadx.api.JadxArgs;
14 | import jadx.api.JadxDecompiler;
15 | import jadx.api.JavaClass;
16 | import jadx.api.ResourceFile;
17 |
18 | public class JadxUtils {
19 | private static final Logger logger = LoggerFactory.getLogger(JadxUtils.class);
20 |
21 | public static JadxDecompiler loadJadx(String apkFilePath) {
22 | File apkFile = new File(apkFilePath);
23 | File outputDir = new File(apkFile.getParent(), "output_temp");
24 |
25 | JadxArgs jadxArgs = new JadxArgs();
26 | jadxArgs.setInputFile(apkFile);
27 | jadxArgs.setOutDir(outputDir);
28 | jadxArgs.setShowInconsistentCode(true);
29 | jadxArgs.setSkipResources(true);
30 | jadxArgs.setDeobfuscationOn(true);
31 | jadxArgs.setUseSourceNameAsClassAlias(true);
32 |
33 | JadxDecompiler jadx = new JadxDecompiler(jadxArgs);
34 | try {
35 | jadx.load();
36 | //jadx.save();
37 | } catch (Exception e) {
38 | logger.error("Error loading APK", e);
39 | }
40 |
41 | return jadx;
42 | }
43 |
44 | public static JavaClass getJavaClassByName(JadxDecompiler jadx, String className) {
45 | for (JavaClass javaClass : jadx.getClasses()) {
46 | if (javaClass.getFullName().equals(className)) {
47 | return javaClass;
48 | }
49 | }
50 | return null; // Class not found
51 | }
52 |
53 | public static JavaClass findApplicationSubclass(JadxDecompiler jadx) {
54 | for (JavaClass javaClass : jadx.getClasses()) {
55 | if (javaClass.getClassNode().getSuperClass().toString().equals("android.app.Application")) {
56 | logger.info("Found Application subclass: {}", javaClass.getFullName());
57 | return javaClass;
58 | }
59 | }
60 | return null;
61 | }
62 |
63 | public static Set getManifestClasses(String apkFilePath, JadxDecompiler jadx) throws Exception {
64 | Set classNames = new HashSet<>();
65 |
66 | String manifestContent = "";
67 | for (ResourceFile resource : jadx.getResources()) {
68 | if (resource.getOriginalName().equals("AndroidManifest.xml")) {
69 | logger.info("Found AndroidManifest.xml");
70 | manifestContent = resource.loadContent().getText().getCodeStr();
71 | break;
72 | }
73 | }
74 |
75 | if (!manifestContent.equals("")) {
76 | // Parse the AndroidManifest.xml
77 | XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
78 | XmlPullParser parser = factory.newPullParser();
79 | parser.setInput(new StringReader(manifestContent)); // Use StringReader here
80 |
81 | int eventType = parser.getEventType();
82 | while (eventType != XmlPullParser.END_DOCUMENT) {
83 | if (eventType == XmlPullParser.START_TAG) {
84 | String name = parser.getName();
85 | if ("activity".equals(name) || "service".equals(name) || "receiver".equals(name) || "provider".equals(name)) {
86 | for (int i = 0; i < parser.getAttributeCount(); i++) {
87 | if ("android:name".equals(parser.getAttributeName(i))) {
88 | String className = parser.getAttributeValue(i);
89 | logger.info("Manifest class found: {}", className);
90 | classNames.add(className.replace(".", "/") + ".class");
91 | }
92 | }
93 | }
94 | }
95 | eventType = parser.next();
96 | }
97 | }
98 |
99 | return classNames;
100 | }
101 |
102 | public static Set getDexClasses(String apkFilePath, JadxDecompiler jadx) throws Exception {
103 | Set classNames = new HashSet<>();
104 |
105 | for (JavaClass cls : jadx.getClasses()) {
106 | logger.info("Dex class found: {}", cls.getFullName());
107 | classNames.add(cls.getRawName().replace('.', '/') + ".class");
108 | }
109 |
110 | return classNames;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/BadUnboxing/src/main/java/com/lauriewired/BadUnboxing.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired;
2 |
3 | import java.awt.datatransfer.DataFlavor;
4 | import java.awt.dnd.DnDConstants;
5 | import java.awt.dnd.DropTarget;
6 | import java.awt.dnd.DropTargetAdapter;
7 | import java.awt.dnd.DropTargetDropEvent;
8 | import java.awt.Dimension;
9 | import java.awt.Font;
10 | import java.awt.GridBagConstraints;
11 | import java.awt.GridBagLayout;
12 | import java.awt.Insets;
13 | import java.io.File;
14 | import java.net.URL;
15 | import java.util.List;
16 |
17 | import javax.swing.ImageIcon;
18 | import javax.swing.JButton;
19 | import javax.swing.JFileChooser;
20 | import javax.swing.JFrame;
21 | import javax.swing.JLabel;
22 | import javax.swing.JPanel;
23 | import javax.swing.JTextField;
24 | import javax.swing.SwingUtilities;
25 | import org.slf4j.Logger;
26 | import org.slf4j.LoggerFactory;
27 |
28 | import com.github.weisj.darklaf.LafManager;
29 | import com.github.weisj.darklaf.theme.DarculaTheme;
30 |
31 | import com.lauriewired.ui.AnalysisWindow;
32 |
33 | public class BadUnboxing {
34 | private static final Logger logger = LoggerFactory.getLogger(BadUnboxing.class);
35 |
36 | public static void main(String[] args) {
37 | // Set the Darklaf Look and Feel
38 | try {
39 | LafManager.install(new DarculaTheme());
40 | } catch (Exception e) {
41 | e.printStackTrace();
42 | }
43 |
44 | SwingUtilities.invokeLater(BadUnboxing::createAndShowGUI);
45 | }
46 |
47 | private static void createAndShowGUI() {
48 | JFrame frame = new JFrame("BadUnboxing");
49 | frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
50 | frame.setSize(600, 400); // Increased size for better spacing
51 |
52 | // Set the window icon
53 | try {
54 | URL imageUrl = BadUnboxing.class.getClassLoader().getResource("icon.png");
55 | if (imageUrl != null) {
56 | ImageIcon imageIcon = new ImageIcon(imageUrl);
57 | frame.setIconImage(imageIcon.getImage());
58 | } else {
59 | logger.error("Icon resource not found");
60 | }
61 | } catch (Exception e) {
62 | logger.error("Failed to load window icon", e);
63 | }
64 |
65 | JPanel panel = new JPanel();
66 | frame.add(panel);
67 | placeComponents(panel, frame);
68 |
69 | frame.setVisible(true);
70 | }
71 |
72 | private static void placeComponents(JPanel panel, JFrame frame) {
73 | panel.setLayout(new GridBagLayout());
74 | GridBagConstraints constraints = new GridBagConstraints();
75 | constraints.fill = GridBagConstraints.HORIZONTAL;
76 | constraints.insets = new Insets(15, 15, 15, 15); // Larger margins around components
77 |
78 | /*
79 | // Label "APK File:"
80 | JLabel label = new JLabel("APK Unpacker");
81 | label.setFont(new Font("Verdana", Font.BOLD, 25)); // Set font style and size
82 | constraints.gridx = 0; // Column 0
83 | constraints.gridy = 0; // Row 0
84 | constraints.gridwidth = 4; // Span across all columns
85 | constraints.anchor = GridBagConstraints.CENTER; // Center alignment
86 | constraints.insets = new Insets(20, 15, 20, 15); // Add padding
87 | panel.add(label, constraints);
88 | */
89 |
90 | // Text Field for file path
91 | JTextField filePathText = new JTextField();
92 | filePathText.setFont(new Font("Verdana", Font.PLAIN, 16)); // Set font style and size
93 | filePathText.setEditable(false);
94 | filePathText.setPreferredSize(new Dimension(275, 30)); // Set preferred size for wider text field
95 | //filePathText.setBorder(BorderFactory.createEmptyBorder()); // Remove the white border
96 | constraints.gridx = 0; // Column 0
97 | constraints.gridy = 1; // Row 1
98 | constraints.gridwidth = 3; // Takes three columns
99 | constraints.anchor = GridBagConstraints.CENTER; // Center alignment
100 | constraints.insets = new Insets(10, 15, 10, 5); // Add padding
101 | panel.add(filePathText, constraints);
102 |
103 | // "Select File" button
104 | JButton fileButton = new JButton("Select File");
105 | constraints.gridx = 3; // Column 3
106 | constraints.gridy = 1; // Row 1
107 | constraints.gridwidth = 1; // Takes one column
108 | constraints.anchor = GridBagConstraints.CENTER; // Center alignment
109 | constraints.insets = new Insets(5, 5, 5, 15); // Add padding
110 | panel.add(fileButton, constraints);
111 |
112 | // Status label
113 | JLabel statusLabel = new JLabel("");
114 | statusLabel.setFont(new Font("Verdana", Font.ITALIC, 14)); // Set font style and size
115 | constraints.gridx = 0; // Column 0
116 | constraints.gridy = 3; // Row 3
117 | constraints.gridwidth = 4; // Span across all columns
118 | constraints.anchor = GridBagConstraints.CENTER; // Center alignment
119 | constraints.insets = new Insets(10, 15, 10, 15); // Add padding
120 | panel.add(statusLabel, constraints);
121 |
122 | // "Unpack" button
123 | JButton unpackButton = new JButton("Generate Unpacker");
124 | constraints.gridx = 0; // Column 0
125 | constraints.gridy = 2; // Row 2
126 | constraints.gridwidth = 4; // Span across all columns
127 | constraints.anchor = GridBagConstraints.CENTER; // Center alignment
128 | constraints.insets = new Insets(10, 15, 10, 15); // Add padding
129 | panel.add(unpackButton, constraints);
130 |
131 | // File and button listeners
132 | setupDragAndDrop(filePathText);
133 | setupFileButtonListener(fileButton, filePathText, panel);
134 | setupUnpackButtonListener(unpackButton, filePathText, statusLabel, frame);
135 | }
136 |
137 | private static void setupDragAndDrop(JTextField filePathText) {
138 | new DropTarget(filePathText, new DropTargetAdapter() {
139 | @Override
140 | public void drop(DropTargetDropEvent evt) {
141 | try {
142 | evt.acceptDrop(DnDConstants.ACTION_COPY);
143 | List droppedFiles = (List) evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
144 | if (!droppedFiles.isEmpty()) {
145 | File file = droppedFiles.get(0);
146 | filePathText.setText(file.getAbsolutePath());
147 | }
148 | } catch (Exception ex) {
149 | logger.error("Error during file drop", ex);
150 | }
151 | }
152 | });
153 | }
154 |
155 | private static void setupFileButtonListener(JButton fileButton, JTextField filePathText, JPanel panel) {
156 | fileButton.addActionListener(e -> {
157 | JFileChooser fileChooser = new JFileChooser();
158 | fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
159 | int option = fileChooser.showOpenDialog(panel);
160 | if (option == JFileChooser.APPROVE_OPTION) {
161 | File selectedFile = fileChooser.getSelectedFile();
162 | filePathText.setText(selectedFile.getAbsolutePath());
163 | }
164 | });
165 | }
166 |
167 | private static void setupUnpackButtonListener(JButton unpackButton, JTextField filePathText, JLabel statusLabel, JFrame frame) {
168 | unpackButton.addActionListener(e -> {
169 | String apkFilePath = filePathText.getText();
170 | if (!apkFilePath.isEmpty()) {
171 | AnalysisWindow.show(frame, apkFilePath); // Open Analysis Window
172 | } else {
173 | statusLabel.setText("Please enter a valid APK file path.");
174 | }
175 | });
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/BadUnboxing/src/main/java/com/lauriewired/analyzer/ReflectionRemover.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired.analyzer;
2 |
3 | import java.util.Arrays;
4 | import java.util.HashSet;
5 | import java.util.Set;
6 | import java.util.Stack;
7 | import java.util.regex.Matcher;
8 | import java.util.regex.Pattern;
9 |
10 | import org.slf4j.Logger;
11 | import org.slf4j.LoggerFactory;
12 |
13 | public class ReflectionRemover {
14 | private static final Logger logger = LoggerFactory.getLogger(ReflectionRemover.class);
15 |
16 | private static Stack reflectiveValues = new Stack<>();
17 | private static final Set analyzedValues = new HashSet<>();
18 |
19 | private static final Set reflectiveKeywords = new HashSet<>(Arrays.asList(
20 | "getMethod",
21 | "invoke",
22 | "getDeclaredField",
23 | "WeakReference",
24 | "forName",
25 | "setAccessible",
26 | "getApplicationContext",
27 | "newInstance"
28 | ));
29 |
30 | public static void removeReflection(StringBuilder javaCode) {
31 |
32 | //System.out.println(javaCode);
33 |
34 | //commentOutMethodUsingReflection(javaCode, surroundWithRegex("forName"));
35 |
36 | /*
37 | commentOutLineUsingReflection(javaCode, surroundWithRegex("getDeclaredField"));
38 |
39 | for (String method : reflectiveValues) {
40 | System.out.println(method);
41 | }
42 |
43 |
44 | //commentOutMethodUsingReflection(javaCode, "[\\s\\.\\(]it[\\s\\.\\);]");
45 | //commentOutLineUsingReflection(javaCode, "[\\s\\.\\(]it[\\s\\.\\);]");
46 |
47 | //commentOutMethodUsingReflection(javaCode, "Class.forName");
48 | //commentOutLineUsingReflection(javaCode, "Class.forName");
49 |
50 |
51 | for (String method : reflectiveValues) {
52 | System.out.println(method);
53 | }*/
54 |
55 | //commentOutLineUsingReflection(javaCode, surroundWithRegex("NJdYmOqBdAmAiWnWkAzIiKhNbFqSfAkTtXyRwCmMbCfNqClHu"));
56 | //commentOutLineUsingReflection(javaCode, surroundWithRegex("bwyY_354421"));
57 |
58 |
59 |
60 | // Initial comment out of reflection
61 | for (String keyword : reflectiveKeywords) {
62 | commentOutMethodUsingReflection(javaCode, surroundWithRegex(keyword));
63 | }
64 | for (String keyword : reflectiveKeywords) {
65 | commentOutLineUsingReflection(javaCode, surroundWithRegex(keyword));
66 | }
67 |
68 | // Add while loop here to keep iterating through reflectiveMethods and reflectiveVariables while they have values
69 | while (!reflectiveValues.isEmpty()) {
70 | String value = reflectiveValues.pop();
71 | if (!analyzedValues.contains(value)) {
72 | commentOutMethodUsingReflection(javaCode, surroundWithRegex(value));
73 | commentOutLineUsingReflection(javaCode, surroundWithRegex(value));
74 | analyzedValues.add(value);
75 | }
76 | }
77 | }
78 |
79 | private static String surroundWithRegex(String value) {
80 | return "[\\[\\]\\s\\.\\(\\)]" + value + "[\\[\\]\\s\\.\\(\\),;]";
81 | }
82 |
83 | public static void commentOutLineUsingReflection(StringBuilder javaCode, String keywordRegex) {
84 | // Compile the keyword as a regex pattern
85 | Pattern keywordPattern = Pattern.compile(keywordRegex);
86 |
87 | // Split the code into lines
88 | String[] lines = javaCode.toString().split("\n");
89 | StringBuilder modifiedCode = new StringBuilder();
90 |
91 | // Pattern to match variable names
92 | Pattern variablePattern = Pattern.compile("([a-zA-Z0-9_]+)\\s*=(\\s*)");
93 | Pattern methodPattern = Pattern.compile(".*\\s(public|private|protected|static|void)+\\s.");
94 |
95 | // Iterate over each line
96 | for (String line : lines) {
97 | Matcher keywordMatcher = keywordPattern.matcher(line);
98 | Matcher methodMatch = methodPattern.matcher(line);
99 |
100 | if (keywordMatcher.find() && !methodMatch.find() && !line.trim().startsWith("//") && !line.trim().startsWith("import")) {
101 | // Check if the reflective call is on the right of an equal sign
102 | Matcher matcher = variablePattern.matcher(line);
103 | if (matcher.find()) {
104 | if (!matcher.group(2).matches(".*" + keywordRegex + ".*")) {
105 | // Parse out the variable name
106 | String variableName = matcher.group(1);
107 | reflectiveValues.push(variableName);
108 |
109 | logger.info("Found reflective variable: " + variableName);
110 | }
111 | }
112 | // Comment out the line if it contains the keyword and is not already commented out
113 | modifiedCode.append("// ").append(line).append(" // BadUnboxing: Line contains reflection and was commented out\n");
114 |
115 | // Don't be too noisy so print up to the first 30 chars of the line
116 | String logLine = line.replaceFirst("^\\s+", "");
117 | logger.info("Commented out reflective line starting with: " + logLine.substring(0, Math.min(logLine.length(), 30)));
118 |
119 | // If the line ends with an opening brace, add "if (true) {"
120 | if (line.trim().endsWith("{")) {
121 | modifiedCode.append("if (true) {\n"); // We just need a dummy placeholder
122 | }
123 | } else {
124 | modifiedCode.append(line).append("\n");
125 | }
126 | }
127 |
128 | // Replace the original code with the modified code
129 | javaCode.setLength(0);
130 | javaCode.append(modifiedCode);
131 | }
132 |
133 | // omg it works. never change this
134 | public static void commentOutMethodUsingReflection(StringBuilder javaCode, String keywordRegex) {
135 | // Compile the keyword as a regex pattern
136 | Pattern keywordPattern = Pattern.compile(keywordRegex);
137 |
138 | // Split the code into lines
139 | String[] lines = javaCode.toString().split("\n");
140 | StringBuilder modifiedCode = new StringBuilder();
141 |
142 | boolean returnContainsReflection = false;
143 | StringBuilder currentMethod = new StringBuilder();
144 | String currentMethodName = null;
145 | int braceDepth = -1;
146 |
147 | for (String line : lines) {
148 | if (line.trim().matches(".*(public|protected|private|static|\\s)+\\s*\\S+\\s+(method\\S+|main|onCreate)\\(.*\\)\\s+[\\w\\s]*\\{") && !line.trim().startsWith("//")) {
149 | // New method start
150 | currentMethod.setLength(0);
151 | returnContainsReflection = false;
152 | currentMethodName = parseMethodName(line);
153 | currentMethod.append(line).append("\n");
154 |
155 | braceDepth = 1;
156 | } else if (braceDepth == 0) {
157 | // This means we've finished a method
158 | if (returnContainsReflection) {
159 | commentOutMethod(currentMethod, modifiedCode);
160 | reflectiveValues.push(currentMethodName);
161 | logger.info("Removing reflective method: " + currentMethodName);
162 | } else {
163 | modifiedCode.append(currentMethod).append("\n");
164 | }
165 |
166 | // Still need to handle the latest line
167 | modifiedCode.append(line);
168 |
169 | braceDepth = -1; // Reset for new method
170 | } else if (braceDepth > 0) {
171 | // Keep up with our braces to keep track of the method length
172 | for (int i = 0; i < line.length(); i++) {
173 | if (line.charAt(i) == '{') {
174 | braceDepth++;
175 | } else if (line.charAt(i) == '}') {
176 | braceDepth--;
177 | }
178 | }
179 |
180 | // Check if the line is a return statement and contains the keyword
181 | Matcher keywordMatcher = keywordPattern.matcher(line);
182 | if (line.trim().startsWith("return") && keywordMatcher.find() && !line.trim().startsWith("//")) {
183 | returnContainsReflection = true;
184 | }
185 | currentMethod.append(line).append("\n");
186 | } else {
187 | modifiedCode.append(line).append("\n");
188 | }
189 | }
190 |
191 | // Replace the original code with the modified code
192 | javaCode.setLength(0);
193 | javaCode.append(modifiedCode);
194 | }
195 |
196 | private static String parseMethodName(String line) {
197 | // Simple regex to extract the method name
198 | String methodSignature = line.trim().split("\\(")[0].trim();
199 | String[] parts = methodSignature.split("\\s+");
200 | return parts[parts.length - 1];
201 | }
202 |
203 | private static void commentOutMethod(StringBuilder methodCode, StringBuilder modifiedCode) {
204 | String[] lines = methodCode.toString().split("\n");
205 | for (String line : lines) {
206 | modifiedCode.append("// BadUnboxing ").append(line).append("\n");
207 | }
208 | modifiedCode.append("// BadUnboxing: Method contains reflection in return statement and was commented out\n\n");
209 | }
210 | }
--------------------------------------------------------------------------------
/BadUnboxing/src/main/java/com/lauriewired/analyzer/IdentifierRenamer.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired.analyzer;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 | import java.util.Random;
6 | import java.util.Set;
7 | import java.util.regex.Matcher;
8 | import java.util.regex.Pattern;
9 |
10 | import org.slf4j.Logger;
11 | import org.slf4j.LoggerFactory;
12 |
13 | import jadx.api.JadxDecompiler;
14 | import jadx.api.JavaClass;
15 | import jadx.api.JavaField;
16 | import jadx.api.JavaMethod;
17 | import jadx.core.dex.nodes.FieldNode;
18 |
19 | public class IdentifierRenamer {
20 | private static final Logger logger = LoggerFactory.getLogger(IdentifierRenamer.class);
21 | private static final int POSTFIX_LENGTH = 8;
22 | private static final String CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
23 | private static final Random RANDOM = new Random();
24 |
25 | public static void renameMethodsAndFields(JavaClass javaClass, JadxDecompiler jadx, Set existingNames) {
26 | // Via JADX API
27 | renameMethods(javaClass, jadx, existingNames);
28 | renameFields(javaClass, jadx, existingNames);
29 | }
30 |
31 | public static StringBuilder renameArgsAndVars(JavaClass javaClass, JadxDecompiler jadx, Set existingNames) {
32 | javaClass.reload(); // Make sure we got the renamed methods and fields
33 |
34 | // Via regular expressions
35 | // Can't find a great way to rename these via the JADX API
36 | StringBuilder javaCode = new StringBuilder();
37 | javaCode.append(javaClass.getCode());
38 |
39 | renameMethodArguments(javaCode, existingNames);
40 | renameLocalVariables(javaCode, existingNames);
41 |
42 | return javaCode;
43 | }
44 |
45 | private static void renameMethodArguments(StringBuilder javaCode, Set existingNames) {
46 | // Pattern to match methods with their arguments
47 | Pattern methodPattern = Pattern.compile("(public|protected|private|static)+\\s+[\\w\\[\\]<>]+\\s+(\\w+)\\s*\\(([^)]*)\\)[\\w\\s]*\\{");
48 |
49 | // Find the last match and work backwards
50 | Matcher methodMatcher = methodPattern.matcher(javaCode);
51 | List matchPositions = new ArrayList<>();
52 |
53 | while (methodMatcher.find()) {
54 | matchPositions.add(methodMatcher.start());
55 | }
56 |
57 | // Process matches in reverse order
58 | for (int i = matchPositions.size() - 1; i >= 0; i--) {
59 | int matchStart = matchPositions.get(i);
60 | methodMatcher = methodPattern.matcher(javaCode);
61 | methodMatcher.find(matchStart);
62 |
63 | String methodSignature = methodMatcher.group();
64 | String arguments = methodMatcher.group(3); // Capture group 3 for arguments
65 | String methodBody = extractMethodBody(javaCode, methodMatcher.end());
66 | String modifiedMethodBody = methodBody;
67 |
68 | if (!arguments.isEmpty()) {
69 | String[] args = arguments.split(",");
70 | for (String arg : args) {
71 | arg = arg.trim();
72 | String[] parts = arg.split("\\s+");
73 | String argName = parts[parts.length - 1];
74 | if (!argName.startsWith("var_") && !argName.startsWith("method_")) {
75 | String uniqueArgName = generateUniqueName(existingNames, argName, "arg_");
76 |
77 | logger.info("Renaming method argument {} to {}", argName, uniqueArgName);
78 |
79 | // Replace all references to this argument within the method body
80 | modifiedMethodBody = modifiedMethodBody.replaceAll("\\b" + argName + "\\b", uniqueArgName);
81 |
82 | // Update the method signature with the new argument name
83 | methodSignature = methodSignature.replaceAll("\\b" + argName + "\\b", uniqueArgName);
84 | }
85 | }
86 | }
87 |
88 | // Replace the method body in the original code
89 | String fullMethod = methodSignature + modifiedMethodBody;
90 | javaCode.replace(methodMatcher.start(), methodMatcher.end() + methodBody.length(), fullMethod);
91 | }
92 | }
93 |
94 | private static void renameLocalVariables(StringBuilder javaCode, Set existingNames) {
95 | // Pattern to match methods with their bodies
96 | Pattern methodPattern = Pattern.compile("(public|protected|private|static)+\\s+[\\w\\[\\]<>]+\\s+(\\w+)\\s*\\(([^)]*)\\)[\\w\\s]*\\{");
97 |
98 | // Find the last match and work backwards
99 | Matcher methodMatcher = methodPattern.matcher(javaCode);
100 | List matchPositions = new ArrayList<>();
101 |
102 | while (methodMatcher.find()) {
103 | matchPositions.add(methodMatcher.start());
104 | }
105 |
106 | // Process matches in reverse order
107 | for (int i = matchPositions.size() - 1; i >= 0; i--) {
108 | int matchStart = matchPositions.get(i);
109 | methodMatcher = methodPattern.matcher(javaCode);
110 | methodMatcher.find(matchStart);
111 |
112 | String methodSignature = methodMatcher.group();
113 | String methodBody = extractMethodBody(javaCode, methodMatcher.end());
114 | String modifiedMethodBody = methodBody;
115 |
116 | // Pattern to match local variable declarations (including array types)
117 | Pattern localVarPattern = Pattern.compile("(\\b\\w+[\\[\\]\\<\\?\\>]*)\\s+(\\b\\w+\\b)\\s*(=|;)");
118 | Matcher localVarMatcher = localVarPattern.matcher(methodBody);
119 |
120 | while (localVarMatcher.find()) {
121 | String varType = localVarMatcher.group(1);
122 | String varName = localVarMatcher.group(2);
123 | if (!varName.matches("\\d+") && !varName.equals("null") &&
124 | !varName.equals("true") && !varName.equals("false") &&
125 | !varName.startsWith("var_")) {
126 |
127 | String uniqueVarName = generateUniqueName(existingNames, varName, "var_");
128 |
129 | // Replace all references to this variable within the method body
130 | if (varName.startsWith("method_")) {
131 | modifiedMethodBody = replaceVariableNamesWithoutMethod(modifiedMethodBody, varName, uniqueVarName);
132 | } else {
133 | modifiedMethodBody = replaceVariableNames(modifiedMethodBody, varName, uniqueVarName);
134 | }
135 |
136 | logger.info("Renaming local variable {} to {}", varName, uniqueVarName);
137 | }
138 | }
139 |
140 | // Replace the method body in the original code
141 | String fullMethod = methodSignature + modifiedMethodBody;
142 | javaCode.replace(methodMatcher.start(), methodMatcher.end() + methodBody.length(), fullMethod);
143 | }
144 | }
145 |
146 | private static String replaceVariableNamesWithoutMethod(String body, String varName, String uniqueVarName) {
147 | StringBuilder result = new StringBuilder();
148 | // This regex looks for the variable name as a whole word, ensuring it isn't part of a longer string or variable name
149 | Pattern pattern = Pattern.compile("\\b" + Pattern.quote(varName) + "\\b(?![\\(])"); // Negative lookahead to avoid matches followed by '('
150 | Matcher matcher = pattern.matcher(body);
151 |
152 | int lastIndex = 0;
153 | while (matcher.find()) {
154 | // Append the part of the body before the match, then append the new unique variable name
155 | result.append(body, lastIndex, matcher.start())
156 | .append(uniqueVarName);
157 | lastIndex = matcher.end();
158 | }
159 | // Append the rest of the body after the last match
160 | result.append(body.substring(lastIndex));
161 | return result.toString();
162 | }
163 |
164 | private static String replaceVariableNames(String body, String varName, String uniqueVarName) {
165 | StringBuilder result = new StringBuilder();
166 | Pattern pattern = Pattern.compile("\\b" + varName + "\\b");
167 | Matcher matcher = pattern.matcher(body);
168 |
169 | int lastIndex = 0;
170 | while (matcher.find()) {
171 | // Check the preceding character
172 | if (matcher.start() == 0 || body.charAt(matcher.start() - 1) != '.') {
173 | result.append(body, lastIndex, matcher.start())
174 | .append(uniqueVarName);
175 | } else {
176 | result.append(body, lastIndex, matcher.end());
177 | }
178 | lastIndex = matcher.end();
179 | }
180 | result.append(body.substring(lastIndex));
181 | return result.toString();
182 | }
183 |
184 | private static String extractMethodBody(StringBuilder javaCode, int startIndex) {
185 | int openBraces = 1;
186 | int currentIndex = startIndex;
187 | while (openBraces > 0 && currentIndex < javaCode.length()) {
188 | char currentChar = javaCode.charAt(currentIndex);
189 | if (currentChar == '{') {
190 | openBraces++;
191 | } else if (currentChar == '}') {
192 | openBraces--;
193 | }
194 | currentIndex++;
195 | }
196 | return javaCode.substring(startIndex, currentIndex);
197 | }
198 |
199 | private static void renameMethods(JavaClass javaClass, JadxDecompiler jadx, Set existingNames) {
200 | for (JavaMethod method : javaClass.getMethods()) {
201 | if (!method.getName().startsWith("method_") && !method.getName().equals("attachBaseContext") &&
202 | !method.getName().equals("onCreate") && !method.getName().equals("")) {
203 |
204 | String uniqueMethodName = generateUniqueName(existingNames, method.getName(), "method_");
205 | logger.info("Renaming method {} to {}", method.getName(), uniqueMethodName);
206 | method.getMethodNode().rename(uniqueMethodName);
207 | }
208 | }
209 | }
210 |
211 | private static void renameFields(JavaClass javaClass, JadxDecompiler jadx, Set existingNames) {
212 | for (JavaField field : javaClass.getFields()) {
213 | FieldNode fieldNode = field.getFieldNode();
214 | if (!field.getName().startsWith("method_") && !field.getName().startsWith("field_")) {
215 | String uniqueFieldName = generateUniqueName(existingNames, field.getName(), "field_");
216 | logger.info("Renaming field {} to {}", field.getName(), uniqueFieldName);
217 | fieldNode.rename(uniqueFieldName);
218 | }
219 | }
220 | }
221 |
222 | private static String generateUniqueName(Set existingNames, String originalName, String prefix) {
223 | String uniqueName;
224 | do {
225 | uniqueName = prefix + originalName + "_" + generateRandomPostfix();
226 | } while (existingNames.contains(uniqueName));
227 | existingNames.add(uniqueName);
228 | return uniqueName;
229 | }
230 |
231 | public static String generateRandomPostfix() {
232 | StringBuilder sb = new StringBuilder(POSTFIX_LENGTH);
233 | for (int i = 0; i < POSTFIX_LENGTH; i++) {
234 | sb.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length())));
235 | }
236 | return sb.toString();
237 | }
238 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/BadUnboxing/src/main/java/com/lauriewired/analyzer/CodeReplacerUtils.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired.analyzer;
2 |
3 | import java.util.HashSet;
4 | import java.util.Set;
5 | import java.util.regex.Matcher;
6 | import java.util.regex.Pattern;
7 |
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 |
11 | public class CodeReplacerUtils {
12 | private static final Logger logger = LoggerFactory.getLogger(CodeReplacerUtils.class);
13 |
14 | // Dummy class defintions
15 | private static final String DUMMY_CONTEXT_CLASS =
16 | "class Context {\n" +
17 | " // Dummy Context class implementation\n" +
18 | "}\n";
19 |
20 | private static final String DUMMY_APPLICATION_CLASS =
21 | "class Application {\n" +
22 | " // Dummy Application class implementation\n" +
23 | "}\n";
24 |
25 | //TODO: update variable patterns to look like this Pattern variablePattern = Pattern.compile("([a-zA-Z0-9_]+)\\s*=\\s*");
26 |
27 | public static String insertImport(String newImport, String classCode) {
28 | // Prefix the new import with 'import ' and suffix with ';'
29 | String formattedImport = "import " + newImport + ";";
30 |
31 | // Check if the formatted import already exists in classCode
32 | if (classCode.contains(formattedImport)) {
33 | // Import already exists, do nothing
34 | return classCode;
35 | }
36 |
37 | // Find the first occurrence of an import statement
38 | int importIndex = classCode.indexOf("import ");
39 |
40 | if (importIndex != -1) {
41 | // Insert the new import one line before the first import statement
42 | int insertIndex = classCode.lastIndexOf("\n", importIndex) + 1;
43 | String modifiedClassCode = classCode.substring(0, insertIndex) + formattedImport + "\n" + classCode.substring(insertIndex);
44 | logger.info("Inserted import " + newImport);
45 | return modifiedClassCode;
46 | } else {
47 | // No import statements found, add the new import at the beginning
48 | String modifiedClassCode = formattedImport + "\n" + classCode;
49 | logger.info("Inserted import " + newImport);
50 | return modifiedClassCode;
51 | }
52 | }
53 |
54 | public static StringBuilder processClassImports(StringBuilder javaCode, String newClassCode) {
55 | // Extract imports from existing code
56 | Set existingImports = new HashSet<>();
57 | Matcher importMatcher = Pattern.compile("(?m)^import\\s+.*?;").matcher(javaCode);
58 | while (importMatcher.find()) {
59 | existingImports.add(importMatcher.group());
60 | }
61 |
62 | StringBuilder imports = new StringBuilder();
63 | importMatcher = Pattern.compile("(?m)^import\\s+.*?;").matcher(newClassCode);
64 | while (importMatcher.find()) {
65 | String importStatement = importMatcher.group();
66 | if (!existingImports.contains(importStatement)) {
67 | imports.append(importStatement).append("\n");
68 | existingImports.add(importStatement);
69 | }
70 | }
71 |
72 | return imports;
73 | }
74 |
75 | /*
76 | * Modifying methods from dalvik.system.DexClassLoader
77 | */
78 | public static String processDexClassLoaderMethods(String classCode) {
79 | // Pattern to find calls to new DexClassLoader with arguments
80 | Pattern dexClassLoaderPattern = Pattern.compile(".*new\\s+DexClassLoader\\(([^,]+),.*");
81 |
82 | Matcher matcher = dexClassLoaderPattern.matcher(classCode);
83 | StringBuffer modifiedCode = new StringBuffer();
84 |
85 | // Replace all occurrences with System.out.println(firstArgument)
86 | while (matcher.find()) {
87 | String firstArgument = matcher.group(1).trim();
88 |
89 | // Replace the call to DexClassLoader with System.out.println(firstArgument)
90 | String replacement = "System.out.println(" + firstArgument + "); // BadUnboxing: Replacing DexClassLoader call with directory print";
91 |
92 | matcher.appendReplacement(modifiedCode, replacement);
93 |
94 | logger.info("Replacing call to DexClassLoader with printing target directory to console");
95 | }
96 | matcher.appendTail(modifiedCode);
97 |
98 | return modifiedCode.toString();
99 | }
100 |
101 | public static void modifyAssetManager() {
102 | // Change asset manager to instead input a file from a folder with the dumped assets
103 | }
104 |
105 | /*
106 | * Modifying methods from android.util.ArrayMap
107 | */
108 | public static String processArrayMapMethods(String classCode, StringBuilder imports) {
109 | classCode = insertImport("java.util.HashMap", classCode);
110 |
111 | // Pattern to find lines containing ArrayMap
112 | Pattern arrayMapPattern = Pattern.compile("^(?!import).*\\bArrayMap\\b.*", Pattern.MULTILINE);
113 |
114 | Matcher matcher = arrayMapPattern.matcher(classCode);
115 | StringBuffer modifiedCode = new StringBuffer();
116 |
117 | // Replace all occurrences of ArrayMap with HashMap and add a comment at the end of the line
118 | while (matcher.find()) {
119 | String line = matcher.group();
120 | String modifiedLine = line.replaceAll("\\bArrayMap\\b", "HashMap") + " // BadUnboxing: Replacing ArrayMap with HashMap";
121 |
122 | logger.info("Replacing call to ArrayMap references with HashMap");
123 |
124 | matcher.appendReplacement(modifiedCode, modifiedLine);
125 | }
126 | matcher.appendTail(modifiedCode);
127 |
128 | return modifiedCode.toString();
129 | }
130 |
131 | /*
132 | * Modifying methods from android.content.pm.ApplicationInfo
133 | */
134 | public static String processApplicationInfoMethods(String classCode, String apkPath) {
135 | classCode = modifySourceDir(classCode, apkPath);
136 | //classCode = modifyNativeLibraryDir(classCode); //TODO
137 |
138 | return classCode;
139 | }
140 |
141 | public static String modifySourceDir(String classCode, String apkPath) {
142 | // Pattern to find lines containing Build.VERSION.SDK_INT
143 | Pattern sdkIntPattern = Pattern.compile(".*\\.sourceDir.*", Pattern.MULTILINE);
144 |
145 | Matcher matcher = sdkIntPattern.matcher(classCode);
146 | StringBuffer modifiedCode = new StringBuffer();
147 |
148 | // Replace all occurrences with the hardcoded value 30 and add a comment at the end of the line
149 | while (matcher.find()) {
150 | String line = matcher.group();
151 | String replacement = "\"" + apkPath.replace("\\", "\\\\\\\\\\\\\\\\") + "\"";
152 | String modifiedLine = line.replaceAll("(sourceDir)|(([a-zA-Z]+\\.)sourceDir|[a-zA-Z]+\\(\\)\\.sourceDir)", replacement);
153 | // Add the comment at the end of the line
154 | modifiedLine += " // BadUnboxing: Replacing sourceDir with path to APK";
155 |
156 | matcher.appendReplacement(modifiedCode, modifiedLine);
157 | logger.info("Replacing call to sourceDir with path to APK");
158 | }
159 | matcher.appendTail(modifiedCode);
160 |
161 | return modifiedCode.toString();
162 | }
163 |
164 | /*
165 | * Modifying methods from android.os.Build
166 | */
167 | public static String processBuildMethods(String classCode) {
168 | classCode = modifyBuildSdkInt(classCode);
169 |
170 | return classCode;
171 | }
172 |
173 | public static String modifyBuildSdkInt(String classCode) {
174 | // Pattern to find lines containing Build.VERSION.SDK_INT
175 | Pattern sdkIntPattern = Pattern.compile(".*SDK_INT.*", Pattern.MULTILINE);
176 |
177 | Matcher matcher = sdkIntPattern.matcher(classCode);
178 | StringBuffer modifiedCode = new StringBuffer();
179 |
180 | // Replace all occurrences with the hardcoded value 30 and add a comment at the end of the line
181 | while (matcher.find()) {
182 | String line = matcher.group();
183 | // Replace SDK_INT with 30
184 | String modifiedLine = line.replaceAll("(SDK_INT)|(([a-zA-Z]+\\.)+SDK_INT)", "30");
185 | // Add the comment at the end of the line
186 | modifiedLine += " // BadUnboxing: Hardcode build SDK_INT";
187 |
188 | matcher.appendReplacement(modifiedCode, modifiedLine);
189 | logger.info("Replacing call to SDK_INT with constant value 30 in line");
190 | }
191 | matcher.appendTail(modifiedCode);
192 |
193 | return modifiedCode.toString();
194 | }
195 |
196 | /*
197 | * Modifying methods from android.app.Application
198 | */
199 | public static void processApplicationMethods(StringBuilder javaCode) {
200 | insertDummyApplicationClass(javaCode);
201 | }
202 |
203 | public static void insertDummyApplicationClass(StringBuilder javaCode) {
204 | // Find the end of the import section
205 | Matcher importMatcher = Pattern.compile("(?m)^import\\s+.*?;").matcher(javaCode);
206 | int lastImportIndex = 0;
207 | while (importMatcher.find()) {
208 | lastImportIndex = importMatcher.end();
209 | }
210 |
211 | // Find the index to insert the dummy class after the last import
212 | int insertIndex = javaCode.indexOf("\n", lastImportIndex) + 1;
213 | javaCode.insert(insertIndex, "\n" + DUMMY_APPLICATION_CLASS + "\n");
214 |
215 | logger.info("Inserted dummy Application class");
216 | }
217 |
218 | /*
219 | * Modifying methods from android.content.Context
220 | */
221 |
222 | public static void processContextMethods(StringBuilder javaCode, String className, String packageName) {
223 | insertDummyContextClass(javaCode);
224 | modifyGetDirMethod(javaCode, className);
225 | modifyGetPackageName(javaCode, packageName);
226 | modifyGetFileStreamPath(javaCode, className);
227 | }
228 |
229 | public static void insertDummyContextClass(StringBuilder javaCode) {
230 | // Find the end of the import section
231 | Matcher importMatcher = Pattern.compile("(?m)^import\\s+.*?;").matcher(javaCode);
232 | int lastImportIndex = 0;
233 | while (importMatcher.find()) {
234 | lastImportIndex = importMatcher.end();
235 | }
236 |
237 | // Find the index to insert the dummy class after the last import
238 | int insertIndex = javaCode.indexOf("\n", lastImportIndex) + 1;
239 | javaCode.insert(insertIndex, "\n" + DUMMY_CONTEXT_CLASS + "\n");
240 |
241 | logger.info("Inserted dummy Context class");
242 | }
243 |
244 | // TODO we might be able to combine this method and the getDir method. Lots of repeated code except regex
245 | private static void modifyGetFileStreamPath(StringBuilder javaCode, String className) {
246 | // Pattern to find getFileStreamPath method calls in both variable assignment and return statement contexts
247 | Pattern getFileStreamPathPattern = Pattern.compile("(.*)getFileStreamPath\\((.*)\\)");
248 |
249 | Matcher matcher = getFileStreamPathPattern.matcher(javaCode);
250 | StringBuffer modifiedCode = new StringBuffer();
251 |
252 | while (matcher.find()) {
253 | String prefix = matcher.group(1); // This captures 'var = ' or 'return ', if present
254 | String fileName = matcher.group(2).trim(); // The file name argument to getFileStreamPath
255 |
256 | // Construct the replacement code using standard Java File operations
257 | String replacement = "new File(System.getProperty(\"user.dir\") + \"/" + className + "_dynamic\", " + fileName + ")";
258 | if (prefix != null && prefix.contains("=")) {
259 | // Case for variable assignment
260 | String varName = prefix.split("\\s*=\\s*")[0].trim();
261 |
262 | replacement = varName + " = " + replacement;
263 |
264 | // Make sure we don't include type if it was included
265 | if (varName.split("\\s").length > 1) {
266 | varName = varName.split("\\s")[1].trim();
267 | }
268 |
269 | replacement += ";\nif (!" + varName + ".exists()) { " + varName + ".mkdirs(); }";
270 | } else {
271 | // Case for return statement
272 | String newFileReplacement = replacement;
273 | String varName = "var_tmp_" + IdentifierRenamer.generateRandomPostfix();
274 | replacement = "File " + varName + " = " + newFileReplacement + ";\n";
275 | replacement += "if (!" + varName + ".exists()) { " + varName + ".mkdirs(); }";
276 | replacement += "\nreturn " + varName + ";";
277 | }
278 |
279 | replacement += " // BadUnboxing: Redirect to dynamic directory";
280 |
281 | // Replace the getFileStreamPath call in the original line
282 | matcher.appendReplacement(modifiedCode, replacement);
283 | logger.info("Replacing call to getFileStreamPath with dynamic directory path");
284 | }
285 | matcher.appendTail(modifiedCode);
286 |
287 | // Replace the original code with the modified code
288 | javaCode.setLength(0);
289 | javaCode.append(modifiedCode);
290 | }
291 |
292 | private static void modifyGetPackageName(StringBuilder javaCode, String packageName) {
293 | // Pattern to find lines containing getPackageName() calls
294 | Pattern getPackageNamePattern = Pattern.compile(".*getPackageName\\(\\)(\\s*;)");
295 | Matcher lineMatcher = getPackageNamePattern.matcher(javaCode);
296 | StringBuffer modifiedCode = new StringBuffer();
297 |
298 | while (lineMatcher.find()) {
299 | String line = lineMatcher.group();
300 | // Replace getPackageName() or any prefix with it using the new regex
301 | String modifiedLine = line.replaceAll("getPackageName\\(\\)|[\\w+\\.]+getPackageName\\(\\)", "\"" + packageName + "\"");
302 | modifiedLine += " // BadUnboxing: Hardcode package name";
303 | lineMatcher.appendReplacement(modifiedCode, modifiedLine);
304 | logger.info("Replacing call to getPackageName with string literal '{}'", packageName);
305 | }
306 | lineMatcher.appendTail(modifiedCode);
307 |
308 | // Replace the original code with the modified code
309 | javaCode.setLength(0);
310 | javaCode.append(modifiedCode);
311 | }
312 |
313 | private static void modifyGetDirMethod(StringBuilder javaCode, String className) {
314 | // Pattern to find getDir method calls in both variable assignment and return statement contexts
315 | Pattern getDirPattern = Pattern.compile("(.*)getDir\\(([^,]+),\\s*\\d+\\s*\\)");
316 |
317 | Matcher matcher = getDirPattern.matcher(javaCode);
318 | StringBuffer modifiedCode = new StringBuffer();
319 |
320 | while (matcher.find()) {
321 | String prefix = matcher.group(1); // This captures 'var = ' or 'return ', if present
322 | String dirName = matcher.group(2).trim(); // The directory name argument to getDir
323 |
324 | // Construct the replacement code using standard Java File operations
325 | String replacement = "new File(System.getProperty(\"user.dir\") + \"/" + className + "_dynamic\", " + dirName + ")";
326 | if (prefix != null && prefix.contains("=")) {
327 | // Case for variable assignment
328 | String varName = prefix.split("\\s*=\\s*")[0].trim();
329 | replacement = varName + " = " + replacement;
330 |
331 | // Make sure we don't include type if it was included
332 | if (varName.split("\\s").length > 1) {
333 | varName = varName.split("\\s")[1].trim();
334 | }
335 |
336 | replacement += ";\nif (!" + varName + ".exists()) { " + varName + ".mkdirs(); }";
337 | } else {
338 | // Case for return statement
339 | String newFileReplacement = replacement;
340 | String varName = "var_tmp_" + IdentifierRenamer.generateRandomPostfix();
341 | replacement = "File " + varName + " = " + newFileReplacement + ";\n";
342 | replacement += "if (!" + varName + ".exists()) { " + varName + ".mkdirs(); }";
343 | replacement += "\nreturn " + varName + ";";
344 | }
345 |
346 | replacement += " // BadUnboxing: Change to current directory";
347 |
348 | // Replace the getDir call in the original line
349 | matcher.appendReplacement(modifiedCode, replacement);
350 | logger.info("Replacing call to getDir with path to dynamic directory based on context");
351 | }
352 | matcher.appendTail(modifiedCode);
353 |
354 | // Replace the original code with the modified code
355 | javaCode.setLength(0);
356 | javaCode.append(modifiedCode);
357 | }
358 | }
--------------------------------------------------------------------------------
/BadUnboxing/src/main/java/com/lauriewired/analyzer/UnpackerGenerator.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired.analyzer;
2 |
3 | import java.io.File;
4 | import java.io.FileWriter;
5 | import java.io.IOException;
6 | import java.util.ArrayList;
7 | import java.util.Arrays;
8 | import java.util.HashSet;
9 | import java.util.Set;
10 | import java.util.regex.Matcher;
11 | import java.util.regex.Pattern;
12 |
13 | import org.slf4j.Logger;
14 | import org.slf4j.LoggerFactory;
15 |
16 | import jadx.api.JadxDecompiler;
17 | import jadx.api.JavaClass;
18 |
19 | public class UnpackerGenerator {
20 | private static final Logger logger = LoggerFactory.getLogger(UnpackerGenerator.class);
21 | private static String apkPath;
22 | private static File baseDir;
23 | private static int importsRecognized;
24 |
25 | private static final Set existingNames = new HashSet<>();
26 |
27 |
28 | private static final Set standardPackages = new HashSet<>(Arrays.asList(
29 | "android",
30 | "com.android",
31 | "dalvik",
32 | "java",
33 | "javax",
34 | "junit",
35 | "org.apache",
36 | "org.json",
37 | "org.w3c.dom",
38 | "org.xml.sax"
39 | ));
40 |
41 | private static final Set androidOnlyImports = new HashSet<>(Arrays.asList(
42 | "android",
43 | "com.android",
44 | "dalvik",
45 | "com.xiaomi"
46 | ));
47 |
48 | public static ApkAnalysisDetails generateJava(JadxDecompiler jadx, String apkFilePath) {
49 | // Calculating how well BadUnboxing processed this sample
50 | int recognizedImports = 0;
51 | String fullQualifiedClassName = "";
52 |
53 | // Packing stubs abuse Application subclass for unpacking code
54 | // Make that our starting point since code runs first upon instantiation
55 | JavaClass applicationClass = JadxUtils.findApplicationSubclass(jadx);
56 |
57 | if (applicationClass != null) {
58 | try {
59 | Set referencedClasses = new HashSet<>();
60 | findReferencedClasses(applicationClass, referencedClasses, jadx); // Need all classes referenced by code
61 |
62 | // Rename the methods and fields first since we'll have to reload the code before renaming args and vars
63 | for (JavaClass currentClass : referencedClasses) {
64 | IdentifierRenamer.renameMethodsAndFields(currentClass, jadx, existingNames);
65 | }
66 |
67 | fullQualifiedClassName = generateUnpackerJava(applicationClass, referencedClasses, apkFilePath, jadx);
68 | } catch (Exception e) {
69 | logger.error("Error generating Unpacker.java", e);
70 | }
71 | } else {
72 | logger.info("No Application subclass found in the APK");
73 | }
74 |
75 | return (new ApkAnalysisDetails(baseDir, fullQualifiedClassName, recognizedImports));
76 | }
77 |
78 | private static String generateUnpackerJava(JavaClass applicationClass, Set referencedClasses, String apkFilePath, JadxDecompiler jadx) {
79 | StringBuilder javaCode = new StringBuilder();
80 |
81 | // Get the APK file
82 | File apkFile = new File(apkFilePath);
83 | apkPath = apkFilePath;
84 |
85 | // Determine the class name based on the APK file name
86 | String apkName = apkFile.getName();
87 | int dotIndex = apkName.lastIndexOf('.');
88 | String baseName = (dotIndex == -1) ? apkName : apkName.substring(0, dotIndex);
89 | String className = "Unpacker_" + (baseName.length() > 10 ? baseName.substring(0, 10) : baseName);
90 | baseDir = new File(apkFile.getParent(), className + "_BadUnboxing");
91 |
92 | // Extract the package name
93 | String fullyQualifiedName = applicationClass.getFullName();
94 | String packageName = fullyQualifiedName.substring(0, fullyQualifiedName.lastIndexOf('.'));
95 |
96 | // Initialize an ArrayList to store referenced class names
97 | ArrayList referencedClassNames = new ArrayList<>();
98 |
99 | // Process the main application subclass (entrypoint)
100 | String appClassCode = processApplicationSubclass(applicationClass, className, jadx);
101 | javaCode.append(appClassCode).append("\n");
102 |
103 | // Process referenced classes
104 | for (JavaClass refClass : referencedClasses) {
105 | if (refClass != applicationClass) {
106 | insertNewClass(javaCode, refClass, jadx);
107 | referencedClassNames.add(refClass.getName());
108 | }
109 | }
110 |
111 | // Do final cleanups on full code that is now completely added
112 | finalProcessing(javaCode, className, packageName);
113 |
114 | try {
115 | outputClassesToFiles(apkFile, className, javaCode, referencedClassNames);
116 | } catch (IOException e) {
117 | logger.error("Error writing unpacker code to files");
118 | e.printStackTrace();
119 | }
120 |
121 | return packageName + "." + className;
122 | }
123 |
124 | private static void outputClassesToFiles(File apkFile, String className, StringBuilder javaCode, ArrayList referencedClassNames) throws IOException {
125 | // Get the directory of the APK file and create a base directory that includes 'src'
126 | File outputDir = new File(baseDir, "src");
127 |
128 | // Create the base directory if it does not exist, including all necessary parent directories
129 | if (!outputDir.exists()) {
130 | outputDir.mkdirs();
131 | }
132 |
133 | // Create settings.json in the base directory for the new unpacker project
134 | createSettingsJson(baseDir);
135 |
136 | // Regex pattern to capture content based on "package" declaration
137 | Pattern packagePattern = Pattern.compile("(package\\s+([\\w\\.]+);).*?(?=package\\s+[\\w\\.]+;|$)", Pattern.DOTALL);
138 | Matcher packageMatcher = packagePattern.matcher(javaCode.toString());
139 |
140 | while (packageMatcher.find()) {
141 | String packageBlock = packageMatcher.group(0);
142 | String fullPackageName = packageMatcher.group(2);
143 | String packagePath = fullPackageName.replace('.', File.separatorChar); // Convert package name to directory path
144 |
145 | // Create the full path for the package inside the 'src' directory
146 | File packageDir = new File(outputDir, packagePath);
147 | if (!packageDir.exists()) {
148 | packageDir.mkdirs(); // Ensure the package directory structure is created
149 | }
150 |
151 | // Extract the class name from the current block of code
152 | Pattern classPattern = Pattern.compile("\\s+class\\s+(?!Application\\b|Context\\b)(\\w+)\\s+\\{"); // Simple class name extraction pattern
153 | Matcher classMatcher = classPattern.matcher(packageBlock);
154 | String fileName = "UnknownClass.java"; // Default file name in case no class name is found
155 | if (classMatcher.find()) {
156 | fileName = classMatcher.group(1) + ".java"; // Use the found class name for the file name
157 | }
158 |
159 | // Write to a new Java file for each package block using the class name
160 | File javaFile = new File(packageDir, fileName);
161 | try (FileWriter writer = new FileWriter(javaFile)) {
162 | writer.write(packageBlock);
163 | logger.info("Generated {} at {}", fileName, javaFile.getAbsolutePath());
164 | }
165 | }
166 | }
167 |
168 | private static void createSettingsJson(File baseDir) throws IOException {
169 | File settingsFile = new File(baseDir, ".vscode/settings.json");
170 |
171 | // Ensure the .vscode directory exists
172 | if (!settingsFile.getParentFile().exists()) {
173 | settingsFile.getParentFile().mkdirs();
174 | }
175 |
176 | // Content of the settings.json file
177 | String settingsContent = "{\n" +
178 | " \"java.project.sourcePaths\": [\"src\"],\n" +
179 | " \"java.project.outputPath\": \"bin\",\n" +
180 | " \"java.project.referencedLibraries\": [\n" +
181 | " \"lib/**/*.jar\"\n" +
182 | " ]\n" +
183 | "}";
184 |
185 | // Write the settings content to the settings.json file
186 | try (FileWriter writer = new FileWriter(settingsFile)) {
187 | writer.write(settingsContent);
188 | logger.info("Created settings.json at {}", settingsFile.getAbsolutePath());
189 | }
190 | }
191 |
192 | private static void finalProcessing(StringBuilder javaCode, String className, String packageName) {
193 | commentPackageName(javaCode);
194 | makeMethodsStatic(javaCode);
195 | makeFieldsStatic(javaCode);
196 | removeKeywordThis(javaCode);
197 | CodeReplacerUtils.processContextMethods(javaCode, className, packageName);
198 | CodeReplacerUtils.processApplicationMethods(javaCode);
199 | commentAndroidSpecificImports(javaCode);
200 | ReflectionRemover.removeReflection(javaCode);
201 | }
202 |
203 | private static void commentPackageName(StringBuilder javaCode) {
204 | // Pattern to match the package name declaration
205 | Pattern packagePattern = Pattern.compile("(?m)^package\\s+.*?;");
206 | Matcher matcher = packagePattern.matcher(javaCode);
207 |
208 | // Buffer to hold the modified code
209 | StringBuffer modifiedCode = new StringBuffer();
210 |
211 | // Comment out the package name declaration
212 | if (matcher.find()) {
213 | String packageStatement = matcher.group();
214 | String commentedPackageStatement = "// " + packageStatement;
215 | matcher.appendReplacement(modifiedCode, commentedPackageStatement);
216 | }
217 | matcher.appendTail(modifiedCode);
218 |
219 | // Replace the original code with the modified code
220 | javaCode.setLength(0);
221 | javaCode.append(modifiedCode);
222 | }
223 |
224 | private static void commentAndroidSpecificImports(StringBuilder javaCode) {
225 | // Pattern to match import statements
226 | Pattern importPattern = Pattern.compile("(?m)^import\\s+.*?;");
227 | Matcher importMatcher = importPattern.matcher(javaCode);
228 |
229 | // Buffer to hold the modified code
230 | StringBuffer modifiedCode = new StringBuffer();
231 |
232 | while (importMatcher.find()) {
233 | String importStatement = importMatcher.group();
234 | // Extract the fully qualified class name from the import statement
235 | String className = importStatement.replaceFirst("import\\s+", "").replaceFirst(";", "").trim();
236 |
237 | // Check if the class name starts with any of the specified prefixes
238 | boolean isAndroidImport = false;
239 | for (String prefix : androidOnlyImports) {
240 | if (className.startsWith(prefix)) {
241 | isAndroidImport = true;
242 | break;
243 | }
244 | }
245 |
246 | // If it is an Android-specific import, comment it out
247 | if (isAndroidImport) {
248 | importStatement = "// " + importStatement;
249 | }
250 |
251 | // Append the modified or unmodified import statement to the buffer
252 | importMatcher.appendReplacement(modifiedCode, importStatement);
253 | }
254 | importMatcher.appendTail(modifiedCode);
255 |
256 | // Replace the original code with the modified code
257 | javaCode.setLength(0);
258 | javaCode.append(modifiedCode);
259 | }
260 |
261 | private static String processApplicationSubclass(JavaClass applicationClass, String className, JadxDecompiler jadx) {
262 | String appClassCode = IdentifierRenamer.renameArgsAndVars(applicationClass, jadx, existingNames).toString();
263 | appClassCode = appClassCode.replace(applicationClass.getName(), className);
264 | appClassCode = appClassCode.replaceAll("extends Application", "");
265 | appClassCode = appClassCode.replaceAll("@Override // android.app.Application", "");
266 | appClassCode = appClassCode.replaceAll("@Override // android.content.ContextWrapper", "");
267 | appClassCode = appClassCode.replaceAll("super\\.attachBaseContext\\(\\w+\\);", "//super.attachBaseContext(context); // BadUnboxing: Remove superclass reference");
268 | appClassCode = appClassCode.replaceAll("super\\.onCreate\\(\\);", "//super.onCreate(); // BadUnboxing: Remove superclass reference");
269 | appClassCode = replaceAttachBaseContextWithMain(appClassCode);
270 |
271 | // Extract imports from the application subclass
272 | StringBuilder imports = new StringBuilder();
273 | Matcher importMatcher = Pattern.compile("(?m)^import\\s+.*?;").matcher(appClassCode);
274 | while (importMatcher.find()) {
275 | imports.append(importMatcher.group()).append("\n");
276 | }
277 |
278 | // Fix keywords for imports inside application subclass
279 | appClassCode = processKeyWordsBasedOnImports(imports, appClassCode);
280 |
281 | return appClassCode;
282 | }
283 |
284 | private static String replaceAttachBaseContextWithMain(String appClassCode) {
285 | // Pattern to find the attachBaseContext method signature and extract the parameter name
286 | Pattern pattern = Pattern.compile("(public|protected|private|\\s*)\\s*void\\s*attachBaseContext\\(Context\\s+(\\w+)\\)");
287 | Matcher matcher = pattern.matcher(appClassCode);
288 |
289 | // Variable to store the Context parameter name
290 | String contextParamName = null;
291 |
292 | // Find and replace the method signature
293 | StringBuffer sb = new StringBuffer();
294 | while (matcher.find()) {
295 | contextParamName = matcher.group(2); // Capture the parameter name
296 | matcher.appendReplacement(sb, "public static void main(String[] args)");
297 | }
298 | matcher.appendTail(sb);
299 | appClassCode = sb.toString();
300 |
301 | // If a parameter name was found, replace all its occurrences in the method body
302 | if (contextParamName != null) {
303 | // Assuming the body of the method is properly isolated if needed for multiple methods
304 | // Replace all occurrences of the Context parameter with "new Context()" (assuming this makes sense in your context)
305 | appClassCode = appClassCode.replaceAll("\\b" + Pattern.quote(contextParamName) + "\\b", "new Context()");
306 | }
307 |
308 | return appClassCode;
309 | }
310 |
311 | private static void makeFieldsStatic(StringBuilder javaCode) {
312 | // Pattern to match field declarations that start with "field_"
313 | // This pattern looks for optional access modifiers, optional "final" keyword,
314 | // any valid Java type (primitive or object), optional array brackets, and field names starting with "field_"
315 | // It supports fields with or without initial access modifiers.
316 | Pattern fieldPattern = Pattern.compile(
317 | "(?m)^\\s*(public\\s+|protected\\s+|private\\s+)?(static\\s+)?(final\\s+)?([\\w\\[\\]\\<\\>]+\\s+)(field_\\w+\\s*)(=\\s*[^;]+)?;");
318 |
319 | Matcher matcher = fieldPattern.matcher(javaCode);
320 | StringBuffer modifiedCode = new StringBuffer();
321 |
322 | while (matcher.find()) {
323 | String accessModifier = matcher.group(1) == null ? "" : matcher.group(1); // Capture access modifiers
324 | String isStatic = matcher.group(2); // Capture the 'static' keyword if it exists
325 | String finalModifier = matcher.group(3) == null ? "" : matcher.group(3); // Capture the 'final' keyword if it exists
326 | String type = matcher.group(4); // Capture the type
327 | String fieldName = matcher.group(5); // Capture the field name
328 | String initializer = matcher.group(6) == null ? "" : matcher.group(6); // Capture the initializer if present
329 |
330 | // Construct the replacement string
331 | String replacement;
332 | if (isStatic == null) {
333 | // If 'static' keyword is missing, add it
334 | replacement = accessModifier + "static " + finalModifier + type + fieldName + initializer + ";";
335 | } else {
336 | // If 'static' keyword is already there, keep the declaration as it is
337 | replacement = matcher.group(0);
338 | }
339 |
340 | // Append the replacement to the buffer
341 | matcher.appendReplacement(modifiedCode, replacement);
342 | }
343 | matcher.appendTail(modifiedCode);
344 |
345 | // Replace the original code with the modified code
346 | javaCode.setLength(0);
347 | javaCode.append(modifiedCode);
348 | }
349 |
350 | private static void makeMethodsStatic(StringBuilder javaCode) {
351 | // Pattern to match lines starting with whitespace followed by public, protected, or private, followed by a single whitespace, and not followed by "static"
352 | Pattern methodPattern = Pattern.compile("(?m)^\\s*(public|protected|private)\\s+(?!static|class)");
353 |
354 | Matcher matcher = methodPattern.matcher(javaCode);
355 |
356 | // Buffer to hold the modified code
357 | StringBuffer modifiedCode = new StringBuffer();
358 |
359 | while (matcher.find()) {
360 | String methodSignature = matcher.group();
361 | // Modify the line to include "static" after the keyword
362 | String modifiedMethod = methodSignature.replaceFirst("(public|protected|private)", "$1 static");
363 | matcher.appendReplacement(modifiedCode, modifiedMethod);
364 | }
365 | matcher.appendTail(modifiedCode);
366 |
367 | // Replace the original code with the modified code
368 | javaCode.setLength(0);
369 | javaCode.append(modifiedCode);
370 | }
371 |
372 | private static void removeKeywordThis(StringBuilder javaCode) {
373 | // Pattern to match all occurrences of "this."
374 | Pattern thisPattern = Pattern.compile("\\bthis\\.");
375 | Matcher matcher = thisPattern.matcher(javaCode);
376 | StringBuffer modifiedCode = new StringBuffer();
377 |
378 | // Replace all occurrences with an empty string
379 | while (matcher.find()) {
380 | matcher.appendReplacement(modifiedCode, "");
381 | }
382 | matcher.appendTail(modifiedCode);
383 |
384 | // Replace the original code with the modified code
385 | javaCode.setLength(0);
386 | javaCode.append(modifiedCode);
387 | }
388 |
389 | private static void insertNewClass(StringBuilder javaCode, JavaClass newCodeClass, JadxDecompiler jadx) {
390 | String newClassCode = IdentifierRenamer.renameArgsAndVars(newCodeClass, jadx, existingNames).toString();
391 |
392 | // Remove the package line and store it
393 | /*
394 | String packageLine = "";
395 | Matcher packageMatcher = Pattern.compile("(?m)^package\\s+.*?;").matcher(newClassCode);
396 | if (packageMatcher.find()) {
397 | packageLine = packageMatcher.group();
398 | newClassCode = newClassCode.replaceFirst("(?m)^package\\s+.*?;", "");
399 | }*/
400 | //TODO keep the package name so we can find it when separating classes
401 |
402 | // Process class imports and update the newClassCode by removing imports
403 | StringBuilder imports = processClassImports(javaCode, newClassCode);
404 |
405 | //TODO removing import moving due to placing files in separate java files
406 | /*
407 | newClassCode = newClassCode.replaceAll("(?m)^import\\s+.*?;", ""); // Remove imports from newClassCode
408 |
409 | // Insert new imports below the package line
410 | int packageLineEnd = javaCode.indexOf(";");
411 | if (packageLineEnd != -1) {
412 | javaCode.insert(packageLineEnd + 1, "\n\n" + imports.toString());
413 | } else {
414 | javaCode.insert(0, packageLine + "\n\n" + imports.toString() + "\n");
415 | }*/
416 |
417 | newClassCode = processKeyWordsBasedOnImports(imports, newClassCode);
418 |
419 | // Append the new class code without the package line and imports
420 | javaCode.append(newClassCode).append("\n");
421 | }
422 |
423 | /*
424 | * For performance, only search for methods and replace them if their parent class
425 | * has been imported
426 | */
427 | private static String processKeyWordsBasedOnImports(StringBuilder imports, String newClassCode) {
428 | // Extract each import statement
429 | Pattern importPattern = Pattern.compile("import\\s+([\\w\\.]+);");
430 | Matcher matcher = importPattern.matcher(imports);
431 |
432 | // For each import statement, check if it is supported and call the corresponding method
433 | while (matcher.find()) {
434 | String importClass = matcher.group(1);
435 | String prefix = importClass.split("\\.")[0];
436 |
437 | if (androidOnlyImports.contains(prefix)) {
438 | switch (importClass) {
439 | case "dalvik.system.DexClassLoader":
440 | logger.info("Processing methods from dalvik.system.DexClassLoader import");
441 | newClassCode = CodeReplacerUtils.processDexClassLoaderMethods(newClassCode);
442 | break;
443 | case "android.os.Build":
444 | logger.info("Processing methods from android.os.Build import");
445 | newClassCode = CodeReplacerUtils.processBuildMethods(newClassCode);
446 | break;
447 | case "android.content.pm.ApplicationInfo":
448 | logger.info("Processing methods from android.content.pm.ApplicationInfo import");
449 | newClassCode = CodeReplacerUtils.processApplicationInfoMethods(newClassCode, apkPath);
450 | break;
451 | case "android.util.ArrayMap":
452 | logger.info("Processing methods from import android.util.ArrayMap import");
453 | newClassCode = CodeReplacerUtils.processArrayMapMethods(newClassCode, imports);
454 | break;
455 | case "android.app.Application":
456 | case "android.content.Context":
457 | // Ignore these because we will process them on all code no matter what
458 | // Adding them as part of the switch anyway because they are technically supported
459 | break;
460 | default:
461 | logger.error("Unknown android import: " + importClass);
462 | }
463 | }
464 | }
465 |
466 | return newClassCode;
467 | }
468 |
469 | private static StringBuilder processClassImports(StringBuilder javaCode, String newClassCode) {
470 | // Extract imports from existing code
471 | Set existingImports = new HashSet<>();
472 | Matcher importMatcher = Pattern.compile("(?m)^import\\s+.*?;").matcher(javaCode);
473 | while (importMatcher.find()) {
474 | existingImports.add(importMatcher.group());
475 | }
476 |
477 | StringBuilder imports = new StringBuilder();
478 | importMatcher = Pattern.compile("(?m)^import\\s+.*?;").matcher(newClassCode);
479 | while (importMatcher.find()) {
480 | String importStatement = importMatcher.group();
481 | if (!existingImports.contains(importStatement)) {
482 | imports.append(importStatement).append("\n");
483 | existingImports.add(importStatement);
484 | }
485 | }
486 |
487 | return imports;
488 | }
489 |
490 | private static void findReferencedClasses(JavaClass javaClass, Set referencedClasses, JadxDecompiler jadx) {
491 | // Add the current class to the referenced set if not already present
492 | if (!referencedClasses.add(javaClass)) {
493 | // Class already processed
494 | return;
495 | }
496 |
497 | String packageName = javaClass.getPackage();
498 | String classCode = javaClass.getCode();
499 |
500 | // Iterate through all classes in the decompiler
501 | for (JavaClass currentClass : jadx.getClasses()) {
502 | if (packageName.equals(currentClass.getPackage()) && !javaClass.equals(currentClass) && !currentClass.getName().equals("R")) {
503 | if (classCode.contains(currentClass.getName())) {
504 | referencedClasses.add(currentClass);
505 | logger.info("Adding class {} to referenced classes", currentClass.getName());
506 | findReferencedClasses(currentClass, referencedClasses, jadx);
507 | }
508 | }
509 | }
510 | }
511 |
512 | private static boolean isCustomClass(String typeName) {
513 | for (String standardPackage : standardPackages) {
514 | if (typeName.startsWith(standardPackage)) {
515 | return false;
516 | }
517 | }
518 | return true;
519 | }
520 | }
--------------------------------------------------------------------------------
/BadUnboxing/src/main/java/com/lauriewired/ui/AnalysisWindow.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired.ui;
2 |
3 | import java.awt.BorderLayout;
4 | import java.awt.Color;
5 | import java.awt.Dimension;
6 | import java.awt.Font;
7 | import java.awt.Insets;
8 | import java.awt.event.KeyAdapter;
9 | import java.awt.event.KeyEvent;
10 | import java.io.BufferedReader;
11 | import java.io.File;
12 | import java.io.IOException;
13 | import java.io.InputStreamReader;
14 | import java.io.PrintStream;
15 | import java.nio.charset.StandardCharsets;
16 | import java.nio.file.Files;
17 | import java.nio.file.Path;
18 | import java.nio.file.Paths;
19 | import java.util.ArrayList;
20 | import java.util.Arrays;
21 | import java.util.Enumeration;
22 | import java.util.List;
23 | import java.util.Set;
24 | import java.util.stream.Collectors;
25 |
26 | import javax.swing.BoxLayout;
27 | import javax.swing.JButton;
28 | import javax.swing.JFrame;
29 | import javax.swing.JMenu;
30 | import javax.swing.JMenuBar;
31 | import javax.swing.JMenuItem;
32 | import javax.swing.JOptionPane;
33 | import javax.swing.JPanel;
34 | import javax.swing.JScrollPane;
35 | import javax.swing.JSplitPane;
36 | import javax.swing.JTextArea;
37 | import javax.swing.JTree;
38 | import javax.swing.SwingUtilities;
39 | import javax.swing.SwingWorker;
40 | import javax.swing.event.TreeSelectionEvent;
41 | import javax.swing.event.TreeSelectionListener;
42 | import javax.swing.tree.DefaultMutableTreeNode;
43 | import javax.swing.tree.DefaultTreeModel;
44 | import javax.swing.tree.TreePath;
45 |
46 | import com.lauriewired.analyzer.ApkAnalysisDetails;
47 | import com.lauriewired.analyzer.DynamicDexLoaderDetection;
48 | import com.lauriewired.analyzer.JadxUtils;
49 | import com.lauriewired.analyzer.UnpackerGenerator;
50 | import jadx.api.JadxDecompiler;
51 |
52 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
53 | import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
54 | import org.fife.ui.rtextarea.RTextScrollPane;
55 | import org.slf4j.Logger;
56 | import org.slf4j.LoggerFactory;
57 |
58 | public class AnalysisWindow {
59 | private static final Logger logger = LoggerFactory.getLogger(AnalysisWindow.class);
60 | private static JTree directoryTree;
61 | private static JTree apkDetailsTree;
62 | private static DefaultMutableTreeNode rootNode;
63 | private static String currentFilePath;
64 | private static RSyntaxTextArea rightPanelEditorPane;
65 | private static JScrollPane apkDetailsScrollPane;
66 | private static CustomProgressBar progressBar;
67 | private static String pathToUnpacker;
68 | private static ApkAnalysisDetails apkAnalysisDetails;
69 |
70 | public static void show(JFrame frame, String apkFilePath) {
71 | frame.setExtendedState(JFrame.MAXIMIZED_BOTH); // Maximize the window
72 | frame.setVisible(true);
73 |
74 | // Create the root node for the APK details tree
75 | DefaultMutableTreeNode apkDetailsRoot = new DefaultMutableTreeNode("APK Summary");
76 |
77 | // Add nodes for different sections
78 | DefaultMutableTreeNode fileNameNode = new DefaultMutableTreeNode("File Name: " + new File(apkFilePath).getName());
79 | DefaultMutableTreeNode fileSizeNode = new DefaultMutableTreeNode("Size: " + new File(apkFilePath).length() / 1024 + " KB");
80 |
81 | apkDetailsRoot.add(fileNameNode);
82 | apkDetailsRoot.add(fileSizeNode);
83 |
84 | apkDetailsTree = new JTree(apkDetailsRoot);
85 | apkDetailsTree.setCellRenderer(new NoIconTreeCell());
86 | apkDetailsTree.setFont(new Font("Verdana", Font.PLAIN, 18));
87 |
88 | apkDetailsScrollPane = new JScrollPane(apkDetailsTree);
89 |
90 | // Initialize the directory tree with the APK name as the root node
91 | File apkFile = new File(apkFilePath);
92 | rootNode = new DefaultMutableTreeNode(new FileNode(apkFile));
93 | directoryTree = new JTree(rootNode);
94 | directoryTree.setFont(new Font("Verdana", Font.PLAIN, 18));
95 |
96 | JScrollPane treeScrollPane = new JScrollPane(directoryTree);
97 |
98 | // Initialize the progress bar
99 | progressBar = new CustomProgressBar();
100 | progressBar.setIndeterminate(true);
101 | progressBar.setPreferredSize(new Dimension(0, 20));
102 | progressBar.setString("Processing");
103 | progressBar.setStringPainted(true);
104 | progressBar.setTextColor(Color.ORANGE);
105 |
106 | // Create a panel for the progress bar
107 | JPanel progressBarPanel = new JPanel(new BorderLayout());
108 | progressBarPanel.add(progressBar, BorderLayout.CENTER);
109 | progressBarPanel.setPreferredSize(new Dimension(0, 20));
110 |
111 | // Create a split pane to combine the directory tree and APK details tree
112 | JSplitPane directoryAndDetailsSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, treeScrollPane, apkDetailsScrollPane);
113 | directoryAndDetailsSplitPane.setDividerLocation(200);
114 | directoryAndDetailsSplitPane.setResizeWeight(0.5); // Ratio between top and bottom panels
115 |
116 | // Create the main left split pane to include the directory/details split pane and the progress bar
117 | JSplitPane leftSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, directoryAndDetailsSplitPane, progressBarPanel);
118 | leftSplitPane.setResizeWeight(0.97); // Ratio between directory/details and progress bar
119 |
120 | // Make the divider invisible
121 | leftSplitPane.setDividerSize(2);
122 |
123 | // Create a RSyntaxTextArea for versatile content display with syntax highlighting
124 | rightPanelEditorPane = new RSyntaxTextArea();
125 | SyntaxUtility.applyCustomTheme(rightPanelEditorPane);
126 | rightPanelEditorPane.setEditable(true); // Make it editable
127 | rightPanelEditorPane.setFont(new Font("Monospaced", Font.PLAIN, 18));
128 | rightPanelEditorPane.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_NONE); // Default to no syntax highlighting
129 |
130 |
131 | // Set darker background color
132 | rightPanelEditorPane.setBackground(new Color(30, 30, 30));
133 | rightPanelEditorPane.setForeground(new Color(230, 230, 230));
134 |
135 | RTextScrollPane rightPanelScrollPane = new RTextScrollPane(rightPanelEditorPane);
136 |
137 | // Create a panel for the buttons and add them to the top right of the right panel
138 | JPanel buttonPanel = new JPanel();
139 | buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.Y_AXIS));
140 |
141 | // Create a font that supports Unicode characters
142 | Font arrowFont = new Font("Arial", Font.PLAIN, 12);
143 |
144 | // Create buttons with Unicode arrow symbols
145 | JButton upButton = new JButton("\u25B2"); // Unicode for black up-pointing triangle
146 | JButton downButton = new JButton("\u25BC"); // Unicode for black down-pointing triangle
147 |
148 | // Set the font for the buttons
149 | upButton.setFont(arrowFont);
150 | downButton.setFont(arrowFont);
151 |
152 | // Set tooltips for the buttons
153 | upButton.setToolTipText("Go to previous BadUnboxing code mod");
154 | downButton.setToolTipText("Go to next BadUnboxing code mod");
155 |
156 | // Set preferred sizes to keep the buttons small
157 | Dimension buttonSize = new Dimension(45, 40);
158 | upButton.setPreferredSize(buttonSize);
159 | downButton.setPreferredSize(buttonSize);
160 |
161 | // Add the buttons to the panel
162 | buttonPanel.add(upButton);
163 | buttonPanel.add(downButton);
164 |
165 | // Set the maximum size of the button panel to match the width of the buttons
166 | buttonPanel.setMaximumSize(new Dimension(buttonSize.width, Integer.MAX_VALUE));
167 |
168 | // Create a panel to hold the button panel and the right panel editor
169 | JPanel rightPanelContainer = new JPanel(new BorderLayout());
170 | rightPanelContainer.add(buttonPanel, BorderLayout.EAST);
171 | rightPanelContainer.add(rightPanelScrollPane, BorderLayout.CENTER);
172 |
173 | // Create a text area to display the console output
174 | JTextArea textArea = new JTextArea();
175 | textArea.setEditable(false);
176 | textArea.setFont(new Font("Monospaced", Font.PLAIN, 14));
177 |
178 | // Set darker background color
179 | textArea.setBackground(new Color(30, 30, 30));
180 | textArea.setForeground(new Color(230, 230, 230));
181 |
182 | // Redirect the console output to the text area
183 | PrintStream printStream = new PrintStream(new TextAreaOutputStream(textArea));
184 | System.setOut(printStream);
185 | System.setErr(printStream);
186 |
187 | // Create a split pane for the right side
188 | JSplitPane rightSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, rightPanelContainer, new JScrollPane(textArea));
189 | rightSplitPane.setDividerLocation(800);
190 | rightSplitPane.setResizeWeight(0.7); // Ratio between top and bottom panels
191 |
192 | // Create a split pane for the left and right sections
193 | JSplitPane mainSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftSplitPane, rightSplitPane);
194 | mainSplitPane.setDividerLocation(400);
195 | mainSplitPane.setResizeWeight(0.3); // Ratio between left and right panels
196 |
197 | frame.getContentPane().removeAll(); // Remove previous components
198 | frame.setLayout(new BorderLayout());
199 | frame.add(mainSplitPane, BorderLayout.CENTER);
200 |
201 | // Add menu bar
202 | JMenuBar menuBar = new JMenuBar();
203 | JMenu fileMenu = new JMenu("File");
204 |
205 | // Create the "Save" menu item
206 | JMenuItem saveMenuItem = new JMenuItem("Save");
207 | saveMenuItem.addActionListener(e -> saveFile());
208 | saveMenuItem.setMargin(new Insets(5, 10, 5, 10));
209 | fileMenu.add(saveMenuItem);
210 |
211 | JMenu runMenu = new JMenu("Run");
212 | // Create the "Execute" menu item
213 | JMenuItem executeMenuItem = new JMenuItem("Execute");
214 | executeMenuItem.setMargin(new Insets(5, 10, 5, 10));
215 | executeMenuItem.addActionListener(e -> {
216 | int response = JOptionPane.showOptionDialog(
217 | frame,
218 | "Are you sure? This should only be executed in a secure malware analysis environment.",
219 | "Confirm Execution",
220 | JOptionPane.YES_NO_OPTION,
221 | JOptionPane.WARNING_MESSAGE,
222 | null,
223 | new String[]{"Continue", "Cancel"},
224 | "Cancel"
225 | );
226 | if (response == JOptionPane.YES_OPTION) {
227 | executeCode(rootNode, textArea);
228 | }
229 | });
230 | runMenu.add(executeMenuItem);
231 |
232 | menuBar.add(fileMenu);
233 | menuBar.add(runMenu);
234 | frame.setJMenuBar(menuBar);
235 |
236 | frame.revalidate();
237 | frame.repaint();
238 |
239 | // Call the APK analysis method
240 | analyzeApk(apkFilePath, apkDetailsRoot);
241 |
242 | addDirectoryTreeSelectionListener();
243 | addRightPanelEditorPaneKeyListener();
244 | upButton.addActionListener(e -> findPreviousOccurrence("BadUnboxing"));
245 | downButton.addActionListener(e -> findNextOccurrence("BadUnboxing"));
246 | }
247 |
248 | private static void addDirectoryTreeSelectionListener() {
249 | directoryTree.addTreeSelectionListener(new TreeSelectionListener() {
250 | @Override
251 | public void valueChanged(TreeSelectionEvent e) {
252 | DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) directoryTree.getLastSelectedPathComponent();
253 | if (selectedNode == null) return;
254 |
255 | String filePath = getFilePath(selectedNode);
256 | if (filePath != null) {
257 | currentFilePath = filePath;
258 | try {
259 | Path path = Paths.get(filePath);
260 | if (Files.isRegularFile(path)) {
261 | String content = new String(Files.readAllBytes(path));
262 | if (filePath.endsWith(".java")) {
263 | rightPanelEditorPane.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
264 | } else if (filePath.endsWith(".json")) {
265 | rightPanelEditorPane.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JSON);
266 | } else {
267 | rightPanelEditorPane.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_NONE);
268 | }
269 | rightPanelEditorPane.setText(content);
270 | rightPanelEditorPane.setCaretPosition(0);
271 | }
272 | } catch (Exception ex) {
273 | ex.printStackTrace();
274 | rightPanelEditorPane.setText("Error loading file: " + ex.getMessage());
275 | }
276 | }
277 | }
278 | });
279 | }
280 |
281 | private static void addRightPanelEditorPaneKeyListener() {
282 | rightPanelEditorPane.addKeyListener(new KeyAdapter() {
283 | @Override
284 | public void keyPressed(KeyEvent e) {
285 | if (e.isControlDown() && e.getKeyCode() == KeyEvent.VK_S) {
286 | saveFile();
287 | }
288 | }
289 | });
290 | }
291 |
292 | private static void executeCode(DefaultMutableTreeNode rootNode, JTextArea textArea) {
293 | try {
294 | // Directory containing the generated Java files (set after APK analysis)
295 | File compilationDir = new File(pathToUnpacker).getParentFile();
296 | File sourceDir = compilationDir.getParentFile();
297 |
298 | // Get all java files in the directory
299 | File[] javaFiles = compilationDir.listFiles((dir, name) -> name.endsWith(".java"));
300 | if (javaFiles == null || javaFiles.length == 0) {
301 | textArea.append("No Java files found to compile.\n");
302 | return;
303 | }
304 |
305 | // Convert the file paths to a format suitable for the javac command
306 | List filePaths = Arrays.stream(javaFiles)
307 | .map(File::getAbsolutePath)
308 | .collect(Collectors.toList());
309 |
310 | // Prepare command list
311 | List command = new ArrayList<>();
312 | command.add("javac");
313 | command.addAll(filePaths);
314 |
315 | // Execute the javac command
316 | ProcessBuilder compileProcessBuilder = new ProcessBuilder(command);
317 | compileProcessBuilder.directory(compilationDir);
318 | Process compileProcess = compileProcessBuilder.start();
319 |
320 | // Redirect compilation output
321 | redirectProcessOutput(compileProcess, textArea);
322 |
323 | compileProcess.waitFor();
324 |
325 | if (compileProcess.exitValue() != 0) {
326 | textArea.append("Compilation failed.\n");
327 | return;
328 | }
329 |
330 | // Execute the main unpacker class
331 | String mainClass = apkAnalysisDetails.getFullyQualifiedClassName();
332 | ProcessBuilder runProcessBuilder = new ProcessBuilder(
333 | "java", "-cp", sourceDir.getAbsolutePath(), mainClass);
334 | runProcessBuilder.directory(sourceDir);
335 |
336 | // Log the command being executed
337 | String commandString = String.join(" ", runProcessBuilder.command());
338 | logger.info("Running execution command: " + commandString);
339 | textArea.append("Running execution command: " + commandString + "\n");
340 | progressBar.setString("Executing");
341 | progressBar.setTextColor(Color.ORANGE);
342 |
343 | Process runProcess = runProcessBuilder.start();
344 |
345 | // Redirect execution output
346 | redirectProcessOutput(runProcess, textArea);
347 |
348 | runProcess.waitFor();
349 |
350 | if (runProcess.exitValue() != 0) {
351 | logger.error("Execution failed");
352 | progressBar.setString("Error");
353 | progressBar.setTextColor(Color.RED);
354 | } else {
355 | logger.info("Completed execution");
356 | progressBar.setString("Complete");
357 | progressBar.setTextColor(Color.WHITE);
358 | updateDirectoryTree(apkAnalysisDetails.getBaseDir());
359 | ((DefaultTreeModel) directoryTree.getModel()).reload(rootNode);
360 | displayUnpackerFile();
361 | logger.info("File tree updated with dynamic artifacts directory");
362 | }
363 | } catch (Exception e) {
364 | e.printStackTrace();
365 | logger.error("Error executing code: " + e.getMessage());
366 | textArea.append("Error executing code: " + e.getMessage() + "\n");
367 | progressBar.setString("Error");
368 | progressBar.setTextColor(Color.RED);
369 | }
370 | }
371 |
372 | private static void redirectProcessOutput(Process process, JTextArea textArea) {
373 | new Thread(() -> {
374 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
375 | String line;
376 | while ((line = reader.readLine()) != null) {
377 | textArea.append(line + "\n");
378 | }
379 | } catch (IOException e) {
380 | e.printStackTrace();
381 | }
382 | }).start();
383 |
384 | new Thread(() -> {
385 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
386 | String line;
387 | while ((line = reader.readLine()) != null) {
388 | textArea.append(line + "\n");
389 | }
390 | } catch (IOException e) {
391 | e.printStackTrace();
392 | }
393 | }).start();
394 | }
395 |
396 |
397 | private static void findNextOccurrence(String searchString) {
398 | String content = rightPanelEditorPane.getText();
399 | int currentPosition = rightPanelEditorPane.getCaretPosition();
400 | int nextPosition = content.indexOf(searchString, currentPosition);
401 |
402 | if (nextPosition != -1) {
403 | rightPanelEditorPane.setCaretPosition(nextPosition);
404 | rightPanelEditorPane.select(nextPosition, nextPosition + searchString.length());
405 | } else {
406 | // If no more occurrences, optionally wrap around to the start
407 | nextPosition = content.indexOf(searchString);
408 | if (nextPosition != -1) {
409 | rightPanelEditorPane.setCaretPosition(nextPosition);
410 | rightPanelEditorPane.select(nextPosition, nextPosition + searchString.length());
411 | }
412 | }
413 | }
414 |
415 | private static void findPreviousOccurrence(String searchString) {
416 | String content = rightPanelEditorPane.getText();
417 | int currentPosition = rightPanelEditorPane.getCaretPosition();
418 | int previousPosition = content.lastIndexOf(searchString, currentPosition - searchString.length() - 1);
419 |
420 | if (previousPosition != -1) {
421 | rightPanelEditorPane.setCaretPosition(previousPosition);
422 | rightPanelEditorPane.select(previousPosition, previousPosition + searchString.length());
423 | } else {
424 | // If no more occurrences, optionally wrap around to the end
425 | previousPosition = content.lastIndexOf(searchString);
426 | if (previousPosition != -1) {
427 | rightPanelEditorPane.setCaretPosition(previousPosition);
428 | rightPanelEditorPane.select(previousPosition, previousPosition + searchString.length());
429 | }
430 | }
431 | }
432 |
433 | private static void saveFile() {
434 | if (currentFilePath != null) {
435 | try {
436 | Files.write(Paths.get(currentFilePath), rightPanelEditorPane.getText().getBytes());
437 | logger.info("File saved: " + currentFilePath);
438 | } catch (IOException e) {
439 | logger.error("Error saving file: " + currentFilePath, e);
440 | }
441 | } else {
442 | logger.warn("No file selected to save.");
443 | }
444 | }
445 |
446 |
447 | private static String getFilePath(DefaultMutableTreeNode node) {
448 | FileNode fileNode = (FileNode) node.getUserObject();
449 | return fileNode.getFile().getAbsolutePath();
450 | }
451 |
452 | private static List isPacked(String apkFilePath, JadxDecompiler jadx) {
453 | List packedClasses = new ArrayList<>();
454 | try {
455 | // Extract and parse AndroidManifest.xml
456 | Set manifestClasses = JadxUtils.getManifestClasses(apkFilePath, jadx);
457 |
458 | // Get classes from dex files
459 | Set dexClasses = JadxUtils.getDexClasses(apkFilePath, jadx);
460 |
461 | // Check if there are any classes in the manifest that are not in the dex files
462 | for (String className : manifestClasses) {
463 | if (!dexClasses.contains(className)) {
464 | logger.info("Class {} found in manifest but not in dex files", className);
465 | packedClasses.add(className);
466 | }
467 | }
468 | } catch (Exception e) {
469 | logger.error("Error checking if APK is packed", e);
470 | }
471 | return packedClasses; // Return the list of packed classes
472 | }
473 |
474 | // Modify the analyzeApk method to update the progress bar
475 | private static void analyzeApk(String apkFilePath, DefaultMutableTreeNode apkDetailsRoot) {
476 | SwingWorker worker = new SwingWorker() {
477 | @Override
478 | protected Void doInBackground() {
479 | logger.info("Loading APK");
480 | JadxDecompiler jadx = JadxUtils.loadJadx(apkFilePath);
481 |
482 | List packedClasses = isPacked(apkFilePath, jadx);
483 | if (!packedClasses.isEmpty()) {
484 | logger.info("APK is packed");
485 |
486 | DefaultMutableTreeNode missingClassesNode = new DefaultMutableTreeNode("Missing Classes");
487 | apkDetailsRoot.add(missingClassesNode);
488 | for (String className : packedClasses) {
489 | missingClassesNode.add(new DefaultMutableTreeNode(className));
490 | }
491 |
492 | List dexLoadingDetails = DynamicDexLoaderDetection.getJavaDexLoadingDetails(jadx);
493 | if (!dexLoadingDetails.isEmpty()) {
494 | logger.info("Generating Java unpacker stub");
495 | DefaultMutableTreeNode classLoaderNode = new DefaultMutableTreeNode("Code Loader Details");
496 | classLoaderNode.add(new DefaultMutableTreeNode("Type: Java"));
497 |
498 | for (String detail : dexLoadingDetails) {
499 | classLoaderNode.add(new DefaultMutableTreeNode(detail));
500 | }
501 |
502 | apkDetailsRoot.add(classLoaderNode);
503 |
504 | apkAnalysisDetails = UnpackerGenerator.generateJava(jadx, apkFilePath);
505 | if (apkAnalysisDetails.getBaseDir() != null) {
506 | SwingUtilities.invokeLater(() -> updateDirectoryTree(apkAnalysisDetails.getBaseDir()));
507 | } else {
508 | logger.error("Error generating Java unpacker code.");
509 | }
510 | } else {
511 | logger.info("Could not find code loader in Java. Probable native packer detected.");
512 | DefaultMutableTreeNode classLoaderNode = new DefaultMutableTreeNode("Code Loader Details");
513 | classLoaderNode.add(new DefaultMutableTreeNode("Type: Native"));
514 | apkDetailsRoot.add(classLoaderNode);
515 | }
516 | } else {
517 | logger.info("APK is not packed");
518 | DefaultMutableTreeNode packerNode = new DefaultMutableTreeNode("Packer");
519 | packerNode.add(new DefaultMutableTreeNode("Not Packed"));
520 | apkDetailsRoot.add(packerNode);
521 | }
522 |
523 | return null;
524 | }
525 |
526 | @Override
527 | protected void done() {
528 | SwingUtilities.invokeLater(() -> {
529 | ((DefaultTreeModel) apkDetailsTree.getModel()).reload(apkDetailsRoot);
530 | displayUnpackerFile();
531 | progressBar.setIndeterminate(false);
532 | progressBar.setValue(100);
533 | progressBar.setString("Complete");
534 | progressBar.setTextColor(Color.WHITE);
535 | });
536 | }
537 | };
538 | worker.execute();
539 | }
540 |
541 | private static void displayUnpackerFile() {
542 | DefaultMutableTreeNode root = (DefaultMutableTreeNode) directoryTree.getModel().getRoot();
543 | DefaultMutableTreeNode targetNode = findNode(root, "Unpacker_", ".java");
544 |
545 | if (targetNode != null) {
546 | String filePath = getFilePath(targetNode);
547 | if (filePath != null) {
548 | currentFilePath = filePath;
549 | try {
550 | // We'll use this path for other things as well
551 | pathToUnpacker = filePath;
552 |
553 | Path path = Paths.get(filePath);
554 | if (Files.isRegularFile(path)) {
555 | String content = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
556 | rightPanelEditorPane.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
557 | rightPanelEditorPane.setText(content);
558 | rightPanelEditorPane.setCaretPosition(0);
559 |
560 | // Expand the path to the target node
561 | TreePath treePath = new TreePath(targetNode.getPath());
562 | directoryTree.scrollPathToVisible(treePath);
563 | directoryTree.setSelectionPath(treePath);
564 | directoryTree.expandPath(treePath);
565 | }
566 | } catch (Exception ex) {
567 | ex.printStackTrace();
568 | rightPanelEditorPane.setText("Error loading file: " + ex.getMessage());
569 | }
570 | }
571 | } else {
572 | rightPanelEditorPane.setText("Entry file not found.");
573 | }
574 | }
575 |
576 | private static DefaultMutableTreeNode findNode(DefaultMutableTreeNode root, String prefix, String suffix) {
577 | Enumeration> e = root.breadthFirstEnumeration();
578 | while (e.hasMoreElements()) {
579 | DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.nextElement();
580 | if (node.getUserObject() instanceof FileNode) {
581 | FileNode fileNode = (FileNode) node.getUserObject();
582 | String fileName = fileNode.getFile().getName();
583 | if (fileName.startsWith(prefix) && fileName.endsWith(suffix)) {
584 | return node;
585 | }
586 | }
587 | }
588 | return null;
589 | }
590 |
591 | private static void updateDirectoryTree(File baseDir) {
592 | DefaultTreeModel treeModel = DirectoryTreeModel.buildTreeModel(baseDir);
593 | directoryTree.setModel(treeModel);
594 | }
595 | }
596 |
--------------------------------------------------------------------------------