├── 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 | ![BadUnboxing logo](https://github.com/LaurieWired/BadUnboxing/blob/main/media/bad_unboxing_logo.png) 2 | [![License](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) 3 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/LaurieWired/BadUnboxing)](https://github.com/LaurieWired/BadUnboxing/releases) 4 | [![GitHub stars](https://img.shields.io/github/stars/LaurieWired/BadUnboxing)](https://github.com/LaurieWired/BadUnboxing/stargazers) 5 | [![GitHub forks](https://img.shields.io/github/forks/LaurieWired/BadUnboxing)](https://github.com/LaurieWired/BadUnboxing/network/members) 6 | [![GitHub contributors](https://img.shields.io/github/contributors/LaurieWired/BadUnboxing)](https://github.com/LaurieWired/BadUnboxing/graphs/contributors) 7 | [![Follow @lauriewired](https://img.shields.io/twitter/follow/lauriewired?style=social)](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 | ![BadUnboxingMainWindow](https://github.com/LaurieWired/BadUnboxing/blob/main/media/Main_Window.png) 45 | 46 | ### Confirm Generated Unpacker Execution 47 | ![BadUnboxingUnpackerExecution](https://github.com/LaurieWired/BadUnboxing/blob/main/media/confirm_execution.png) 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 | --------------------------------------------------------------------------------