id183488 IMPLEMENTATION OF A OBFUSCATOR FOR THE JAVA VIRTUAL MACHINE JOSE FRANCISCO UDAETA ARCE Thesis supervisor: JORDI VENTAYOL MARIMON (B38 IBERIA, S.L.U. ) Tutor: DAVIDE CAREGLIO (Department of Computer Architecture) Degree: Bachelor's Degree in Informatics Engineering (Information Technologies) Bachelor's thesis Facultat d'Informàtica de Barcelona (FIB) Universitat Politècnica de Catalunya (UPC) - BarcelonaTech 22/01/2024 Contents 1 Context of the project 6 2 Scope and objectives 2.1 Specific objectives . . . . . . . . . . . . . . . . . . . 2.1.1 String obfuscation . . . . . . . . . . . . . . 2.1.2 Constant obfuscation . . . . . . . . . . . . . 2.1.3 Renaming of variables, functions and classes 2.1.4 Control flow obfuscation . . . . . . . . . . . . . . . . 7 8 8 9 9 9 . . . . 9 9 10 10 10 3 Methodology 3.1 Adoption of Agile Methodology 3.2 Project Organization and Tools 3.3 Testing Strategy . . . . . . . . 3.4 Documentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Planning and Scheduling 10 5 Task descriptions 5.1 Project Planning . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.1 Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.2 Planning . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.3 Costs and Sustainability . . . . . . . . . . . . . . . . . 5.1.4 Final Documentation . . . . . . . . . . . . . . . . . . . 5.2 Research . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2.1 Research about the JVM, APKs, and Android runtimes (R8) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2.2 Research about Obfuscation techniques . . . . . . . . . 5.2.3 Getting Familiar with Proguard Codebase . . . . . . . 5.2.4 Research about Gradle . . . . . . . . . . . . . . . . . . 5.3 Research, Design, Implementation and Testing . . . . . . . . . 5.3.1 First Sprint: String Obfuscator . . . . . . . . . . . . . 5.3.2 Second Sprint: Constant Obfuscator . . . . . . . . . . 5.3.3 Third Sprint: Symbols Obfuscator . . . . . . . . . . . 5.3.4 Fourth Sprint: Control Flow Obfuscator . . . . . . . . 5.4 Integration with Gradle . . . . . . . . . . . . . . . . . . . . . 5.5 Final Preparations . . . . . . . . . . . . . . . . . . . . . . . . 5.5.1 Code Cleaning and Refactoring . . . . . . . . . . . . . 5.5.2 Preparing the Final Layout of the Thesis and Presentation Preparations . . . . . . . . . . . . . . . . . . . . 11 11 11 11 11 11 11 2 11 12 12 12 12 12 13 13 13 13 13 13 14 5.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 6 Alternatives and Action Plan 6.1 Risk Assessment . . . . . . 6.2 Alternative Approaches . . . 6.2.1 Technical Expertise . 6.2.2 Time Management . 6.3 Action Plan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Finantial Management 7.1 Human Resources . . . . . . . . . . . . . . . . 7.2 Hardware Costs . . . . . . . . . . . . . . . . . 7.2.1 Proportional Cost for the Project . . . 7.3 Software Costs . . . . . . . . . . . . . . . . . 7.3.1 JetBrains IntelliJ Community Edition 7.3.2 LaTeX . . . . . . . . . . . . . . . . . . 7.3.3 Gantter for Google Drive . . . . . . . 7.3.4 JADX . . . . . . . . . . . . . . . . . . 7.4 Miscellaneous Costs . . . . . . . . . . . . . . . 7.5 Contingencies . . . . . . . . . . . . . . . . . . 7.6 Unexpected events . . . . . . . . . . . . . . . 7.6.1 Inexperience / Lack of Knowledge . . 7.6.2 Unexpected Event 2: Hardware Failure 7.7 Final finantial summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 18 18 18 18 18 . . . . . . . . . . . . . . 19 19 21 21 22 22 22 22 22 22 23 23 23 24 24 8 Time and Cost Management Parameters 24 8.1 Time Deviation Parameters . . . . . . . . . . . . . . . . . . . 25 8.2 Cost Deviation Parameters . . . . . . . . . . . . . . . . . . . . 25 9 Sustainability report 9.1 Self evaluation in sustainability 9.2 Environmental aspect . . . . . . 9.2.1 PPP . . . . . . . . . . . 9.2.2 Product Lifetime . . . . 9.2.3 Risks . . . . . . . . . . 9.3 Economic aspect . . . . . . . . 9.3.1 PPP . . . . . . . . . . . 9.3.2 Product Lifetime . . . . 9.3.3 Risks . . . . . . . . . . 9.4 Social aspect . . . . . . . . . . 9.4.1 PPP . . . . . . . . . . . 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 26 27 27 29 30 30 30 31 32 32 32 9.4.2 9.4.3 Product Lifetime . . . . . . . . . . . . . . . . . . . . . 33 Risks . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 10 Final Project Planification 10.1 Cost Modifications . . . . . . . . . . . . 10.1.1 Final Planning and Scheduling . 10.1.2 Modified Human Resources Costs 10.1.3 Final finantial summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 35 36 39 39 11 Obfuscation and the JVM, an introduction 41 11.1 Overview of Existing Obfuscation Techniques . . . . . . . . . 41 12 The JVM 12.1 The JVM architecture . . . . . . . . . 12.2 The .class file format . . . . . . . . . . 12.2.1 The constant pool . . . . . . . 12.2.2 Fields array . . . . . . . . . . . 12.2.3 Methods array . . . . . . . . . 12.3 The JVM instruction set . . . . . . . . 12.3.1 Constant values . . . . . . . . . 12.3.2 Local Variable Instructions . . 12.3.3 Stack Operations . . . . . . . . 12.3.4 Classes and method invocation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 43 44 46 48 49 53 54 55 56 56 13 Proguard 13.1 What is Proguard? . . . . . . . . . . . . 13.2 Proguard architecture and functionalities 13.2.1 The proguard configuration file . 13.2.2 Proguard-core . . . . . . . . . . . 13.3 Proguard use example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 59 60 61 63 65 14 Symbols obfuscator design and implementation 14.1 Limitations of Current Symbol Obfuscation in Proguard . . . 14.2 Design Goals . . . . . . . . . . . . . . . . . . . . . . . . . . . 14.3 Implementation Details . . . . . . . . . . . . . . . . . . . . . . 67 67 68 68 15 String obfuscator design and implementation 82 15.1 Design goals . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 15.2 Implementation details . . . . . . . . . . . . . . . . . . . . . . 83 16 Results of the project 92 16.1 Impact of Enhancements . . . . . . . . . . . . . . . . . . . . . 93 4 16.2 Limitations and Future Work . . . . . . . . . . . . . . . . . . 94 17 Conclusion A Code Samples A.1 Main Java code . . . . . . . A.2 Alumno class Java code . . A.3 Proguard configuration file . A.4 Proguard.java . . . . . . . . A.5 AppView.java . . . . . . . . A.6 Obfuscator.java . . . . . . . A.7 ClassObfuscator.java . . . . A.8 NameFactory.java . . . . . . A.9 SimpleNameFactory.java . . A.10 ComplexNameFactory.java . A.11 StringObfusatorVisitor.java 95 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 96 96 97 99 115 116 135 151 151 155 158 B Program outputs 162 B.1 javap -c -v -private output of Main java code . . . . . . . . . . 162 B.2 javap -c -v -private output of Main java code + Proguard . . . 167 B.3 javap -c -v -private output of Alumno java code . . . . . . . . 171 B.4 javap -c -v -private output of Alumno java code + Proguard . 177 B.5 javap -c -v -private output of Main java code + Proguard Enhanced . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 B.6 javap -c -v -private output of Alumno java code + Proguard Enhanced . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183 B.7 jadx-gui screenshots of the Main java code without Proguard Enhanced . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 B.8 jadx-gui screenshots of the Alumno java code without Proguard Enhanced . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188 B.9 jadx-gui screenshots of the Main java code + Proguard Enhanced189 B.10 jadx-gui screenshots of the Alumno java code + Proguard Enhanced . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190 5 1 Context of the project In the digital era we are in, smartphones have become an indispensable part of our lives. They are not just communication devices but serve as gateways to a lot of important services: social media, commerce, banking, government apps, healthcare, etc. Given the importance of smartphones in the modern world, the security of mobile applications and mobile operating systems is really important. Build38, the cybersecurity company where I am currently working as an intern, addresses the need for security in mobile applications for both Android and iOS operating systems, specialized in cloud-based device attestation, cryptography utilities, and multiple layers of app security. However, despite various security measures, mobile applications using their solution remain susceptible to numerous attacks, and one that the industry seems to be seeking in order to accommodate itself is protection against reverse engineering. Reverse engineering is a technique where an attacker disassembles a compiled application in order to understand how it works. This is usually done with existing tools that convert machine code (for example Android JVM code) to an intermediate representation or readable source code. What was once a skill possessed by crackers, modders, and malware analysts, reverse engineering has increasingly become a sometimes necessary skill for security testers and developers as well. Understanding reverse engineering and being in the mind of someone that is trying to understand how an application works is crucial for developers and people in charge of the security of an organization. Even if there are multiple ways of adressing reverse engineering efforts (SSL pinning, E2E encryption in network communication, preventing running the app in compromised devices, and all sorts of trickery related to dynamic analysis) when it comes to static (meaning that running the app is not needed to understand how it works) or dynamic analysis; code obfuscation is one of the most important, since it’s normally a slow effort operation but gives large security through obscurity benefits. Obfuscation serves as a powerful countermeasure against reverse engineering. It does this by altering the code to make it more complex to analyze, being more difficult to understand while an attacker is reading the code or to prevent automated attempts to deobfuscate it, but without changing its underlying app logic. This aligns with the standards set by the Open Web Application Security Project (OWASP), specifically their Mobile Application Security Verification Standard for Resilience (MASVS-R) [1]. OWASP is a nonprofit organization focused on improving the security of software. Their MASVS-R guidelines recommend 6 implementing defense-in-depth measures to enhance an app’s ability to specific client-side attacks. Adhering to these controls can safeguard valuable business assets, mitigate financial losses, and reduce both legal and loss of reputation risks. More specifically, inside the MASVS-R, the MASVS-RESILIENCE-3 [2] standard emphasizes the importance of anti-static analysis mechanisms, aimed at stopping the attackers’ efforts to understand an application through static analysis. Given this context, the focus of this project is to develop an obfuscator that aims to protect Android native applications (written in Kotlin or Java), providing an additional layer of security against reverse engineering attempts, also helping my internship company to give me training and maybe using this tool in real customers since it is a demanded feature. It’s worth mentioning that obfuscators for JVM-based languages like Java and Kotlin already exist. Proguard, for example, is a well-known tool that comes bundled with the main Android IDE: Android Studio [3]. Its features include removing unused classes, methods, and attributes; optimizing bytecode; and obfuscating class names, methods, and variables. Another more advanced but proprietary and private solution is Dexguard: it offers additional layers of security, such as obfuscation of strings, control flow obfuscation, preventive transformation (exploiting weaknesses in existing deobfuscators), and many other robust and complex obfuscation techniques that make reverse engineering more challenging. Given that Proguard’s source code is open-source and open for modification, being also the base for Dexguard, it provides a good starting point to build from, also seen with good eyes by the principal stakeholder, the thesis tutor. The project aims to extend Proguard by incorporating more advanced features that expand it, such as string obfuscation, constant obfuscation, finding alternatives to the actual renaming techniques and control flow obfuscation. Designing a completely new codebase for this project would take a lot of the time and would be technically challenging. In the subsequent sections, this document will delve deeper into the scope and technical objectives of this project. 2 Scope and objectives The general objective of this project is to develop a robust obfuscator for Java and Kotlin applications for Android. The tool aims to extend the already existing open source tool ”Proguard” that in some cases fall short to the lack 7 of more advanced features, to provide additional obfuscation techniques in conjunction with the already existing ones. Ease of integration is also a key feature of this project to ensure it is really applicable to existing codebases from apps with ease. Proguard as an existing tool can be incorporated into already existing applications with Gradle as a plugin, the de facto building tool for Android development, and this project aims to do it in the same way. It is also intended for it to be a regular executable program that can be called from the command terminal and be portable for most used operating systems since it will extend the Java codebase of Proguard. This project does not aim to be at the level of the state-of-art techniques that some current solutions like Dexguard offer as a product and does not intend to do anything revolutionary due to the lack of previous knowledge and time available for the project, it’s a way of learning and doing something that already exists but is valuable knowledge nonetheless, also to understand the architecture and techniques used in obfuscation programs and having the experience to extend open source projects while also creating a base for software that may be still be developed in the future. 2.1 Specific objectives To add value to the project, some extended features are intended to be implemented that make the reverse engineering problem more difficult. While all these techniques are important, since the time estimations and technical knowledge are difficult to know afterhand, the approach to be taken with the development of features can vary while the project progresses and the priority of the objectives is ordered from the most to less critical to fulfill. 2.1.1 String obfuscation As said in [4], static data like character strings and readable text findable in the code can contain valuable information to a reverse engineer and give them context of what a piece of the executable they are investigating does. Also it can give the reverse engineer some values to endpoints, API REST keys, or other secrets used in the code base. This functionality will aim to hide this valuable information to make the investigation more time consuming. 8 2.1.2 Constant obfuscation In the same fashion of string obfuscation, constant values can also be interesting to reverse engineers, for example in class constructors, function calls, etc. This can be seen as a superset of string obfuscation. 2.1.3 Renaming of variables, functions and classes It is usual when reversing Android apps that deobfuscator tools like JADX are used in the process to transform compiled code to a readable source code. If one is not careful in the process of building their app to be distributed, they can include important symbols like class names, variable names or function names in the end product, which gives a reverse engineer a really easy path to understand the application. Proguard comes with existing features that offer the obfuscation of all these symbols, but in this project I intend to extend this in a more strong way. 2.1.4 Control flow obfuscation In the field of reverse engineering, reading the sequential flow of the source code can give a lot of useful information. For example, loops and conditional statements can show a lot about what a program does. While there’s a lot of study on how to hide a program’s structure through code obfuscation, it’s still difficult to trick automated tools that aim to undo the obfuscation. Many methods try to make the code hard to read by wrapping logic in classes or changing the program’s usual flow in intricate ways. This project wants to add to the existing ways to make code harder to understand. Some of these methods come from established techniques used by compilers or existing obfuscating techniques. 3 Methodology The methodology for this project is designed as a hybrid approach, combining Agile development principles with extensive literature research. This approach allows for flexibility, iterative development, and close collaboration with stakeholders. 3.1 Adoption of Agile Methodology The Agile development framework is adopted for several reasons: 9 • Frequent Communication: Regular interaction with the stakeholder, who is my tutor at Build38 company, ensures that the project stays aligned with the objectives defined. • Iterative Development: Each obfuscation technique is treated as an iteration, allowing for manageable work chunks consisting of research, design, implementation, and testing. • Flexibility: Agile allows for adaptability in the project timeline, making it possible to clarify or modify objectives as needed. 3.2 Project Organization and Tools • Code Management: Git is used for version control, ensuring that code changes are properly tracked. • Task Management: Jira is employed for project management, as it is a tool with which I am already familiar. It helps in organizing tasks and monitoring project progress. 3.3 Testing Strategy • Manual Testing: Tools like JADX will be used to manually verify the effectiveness of each obfuscation technique. • Automated Testing: JUnit tests will ensure that the program’s core functionalities remain intact after each iteration of obfuscation. 3.4 Documentation Upon the completion of each functionality, a detailed summary will be drafted. This summary will outline the work accomplished and the challenges encountered, and it will be included in the project’s final documentation. 4 Planning and Scheduling The project is set to start on the 18th of September, 2023, and will run through to the 21st of January of 2024, the day before the presentation. That is 126 days for the project completion. Note that the initial plan may be subject to changes due to the project’s progress. Additionally, the adoption of Agile methodologies means that new 10 requirements could emerge or other requirements can be changed, affecting the original plan. 5 Task descriptions 5.1 5.1.1 Project Planning Scope The initial phase of the project is to define the scope, which will involve a comprehensive study of the project’s requirements and objectives. It has a duration of 24.5 hours, and in this part the base for the project will be laid. This phase will set the direction for the project by outlining the problem domain, scope of research, intended objectives and some problems that may arise. 5.1.2 Planning This part has 8.25 hours allocated, this stage is focused on creating a detailed project plan. This includes the establishment of milestones, the selection of tools and technologies, and the planning of the sprints, and the definition of alternatives and action plan. 5.1.3 Costs and Sustainability This 9.25 hours segment will involve predicting the economical and enviromental cost of the project. 5.1.4 Final Documentation With a time of 18.25 hours, this stage involves collecting all the other parts done into a final project report plan. This will include the previous parts of 5.1.1, 5.1.2 and 5.1.3 and more aditional information. 5.2 Research 5.2.1 Research about the JVM, APKs, and Android runtimes (R8) Understanding the technical part of the subject in matter is needed to do this project succesfully. This time will be invested in researching the inner 11 workings of the Java Virtual Machine, Android Package files (APKs), and the Android runtime environment (R8). 5.2.2 Research about Obfuscation techniques A deep dive into existing obfuscators and their underlying techniques will provide insights into how to construct the project’s obfuscator effectively. Main resources will be [4], the source code of Proguard, multiple papers related with the topic, internet research and other employees of the company I’m working right now. 5.2.3 Getting Familiar with Proguard Codebase Before diving into implementation, understanding the architecture of Proguard codebase is really important. Setting up the repository, doing testing with existent binaries and attatching debuggers to see the workflow will help me in understanding better how it works and will make extending its functionalities and integrating new features more easy. 5.2.4 Research about Gradle Understanding Gradle is crucial for the project’s goal of easy integration with already existing Android projects. This will include learning how to build custom plugins and also see how proguard does it. 5.3 Research, Design, Implementation and Testing This section is dedicated to the ”meat and potatoes” of the project, each objective will have a dedicated Sprint and similar structure, and will come with some meetings with the project’s director. 5.3.1 First Sprint: String Obfuscator Research This step will be to understand how string obfuscation works and the different techniques available in literature and other codebases. Design and implementation The next step will involve actual design, code development and analyzing how will be the use case. Testing Rigorous tests will be conducted to the used technique to ensure its effectiveness, in this case manual testing will be done using JADX to view the decompiled code and automated testing will be used for the 12 correctness of the program. This can happen in parallel with the design and implementation phase. Documentation Finally, a documentation step to record the design choices and any issues or challenges faced. Review At the end of the Sprint, a review meeting is held to present the completed work to the project director. 5.3.2 Second Sprint: Constant Obfuscator Similar to the first sprint, this will involve a cycle of research, design, implementation, testing, and documentation, but focused on constant obfuscation. 5.3.3 Third Sprint: Symbols Obfuscator Like the previous sprints, this will involve research, design, implementation, and testing cycle. This time the focus will be on creating an obfuscating names of variables, functions, and classes. 5.3.4 Fourth Sprint: Control Flow Obfuscator This sprint will be dedicated to control flow obfuscation techniques. Again, this will follow the standard cycle of research, design, implementation, testing, and documentation. 5.4 Integration with Gradle One of the objectives of this obfuscator is the ease of integration of it, in this task, the task will be to analyze and design a way for the obfuscation techniques to be used in Android projects using the Gradle build system. This will also come with a implementation and testing part that will be manual in this case, decompiling with JADX. 5.5 5.5.1 Final Preparations Code Cleaning and Refactoring Prior to final submission, the codebase will undergo a some review, refactoring, and documentation to ensure it is clean and readable. 13 5.5.2 Preparing the Final Layout of the Thesis and Presentation Preparations The final layout of the thesis will be organized, with a review of all chapters for coherence and ortographical errors, along with checking the citations and appendices. Concurrently, the slides for the presentation will be prepared and with a demo of the obfuscation techniques. 5.6 Conclusion Due to the Agile nature of this project, not all the objectives may be completed within the short timeframe, this is agreed previously and is acceptable. Any such deviations will be covered in the ”Alternatives and Action Plan” section. In Table 1 a summary of the identifiers, names and time stimations is given which add to 450 hours, considering the context of the project is inside the TFG which is 18 ETCS which are aproximatedly 450 hours also, it seems to meet the university requirements. A Gantt diagram 5.6 is also included in the following page with the tasks listed. In a real project with more people, each feature can be parallelized and make the project completion faster, but since the project is only done by the author, the Gantt is almost sequential. 14 Table 1: Task List and Hours 16 Task Code PL-01 PL-02 PL-03 PL-04 RES-01 RES-02 RES-03 RES-04 STR-R STR-DI STR-T STR-DOC STR-REV CONST-R CONST-DI CONST-T CONST-DOC CONST-REV SYM-R SYM-DI SYM-T SYM-DOC Task name Scope Planning Costs and sustainability Final documentation Research about the JVM, APKs, and Android runtimes (R8) Research about Obfuscation techniques Getting Familiar with Proguard Codebase Research about Gradle String Obfuscator Research String Obfuscator Design and implementation String Obfuscator Testing String Obfuscator Documentation String Obfuscator Review Constant Obfuscator Research Constant Obfuscator Design and implementation Constant Obfuscator Testing Constant Obfuscator Documentation Constant Obfuscator Review Symbols Obfuscator Research Symbols Obfuscator Design and implementation Symbols Obfuscator Testing Symbols Obfuscator Documentation Hours Task dependencies 24,50 8,25 PL-01 9,25 PL-02 18,25 PL-03 10,00 PL-04 10,00 RES-01 10,00 RES-02 10,00 RES-03 10,00 RES-01,02,03,04 40,00 STR-R 10,00 STR-DI 5,00 STR-T 2,00 STR-R 10,00 STR-T 40,00 CONST-R 10,00 CONST-DI 5,00 CONST-T 2,00 CONST-R 10,00 CONST-T 40,00 SYM-R 10,00 SYM-DI 5,00 SYM-T Continued on next page 17 Task Code SYM-REV CFLOW-R CFLOW-DI CFLOW-T CFLOW-DOC CFLOW-REV GRA-DI GRA-T GRA-DOC GRA-REV FINAL-01 FINAL-02 Total Hours Table 1 – Continued from previous page Task name Symbols Obfuscator Review Control Flow Obfuscator Research Control Flow Obfuscator Design and implementation Control Flow Obfuscator Testing Control Flow Obfuscator Documentation Control Flow Obfuscator Review Gradle integration Design and implementation Gradle integration Testing Gradle integration Documentation Gradle integration Review Code Cleaning and Refactoring Preparing the Final Layout of the Thesis and Presentation Preparations Full final thesis project Hours 2,00 10,00 40,00 10,00 5,00 2,00 40,00 10,00 4,00 2,00 5,00 20,00 449,25 Task dependencies SYM-R SYM-T CFLOW-R CFLOW-DI CFLOW-T CFLOW-R CFLOW-R GRA-DI GRA-T GRA-DI *-DI * (ALL TASKS) 6 Alternatives and Action Plan This section will outline alternative approaches and action plans to manage uncertainties and risks, ensuring that the project remains on track to achieve its objectives. 6.1 Risk Assessment A preliminary risk assessment will identify potential challenges that could hinder the progress of the project. • Lack of Technical Expertise: While extending Proguard, I may encounter technical complexities that require more time than expected, this is relevant in all ”DI” and ”T” tasks of each sprint. • Time Constraints: The project is time-bound, and some objectives may not be achieved within the planed timeframe. 6.2 6.2.1 Alternative Approaches Technical Expertise In case I find that extending Proguard or the obfuscation techniques becomes too complicated due to a lack of specific technical knowledge: • Seek expert advice from my tutor in my cybersecurity company. • As it was said in the first delivery, some features can be prioritized over others, the priority is in this order of importance: STR, CONST, SYM and lastly CFLOW. 6.2.2 Time Management If we are falling behind the schedule: • Re-evaluate the objectives and consider simplifying or dropping features that are less critical. • Increase the work hours temporarily, if feasible. 6.3 Action Plan • Weekly Assessments: Every week, assess the project’s status in terms of the schedule, objectives met, and feedback received. 18 • Mid-Project Review: Conduct a comprehensive review halfway through the project to evaluate if objectives need to be adjusted. By preparing for these scenarios, the project will be able to adapt to changes and the nature of Agile development. 7 Finantial Management Effective financial management is an important part of any project, especially one of the complexity and scope like the development of this obfuscator. In the following sections we will estimate costs and resources to ensure the successfull completion of the project. 7.1 Human Resources Human resource costs are an important factor in the budget of the project. For the purpose of this project, we have primarily two members involved, each with two specific roles: A project manager (P.M) and programmer (PROG). The difference in their roles need different skills and different salaries. In the table 2, we present the financial cost per hour for the two team members, delineating both the gross and net salaries. As a source we use [5] and [6] The net salary is the amount received by the employee after deductions for Social Security and finance taxes. For the employer, there is an additional Social Security tax, which in our case stands at 35% of the gross salary of each worker. Therefore, the total wage cost to the company for each employee is the gross salary multiplied by 1.35. Also the gross salary is the net salary scaled up by a factor of 1.21, accounting for taxes like IRPF and other deductions. Member P.M PROG Net salary 18,44 € 16,41 € Gross salary 22,68 € 20,18 € Gross salary + Taxes (35%) 30,62 € 27,25 € Table 2: Table with of Human Resources roles of the project Once we know what are the resources needed per hour for each member, we can assign for each task defined in the tasks table 3 in order to estimate the costs in human resources for the project. 19 Task Code PL-01 PL-02 PL-03 PL-04 RES-01 RES-02 RES-03 RES-04 STR-R STR-DI STR-T STR-DOC STR-REV CONST-R CONST-DI CONST-T CONST-DOC CONST-REV SYM-R SYM-DI SYM-T SYM-DOC SYM-REV CFLOW-R CFLOW-DI CFLOW-T CFLOW-DOC CFLOW-REV GRA-DI GRA-T GRA-DOC GRA-REV FINAL-01 FINAL-02 Total Hours Member P.M P.M P.M P.M PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG P.M Hours 24,50 8,25 9,25 18,25 10,00 10,00 10,00 10,00 10,00 40,00 10,00 5,00 2,00 10,00 40,00 10,00 5,00 2,00 10,00 40,00 10,00 5,00 2,00 10,00 40,00 10,00 5,00 2,00 40,00 10,00 4,00 2,00 5,00 20,00 449,25 Cost per hour 30,62 € 30,62 € 30,62 € 30,62 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 30,62 € Table 3: Full table of Human Resources cost 20 Cost of task 750,18 € 252,61 € 283,23 € 558,81 € 272,49 € 272,49 € 272,49 € 272,49 € 272,49 € 1.089,95 € 272,49 € 136,24 € 54,50 € 272,49 € 1.089,95 € 272,49 € 136,24 € 54,50 € 272,49 € 1.089,95 € 272,49 € 136,24 € 54,50 € 272,49 € 1.089,95 € 272,49 € 136,24 € 54,50 € 1.089,95 € 272,49 € 109,00 € 54,50 € 136,24 € 612,39 € 12.512,03 € 7.2 Hardware Costs The hardware devices required for the development of this project are essential for almost all tasks: coding, testing (thus the phone added to the material), and documenting need to be done in a computer. As the project will be solely completed by the author of this thesis, only one set of hardware resources will be needed. The estimated cost of these hardware components is detailed in the table 4. This material is already given by the company the author is interning so realistically this cost is just indicative. Hardware Material MacBook Pro M2 14” Poco X3 NFC Total Cost 1.618,85 € 269,00 € 1.887,85 € Table 4: Table of Hardware costs 7.2.1 Proportional Cost for the Project The costs mentioned above are acquisition costs for the devices. However, to accurately calculate the proportional cost for the current project. To use as a guidence, we can assume that these hardware resources can be amortized over a period of 4 years, following which they should be replaced due to obsolescence. Considering that each year in Spain has approximately 250 working days, and that each working day consists of 8 hours, this leads to 8000 hours of hardware device use in a four-year period. Now to estimate the hardware cost of the project, we can do that by dividing the cost in hardware per hour with this formula. Hardware €/h = Cost hardware 1887.85 € = = 0.236 €/h Hours of use (4y) 8000 hours (1) And then we can use that Hardware cost per hour to get the cost in hardware of the project: Cost in Hardware of project = Hardware cost per hour × Hours project = 0.236 €/h × 449.25 h = 106.02 € (2) 21 7.3 Software Costs The expenditure on software resources for this project is minimal, given that most of the software tools employed are freely accessible. Here we list and discuss the essential software assets that will be utilized during the development of the project: 7.3.1 JetBrains IntelliJ Community Edition For the development environment, the JetBrains IntelliJ Community Edition will be employed. This Integrated Development Environment (IDE) is free and available for Java and Android development. 7.3.2 LaTeX For the documentation and preparation of this report, LaTeX will be used. It is a writing system, and its features come at no cost. 7.3.3 Gantter for Google Drive Project management and scheduling will be facilitated through Gantter for Google Drive, a cloud-based project management solution. It has a monthly subscription cost of 5€ per user. Even if this project spans approximately in four months, only in the first phase of the project needs the use of this software so the total cost would amount to 5€. 7.3.4 JADX To assist in decompiling and analyzing Android applications for testing the obfuscator, JADX will be used. It is an open-source tool that is free to use. In summary, the majority of software resources for this project come at no expense, with the exception of Gantter for Google Drive. The latter, with a total cost of $5, it is so minimal that the overall software-related expenditure for this project is negligible. 7.4 Miscellaneous Costs Apart from the expenditures such as human resources, hardware, and software, there are additional miscellaneous costs that are crucial for the successful completion of the project: office utilities, internet connectivity, and office supplies. 22 Given that the project is being conducted at the Build38 office, where I am currently interning, certain costs such as utilities and internet are effectively covered as part of the internship and given by the employer. However, for the purpose of the financial analysis, it’s beneficial to consider an alternative setting to estimate these expenses. In this regard, I refer to the rates of Ágora Coworking space located in the Sant Andreu neighborhood in Barcelona. This coworking space offers some facilities needed for any job: a meeting room, printing services, a break area, Internet access, Wifi and 24-hour access to the place. The rate for using this coworking space is 165€ per month. The project is slated to last for 4 months, so the cost of the full project regarding the office expenditures would be 660,00€. 7.5 Contingencies Contingency planning is an important aspect of financial management, serving as a ”life jacket” against problems that may arise during the project lifespan. For the purpose of this project, I have allocated a contingency budget at 10% of the overall costs for each major resource category. Resource Human Resources Cost Hardware costs Software costs Office Costs Total costs Cost w/o contingencies 12.512,03 € 106,02 € 5,00 € 660,00 € 13.283,05 € Contingencies 1.251,20 € 10,60 € 0,50 € 66,00 € 1.328,31 € Table 5: Resource Costs with contingencies 7.6 Unexpected events Project management involves planning and meticulous execution, yet unexpected events are almost a given in any development cycle. These are variables that cannot be entirely controlled and may entail extra costs that could impact the overall budget and timeline. 7.6.1 Inexperience / Lack of Knowledge The first major obstacle is the author inexperience and limited technical knowledge in the domain. Such a shortfall could lead to delays in task 23 execution and therefore alter the pre-defined project timeline. To mitigate this risk, we plan to allocate an additional 20% of the estimated time. Specifically, an extra 90 hours are allocated, calculated as 20% of the initial 450-hour estimate. Considering the labor cost of 26.24 €/hour for a programmer as said in 3, this would incur an additional expense of 2361.60€. If the delay becomes unmanageable, the project scope will be reduced moderately as said in the scope part of this document. The likelihood of this event occurring is assessed at 30%. Realistically, this may not even be needed, as was said in the Methodology section of this thesis, the objectives are really flexible and the completion of all the features is not a must. 7.6.2 Unexpected Event 2: Hardware Failure Another potential issue could be hardware failure, which would necessitate the replacement of the affected devices to avoid delays. A complete hardware failure is estimated to cost an additional 1,887.85€. This event is considered to have a low probability of occurrence, estimated at 5 Event Inexperience HW Failure Cost 2.361,60 € 1.887,85 € Probability 30% 5% Total Cost 708,48 € 94,39 € 802,87 € Table 6: Table with the unexpected events costs and probabilites 7.7 Final finantial summary In table 7 we show the sum of all the final costs for each section in the economic side of the project. 8 Time and Cost Management Parameters To ensure that the project happens as expected both in terms of time and budget, we define a set of parameters to monitor how it is going. These parameters are based on various metrics that will be recorded throughout the development cycle of the project. They will help identify any deviations in the time and costs allocated for different tasks and resources. 24 Type of cost Human Resources Hardware Resources Software Resources Miscellaneous (Office, energy, etc.) Contingency Unexpected Events Total Cost 12.512,03 € 106,02 € 5,00 € 660,00 € 1.281,96 € 802,87 € 15.414,23 € Table 7: Full Resource Costs 8.1 Time Deviation Parameters The time deviation for each task and the whole project can be calculated using the following formulas: Time deviation for ith task = Testimated,i − Treal,i (3) where Testimated,i is the time estimated for the ith task and Treal,i is the real time consumed for the ith task. Time deviation for the whole project = Testimated, total − Treal, total (4) where Testimated, total = 449.25 hours is the time estimated for the entire project and Treal, total is the real time consumed for the entire project. 8.2 Cost Deviation Parameters Cost deviations can occur at the level of individual tasks, hardware resources, and any unexpected events. They can be calculated as follows: Cost deviation in tasks = (Time deviation for ith task) × Ctask,i (5) where Ctask,i is the cost per hour for the ith task. Cost deviation in hardware resources = Cestimated, hardware − Creal, hardware (6) where Cestimated, hardware is the estimated cost of hardware resources and Creal, hardware is the actual cost of hardware resources. 25 Cost deviation in unexpected events = Cestimated, unexpected − Creal, unexpected (7) where Cestimated, unexpected is the estimated cost for unforeseen events and Creal, unexpected is the real cost for unexpected events. Total Cost Deviation for Project = n X (Cost deviation in tasks)i i=1 + Cost deviation in hardware resources + Cost deviation in unexpected events (8) where n is the total number of tasks in the project. (Cost deviation in tasks)i represents the cost deviation for the ith task. By regularly evaluating these parameters, we can manage and adjust our time and resources to keep the project on track. 9 9.1 Sustainability report Self evaluation in sustainability My perspective on sustainability is rooted in the ideas of having a balance between economic sustainability, social wellness, and environmental conservation. In my path in the Informatics Engineering career I came across multiple economic models that aim for sustainable development, such as the circular economy or the ecological economy. These frameworks offer diverse paths to long-term sustainability. In the professional setting of my internship, which revolves around cybersecurity for mobile devices, I have learned to appreciate the roles, rights, and responsibilities of various teams: developers, companies, law-makers, or consumers. I understand that responsible tech use and ethical coding practices can contribute to sustainability. I’m also aware of global initiatives like the UN’s Sustainable Development Goals and IPCC reports that offer projects for the better sustainability on an international scale. While my focus has been on the technical aspects, I do recognize that the computing industry has real-world impacts on the environment, society, and 26 economy. For putting some examples: the amount of energy used by data centers, electronic waste from outdated electronic devices, and ethical concerns in data security all add layers of complexity to the notion of sustainability within cybersecurity. Although I may not yet have a deep understanding of specific metrics like carbon footprints, everybody should know the importance of these factors and I am dedicated to expanding my knowledge. As for the social and economic dimensions of my work, my final thesis project aims to develop an Android Obfuscator. This tool could enhance security across a variety of applications and contribute to society by putting in safety user information. Although I have some exposure to economic assessment tools like the DAFO analysis through my path in university, my skills in evaluating the economic sustainability of a project are still in the early phases. In conclusion, I’m very aware that any software I develop will have social impact, interacting with multiple people, stakeholders, companies, etc. Whether it’s potential job changes, ethical issues, or environmental considerations, my contributions will have an effect in the world. So, as I progress in my career, I plan to still learn and understand about sustainability in every aspect it is possible and apply it in software in the will to make things better for the world. 9.2 9.2.1 Environmental aspect PPP 9.2.1.1 Initial phase The environmental aspect of a project in development is becoming increasingly significant in today’s world. In the context of this project focused on developing an obfuscator, the environmental impact is basically done by the energy usage and the electronic waste. Starting with the primary impact, the primary tool for this project is a ”MacBook Pro 13 inches (2022 model)” Calculations for the energy consumption of this device have already been made by Apple in [7]. With the laptop mostly in ”Idle—Display On” mode during the 450 hours of work dedicated to the project, it will consume about 1.152 kWh of energy. Here’s how the kWh value was calculated: 27 Power in W 1000 2.56 W = 1000 = 0.00256 kW Power in kW = Energy in kWh = Power in kW × Time in hours = 0.00256 kW × 450 hours = 1.152 kWh Also, I’ll be using a Poco X3 smartphone for a few hours during testing (around 50 hours). However, the energy consumption of smartphones is generally minimal compared to laptops, especially for such a short period, and as such, it has not been explicitly calculated. Transportation energy is also negligible as I walk to my office, thus leaving the primary ecological footprint to the MacBook. To minimize this environmental impact, several measures can be implemented such as virtual meetings with the stakeholders, optimized workflows by using the computing resources wisely or using renewable sources of energy to power the devices. In conclusion, the environmental impact of this final thesis project is mainly restricted to the energy usage of the MacBook Pro, which is relatively low. 9.2.1.2 Ending phase In the final phase of the project, we reason about its environmental impact, focusing on energy use in the development phase. The MacBook Pro and a mobile phone, which was ultimately unused, were the main devices. The project took 448.25 hours, consuming approximately 1.152 kWh of energy, as estimated initially. Options to lessen the environmental impact seem limited, given the reliance on computing devices. Using a lower energy-consuming computer might only slightly reduce the impact. In conclusion, while some reductions in environmental footprint are challenging, future projects could consider more efficient devices or renewable energy to lessen environmental effects. 28 9.2.2 Product Lifetime 9.2.2.1 Initial phase In cybersecurity, the environmental impact of software is often an overlooked aspect. This part of the analysis aims to explore the environmental aspects associated with the lifetime of such an obfuscator in production. One of the primary problems in the environmental impact of an any obfuscator is its potential to increase computational work. Obfuscation techniques can make code less readable and harder to reverse-engineer and while this is useful for avoiding cyberattacks, it can also make the obfuscated code more computationally intensive to execute. This increase in computational workload can translate into greater CPU cycles and higher energy consumption of the mobile phones. That energy consumption can increase rapidly if the application is downloaded by a lot of users. Therefore, it can be a good choice for the obfuscator to try to achieve a balance between good obfuscation and energy efficiency, and that can be a diferentation about other obfuscators. It must be take in account that, even if in some cases the obfuscators generate more code, they also sometimes delete some unused code or make code that is more CPU performant, but it’s not always the case. In the other hand, maybe there is an indirect environmental benefit of robust obfuscation. Cybercrime, generates significant internet traffic and need some type of infrastructure to work, for example data centers which are themselves large consumers of electricity. A strong obfuscation tool can act as a proactive measure to deter cybercriminal activities by making it more challenging to reverse-engineer applications. 9.2.2.2 Ending Phase As we approach the concluding phase of the project, certain considerations come to the forefront, particularly regarding resource utilization and environmental implications. A primary focus is on the computation resources, inclusive of energy aspects, required for obfuscating programs and the obfuscation phase itself. When delving into specifics, the obfuscation process implemented in this project introduces an additional load, approximately five instructions per string obfuscated. However, due to the interpretive nature of the Java Virtual Machine (JVM) and the variability in JVM implementations, it becomes a 29 complex task to precisely quantify the increase in CPU cycles attributable to these additional instructions. Another aspect to consider is whether this project contributes to a decrease in the consumption of other resources. While it is evident that the project does not lessen computational resource usage — given it introduces additional instructions and thus increases overhead — it is noteworthy that employing tools like Proguard in the Shrinking and Optimization phase can enhance application performance. This enhancement might offset some of the initial increases in resource use, yet, it is important to acknowledge that, on the whole, the project is likely to contribute to a more pronounced ecological footprint. 9.2.3 Risks There exist certain scenarios where the project’s ecological footprint could expand. Primarily, this could occur if additional hours are required to finalize other obfuscation techniques, such as Constant Obfuscation and Control Flow Obfuscation. Implementing these techniques would demand more computational resources, thereby increasing energy consumption. Another minor risk involves potential bugs in the implementation of techniques like String Obfuscation. These bugs might lead to application crashes on devices or non ending loops that increase computational resources, but this risk is relatively insignificant and should not substantially affect the overall environmental impact. 9.3 9.3.1 Economic aspect PPP 9.3.1.1 Initial phase The economic aspect of the PPP can be found in the Finantial Management section 7. 9.3.1.2 Ending phase The economic aspect of the PPP after finishing the project can be found in the Final finantial summary in 7, including the justification in overcosts and in less features implemented. 30 9.3.2 Product Lifetime 9.3.2.1 Initial Phase For details on initial costs, please refer to Section 7 of this project. The economic aspect of the project is quite promising considering the demand for obfuscation services in the mobile application security ecosystem. While the project itself may not offer any advantages over existing solutions in terms of economics, its alignment with a cybersecurity startup that offers products in mobile security provides a unique opportunity for real-world implementation of the idea. By utilizing the already established infrastructure and resources at the startup I’m intering, the overhead costs for the project’s development can be mitigated substantially. At present, Android Obfuscators are generally created either by independent developers as open-source projects or by specialized companies offering them as a Service (SaaS), for example DexGuars [8]. These solutions vary in cost and effectiveness, with some offering only basic obfuscation techniques, while premium services may incorporate advanced algorithms to make reverse engineering considerably more challenging. Economically speaking, this project may not offer a significant advantage over existing solutions. However, the integration into an existing product suite of a startup specializing in mobile security provides a client base that can be used to offer it as a product. Moreover, by being a part of an existing product line focused on various aspects of mobile security in the company I’m intering: from server attestation, encryption utilites, RASP, etc. this obfuscator can be offered as an add-on or as part of a package, enhancing the startup’s value in the market. The software’s economic lifetime is directly tied to its effectiveness and ability to adapt to new reverse engineering techniques and the profitability of it. As this is a field that is continuously evolving, it will be necessary to allocate resources for ongoing research and development. 9.3.2.2 Ending phase The estimated lifetime cost of the project include the maintenance, and any upgrades or enhancements. This estimation can be drawn from the initial financial analysis presented in Section 7 and extrapolated over the projected lifespan of the project. Factors to consider include: • Finishing the Future work: Some features in the String Obfuscation 31 can be still developed and improved, also finishing the other obfuscation techniques that did not end up in the projet • Ongoing Maintenance: With the Programmer salaries one can calculate the annual maintenance costs, in regular updates and bug fixes. To have more the viability of cost reduction, we can try publishing this project in the internet as an open-source extension of Proguard to reduce costs associated with the Human Resources. 9.3.3 Risks The successful implementation and market acceptance of this project face several risks, which are critical to acknowledge and address. Even if this project does not aim to be production ready and was more of an academic an learning exercise, one never knows what can happen to it. The mobile application security ecosystem is highly competitive, with established players like DexGuard offering sophisticated obfuscation services. Also, the rapid evolution of this sector means that the project must continuously innovate to be relevant. Also, the field of mobile application development is rapidly evolving, with a growing trend towards web applications over native Android apps. This shift could lead to a reduced demand for Android-specific obfuscation tools. The project’s roadmap includes the development and improvement of various features, such as improving the String Obfuscation and other obfuscation techniques. There is a risk associated with the completion of these features, both in terms of technical feasibility and human resources that need inversting time and money. 9.4 9.4.1 Social aspect PPP 9.4.1.1 Initial Phase Doing a final thesis project in the field of cybersecurity is a pretty cool personal project for me. As an informatics student, my interest in cybersecurity was already there. I am concurrently interning at a startup that specializes in mobile device security so this with this mix between my academic research and practical real-world work I expect to increase my knowledge about cybersecurity a lot, especially in the area of reverse engineering. 32 I find reverse engineering intriguing, I already did some revere engineering to games and software I find curious in the past, and constructing an obfuscator offers a practical opportunity to have a more deep knowledge of it. In the work my supervisors and colleagues have been supportive and interested in the project’s progress, validating the project relevance and potential contributions to the field. Overall, the personal impact in me I think will make me happy (Hopefully) and teach me a lot in an area I’m interested. 9.4.1.2 Ending phase The journey through this project has been a cool learning experience, both personally and professionally. One of the key takeaways for me was gaining knowledge about the Java Virtual Machine (JVM), programming langauge/compilers, obfuscators, and the intricacies of code patching. These elements are central to my work in my intership in a cybersecurity company and this knowledge has enhanced my expertise in this area. On a personal level, this project taught me the importance of setting realistic goals. Initially, I intended to do a much larger-scale project. However, as I progressed, I realized that it was too ambitious and forced me to take the decision to change the focus of the project. This shift was important for the successful completion of this project. Although I did not manage to implement all the planned features, such as the Constants obfuscator and Control Flow obfuscator, I feel that what I have accomplished is really good. Professionally, the knowledge and skills acquired during this project will be surely used. In my career in cybersecurity, particularly in a role that involves working with obfuscators and focusing on mobile development using Java, the insights gained will be (and are being) applicable to my day to day job. The project has contributed to my professional growth and my personal development. 9.4.2 Product Lifetime 9.4.2.1 Initial phase The long-term impact in society of the Android obfuscator I am developing may be big and it extends beyond developers and security experts. In an era where data breaches and security incidents are common, robust application security 33 is really important. By offering a tool that makes it significantly challenging to reverse-engineer Android applications, it can empower developers and companies to protect intellectual property and sensitive data with low effort but big reward. The project contributes to the overall security of everybody that may use it, benefitting developers, large corporations and obviously final users. The company where I’m currently interning also will somehow be affected by this project. Firstly, the obfuscator may adds to the suite of security solutions they already offer, potentially attracting more clients and diversifying their product portfolio. Secondly, having an in-house solution for Android application obfuscation allows them to offer a more comprehensive package to clients looking for end-to-end mobile security solutions. Third and last, it will give me training which in the end will also benefit them at the end. In a world where mobile applications have such an importance in everyones life, security measures like this have a positive impact on user trust and society overall happiness. 9.4.2.2 Ending phase The impact of this project can extend my personal and professional growth. By making this tool available online and this thesis free to be read, it stands to benefit anyone interested in learning or in enhancing the security of their Android applications. The availability of an obfuscator democratizes access to security measures that benefit everyone in society. Moreover, the project directly contributes to the ongoing struggle against cybercrime. By complicating the reverse engineering process, it poses some hassle for cybercriminals, thereby protecting the integrity of applications and the sensitive data they may contain. In terms of solving the problem initially posed, the project has achieved its objectives; the core aim of making reverse engineering more challenging has been successfully met. 9.4.3 Risks About the risks in the society, I can’t see how this project should be bad for a particular segment of the population with good intentions. Also I don’t think this project creates a really big dependency, since other obfuscators exist in the market that are free and maybe even more complete 34 than mine. 10 Final Project Planification Initially, the project was focused on developing an obfuscation tool specifically for Android applications. However, as the project progressed, several key developments led to a strategic shift: • In-Depth Research on JVM and Android Infrastructure: The project required a lot of understanding of the JVM’s architecture, and before the strategic shift, about Android APKs, and the Android R8 tool. This research was more challenging than anticipated, shedding light on the complexity of the Android ecosystem. • Analysis of Existing Literature and Obfuscation Techniques: A review of existing literature on obfuscation techniques was conducted. This analysis provided valuable information but also highlighted how advanced were some of the techniques of current tools like the Control Flow Obfuscation. • Study of Proguard Codebase: A in depth study of the Proguard codebase was undertaken. The complexity encountered, specially in the code style and the previous knowledge to understand it was greater than expected, which impacted the project’s timeline and made me drop 2 of the 4 features planned (The Constants obfuscator and the Control Flow Obfuscator). Given these challenges and the time constraints, coupled with the wrong initial predictions of the technical knowledge needed, the project’s direction shifted towards developing a more generalized JVM obfuscator. This change was necessary to maintain project viability within the available time frame, also dropping the use of Gradle and it’s Android Plugin. The final implementation finally focuses on the Symbol Obfuscation and String Obfuscation techniques, which are nonetheless important and part of the project. 10.1 Cost Modifications The most significant changes in the project’s cost structure pertain to human resources. Although the total number of planned hours is close to the actual hours spent, the scope of work completed is less extensive than initially projected. This outcome can be attributed to several factors: 35 • Ambitious Initial Planning: At the project’s planification, the objectives set were highly ambitious. • Adaptability and Flexibility: As highlighted in section 7.6.1 of the GEP deliverable, there was a 30% likelihood of encountering significant challenges that could necessitate a change in objectives. The project plan was designed with flexibility in mind, understanding that not all features might be completed. • Realignment of Goals: The encountered obstacles and the subsequent shift in project focus needed modifications on the original objectives. This was needed because while reducing the scope of the project, it allowed for a more achievable and focused development process. • Efficient Use of Resources: Despite the reduced scope, the efficient allocation and utilization of human resources ensured that the project remained within the overall planned hours. In conclusion, the changes in the cost structure, primarily in human resources, shows the project’s adaptive nature that was already planned in the scope of this project. This changes allowed me to delivery of a functional JVM obfuscation tool and learn a lot from it. 10.1.1 Final Planning and Scheduling This final phase of the project involves a detailed planning and scheduling of what has been done, taking into account the strategic shift in focus and the realignment of goals. The project is now centered on developing a generalized JVM obfuscator, with emphasis on Symbol Obfuscation and String Obfuscation techniques. We can find the Gantt Diagram in 10.1.1 and the table with all the tasks and it’s hours used in 8. 36 38 Task Code PL-01 PL-02 PL-03 PL-04 RES-01 RES-02 RES-03 RES-04 STR-R STR-DI STR-T STR-DOC STR-REV SYM-R SYM-DI SYM-T SYM-DOC SYM-REV FINAL-01 FINAL-02 Total Hours Task name Scope Planning Costs and sustainability Final documentation Research about the JVM, APKs, and Android runtimes (R8) Research about Obfuscation techniques Getting Familiar with Proguard Codebase Research about Gradle String Obfuscator Research String Obfuscator Design and implementation String Obfuscator Testing String Obfuscator Documentation String Obfuscator Review Symbols Obfuscator Research Symbols Obfuscator Design and implementation Symbols Obfuscator Testing Symbols Obfuscator Documentation Symbols Obfuscator Review Code Cleaning and Refactoring Preparing the Final Layout of the Thesis and Presentation Preparations Full final thesis project Hours 24,50 8,25 9,25 18,25 17,00 20,00 115,00 12,00 20,00 49,00 10,00 6,00 2,00 30,00 30,00 5,00 5,00 2,00 15,00 50,00 448,25 Task dependencies PL-01 PL-02 PL-03 PL-04 RES-01 RES-02 RES-03 RES-01, RES-02, RES-02. RES-04 STR-R STR-DI STR-T STR-R STR-T SYM-R SYM-DI SYM-T SYM-R STR-DI, SYM-DI \* (ALL TASKS) 10.1.2 Modified Human Resources Costs In the table in 9, we can observe the final costs in human resources, comparing to what was planned in 3, we are using 73,88 € more than what was predicted. Table 9: Final costs of the project in Human Resources Activity PL-01 PL-02 PL-03 PL-04 RES-01 RES-02 RES-03 RES-04 STR-R STR-DI STR-T STR-DOC STR-REV SYM-R SYM-DI SYM-T SYM-DOC SYM-REV FINAL-01 FINAL-02 Human Resources Cost Cost 750,18 € 252,61 € 283,23 € 558,81 € 463,23 € 544,98 € 3.133,61 € 326,99 € 544,98 € 1.335,19 € 272,49 € 163,49 € 54,50 € 817,46 € 817,46 € 136,24 € 136,24 € 54,50 € 408,73 € 1.530,98 € 12.585,91 € Hours 24,50 8,25 9,25 18,25 17,00 20,00 115,00 12,00 20,00 49,00 10,00 6,00 2,00 30,00 30,00 5,00 5,00 2,00 15,00 50,00 448,25 Cost per hour 30,62 € 30,62 € 30,62 € 30,62 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 27,25 € 30,62 € Member P.M P.M P.M P.M PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG PROG P.M 10.1.3 Final finantial summary Since no other changes have been done in the project more than in the human resources, we can do the final finantial report of this project. As we can see in 10, we expected to use in 7 2.057,30 € more that what we end up using, but with the drawback that we end up not inclusing 2 obfuscation features and the Android Plugin. 39 Type of cost Human Resources Hardware Resources Software Resources Miscellaneous (Office, energy, etc.) Contingency Unexpected Events Total Cost 12.585,91 € 106,02 € 5,00 € 660,00 € 0,00 € 0,00 € 13.356,93 € Table 10: Full economical resources used in the project 40 11 Obfuscation and the JVM, an introduction Software distribution can came in a lot of forms, it can contain most of the original source code information, like Javascript which is interpreted at runtime, or differ significantly from the original source, like C code compiled for a specific CPU architecture. Java bytecode, which is central to this project, is a notable example that falls in between these two extremes. Due to its nature, Java bytecode is relatively easy to decompile, raising the risk of malicious reverse engineering by threat actors. No matter the software, a skilled programmer with enough time, effort, and access can reverse engineer it. This process involves decompiling the software (using tools like disassemblers or decompilers) to analyze its data structures and operational logic. An obfuscator is a tool designed to transform a program into a version that is harder to comprehend and reverse engineer. This project references the paper ”A Taxonomy of Obfuscating Transformations”[4], which discusses obfuscation reasons and details various code transformations for obfuscation. This project aims to explore different methods for protecting software against reverse engineering, particularly focusing on the Java Virtual Machine (JVM). We’ll look at applying obfuscation to the JVM, develop a small proof of concept, and discuss the tools used for this purpose. 11.1 Overview of Existing Obfuscation Techniques In this section, we will provide a high-level overview of the types of obfuscation techniques that exist as concepts and explaining its essence. These techniques in [4] can be categorized into four main types: Layout Obfuscation, Data Obfuscation, Control Obfuscation, and Preventive Transformations. • Layout Obfuscation: This type involves modifying the non-executable parts of the code, such as formatting and comments. Simple transformations include removing white spaces, newlines, and comments which are usually present to make the code readable and more complex transformations can be hiding the symbols (function names, class names, etc.) of the executable. Layout obfuscation does not significantly increase the difficulty of understanding the executable part of the code, it does remove clues that might be useful for someone trying to reverse-engineer the logic. 41 • Data Obfuscation: This technique focuses on the way data is stored and manipulated within the application. Data obfuscation can involve changing the encoding of data or altering variable lifetimes, data structures and their operations less intuitive. For example, scalar variables might be merged, strings can be constructed on runtime instead of declared statically or object-oriented features like inheritance can be modified to change the true nature of the Classes. • Control Obfuscation: These transformations aim to obscure the flow of control within the application. It can range from inlining and outlining methods to inserting redundant or dead code. This not only confuses the control flow analysis during a dynamic analysis reverse engineering attempt but can also introduce false code paths, making some automated static analysis tools less effective. • Preventive Transformations: Unlike the other types of obfuscation that aim to make code difficult to read and understand, preventive transformations are designed to avoid automatic deobfuscation and decompilation techniques. This includes exploiting weaknesses in decompilers or making changes that counter known deobfuscation algorithms. Each of these obfuscation techniques offers different levels of potency and resilience against reverse engineering, and they are often used in combination to provide a more effective obfuscation solution. The choice of which techniques to use can depend on the balance between the desired level of obfuscation and the performance overhead introduced by these transformations. For the context of this project and the Java Virtual Machine, some metadata is added in the executable files (the .class files), for example for debugging reasons, a mapping of instruction offsets and original source code line numbers is present and can be removed, so the removal of this type of information can be categorized as layout obfuscation. Also, data like constants of the executable is contained in the .class file, so someone who is interested in hiding these values through data obfuscation, would have to “modify” this zone. 12 The JVM The Java Virtual Machine (JVM) is what its name essentially means, a virtual machine, more concretely a virtual CPU with its own memory management and set of instructions. Being more specific, it’s an specification of a machine that one is free to implement, that’s the reason why different implementations 42 of the JVM exist, with different types of uses, optimizations and performance needs (for example the JVM used in Android are called Dalvik and more newly ART, for server side code Open JDK [9] or HotSpot JVM [10] or for low end devices Avian JVM[11]). For the context of this project, the Java SE 7 Edition is used to comment it and reference it, but future versions are backward compatible so the same concepts should apply. This architecture is essential to Java’s ability to promise the ”write once, run anywhere.” It serves as an intermediary, processing the Java instructions (more commonly called bytecode) into a runtime executed language that only needs the executable of this virtual machine of the real CPU where it is ran to work. This feature is key to Java’s widespread use, as it allows Java programs to run on various devices without needing modification. Another important feature of the JVM is that is language agnostic, meaning that, even if designed for the Java language, other languages that output valid JVM bytecode and conform to the .class file structure can be designed and already exist with a lot of relevance (e.g., Kotlin or Scala) 12.1 The JVM architecture Unlike traditional CPUs, which are register-based, the JVM is a LIFO stackbased machine, where intermediate arithmetic-logical instructions or function calls to be executed need to push the arguments in the stack and in return values can be pushed. A simple example of this behavior can be a simple addition instruction of two integers 1, where 2 integers (4 and 1) are in top of the stack and after the execution of the iadd [12] instruction, both values have been popped and the result of the addition is pushed in top of the stack. To understand the use of methods (passing parameters and returning values) one can use this same concept but being the JVM used mainly for the Java language, an object-oriented language, a reference to an object/interface/array [13] is returned in that case; for example, when constructors are called. In the JVM, every thread gets its own stack [14]. These stacks hold frames [15], similar to the concept of a context in traditional CPU architectures code execution, these frames indicate a new context of a method/function. These frames store: • The local variable table that is represented as an array and where each entry has a value of a local variable defined in code, and can be 43 Figure 1: Stack status example after the iadd instruction. accessed with some JVM bytecode instructions 12.3.2 to operate over it. • The operand stack context where intermediate values used by the JVM, for example the 4 and 1 values in the iadd example • The return values of called functions • The management data structure needed for the dynamic linking of other classes needed by that class • A reference to the runtime constant pool of the actual class. The runtime constant pool is a data structure that, as its own name indicates, it contains a pool of constants: Predefined strings, integers, floats, and class metadata. This will later be explained in the .class file structure section of this document 12.2 but is of high relevance for the objective of building an obfuscator for the JVM. At the same time, at runtime a program counter (PC) [16] pointing to the current bytecode instruction is maintained for the current method in execution, and control flow of the program is modified with this virtual register to point to different instruction offsets. 12.2 The .class file format The class file [17], produced by the compilation process of JVM-targeted programming languages, is a binary file containing the compiled source code in a format that the JVM can execute and use to construct its internal data structures. 44 Primarily, a class file contains the bytecode of the source code that the JVM will parse and execute. The file also includes a constant pool, an important collection of constants such as method and field references. The role of this pool is significant in Java’s dynamic linking, enabling the JVM to resolve references to classes, methods, and fields at runtime. In addition to bytecode, the class file encapsulates metadata about the class itself. This metadata includes information like the class version, its fields, methods, interfaces, access flags (private, public, static, final, etc.) and various types of metadata. This structure ensures that the JVM has all necessary instructions and resources to execute the programs. This is what the ClassFile structure looks like 1: Listing 1: Class file structure 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; } The u1, u2 and u4 should be interpreted as unsigned integers of byte size of 1, 2 and 4 respectively. The types cp_info , field_info , method_info and attribute_info should be interpreted as unions with different types of structures depending on a tag. 45 12.2.1 The constant pool The constant pool in the class file is an array of cp_info 2 entries with a length of consant_pool_count-1 , with max size of 21 6 − 1 since it’s of u2 size. When initializing the JVM, this table is used to construct the runtime constant pool, what is equivalent to the symbols table in other executable file formats. Each item of the constant pool haves the following structure: Listing 2: cp_info structure 1 2 3 4 cp_info { u1 tag; u1 info[]; } And the tag value indicates different types of structures depending on its value, in the 11 table extracted from [18]. Table 11: Constant pool tags Constant Type Value CONSTANT_Class CONSTANT_Fieldref CONSTANT_Methodref CONSTANT_InterfaceMethodref CONSTANT_String CONSTANT_Integer CONSTANT_Float CONSTANT_Long CONSTANT_Double CONSTANT_NameAndType CONSTANT_Utf8 CONSTANT_MethodHandle CONSTANT_MethodType CONSTANT_InvokeDynamic 7 9 10 11 8 3 4 5 6 12 1 15 16 18 Here’s a brief explanation of each tag type: • CONSTANT_Class: Represents a class or an interface. The info[] 46 contains an index to a CONSTANT_Utf8 entry inside the constant_pool that points to the name of the class containing a fully qualified name of a class or interface. • CONSTANT_Fieldref: Describes a field in a class or interface. The info[] contains indices to the constant pool pointing to a CONSTANT_Class entry representing the class or interface of that field and a CONSTANT_NameAndType entry with the name and descriptor of the field. • CONSTANT_Methodref: Similar to CONSTANT_Fieldref , but for class methods. The info[] includes indices to CONSTANT_Class and CONSTANT_NameAndType entries representing the class and the name and descriptor of the method. • CONSTANT_InterfaceMethodref: Like CONSTANT_Methodref , but for interface methods. • CONSTANT_String: Represents a constant string object. The info[] contains an index to a CONSTANT_Utf8 entry that contains the string value. • CONSTANT_Integer: Represents a signed integer constant. The info[] contains the bytes of the integer value. • CONSTANT_Float: Represents a float numeric constant. The info[] contains the bytes of the float value in IEEE 754 floating-point single format. • CONSTANT_Long: Represents a long numeric constant. The info[] contains the 8 bytes of the long value. • CONSTANT_Double: Represents a double numeric constant. The info[] contains the bytes of the double value in IEEE 754 floatingpoint double format. • CONSTANT_NameAndType: Contains a name and a descriptor. The info[] includes indices to two CONSTANT_Utf8 entries; one for the name and one for the descriptor. • CONSTANT_Utf8: Contains a string in UTF-8 format. The info[] includes the length of the string followed by the string itself. • CONSTANT_MethodHandle: Represents a method handle. The info[] contains information about the kind of method handle and an index to a CONSTANT_InterfaceMethodref , CONSTANT_Fieldref 47 or CONSTANT_Methodref . It is mainly used in the resolution of symbols [19] in the JVM runtime when executing some instructions like ( getfield , getstatic , instanceof , invokedynamic , invokeinterface , invokespecial , invokestatic , invokevirtual ). • CONSTANT_MethodType: Represents a method type. The info[] contains an index to a CONSTANT_Utf8 entry containing the method descriptor. • CONSTANT_InvokeDynamic: Used for support of the invokedynamic instruction. The info[] contains indices to a bootstrap method in the attributes[] array in the class file and a CONSTANT_NameAndType entry describing the method. To better understand the constant pool one can, use some tools to navigate this file format, in this case I will use the code in A.1, a Main.java file with a Main class that we are going to use accross the project as dummy code. And using javap -c -v -private [class_name] , a built-in java disassembler that comes with OpenJDK to the compiled .class file, we can see its constant pool in the appendix in the Constant Pool section in B.1 As we can see, for such a small program, the constant pool already has a lot of values, and all of them reference each other in a tree-like structure of references, where UTF-8 strings are at the leaves. One can easily see why this data structure is important for the purpose of building a code obfuscator: class names, variables and members of a class, constants like Strings and Numeric values reside in this section and give a lot of important information to reverse engineers. With some caution one could simply modify the names of symbols like variables and class names and call that “symbol obfuscation” as a type of layout obfuscation, always following the rules that lie inside the class file specification. 12.2.2 Fields array The fields[] array is a collection of field_info 3 entries. Each field_info entry has the following structure: Listing 3: field_info structure 1 field_info { 48 2 3 4 5 6 7 } u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; As a summary of what these fields contain: • access_flags: This is a set of flags that denote the access permissions (such as public, private, protected) and properties (like static, final, volatile) of the field. • name_index: An index into the constant_pool array, pointing to a CONSTANT_Utf8 entry. This entry contains the name of the field. • descriptor_index: Another index into the constant_pool array, this time pointing to a CONSTANT_Utf8 entry that describes the type of the field. • attributes[attributes_count]: An array of attribute_info structures providing extra information about the field. Common attributes include ConstantValue for fields with constant initializers, and Synthetic or Deprecated markers. We can see an example 2 of the Fields Array of the code in the Alumno example class (A.2). For this the BinaryInternalsViewer [20] program is used, which is useful for this purpose of file visualization. As we can observe in 2, the fields array has three entries that correspond to the three fields of the Alumno class. For the purpose of this project, the most important part is the name_index part, that we can use to change the name of the constant pool entry pointed by the field entry of this class to perform symbol obfuscation. We will see more in detail in the 14 section of this project on how to do it programmatically. 12.2.3 Methods array The methods array in the class file format of the JVM is the one that stores compiled method information. This part is one of the most central parts of the JVM’s class file structure because each entry in the methods array represents a method in the class and those classes contains the actual bytecode with the 49 Figure 2: Fields array of the Alumno class in BinaryInternalsViewer program instructions. A method entry in the methods array is defined by the method_info structure 4. The method_info entry consists of the following structure: Listing 4: method_info structure 1 2 3 4 5 6 7 } method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; As a summary of the fields in the method_info structure: • access_flags: Specifies access permissions (public, private, etc.) and properties (abstract, static, etc.) of the method. • name_index: An index to the constant pool of type CONSTANT_Utf8_info where the method’s name is stored. • descriptor_index: An index to the constant pool’s descriptor of the method of type CONSTANT_Utf8_info , indicating the method’s 50 signature (return type, parameters). • attributes[attributes_count]: An array of attribute_info structures providing extra information about the method. We can see how this structure can be useful for layout obfuscation purposes, in the case of this project to hide the names of the methods that give a lot of information to reverse engineers. The attribute_info array can include various types of attributes like Code , Exceptions , LineNumberTable , LocalVariableTable , and others, which provide details necessary for method execution and debuggers. The Code attribute in the attribute_info array in methods[] is particularly relevant for data obfuscation purposes, especially when it comes to string obfuscation and bytecode instruction patching. This attribute contains the actual bytecode of the method, including bytecode instructions, the method’s bytecode length, local variable table, and the operand stack. If correctly manipulated, this structure can allow us to hide strings by generating code that constructs the strings at runtime, for example. Beyond string obfuscation, more complex obfuscation techniques might involve patching bytecode instructions directly within the Code attribute. This can include: • Instruction Substitution: Replacing certain bytecode instructions with others while maintaining functional equivalence. • Control Flow Obfuscation: Modifying the method's control flow to make reverse engineering more difficult, often by adding redundant or misleading instructions. • Dead Code Insertion: Injecting non-functional bytecode that does not affect the method's outcome but complicates decompilation. We can see an example in 2 with the Methods Array of the code in the Alumno example class (A.2). For this the BinaryInternalsViewer [20] program is used again. In 3, we can observe that all the methods of the Alumno class are in there, and one additional one called <init> , which is given by the compiler to the constructors classes they process as mentioned in [21]. In 4 we see the fields previously mentioned like access_flags , name_index pointing to a constant pool entry, etc. More importantly, in the attibutes 51 Figure 3: Methods Array of the Alumno class Figure 4: Methods Array initialization and getId methods) array the Code attribute appears 5. The inner structure of the Code is the following one [22]: Listing 5: Code_attribute structure 1 2 3 4 5 6 7 8 9 10 11 12 13 Code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length]; 52 14 15 16 } u2 attributes_count; attribute_info attributes[attributes_count]; • attribute_name_index: Links to the constant_pool table with the value ”Code”. This string indicates that this is a Code attribute. • attribute_length: Specifies the total length of the attribute, not including the attribute_name_index and the attribute_length . • max_stack: Determines the maximum depth of the operand stack during method execution. • max_locals: Indicates the count of local variables, in the local variables, we also count those for the methods parameters. • code[code_length]: Contain the actual bytecode of the method. • exception_table[exception_table_length]: Detail specifics of the exception handlers. • attributes[attributes_count]: Contains additional attributes in the Code attribute. (Yes, Attributes can have Attributes) Regarding the max_stack value, this value is calculated on compile time and it’s useful for pre allocating the space of the operand stack and memory safety. This can be calculated knowing if an JVM instruction adds, subtracts or does not modify the operand stack and when creating the bytecode having a counter that keeps track of the operand stack depth. This means that, if in our obfuscator we do modify the code[] array, we must keep track of the changes that we do in the operand stack when doing so. 12.3 The JVM instruction set The JVM instruction set is the integral part of how Java bytecode is executed. The instruction set operates on a stack-based architecture and it also operates in other data structures contained in the frame of the actual method: the variables table and the runtime constant pool. Bytecode instructions take one byte for representing the opcode, and depending on the opcode the instruction can have more bytes to it if needed for passing arguments to that instruction. In the JVM instruction set, of the possible 256 opcodes available with one byte, 200 opcodes are used in the specification. For the scope of this project, 53 not all of them will be explained in here, but all of them are available in [23]; instead we can focus in specific categories of instructions that are useful for the obfuscation purposes of this project. The classification on the bytecode instructions are based on the following talk given by Charles Nutter from Oracle [24]. 12.3.1 Constant values In this section we are going to simply show what instructions exist regarding constant values, for example for pushing integers for later use them in method invocation or simply for some arithmetic operations. Value 0x01 0x02-0x08 0x09-0x0A 0x0B-0x0D 0x0E-0x0F 0x10 0x11 0x12 0x14 Mnemonic aconst_null iload_[m-5] lconst_[0,1] fconst_[0,1,2] dconst_[0,1] bipush sipush ldc ldc2_w Description Push null on stack Push integer [-1 to 5] on stack Push long [0 or 1] on stack Push float [0.0, 1.0, 2.0] on stack Push double [0.0, 1.0] on stack Push byte value to stack as integer Push short value to stack as integer Push 32-bit constant to stack (int, float, string) using a runtime constant pool index to reference it. Push 64-bit constant to stack (long, double) using a runtime constant pool index to reference it. Table 12: Constant Bytecode Instructions and Descriptions In 12 we can observe that some of the instructions are too specific, pushing integers in the range of -1 and 5 may not seem useful because you can do it all with the ldc and ldc2_w instructions, but this was done for the purpose of file size optimization, in a context where data transmission over the internet was much slower that nowadays. Is in these instructions where the whole picture starts to make sense: that is the reason why in the 12.2.1 section the CONSTANT_String , CONSTANT_Integer , CONSTANT_Float , CONSTANT_Long and CONSTANT_Double constant types exist; because the Java compiler would collect them (if they are not the ranges of the specific value instructions) and create constant pool entries with predefined code constants. 54 As a little example in 5, we can use the disassembled version of the Main class in A.1 whose full bytecode instructions are in the B.1 extracted with javap for the Main method of this class. In the offset 8 we can see how this instructions aim to create an instance of the Alumno class with a name and an age and the stack status after each of these instructions: Listing 6: Little bytecode snippet from the Main class from A.1 and B.1 1 2 3 4 5 8: new #21 // class org/example/Alumno 11: dup 12: ldc #23 // String Fran 14: bipush 23 16: invokespecial #25 // Method org/example/Alumno."<init>":(Ljava/lang/String;I)V Figure 5: Stack state after each bytecode instruction. 12.3.2 Local Variable Instructions In this section, we will explore instructions that interact with the local variable table that is inside a frame, as explained in 12.1. These instructions 13 are used mainly for manipulating and accessing data stored in the local variables table within a method frame. The local variable table is basically a fixed size array with values indexed starting from 0. This table contains the values of local variables declared in the Java code, normally in order of appearance in the code. 55 As a little detail, the this Java keyword that is used in instance methods to operate over a single instance is always at index 0 as a reference value of the Local Variable table. The correspondence of these values to identifiers/symbols and types is done with the LocalVariableTable [25] and LocalVariableTypeTable [26] Attributes, which are optional on the Code structure 5. Since they are optional because the bytecode only cares about the local variable table indices, we don’t have to include the LocalVariableTable Attribute then to hide the local variables symbols. 12.3.3 Stack Operations Since the JVM it’s a stack based machine, there is need for stack operation juggling for moving values along the stack (operations like popping, duplicating, swapping, etc.) 14. We can already find an example of that in 5 when the dup instruction is called to duplicate the object reference to the same Alumno instance that in another case would be deleted from the stack and not accessible anymore since invokespecial pops the values from the stack after called. 12.3.4 Classes and method invocation In the JVM, method invocation and class operations are an esential part of it, since it is focused on an OOP style of programming, it has to have bytecode support for it. Also, these operations are vital for understanding how obfuscation works at the bytecode level, especially for this project where bytecode patching will be needed for the String obfuscation. Creating new instances of classes and accessing their fields are essential operations in Java for its OOP paradigm. The new opcode is used to create a new instance of a class. 1 For instance, in the bytecode snippet in 5, where new is used to create an instance of the Alumno class. Also, getfield and putfield can be used to access and modify the fields of objects, respectively. The JVM also provides several opcodes for method invocation: invokevirtual , 1 Actually, the instance after the new instruction is pushed to the stack as a methodref , and then you need to call with invokespecial the <init> method, also known as the constructor for that instance. So creating the object and constructing it are two different atomic instructions. 56 Value 0x15 Mnemonic iload 0x16 lload 0x17 fload 0x18 dload 0x19 aload 0x1A-0x2D Packed loads 0x36 istore 0x37 lstore 0x38 fstore 0x39 dstore 0x3A astore 0x3B-0x4E 0x84 Packed stores iinc Description Load an integer from a local variable onto the stack. Load a long value from a local variable onto the stack. Load a float value from a local variable onto the stack. Load a double value from a local variable onto the stack. Load a reference from a local variable onto the stack. Compact instructions like iload_0, aload_3, etc., for loading data from local variables. Store an integer from the stack into a local variable. Store a long value from the stack into a local variable. Store a float value from the stack into a local variable. Store a double value from the stack into a local variable. Store a reference from the stack into a local variable. Compact instructions like fstore_2, dstore_0, etc., for storing data into local variables. Increment a specified integer local variable by a given value. Table 13: Local Variable Bytecode Instructions and Descriptions invokespecial , invokestatic , invokeinterface , and invokedynamic . Each of these serves a specific purpose: • invokevirtual is used to invoke instance methods, they need a reference to the object at the bottom at the stack and arguments are pushed starting from the left and ending to the right. We can see an example for it in the 5, even if the invokespecial #25 is a different instruction, it follows the same pattern for passing arguments. The return value, if there is one, is pushed to the stack. 57 Value 0x00 0x57 0x58 0x59 0x5A 0x5B 0x5C 0x5D 0x5E 0x5F Mnemonic nop pop pop2 dup dup_x1 dup_x2 dup2 dup2_x1 dup2_x2 swap Description Do nothing. Discard top value from stack Discard top two values Duplicate and push top value again Dup and push top value below second value Dup and push top value below third value Dup top two values and push ...below second value ...below third value Swap top two values Table 14: Stack Manipulation Bytecode Instructions • invokespecial is particularly important for invoking instance constructor methods ( <init> ) and private methods. We can see a real example in 5 and 6. The return value, if there is one, is pushed to the stack. • invokestatic is used for invoking static methods of a class. It works in the same way the invokevirtual and invokespecial in passing arguments, but since it’s not binded to an specific instance (it’s a static method), the instruction comes with 2 bytes indicating a CONSTANT_Methodref index of the constant pool 12.2.1 of the static method class and signature. • invokeinterface is for invoking methods declared by interfaces. Works similar to the invokevirtual and invokespecial instructions since is binded to an instance. • invokedynamic is introduced in Java 7 and supports dynamic language features. This instructions will be used on the string obfuscation and can be used for other purposes like Java code instrumentation for example. Code patching involves modifying the bytecode during the obfuscation process to alter the behavior of the program, in this project specifically will be used to construct strings in run time. This process requires careful manipulation of method calls and class instances and in taking care of the stack. We can see the instructions in table 15. 58 Value 0xB2 0xB3 0xB4 0xB5 0xB6 0xB7 0xB8 0xB9 0xBA Mnemonic getstatic putstatic getfield setfield invokevirtual invokespecial invokestatic invokeinterface invokedynamic 0xBB 0xC0 0xC1 new checkcast instanceof Description Fetch static field from class Set static field in class Get instance field from object Set instance field in object Invoke instance method on object Invoke constructor or ”super” on object Invoke static method on class Invoke interface method on object Invoke method dynamically on object (Java 7) Construct new instance of object Attempt to cast object to type Push nonzero if object is instanceof specified type Table 15: Field and Method Manipulation Bytecode Instructions 13 Proguard After briefly talking in the past section about the theory of the Java Virtual Machine, its operations and inner workings, it’s time to get our hands dirty and do something with this knowledge. 13.1 What is Proguard? Proguard is an open-source tool used for Java code optimization and obfuscation. It functions by shrinking, obfuscating, and optimizing bytecode. This tool aimed to reduce the size of applications, protect them against reverse engineering, and can sometimes improve their performance. Proguard can be used for Java applications in various platforms, including desktop, server-side, and particularly it was widely used in mobile app development. Proguard was conceived in the early 2000s by Eric Lafortune as a hobby project [27]. Officially launched on 2002, it initially offered basic functionality like shrinking (tree-shaking) and name obfuscation, later incorporating bytecode optimization in 2004. What started as a small-scale project gradually evolved into a known optimization tool for Java, particularly after its integration into the Android SDK in 2010 with Android 2.3 Gingerbread, until the appearance of the Google own shrinker and obfuscator R8, which was made to be a 59 drop-in replacement that was even compatible to the configuration files used by Proguard. In the following years, Proguard started the development of further tools and companies. For instance, Dexguard, a more advanced (contains control flow obfuscation, string obfuscation, hiding resources, etc.) but closed source tool built on top of Proguard’s base in 2012. Additionally, Guardsquare, the company that sell the use of DexGuard was founded in 2014, offering also other security solutions. Proguard’s integration into the Android SDK was a turning point, establishing it as the de facto tool for code shrinking and obfuscation in Android development. This integration allowed developers to efficiently reduce the size of their Android applications and protect them from reverse engineering. As an open-source tool, Proguard is a valuable opportunity for learning and extending its functionalities. The project’s source code is publicly available on Github [28], and would allow us to understand its inner workings and apply this knowledge to enhance the tool to specific needs of this project; in this case, extending Proguard to include String obfuscation and modify the already existent symbol obfuscation techniques. 13.2 Proguard architecture and functionalities Proguard operates through a pipeline type of process with 4 key steps: shrinking, optimizing, obfuscating, and preverifying. [29]. • Shrinking: The primary step in Proguard’s workflow is shrinking. Here, Proguard analyzes the bytecode to identify and eliminate unused classes, fields, methods, and attributes. This step is used for reducing the application’s size and removing redundant elements that do not contribute to its functionality. • Optimizing: Following shrinking, Proguard optimizes the bytecode. This optimization includes a range of modifications, for example the removal of unused instructions, making classes and methods private, static, or final, and inlining methods. These optimizations contribute to the space efficiency and performance of the application. • Symbols Obfuscation: The third step involves symbol obfuscation. In this phase, Proguard renames the packages, classes, fields, and methods using short, meaningless names. This process aids in protecting the application from reverse engineering by making it challenging to 60 interpret the code structure and logic and it’s the one we want to modify in this project to be more complex instead of using short names. • Preverification: The final step in Proguard’s process is preverification. This involves checking the correctness on the already modified classes,. Preverification ensures that the bytecode adheres to the Java platform’s standards, for example, one thing that is verified in here is that the values in the Code attribute, the stack_max value are valid, or that the types of items in the stack are valid when passing parameters in a method. This is a technique called partial evaulation, sometimes used in compilers. Proguard begins by reading the input jars, which can be in various formats such as .jar s, .zip s, or .apk s, or directories with .class files. After processing through the steps mentioned above, Proguard writes the processed results back to one or more output files in similar formats. This flexibility in handling different file types makes Proguard adaptable to various project needs, for example for Java server code one can use a .jar and in case of an Android application it can use the .apk . To determine what parts of the code to preserve or obfuscate, Proguard requires the specification of entry points, such as classes with main methods, applets, or Android activities. These entry points guide Proguard in identifying the necessary code components, ensuring that the final application functions as intended. Since Proguard is quite a complex program, it uses a configuration file as an input where you can pass arguments to it to modify its behaviour, indicate input files, output files and mark the entry points. One executes the program like this: java -jar proguard.jar @configuration_file.pro , we can see an example of the configuration file in the Appendix, Proguard configuration file A.3. 13.2.1 The proguard configuration file The configuration file for Proguard is quite intuitive, yet a few key parameters are worth mentioning. Primarily, the -injars and -outjars options are used for specifying the programs to be obfuscated. They determine the input and output JAR files respectively. Another important parameter is -libraryjars , which directs Proguard to the Java Runtime Environment (JRE), essential for locating Java library functions. Another notable feature is the -printmapping out.map argument. This 61 functionality is particularly valuable for debugging: it records how symbols are transformed during the Symbol Obfuscation process. In cases where one might prefer not to perform certain modifcations like obfuscation, optimization, shrinking, or preverification because they are enabled by default, they can be disabled using the -dontobfuscate , -dontoptimize , -dontshrink , and -dontpreverify commands, respectively. To mark the entry points of our code (Main program, Android Activities, etc.), we can use the -keepclasseswithmembers 7 or similars like -keepclassmembers 8, and with regex we can match all public classes that have a main method, or preserve some class members that don’t need symbol obfuscation. Listing 7: -keepclasseswithmembers example from the Proguard configuration file in A.3 -keepclasseswithmembers public class * { public static void main(java.lang.String[]); } Listing 8: -keepclassmembers example from the Proguard configuration file in A.3 -keepclassmembers class * implements java.io.Serializable { static final long serialVersionUID; static final java.io.ObjectStreamField[] serialPersistentFields; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); } This regex functionality is really useful for other arguments, for example the -keepattributes *Anotation* keeps all the Attributes that contain the Anontation string. ( RuntimeVisibleAnnotations , RuntimeInvisibleAnnotations , RuntimeVisibleParameterAnnotations , etc.). We can compare the full javap -c -v -private output of the code for the Main class in A.1 to it’s javap output B.1 and the one with the Proguard changes for the javap command in B.2 using the configuration file in A.3. 62 13.2.2 Proguard-core Guardsquare introduced Proguard-core in 2020 as a tool used in the Proguard obfuscator. This tool primarily modifies .class files and other files related to the Java Virtual Machine (JVM), so technically Proguard builds on top of the Proguard-core library. The tool includes data classes that accurately mirror the binary file specification, allowing for a detailed representation of the file structures inside the code. They are minimalist in essence, having only essential methods, but most importantly including several accept methods facilitating operations by visitor classes using the visitor pattern. It also contains classes implementing visitors designed to read from files, write to files, modify the Class and inner attribute values such as methods, classes, attributes, and fields. Other feature is its ability to recognize instruction patterns in method bytecode, aiding in bytecode instruction patching. Also, the tool can conduct abstract code evaluations, including partial evaluations, which are useful for advanced code analysis and optimization purposes. Proguard-core’s architecture comprises a lot of small classes, to the point where even loops and conditional statements are encapsulated within individual classes. While these classes might appear verbose and redundant, they contribute to a modular code base that is heavily based on the visitor pattern. This library extensively uses the visitor pattern in its operations. Visitor classes are designed to perform a range of operations on data: reading, editing, transformation, and analysis, among others. These classes typically include various visit methods to interact with data classes of corresponding basic types. For instance, a Java class contain a constant pool with different types of constants: integers, floats, strings, etc. The data classes like IntegerConstant , FloatConstant , StringConstant , are all derived from the Constant base class. Then, a ConstantVisitor interface includes methods like visitIntegerConstant , visitFloatConstant , visitStringConstant , etc, allowing diverse operations on these constants. The thinking behind for this design is the stability of data classes: Since the data classes are not going to change any time soon, it does make sense to keep the data classes untouched and mirroring the JVM specification and not adding methods for all of them that would make them really complex, the is better to add operations declaring Visitors and using the visitor interface. 63 The visitor pattern in Proguard-core involves using interfaces to process elements within a data structure, with each interface having multiple implementations. We can see an example in the code snippet below, that pertains to the Preverifier.java class inside Proguard. The library classes heavily use constructor-based dependency injection, which can be seen as commands that are chained in an immutable structure, via constructors. Listing 9: Snippet code from the Preverification step in Proguard’s pipeline 1 2 3 4 5 6 7 /** * Performs preverification of the given program class pool. */ @Override public void execute(AppView appView) { logger.info("Preverifying..."); 8 // Clean up any old processing info. appView.programClassPool.classesAccept(new ClassCleaner()); 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 } // Preverify all methods. // Classes for JME must be preverified. // Classes for JSE 6 may optionally be preverified. // Classes for JSE 7 or higher must be preverified. appView.programClassPool.classesAccept( new ClassVersionFilter(configuration.microEdition ? VersionConstants.CLASS_VERSION_1_0 : VersionConstants.CLASS_VERSION_1_6, new AllMethodVisitor( new AllAttributeVisitor( new CodePreverifier(configuration.microEdition))))); As an example we can see the execute() method of the Preverifier.java class within Proguard-core in 9, the primary function is to preverify a given program class pool, an essential step in ensuring that we have valid Java bytecode and we are not messing the stack at any point. The method takes a AppView appView as an argument. This appView is a data structure containing all the class-related information that Proguard needs to process. Specifically, it contains the programClassPool , a collection of 64 Clazz objects, each representing a Java class file 12.2. The core of the method relies heavily on the visitor pattern; in this case, the method iterates over each class in the programClassPool and applies various visitors to them. The first (Class)visitor applied is the ClassVersionFilter . This filter determines which classes should be processed based on their version. The version threshold is set based on whether the configuration is for the Micro Edition or Standard Edition (JSE) Java. After filtering, each class undergoes further processing. The AllMethodVisitor (A ClassVisitor) is applied, iterating over all methods in each class. This visitor is wrapped inside the AllAttributeVisitor (A MemberVisitor, so it encompass both fields and methods), which then applies the CodePreverifier (an AttributeVisitor) to each method’s Code attribute. The CodePreverifier checks the bytecode of each method, ensuring that the stack and typing are correct. The chaining of visitors, as seen in the nested structure of the method call in 9, demonstrates what we said earlier about the command-like pattern. It allows for a modular approach where different operations can be sequenced or combined in a variety of ways. By the end of the execute method, every class in the programClassPool has been filtered, its methods and attributes have been iterated over, and the bytecode has been preverified for compliance with the JVM use. 13.3 Proguard use example To give an example of what Proguard does at low level, we are going to pass it to the Alumno and Main classes in our dummy project (A.2 and A.1). This will also be useful to compare how it behaves when trying our own extension of Proguard. When passing Proguard to our programs, first things we see in the .jar file when extracted with zip , is that the name of the Alumno class has changed 10. The no directory is the one with no obfuscation, and the yes directory is the one obfuscated with Proguard. Listing 10: Output of the file system directories left after unzipping the .jar of the unobfuscated jar and the Proguard obfuscated jar 1 $ tree -A yes no 65 2 3 4 5 6 7 8 9 10 11 12 13 14 15 no��� META-INF���� MANIFEST.MF��� org��� example��� Alumno.class��� Main.class yes��� META-INF���� MANIFEST.MF��� org��� example��� Main.class��� a.class As we can see, the Alumno.class file has been renamed as a.class , but since in A.3 we are using the -keepclasseswithmembers argument indicating to not obfuscate classes with public static void main(java.lang.String[]) , the Main.class is left unmodified in the class name and main method. If we go inside the javap -c -v -private output on both files in B.1 (without Proguard) and B.2 (with Proguard), we can directly appreciate some differences: 1. File Size Reduction: The size of the class file has been reduced from 1244 bytes to 875 bytes. Proguard performs optimizations, as said before, by removing unused instructions and constants. 2. Obfuscation of Class Names: The class org.example.Alumno has been renamed to org.example.a . This an example of with Proguard’s symbol obfuscation, which are not explicitly specified in the configuration file but is done by default. 3. Obfuscation of Method Names: Method names are simplified to a , b , c , etc., which is a typical result of Proguard’s symbols obfuscation. We see this in the private static void printAlumno(org.example.Alumno); method that has been renamed as private static void a(org.example.a); . 4. Reduction in Constant Pool Entries: The constant pool is now smaller as part of the Proguard shrinking process which includes the removal of unused constants and Attribute names that are deleted. 5. Removal of Debug Information: The LineNumberTable and 66 LocalVariableTable attributes are missing in the B.2 output. This is part of Proguard’s behavior to strip out this information for size reduction and obfuscation. There is no point in this information to be in a Production program.. 6. Preserved Elements: The configuration file A.3 has some rules to preserve certain elements: • All public classes with main methods are preserved, because of the -keepclasseswithmembers flag. That is why the main method still appears in B.2. • Native methods and their classes are preserved ( -keepclasseswithmembernames ). • Enumerations and serialization-related members are explicitly preserved. The output for javap -c -v -private for the Alumno class can be found in B.3 (Without Proguard) and B.4 (With Proguard). The outputs are essentially similar that the ones with the Main class and we can extract similar conclusion, but it’s worth mentioning the obfuscation also of fields in the Alumno class, that we did not saw in the Main class because it does not have any fields. 14 Symbols obfuscator design and implementation In Section 13.3, we examined Proguard’s existing Symbol Obfuscation feature. However, during the project’s conceptual phase with my tutor, we identified several limitations in Proguard’s current obfuscation capabilities. These limitations prompted the development of an extended symbols obfuscator specifically for the JVM. This chapter outlines the design and implementation of this enhanced obfuscator. 14.1 Limitations of Current Symbol Obfuscation in Proguard Before delving into the design of the new obfuscator, we have to understand what are the problems of Proguard’s existing symbol obfuscation. Proguard, as it stands, provides a basic level of obfuscation for JVM symbols. However, this obfuscation is often predictable and follows a set pattern (as we saw in 13.3) or ( a , b , c , etc.), making it easier for reverse engineers to decipher 67 the original names. In [4], one of the things that are talked in terms of evaluating obfuscation is the length of the code and the complexity of its symbols, specially when the static analysis done by a reverse engineer is made manually. 14.2 Design Goals The primary goal of the extended symbols obfuscator is to introduce a higher level of complexity with more lengthy and unpredictable names giving in the obfuscation process. This is achieved by understanding Proguard’s codebase and incorporating a more complex name generator. The design aims to obfuscate the following JVM values more effectively: • Package Names – Here a limitation appears in the file size length in different operative systems file systems which is usually 255 bytes ([30], [31]) • Class Names • Members (Fields and Methods) • Local Variables – Since the JVM does not have symbols in the Local Variable Table which is simply an array, 12.3.2, and since they are stripped by default in Proguard, we don’t have to do much in here.2 14.3 Implementation Details For doing this, the focus first is on try to understand how is Proguard doing the symbols obfuscation and try to modify it, in the end, the whole process is already being taken care for us, we just have to slightly tune it. To do that, we can refer to the main Proguard executable, in proguard/Proguard.java inside the Proguard source code (A.4). In the main method 11, after checking that the command line arguments are correct, Proguard parses and loads the Proguard Configuration data structure using the input file A.3 passed as 2 As a little idea that I had at the end of the project: since the Attribute that gives debuggers the information about local variable names is the LocalVariableTable , that Proguard strips by default, maybe we can obfuscate this and include the LocalVariableTable attribute to maybe trick some decompilers. 68 an argument. After doing that, it creates a new Proguard instance with the Configuration instance and calls the Proguard.execute() method. Listing 11: Main function of the Proguard.java file A.4 1 2 3 4 public static void main(String[] args) { // Getting the current working directory String currentDirectory = System.getProperty("user.dir"); 5 6 7 8 9 10 11 12 13 // Printing the current directory System.out.println("Current working directory: " + currentDirectory); if (args.length == 0) { logger.warn(VERSION); logger.warn("Usage: java proguard.Proguard [options ...]"); System.exit(1); } 14 15 16 // Create the default options. Configuration configuration = new Configuration(); 17 18 19 try { 20 21 22 23 24 // Parse the options specified in the command line arguments. try (ConfigurationParser parser = new ConfigurationParser(args, System.getProperties())) { parser.parse(configuration); } 25 // Execute Proguard with these options. new Proguard(configuration).execute(); 26 27 28 29 30 31 } catch (Exception ex) { logger.error("Unexpected error", ex); 32 33 34 } System.exit(1); 35 36 System.exit(0); 69 37 } When creating the Proguard instance in line 27, a few fields are used for the Proguard operation that are worth mentioning: Listing 12: Fields of the Proguard class in Proguard.java A.4 1 2 3 private final AppView appView; private final PassRunner passRunner; private final Configuration configuration; In 12, the appView instance is a data class that contains data about the input files we provided to Proguard, meaning that the programs we cant to obfuscate ( programClassPool ), the libraries used by those programs ( libraryClassPool ) and other resources resourceFilePool (For example the MANIFEST.MF inside a .jar . In the pools contained in the appView , there is a set of Clazz (the Class namespace was already occupied) instances mirroring the .class file specification 12.2. These pool classes also have classesAccept(ClassVisitor classVisitor) methods that allow to operate in a for-every-class-in-the-pool-do-this way. Listing 13: Fields of the AppView class in AppView.java A.5 1 2 3 4 // App public public public model. final ClassPool programClassPool; final ClassPool libraryClassPool; final ResourceFilePool resourceFilePool; Once understoods the AppView class and the Proguard class, we are ready to understand the execute() function in Proguard.java A.4, in there we can see the high level pipeline where the obfuscation, optimization, shrinking and preverifying happen 14: Listing 14: execute() function of the Proguard.java file in A.4 1 2 3 4 5 6 public void execute() throws Exception { // ... try { // ... 70 Proguard class in the 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 checkConfiguration(); // .. readInput(); if (configuration.shrink || configuration.optimize || configuration.obfuscate || configuration.preverify) { clearPreverification(); } if (configuration.printSeeds != null || configuration.backport || configuration.shrink || configuration.optimize || configuration.obfuscate || configuration.preverify || configuration.addConfigurationDebugging || configuration.keepKotlinMetadata) { initialize(); mark(); } // ... if (configuration.keepKotlinMetadata) { stripKotlinMetadataAnnotations(); } if (configuration.optimize || configuration.obfuscate) { introducePrimitiveArrayConstants(); } // ... if (configuration.preverify || configuration.android) { inlineSubroutines(); } if (configuration.shrink) { shrink(false); } // ... 71 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 if (configuration.optimize && filter.matches(Optimizer.LIBRARY_GSON)) { optimizeGson(); } if (configuration.optimize) { optimize(); linearizeLineNumbers(); } if (configuration.obfuscate) { obfuscate(); } if (configuration.keepKotlinMetadata) { adaptKotlinMetadata(); } if (configuration.optimize || configuration.obfuscate) { expandPrimitiveArrayConstants(); } // ... if (configuration.preverify) { preverify(); } // Trim line numbers after preverification as this might // also remove some instructions. if (configuration.optimize || configuration.preverify) { trimLineNumbers(); } if (configuration.shrink || configuration.optimize || configuration.obfuscate || configuration.preverify) { sortClassElements(); } if (configuration.programJars.hasOutput()) 72 { 93 writeOutput(); } if (configuration.dump != null) { dump(); } 94 95 96 97 98 99 100 101 102 103 104 105 106 } } catch (UpToDateChecker.UpToDateException ignore) {} catch (IncompleteClassHierarchyException e) { // ... } So, since the obfuscation of the symbols happens in the obfuscate() function, we may have to take a look at it 15: Listing 15: obfuscate() Proguard.java file in A.4 1 2 3 function of the Proguard class in the private void obfuscate() throws Exception { passRunner.run(new ObfuscationPreparation(configuration), appView); 4 5 6 // Perform the actual obfuscation. passRunner.run(new Obfuscator(configuration), appView); 7 8 9 10 11 12 // Adapt resource file names that correspond to class names, if necessary. if (configuration.adaptResourceFileNames != null) { passRunner.run(new ResourceFileNameAdapter(configuration), appView); } 13 14 15 // Fix the Kotlin modules so the filename matches and the class names match. passRunner.run(new NameObfuscationReferenceFixer(configuration), appView); 16 17 if (configuration.keepKotlinMetadata && 73 18 { 19 20 21 22 } } configuration.enableKotlinAsserter) passRunner.run(new KotlinMetadataVerifier(configuration), appView); In the obfuscate() snippet in 15, we can see that every step inside the functions occurs using the public void run(Pass pass, AppView appView) method of the passRunner inside the Proguard class, this is used basically for benchmarking how much time does each Pass takes to complete, and each of the classes that encapsulate the logic of the code transformations we see like Obfuscator , ResourceFileNameAdapter , etc. Have to implement the Pass interface with the execute() method. The class we are interested in terms of modifying the already existant symbol obfuscation is the Obfuscator class (Full source code: A.6). The ObfuscationPreparation is used to mark which symbols we told Proguard in the configuration file we do not want to obfuscate so the Obfuscator class ignores them. In the Obfuscator class a lot of operations are done but they are there for a good reason: a naive attempt on doing obfuscation would be to simply change the CONSTANT_UTF8 values containing the symbols in the class pool of a class, but we end up in a problem: The javac compiler also includes references to the class we want to modify in other classes in our project to access their name-space! So we need to link this classes members one with each other across all the project to apply those changes downstream, we can see that with the MethodLinker() class. Inside the Obfuscator class, in the execute() method, there is the following line 16: Listing 16: Line 245 of the Obfuscator class in Obfuscator.java in A.6 1 2 3 4 5 6 7 appView.programClassPool.classesAccept( new ClassObfuscator(appView.programClassPool, appView.libraryClassPool, classNameFactory, packageNameFactory, configuration.useMixedCaseClassNames, configuration.keepPackageNames, 74 configuration.flattenPackageHierarchy, configuration.repackageClasses, configuration.allowAccessModification, configuration.keepKotlinMetadata)); 8 9 10 11 This class ClassObfuscator in A.7 implements the ClassVisitor interface and is the one that does the magic of creating new names and marking them for later renaming for each package, class and it’s members. Since this is opperating in a programClassPool 3 , it would execute the visitProgramClass method of the ClassVisitor function on the ClassObfuscator class, we can see an snippet of it’s inner workings in 17, in there we see it come up with names for classes that are not used in the actual package so they don’t conflict with existent ones, it uses the generateUniqueClassName method in line 21 for each ProgramClass inside the programClassPool . In the generateUniqueClassName in 18 we can see that the magic comes at the in the newClassName variable in line 10 variable, where a call to the another generateUniqueClassName (this one needs 2 arguments) in line 22 is done. That newClassname then is passed as setNewClassName method that marks each Clazz instance with any value that can be shared to future Pass es, in this case it will be used by the code that actually manipulates the Clazz instances and modify all other classes linked to those (Remember the MethodLinker() class?). Listing 17: visitProgramClass method of the ClassObfuscator class in ClassObfuscator.java in A.7 1 2 3 4 5 6 7 8 9 @Override public void visitProgramClass(ProgramClass programClass) { // Does this class still need a new name? newClassName = newClassName(programClass); if (newClassName == null) { // Make sure the outer class has a name, if it exists. The name will // be stored as the new class name, as a side effect, so we'll be 3 The programClassPool contains ProgramClass instances, a subclass of the Clazz abstract class that mirrors the .class file specification 12.2) 75 // able to use it as a prefix. programClass.attributesAccept(this); 10 11 12 // Figure out a package prefix. The package prefix may actually be // the an outer class prefix, if any, or it may be the fixed base // package, if classes are to be repackaged. String newPackagePrefix = newClassName != null ? newClassName + TypeConstants.INNER_CLASS_SEPARATOR : newPackagePrefix(ClassUtil.internalPackagePrefix(programClass.getName())); 13 14 15 16 17 18 19 // Come up with a new class name, numeric or ordinary. newClassName = newClassName != null && numericClassName ? generateUniqueNumericClassName(newPackagePrefix) : generateUniqueClassName(newPackagePrefix); 20 21 22 23 24 25 26 27 } } setNewClassName(programClass, newClassName); Listing 18: generateUniqueClassName method of the ClassObfuscator class in ClassObfuscator.java in A.7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private String generateUniqueClassName(String newPackagePrefix) { // Find the right name factory for this package. NameFactory classNameFactory = (NameFactory)packagePrefixClassNameFactoryMap.get(newPackagePrefix); if (classNameFactory == null) { // We haven't seen classes in this package before. // Create a new name factory for them. classNameFactory = new SimpleNameFactory(useMixedCaseClassNames); if (this.classNameFactory != null) { classNameFactory = new DictionaryNameFactory(this.classNameFactory, classNameFactory); } 17 18 packagePrefixClassNameFactoryMap.put(newPackagePrefix, 76 classNameFactory); 19 } 20 21 22 23 } return generateUniqueClassName(newPackagePrefix, classNameFactory); In 18, the important part here happens in line 10 where the NameFactory classNameFactory (A.8) class is used, this interface is the one that creates the name with the .nextName() method in it and when calling generateUniqueClassName(String newPackagePre in line 22 we are passing a SimpleNameFactory that implements the NameFactory interface. In this method 19 we take care we are not repeating class names inside a package and using the classNameFactory.nextName() function to come up with new names. Listing 19: generateUniqueClassName method of the ClassObfuscator class in ClassObfuscator.java in A.7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /** * Creates a new class name in the given new package, with the given * class name factory. */ private String generateUniqueClassName(String newPackagePrefix, NameFactory classNameFactory) { // Come up with class names until we get an original one. String newClassName; String newMixedCaseClassName; do { // Let the factory produce a class name. newClassName = newPackagePrefix + classNameFactory.nextName(); 16 17 18 19 newMixedCaseClassName = mixedCaseClassName(newClassName); } while (classNamesToAvoid.contains(newMixedCaseClassName)); 20 21 22 23 // Explicitly make sure the name isn't used again if we have a // user-specified dictionary and we're not allowed to have mixed case // class names -- just to protect against problematic 77 dictionaries. if (this.classNameFactory != null && !useMixedCaseClassNames) { classNamesToAvoid.add(newMixedCaseClassName); } 24 25 26 27 28 29 30 31 } return newClassName; So from this we can conclude that the names that are then used in the symbol obfuscation on Proguard are generated by the SimpleNameFactory class (full source code in A.9) So the way we are going to improve the existent symbol obfuscation is by substituting every use of the SimpleNameFactory to one that generate more complex names and implements the NameFactory interface, so we can just create a drop-in replacement for it. To do that, the ComplexNameFactory class is created. 1 2 [label=lst:complexnamefactory_java,caption=Implementation of the \code{ComplexNameFactory in \ref{subsec:complexnamefactory_dot_java}] package proguard.obfuscate; 3 4 5 import java.util.Arrays; import java.util.Random; 6 7 8 9 10 11 12 13 14 15 /** * This <code>NameFactory</code> generates unique more complex names, using a combination of * characters, numbers, and some special characters. */ public class ComplexNameFactory implements NameFactory { private static final int CHARACTER_COUNT = 52; // a-z, A-Z private static final int DIGIT_COUNT = 10; // 0-9 private static final char[] SPECIAL_CHARACTERS = {'_', '-', '$', '@', '#'}; // 255 is used for MAX_LENGTH because even if the max value for a utf8 char is 2^16-1, if we generate package names bigger than that, when extracting a jar file with any utility it will fail since maximum filename and directory sizes are 78 16 255 in unix and windows private static final int MAX_LENGTH = 50; // Maximum length of the generated name 17 18 19 20 21 22 23 24 /** + + * Array of windows reserved names. * This array does not include COM{digit} or LPT{digit} as {@link SimpleNameFactory} does not generate digits. + * This array must be sorted in ascending order as we're using {@link Arrays#binarySearch(Object[], Object)} on it. + */ private static final String[] reservedNames = new String[] {"AUX", "CON", "NUL", "PRN"}; private final Random random = new Random(); 25 26 27 28 29 /** * Resets the name generation index. */ public void reset() {} 30 31 32 33 34 35 36 37 38 39 40 /** * Generates the next complex name. */ public String nextName() { int length = random.nextInt(MAX_LENGTH) + 1; // Ensure at least 1 character StringBuilder nameBuilder = new StringBuilder(length); for (int i = 0; i < length; i++) { nameBuilder.append(randomCharacter()); } String newName = nameBuilder.toString(); 41 // Avoid reserved names if (Arrays.binarySearch(ComplexNameFactory.reservedNames, newName.toUpperCase()) >= 0) { return nextName(); // Recursively generate a new name if reserved } 42 43 44 45 46 47 48 } return newName; 49 50 /** 79 * Generates a random character (letter, digit, or special character). */ private char randomCharacter() { int choice = random.nextInt(CHARACTER_COUNT + DIGIT_COUNT + SPECIAL_CHARACTERS.length); if (choice < CHARACTER_COUNT) { return (char) ((choice % 26) + (choice < 26 ? 'a' : 'A')); } else if (choice < CHARACTER_COUNT + DIGIT_COUNT) { return (char) ('0' + (choice - CHARACTER_COUNT)); } else { return SPECIAL_CHARACTERS[choice - CHARACTER_COUNT DIGIT_COUNT]; } } 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 } // Main method for testing public static void main(String[] args) { System.out.println("Complex names:"); ComplexNameFactory factory = new ComplexNameFactory(); for (int i = 0; i < 100; i++) { System.out.println(" [" + factory.nextName() + "]"); } } What this code does at high level is generate complex names by combining characters, numbers, and some special characters. The ComplexNameFactory class, as seen in ??, extends the NameFactory interface. In ComplexNameFactory we implement reset() (which it does nothing), and most importantly the nextName() , which is responsible for generating the another complex name. The nextName() method generates names with a length up to MAX_LENGTH , which is set to 50 characters. This limit ensures compatibility with file system limitations on name lengths. The method generates a string composed of a random mix of characters, digits, and a set of special characters (’_’, ’-’, ’$’, ”, ’#’). To avoid conflicts with reserved names in Windows, the method checks the generated name against a list of reserved names (”AUX”, ”CON”, ”NUL”, 80 ”PRN”). If the generated name matches one of these reserved names, nextName() is called again to generate a new name. In conclusion the implementation of ComplexNameFactory improves the complexity of the names generated for classes, methods, and fields during the obfuscation process. Replacing all uses of SimpleNameFactory in Proguard codebase with the ComplexNameFactory , leave us with more lengthy and complex symbols. The replacements done are the following ones: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 1 2 3 4 5 6 diff --git a/base/src/main/java/proguard/obfuscate/ClassObfuscator.java b/base/src/main/java/proguard/obfuscate/ClassObfuscator.java index 4c3a4fd..3fda213 100644 --- a/base/src/main/java/proguard/obfuscate/ClassObfuscator.java +++ b/base/src/main/java/proguard/obfuscate/ClassObfuscator.java @@ -456,7 +456,7 @@ implements ClassVisitor, { // We haven't seen packages in this superpackage before. Create // a new name factory for them. packageNameFactory = new SimpleNameFactory(useMixedCaseClassNames); + packageNameFactory = new ComplexNameFactory(); if (this.packageNameFactory != null) { packageNameFactory = @@ -506,7 +506,7 @@ implements ClassVisitor, { // We haven't seen classes in this package before. // Create a new name factory for them. classNameFactory = new SimpleNameFactory(useMixedCaseClassNames); + classNameFactory = new ComplexNameFactory(); if (this.classNameFactory != null) { classNameFactory = diff --git a/base/src/main/java/proguard/obfuscate/Obfuscator.java b/base/src/main/java/proguard/obfuscate/Obfuscator.java index 22d330b..4f0bbee 100644 --- a/base/src/main/java/proguard/obfuscate/Obfuscator.java +++ b/base/src/main/java/proguard/obfuscate/Obfuscator.java @@ -373,7 +373,7 @@ public class Obfuscator implements Pass } 7 8 9 - // Come up with new names for all class members. NameFactory nameFactory = new SimpleNameFactory(); 81 10 11 12 13 14 15 16 17 18 19 + NameFactory nameFactory = new ComplexNameFactory(); if (configuration.obfuscationDictionary != null) { nameFactory = @@ -489,7 +489,7 @@ public class Obfuscator implements Pass // Some class members may have ended up with conflicting names. // Come up with new, globally unique names for them. NameFactory specialNameFactory = new SpecialNameFactory(new SimpleNameFactory()); + new SpecialNameFactory(new ComplexNameFactory()); 20 21 22 1 2 3 4 5 6 7 8 9 10 11 // Collect a map of special names to avoid // [descriptor - new name - old name]. diff --git a/base/src/main/java/proguard/obfuscate/UniqueMemberNameFactory.java b/base/src/main/java/proguard/obfuscate/UniqueMemberNameFactory.java index 7be758a..fb78f16 100644 --- a/base/src/main/java/proguard/obfuscate/UniqueMemberNameFactory.java +++ b/base/src/main/java/proguard/obfuscate/UniqueMemberNameFactory.java @@ -48,7 +48,7 @@ public class UniqueMemberNameFactory implements NameFactory { return new UniqueMemberNameFactory( new PrefixingNameFactory( new SimpleNameFactory(), INJECTED_MEMBER_PREFIX), clazz); + new ComplexNameFactory(), INJECTED_MEMBER_PREFIX), clazz); } The results of the new symbol obfuscation features we implemented are going to be discussed in 16 in conjunction to the changes of the string obfuscator in 15. 15 String obfuscator design and implementation In our previous discussions on Proguard’s symbol obfuscation (Section 14), we delved into adding new capabilities by introducing more complex naming strategies. This chapter focuses on another crucial aspect of obfuscation: String Obfuscation. String obfuscation is vital in protecting sensitive information like URLs, API keys, or other critical data within the codebase that can 82 give useful information to reverse engineers. This section will outline the design and implementation of the String Obfuscator that extends Proguard’s codebase. Unlike in the Symbols obfuscator section, this feature is not included in Proguard whatsoever (but it is in it’s privative big brother: DexGuard[8]), so we can’t delve into it’s limitations because it simply does not exist and we are adding a completely new feature. 15.1 Design goals Effective string obfuscation should render these strings unreadable or nontrivial to decipher. The design of the String Obfuscator is guided by the following principles: • Effectiveness: The obfuscation must significantly complicate the reverse engineering process. • Efficiency: The obfuscation should introduce minimal overhead to the runtime performance. • Compatibility: The solution should be compatible with existing Java and JVM standards, ensuring no disruptions in the application functionality. In this case since this is a Proof of Concept done educational purposes, doing something that aims to follow the effectiveness principle is complicated, because this technique can be as complicated as far as one imagination can travel and much more advanced trickeries can be done (and also are being done in the wild). In this case, we are just going to encode the strings in our program using Base64, which is obviously straightforward to decipher, but still a good learning resource. 15.2 Implementation details The implementation of the String Obfuscator involves extending Proguard’s existing codebase. The primary objective is to transform string literals in the bytecode into a non-readable format and then decode them at runtime. The key steps we are going to follow are: • Identification of String Literals: Scanning the codebase to identify all string literals ( CONSTANT_String )11 that need obfuscation. 83 • Transformation of String Literals: Each identified string is encoded using a chosen method (in this case, Base64 encoding) and replaced in the code. • Runtime Decoding: Implementing a decoder in the application to convert the obfuscated strings back to their original form during runtime by patching bytecode directly into the Code attribute inside the Methods 12.2.3 in the program classes. To do this, the StringObfuscationPass class is created, implementing the Pass interface to be called from the obfuscate() function in the Proguard class in Proguard.java A.4. We can see this change in line 6 of 20 and the whole StringObfuscationPass class implementation in 21. Listing 20: The StringObfuscationPass added to the obfuscate() function inside the Proguard class A.4 1 2 3 4 5 6 7 /** * Performs the obfuscation step. */ private void obfuscate() throws Exception { passRunner.run(new StringObfuscationPass(configuration), appView); // !!!!! Our new addition !!!!! passRunner.run(new Shrinker(configuration, true), appView); 8 9 passRunner.run(new ObfuscationPreparation(configuration), appView); 10 11 12 // Perform the actual obfuscation. passRunner.run(new Obfuscator(configuration), appView); 13 14 15 16 17 18 // Adapt resource file names that correspond to class names, if necessary. if (configuration.adaptResourceFileNames != null) { passRunner.run(new ResourceFileNameAdapter(configuration), appView); } 19 20 21 // Fix the Kotlin modules so the filename matches and the class names match. passRunner.run(new 84 NameObfuscationReferenceFixer(configuration), appView); 22 23 24 25 26 27 28 } if (configuration.keepKotlinMetadata && configuration.enableKotlinAsserter) { passRunner.run(new KotlinMetadataVerifier(configuration), appView); } As we can see in 21, the execute() function of this pass visits all classes in the appView.programClassPool with accept and uses the StringObfuscatorVisitor class to actually perform the string obfuscation. Listing 21: The StringObfuscationPass class that iterates over every ProgramClass inside the programClassPool ?? 1 package proguard.obfuscate; 2 3 4 5 6 7 8 9 import import import import import import import org.apache.logging.log4j.LogManager; org.apache.logging.log4j.Logger; proguard.AppView; proguard.Configuration; proguard.classfile.visitor.AllClassVisitor; proguard.classfile.visitor.ClassVisitor; proguard.pass.Pass; 10 11 12 13 14 15 16 public class StringObfuscationPass implements Pass { Configuration configuration; private static final Logger logger = LogManager.getLogger(StringObfuscationPass.class); public StringObfuscationPass(Configuration configuration) { this.configuration = configuration; } 17 18 19 20 21 22 23 24 25 @Override public void execute(AppView appView) throws Exception { appView.programClassPool.accept( new AllClassVisitor( new StringObfuscatorVisitor() ) ); 85 26 27 28 } } The ”meat and potatoes” of the String Obfuscation technique happens in the StringObfuscatorVisitor class in A.11, let’s analyse how are we doing it in 22. Listing 22: The StringObfuscationVisitor class that ProgramClass es and its attributes A.11 to obfuscate Strings 1 visits package proguard.obfuscate; 2 3 4 // ... Removed imports because they are verbose import java.util.Base64; 5 6 7 8 9 10 11 12 13 14 15 public class StringObfuscatorVisitor implements ClassVisitor, AttributeVisitor, InstructionVisitor, ConstantVisitor { enum STATE_OF_STRING { OLDIE, NEW_OBFUSCATED, IGNORE } private static final Logger logger = LogManager.getLogger(StringObfuscatorVisitor.class); private final CodeAttributeEditor codeAttributeEditor; 16 17 18 19 20 public StringObfuscatorVisitor() { codeAttributeEditor = new CodeAttributeEditor(true, false); codeAttributeEditor.reset(10000); } 21 22 23 24 25 26 27 private Instruction[] generateDecodingInstructions(int indexOfObfuscatedString, ProgramClass programClass) { InstructionSequenceBuilder instructionBuilder = new InstructionSequenceBuilder(programClass); return instructionBuilder .new_("java/lang/String") .dup() .invokestatic("java/util/Base64", "getDecoder", "()Ljava/util/Base64$Decoder;") 86 28 29 30 31 32 } .ldc_(indexOfObfuscatedString) .invokevirtual("java/util/Base64$Decoder", "decode", "(Ljava/lang/String;)[B") .invokespecial("java/lang/String", "<init>", "([B)V") .instructions(); 33 34 35 36 @Override public void visitAnyClass(Clazz clazz) { } 37 38 39 40 41 @Override public void visitProgramClass(ProgramClass programClass) { programClass.methodsAccept(new AllAttributeVisitor(this)); } 42 43 44 45 46 47 @Override public void visitCodeAttribute(Clazz clazz, Method method, CodeAttribute codeAttribute) { logger.info("Processing code attribute in class: " + clazz.getName() + ", method: " + method.getName(clazz)); codeAttribute.instructionsAccept(clazz, method, new ClassPrinter()); codeAttribute.instructionsAccept(clazz, method, this); 48 49 50 51 52 53 54 55 56 } // Do the actual change in the code attribute codeAttributeEditor.visitCodeAttribute(clazz, method, codeAttribute); // At the end, let's reset the codeAttributeEditor so it does not mess up other methods processing codeAttributeEditor.reset(10000); logger.info("Final result: " + clazz.getName() + ", method: " + method.getName(clazz)); codeAttribute.instructionsAccept(clazz, method, new ClassPrinter()); 57 58 59 60 @Override public void visitAnyInstruction(Clazz clazz, Method method, CodeAttribute codeAttribute, int offset, Instruction instruction) { } 87 61 62 63 64 65 66 67 68 69 @Override public void visitConstantInstruction(Clazz clazz, Method method, CodeAttribute codeAttribute, int offset, ConstantInstruction constantInstruction) { if (constantInstruction.opcode == Instruction.OP_LDC) { logger.info("Found LDC instruction at offset " + offset + " in method " + method.getName(clazz) + " of class " + clazz.getName()); int constIndex = constantInstruction.constantIndex; clazz.constantPoolEntryAccept(constIndex, this); Constant constantToReplace = ((ProgramClass) clazz).getConstant(constIndex); STATE_OF_STRING stateOfConstant = (STATE_OF_STRING) constantToReplace.getProcessingInfo(); 70 71 72 73 74 75 76 } } if (stateOfConstant == STATE_OF_STRING.OLDIE){ Instruction[] newInstructions = generateDecodingInstructions(constantToReplace.getProcessingFlags(), (ProgramClass) clazz); codeAttributeEditor.replaceInstruction(offset, newInstructions); } 77 78 79 80 81 @Override public void visitAnyConstant(Clazz clazz, Constant constant) { constant.setProcessingInfo(STATE_OF_STRING.IGNORE); } 82 83 84 85 86 87 88 89 @Override public void visitStringConstant(Clazz clazz, StringConstant stringConstant) { logger.info("Visiting String Constant: " + stringConstant.getString(clazz) + " in class " + clazz.getName()); ConstantPoolEditor constantPoolEditor = new ConstantPoolEditor((ProgramClass) clazz); String originalString = stringConstant.getString(clazz); String obfuscatedString = Base64.getEncoder().encodeToString(originalString.getBytes()); stringConstant.setProcessingInfo(STATE_OF_STRING.OLDIE); 88 90 logger.info("Obfuscated String: Original: " + originalString + " -> Obfuscated: " + obfuscatedString); int indexOfAddedString = constantPoolEditor.addStringConstant(obfuscatedString); Constant addedConstant = ((ProgramClass) clazz).getConstant(indexOfAddedString); addedConstant.setProcessingInfo(STATE_OF_STRING.NEW_OBFUSCATED); 91 92 93 94 95 96 97 } 98 stringConstant.setProcessingFlags(indexOfAddedString); addedConstant.setProcessingFlags(stringConstant.u2stringIndex); 99 100 101 102 103 } @Override public void visitUtf8Constant(Clazz clazz, Utf8Constant utf8Constant) { } The StringObfuscatorVisitor class is the core component of the string obfuscation process in the extended Proguard system. It implements multiple interfaces including ClassVisitor , AttributeVisitor , InstructionVisitor , and ConstantVisitor , allowing it to traverse and manipulate various elements of the Java bytecode. The class’s primary responsibility is to visit all ProgramClass instances and their attributes, applying string obfuscation. It is designed to identify string constants within the code and replace them with obfuscated versions, using Base64 encoding in line with the design goals mentioned in Section 15. The key parts of the StringObfuscatorVisitor class are the following ones: • generateDecodingInstructions: This method constructs the bytecode instructions necessary for decoding the obfuscated strings at runtime. It creates a sequence of instructions that, when executed, will decode the Base64 encoded strings back to their original form. This instructions will be used to patch the actual bytecode. • visitProgramClass: This method is called for each ProgramClass in the application, initiating the string obfuscation process for each class. • visitCodeAttribute: This method processes the CodeAttribute of methods within a class. It scans for string-related instructions (In this 89 case, the ldc bytecode instruction) and applies the obfuscation logic defined in the generateDecodingInstructions method. • visitConstantInstruction: Here, the method is performed after visiting the Code Attribute and filters by bytecode instructions that reference constants in the constant pool 12.2.1, inside of that, we identify ldc instructions (that load string constants) and we apply the necessary patching at the bytecode level to obfuscate the string values. To do that we are helped by the codeAttributeEditor class available in the Proguard-core library, that allow us to replace instructions and takes care of updating the max_stack property in the Code attribute so it outputs almost-working 4 bytecode instructions. • visitStringConstant: This method handles the actual obfuscation of string constants. It replaces the original string constants with their base64 obfuscated versions within the constant pool of the class and inserts the new constants in the constant_pool so they are available for later patching in the visitConstantInstruction . The reasoning of using the generateDecodingInstructions 23 is that we want to substitute what it would normally be a normal string initialization ( ldc the String constant and then create a String instance), for a stack operation that hides the string but leaves the stack in the same state as if it was a normal string initialization. We can see an example graphical example of what the binary patching does at the operating stack when executing it in 6. Listing 23: Implementation of the generateDecodingInstructions 1 2 3 4 5 6 7 8 private Instruction[] generateDecodingInstructions(int indexOfObfuscatedString, ProgramClass programClass) { InstructionSequenceBuilder instructionBuilder = new InstructionSequenceBuilder(programClass); return instructionBuilder .new_("java/lang/String") .dup() .invokestatic("java/util/Base64", "getDecoder", "()Ljava/util/Base64$Decoder;") .ldc_(indexOfObfuscatedString) .invokevirtual("java/util/Base64$Decoder", "decode", "(Ljava/lang/String;)[B") 4 The reason because it is almost-working is that it does not check for type validity for example, so you can still insert non sensical instructions. 90 9 10 11 } .invokespecial("java/lang/String", "<init>", "([B)V") .instructions(); Figure 6: Stack status when using unobfuscated String initialization (left in red) and after patching the code to obfuscate it (right in green) A part of the StringObfuscatorVisitor class is its state management system, which tracks the state of each string constant. This is crucial for determining whether a string constant has already been obfuscated or should be ignored in the visitConstantInstruction , ensuring the obfuscation process is applied correctly and that we don’t obfuscate already obfuscated strings. Changes in Obfuscation.java file to add the StringObfuscationPass are needed to integrate the string obfuscation into the existing obfuscation process 24, in this case just adding a new Pass and a Shrinker pass because some unused constants in the constant_pool sometimes remain in there. Listing 24: Changes done to the Proguard.java A.4 file to add the String Obfuscation feature 1 2 3 4 5 6 7 diff --git a/base/src/main/java/proguard/ProGuard.java b/base/src/main/java/proguard/ProGuard.java index 711d7b5..ed71054 100644 --- a/base/src/main/java/proguard/ProGuard.java +++ b/base/src/main/java/proguard/ProGuard.java @@ -31,10 +31,7 @@ import proguard.configuration.InitialStateInfo; import proguard.evaluation.IncompleteClassHierarchyException; import proguard.logging.Logging; 91 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import proguard.mark.Marker; - import proguard.obfuscate.NameObfuscationReferenceFixer; - import proguard.obfuscate.ObfuscationPreparation; - import proguard.obfuscate.Obfuscator; - import proguard.obfuscate.ResourceFileNameAdapter; + import proguard.obfuscate.*; import proguard.optimize.LineNumberTrimmer; import proguard.optimize.Optimizer; import proguard.optimize.gson.GsonOptimizer; @@ -499,6 +496,9 @@ public class ProGuard */ private void obfuscate() throws Exception { + passRunner.run(new StringObfuscationPass(configuration), appView); + passRunner.run(new Shrinker(configuration, true), appView); + passRunner.run(new ObfuscationPreparation(configuration), appView); 25 26 // Perform the actual obfuscation. 16 Results of the project The testing of the JVM obfuscator was primarily conducted through manual methods. The initial step in verifying the effectiveness of the obfuscation process involved executing the obfuscated code using the command java -jar out_untitled.main.jar . This execution was compared against the output of the unobfuscated code, run with java -jar untitled.main.jar . Successful obfuscation was indicated by the obfuscated code producing the same output as the unobfuscated code, confirming the integrity of the jar after the obfuscation process. Further manual testing was carried out to ensure that strings, as detailed in Section 15, and symbols, as described in Section 14, were correctly obfuscated. This was achieved through the use of jadx-gui, a widely utilized Java Decompiler that attempts to convert .class files into Java code, and javap, the standard Java disassembler. The results of the javap disassembly can be observed in B.5 for the Main class, and in B.6 for the Alumno class. We can compare this with the B.1 and B.3, in there we see that the original Main and Alumno classes have been significantly transformed postobfuscation. Here are the main points that have been modified in the .class 92 file: • Method and Class Names: The obfuscated Main class in B.5 and Alumno class in B.6 show a notable transformation in the names of methods and classes. For instance, the class name Alumno is changed to hq2U$V95XxR , and method names are replaced with obfuscated strings like "1feP" and "AyR1t9wmEUrtz#Jn9Z" . This contrasts with the original, more readable names in B.1 and B.3. • String Literals: The obfuscation of string literals is evident. We don’t see any traces of cleartext anymore. The original human-readable strings are replaced with Base64 encoded strings, as seen in the obfuscated Main class in B.5. This encoding makes it difficult to understand the original string content without decoding it first. • Structure and Size: The structure and size of the classes have been altered; the obfuscated classes have a different constant pool arrangement, and the bytecode size varies because we patched the instructions in 14 to generate the Strings dinamically. • Field Names: Similar to method names, field names in the Alumno class are altered to cryptic strings like "0@Gfy2" and "Zp9EaOz8gTYnlrcg9T_B-Rj8YBp" in B.6, compared to the names in the original class B.3. The successful execution of the obfuscated code with the same output as the original code confirms that the obfuscation process preserved the functionality while obfuscating the code structure, so we can call that a success. Additionally, the outcomes of the jadx-gui decompilation process are presented in the Appendix. The decompiled results for the Main class are available in Section B.9, and for the Alumno class in Section B.10. These sections include screenshots of the decompiled code, where we can see in a more readable way the changes done to the symbols and strings of the Main A.1 and Alumno A.2 classes. 16.1 Impact of Enhancements The enhancements made to the JVM obfuscator, in extension of proguard’s features, have both positive impacts and some trade-offs. Here’s an analysis of these aspects: • Increased Size Due to Base64 Encoding: One of the noticeable effects of the obfuscation process is the increase in size of the obfuscated classes. This is due to the use of Base64 encoding for string literals: 93 Base64 encoding, by its nature, increases the verbosity of the data, leading to a larger size in the bytecode since those strings have to live in the constant_pool . For example, a simple string like ”Hello World” when encoded in Base64 becomes significantly longer (aprox. a 33% [32]), so if one haves a lot of Strings in the class files, it will increase this size in a linear fashion. This can be seen when comparing the size of classes in the proguard obfuscation (B.2 and B.4) and the one with our own obfuscation techniques as seen in B.5 and B.6. • Performance Overhead and Increased JVM Memory Usage: The enhancements also introduce a performance overhead during both the obfuscation process and the execution of the obfuscated code. The obfuscator now performs additional operations, such as encoding strings to Base64 and renaming symbols, which require more computational resources but since it’s done once, it’s not really important. What is more important is the execution of the obfuscated code, which will be slower compared to the original code. This is because the JVM needs to perform extra steps like decoding Base64 strings at runtime, which can lead to increased CPU usage. Also, the obfuscated symbols and names are typically longer, leading to an increased memory footprint in the JVM, as it needs to link and manage these larger identifiers. 16.2 Limitations and Future Work Despite the successful implementation of the enhanced JVM obfuscator, there are certain limitations and areas for future work: • Base64 as a Weak Obfuscation Technique: While Base64 encoding provides a basic level of obfuscation, it is quite easy to break. In a production environment, more sophisticated techniques could be employed. For example, integrating native methods that are more difficult to decompile for decoding or using encryption directly could significantly increase the obfuscation strength. • Lack of Obfuscation for Predefined Strings in Class Fields: The current implementation does not obfuscate predefined strings in class fields. Although technically feasible, this feature was complex to implement and prone to bugs. Addressing this limitation in future versions would enhance the overall effectiveness of the obfuscation process. • Automation of Testing: The testing process for the obfuscator can be made more efficient through automation. Automated testing could 94 involve executing methods directly and applying the obfuscator to larger projects or .jar s containing multiple classes. This would provide a more comprehensive evaluation of the obfuscator’s features and improve in finding possible bugs. • More Robust Obfuscation Techniques: Building upon the experience gained with the Proguard-core library, future work could explore more complex obfuscation techniques. These might include more advanced symbol mangling, control flow obfuscation, and the introduction of dummy code to confuse decompilers and reverse engineers. Such enhancements would make the obfuscation process more resilient against the reverse engineering state of art of tools and techniques. In summary, while the enhanced JVM obfuscator represents a significant improvement over its predecessors, there remains ample scope for further development and refinement to address its current limitations and adapt to the evolving landscape of software reverse engineering and security. 17 Conclusion This project started with an overview of the available techniques in the state of the art on Obfuscation, we delved into the JVM specification as a virtual machine and its file formats like the .class file and instruction set. We also provided an overview of what is Proguard, the famous open source obfuscator, what features does it come with, how it builds upon the Proguardcore and provided some disassembly examples of its execution in the .class files. In the practical side, we extended Proguard adding 2 new features: A more complex symbols obfuscation and a new way to obfuscate Strings in the Java executable, both of them explaining the rationale behind them. Both of them were recollected in the 16 and tested to be successful. After analyzing the results, we analyzed what impact it can have in the security and performance of Java programs, explored its limitations and reasoned about future work that could be done like adding obfuscation to the field Strings, or developing better hiding of Strings with encryption or native methods. In conclusion, the project can be deemed successful as a learning exercise on getting to know obfuscators, the internals of a language like Java and for personal research purposes. 95 A Code Samples A.1 1 Main Java code package org.example; 2 3 4 5 6 7 public class Main { public static void main(String[] args) { System.out.println("Hello world!"); Alumno a1 = new Alumno("Fran", 23); Alumno a2 = new Alumno("Nerea", 7); 8 9 10 11 } 12 printAlumno(a1); a1.cumple(); printAlumno(a1); 13 14 15 16 17 18 19 20 21 } private static void printAlumno(Alumno a) { System.out.println(String.format("Nombre del alumno: %s\tedad del alumno: %s\tid del alumno: %s", a.getNombre(), a.getEdad(), a.getId()) ); } A.2 1 Alumno class Java code package org.example; 2 3 import java.util.UUID; 4 5 public class Alumno { 6 7 8 9 private String nombre; private int edad; private UUID id; 10 11 12 13 public Alumno (String nombre, int edad) { this.nombre = nombre; this.edad = edad; 96 14 } 15 this.id = UUID.randomUUID(); 16 public UUID getId() { return id; } 17 18 19 20 public int getEdad() { return edad; } 21 22 23 24 public String getNombre() { return nombre; } 25 26 27 28 public void setNombre(String nombre) { this.nombre = nombre; } 29 30 31 32 public void setEdad(int edad) { this.edad = edad; } 33 34 35 36 37 38 39 40 41 } public int cumple () { this.edad = this.edad + 1; return this.edad; } A.3 Proguard configuration file -verbose # To print the seeds -printseeds # Specify the input jars, output jars, and library jars. -injars testing_jar/untitled.main.jar -outjars testing_jar/out_untitled.main.jar 97 # Before Java 9, the runtime classes were packaged in a single jar file. #-libraryjars <java.home>/lib/rt.jar # As of Java 9, the runtime classes are packaged in modular jmod files. -libraryjars <java.home>/jmods/java.base.jmod(!**.jar;!module-info.class) #-libraryjars <java.home>/jmods/..... #-libraryjars junit.jar #-libraryjars servlet.jar #-libraryjars jai_core.jar #... # Save the obfuscation mapping to a file, so you can de-obfuscate any stack # traces later on. Keep a fixed source file attribute and all line number # tables to get line numbers in the stack traces. # You can comment this out if you're not interested in stack traces. -printmapping out.map # Preserve all annotations. -keepattributes *Annotation* # You can print out the seeds that are matching the keep options below. #-printseeds out.seeds # Preserve all public applications. -keepclasseswithmembers public class * { public static void main(java.lang.String[]); } # Preserve all native method names and the names of their classes. -keepclasseswithmembernames,includedescriptorclasses class * { 98 } native <methods>; # Preserve the special static methods that are required in all enumeration # classes. -keepclassmembers,allowoptimization enum * { public static **[] values(); public static ** valueOf(java.lang.String); } # Explicitly preserve all serialization members. The Serializable interface # is only a marker interface, so it wouldn't save them. # You can comment this out if your application doesn't use serialization. # If your code contains serializable classes that have to be backward # compatible, please refer to the manual. -keepclassmembers class * implements java.io.Serializable { static final long serialVersionUID; static final java.io.ObjectStreamField[] serialPersistentFields; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); } # Your application may contain more items that need to be preserved; # typically classes that are dynamically created using Class.forName: # -keep public class com.example.MyClass # -keep public interface com.example.MyInterface # -keep public class * implements com.example.MyInterface A.4 1 Proguard.java package proguard; 99 2 3 // ... Imports, ignored because they are too verbose 4 5 6 7 8 9 10 11 12 13 /** * Tool for shrinking, optimizing, obfuscating, and preverifying Java classes. * * @author Eric Lafortune */ public class Proguard { private static final Logger logger = LogManager.getLogger(Proguard.class); public static final String VERSION = "Proguard, version " + getVersion(); 14 15 16 17 18 19 20 21 /** * A data object containing pass inputs in a centralized location. Passes can access and update the information * at any point in the pipeline. */ private final AppView appView; private final PassRunner passRunner; private final Configuration configuration; 22 23 24 25 26 27 28 29 30 31 32 /** * Creates a new Proguard object to process jars as specified by the given * configuration. */ public Proguard(Configuration configuration) { this.appView = new AppView(); this.passRunner = new PassRunner(); this.configuration = configuration; } 33 34 35 36 37 38 39 /** * Performs all subsequent Proguard operations. */ public void execute() throws Exception { Logging.configureVerbosity(configuration.verbose); 100 40 41 logger.always().log(VERSION); 42 43 44 45 try { checkGpl(); 46 47 48 49 50 51 // Set the -keepkotlinmetadata option if necessary. if (!configuration.dontProcessKotlinMetadata) { configuration.keepKotlinMetadata = requiresKotlinMetadata(); } 52 53 54 55 56 if (configuration.printConfiguration != null) { printConfiguration(); } 57 58 checkConfiguration(); 59 60 61 62 63 if (configuration.programJars.hasOutput()) { checkUpToDate(); } 64 65 66 67 68 if (configuration.targetClassVersion != 0) { configuration.backport = true; } 69 70 readInput(); 71 72 73 74 75 76 77 78 if (configuration.shrink || configuration.optimize || configuration.obfuscate || configuration.preverify) { clearPreverification(); } 79 80 81 if (configuration.printSeeds != null || configuration.backport || 101 82 83 84 85 86 87 88 { 89 90 91 } configuration.shrink || configuration.optimize || configuration.obfuscate || configuration.preverify || configuration.addConfigurationDebugging || configuration.keepKotlinMetadata) initialize(); mark(); 92 93 checkConfigurationAfterInitialization(); 94 95 96 97 98 99 100 if (configuration.addConfigurationDebugging) { // Remember the initial state of the program classpool and resource filepool // before shrinking / obfuscation / optimization. appView.initialStateInfo = new InitialStateInfo(appView.programClassPool); } 101 102 103 104 105 if (configuration.keepKotlinMetadata) { stripKotlinMetadataAnnotations(); } 106 107 108 109 110 111 if (configuration.optimize || configuration.obfuscate) { introducePrimitiveArrayConstants(); } 112 113 114 115 116 if (configuration.backport) { backport(); } 117 118 119 120 121 if (configuration.addConfigurationDebugging) { addConfigurationLogging(); } 122 102 123 124 125 126 if (configuration.printSeeds != null) { printSeeds(); } 127 128 129 130 131 132 if (configuration.preverify || configuration.android) { inlineSubroutines(); } 133 134 135 136 137 if (configuration.shrink) { shrink(false); } 138 139 140 141 142 // Create a matcher for filtering optimizations. StringMatcher filter = configuration.optimizations != null ? new ListParser(new NameParser()).parse(configuration.optimizations) : new ConstantMatcher(true); 143 144 145 146 147 148 if (configuration.optimize && filter.matches(Optimizer.LIBRARY_GSON)) { optimizeGson(); } 149 150 151 152 153 154 if (configuration.optimize) { optimize(); linearizeLineNumbers(); } 155 156 157 158 159 if (configuration.obfuscate) { obfuscate(); } 160 161 162 163 if (configuration.keepKotlinMetadata) { adaptKotlinMetadata(); 103 164 } 165 166 167 168 169 170 if (configuration.optimize || configuration.obfuscate) { expandPrimitiveArrayConstants(); } 171 172 173 174 175 if (configuration.targetClassVersion != 0) { target(); } 176 177 178 179 180 if (configuration.preverify) { preverify(); } 181 182 183 184 185 186 187 188 // Trim line numbers after preverification as this might // also remove some instructions. if (configuration.optimize || configuration.preverify) { trimLineNumbers(); } 189 190 191 192 193 194 195 196 if (configuration.shrink || configuration.optimize || configuration.obfuscate || configuration.preverify) { sortClassElements(); } 197 198 199 200 201 if (configuration.programJars.hasOutput()) { writeOutput(); } 202 203 204 205 206 if (configuration.dump != null) { dump(); } 104 207 208 209 210 211 212 213 214 215 216 217 218 } } catch (UpToDateChecker.UpToDateException ignore) {} catch (IncompleteClassHierarchyException e) { throw new RuntimeException( System.lineSeparator() + System.lineSeparator() + "It appears you are missing some classes resulting in an incomplete class hierarchy, " + System.lineSeparator() + "please refer to the troubleshooting page in the manual: " + System.lineSeparator() + "https://www.guardsquare.com/en/products/proguard/manual/troubleshooting# + System.lineSeparator() ); } 219 220 221 222 223 224 225 226 227 /** * Checks the GPL. */ private void checkGpl() { GPL.check(); } 228 229 230 231 232 233 234 235 236 237 238 private boolean requiresKotlinMetadata() { return configuration.keepKotlinMetadata || (configuration.keep != null && configuration.keep.stream().anyMatch( keepClassSpecification -> ! keepClassSpecification.allowObfuscation && ! keepClassSpecification.allowShrinking && "kotlin/Metadata".equals(keepClassSpecification )); } 239 240 241 242 /** * Prints out the configuration that Proguard is using. 105 243 244 245 246 */ private void printConfiguration() throws IOException { PrintWriter pw = PrintWriterUtil.createPrintWriterOut(configuration.printConfiguration); 247 248 249 250 251 252 } try (ConfigurationWriter configurationWriter = new ConfigurationWriter(pw)) { configurationWriter.write(configuration); } 253 254 255 256 257 258 259 260 261 /** * Checks the configuration for conflicts and inconsistencies. */ private void checkConfiguration() throws IOException { new ConfigurationVerifier(configuration).check(); } 262 263 264 265 266 267 268 269 270 /** * Checks whether the output is up-to-date. */ private void checkUpToDate() { new UpToDateChecker(configuration).check(); } 271 272 273 274 275 276 277 278 279 280 /** * Reads the input class files. */ private void readInput() throws Exception { // Fill the program class pool and the library class pool. passRunner.run(new InputReader(configuration), appView); } 281 282 283 /** 106 284 285 286 287 288 289 * Clears any JSE preverification information from the program classes. */ private void clearPreverification() throws Exception { passRunner.run(new PreverificationClearer(), appView); } 290 291 292 293 294 295 296 297 298 299 300 301 302 /** * Initializes the cross-references between all classes, performs some * basic checks, and shrinks the library class pool. */ private void initialize() throws Exception { if (configuration.keepKotlinMetadata) { passRunner.run(new KotlinUnsupportedVersionChecker(), appView); } passRunner.run(new Initializer(configuration), appView); 303 304 305 306 307 308 309 } if (configuration.keepKotlinMetadata && configuration.enableKotlinAsserter) { passRunner.run(new KotlinMetadataVerifier(configuration), appView); } 310 311 312 313 314 315 316 317 318 319 /** * Marks the classes, class members and attributes to be kept or encrypted, * by setting the appropriate access flags. */ private void mark() throws Exception { passRunner.run(new Marker(configuration), appView); } 320 321 107 322 323 324 325 326 327 328 /** * Strips the Kotlin metadata annotation where possible. */ private void stripKotlinMetadataAnnotations() throws Exception { passRunner.run(new KotlinAnnotationStripper(configuration), appView); } 329 330 331 332 333 334 335 336 /** * Checks the configuration after it has been initialized. */ private void checkConfigurationAfterInitialization() throws Exception { passRunner.run(new AfterInitConfigurationVerifier(configuration), appView); } 337 338 339 340 341 342 343 344 /** * Replaces primitive array initialization code by primitive array constants. */ private void introducePrimitiveArrayConstants() throws Exception { passRunner.run(new PrimitiveArrayConstantIntroducer(), appView); } 345 346 347 348 349 350 351 352 353 /** * Backports java language features to the specified target version. */ private void backport() throws Exception { passRunner.run(new Backporter(configuration), appView); } 354 355 356 357 /** * Adds configuration logging code, providing suggestions on improving 108 358 359 360 361 362 363 * the Proguard configuration. */ private void addConfigurationLogging() throws Exception { passRunner.run(new ConfigurationLoggingAdder(), appView); } 364 365 366 367 368 369 370 371 372 373 /** * Prints out classes and class members that are used as seeds in the * shrinking and obfuscation steps. */ private void printSeeds() throws Exception { passRunner.run(new SeedPrinter(configuration), appView); } 374 375 376 377 378 379 380 381 382 383 /** * Performs the subroutine inlining step. */ private void inlineSubroutines() throws Exception { // Perform the actual inlining. passRunner.run(new SubroutineInliner(configuration), appView); } 384 385 386 387 388 389 390 391 392 /** * Performs the shrinking step. */ private void shrink(boolean afterOptimizer) throws Exception { // Perform the actual shrinking. passRunner.run(new Shrinker(configuration, afterOptimizer), appView); 393 394 395 396 if (configuration.keepKotlinMetadata && configuration.enableKotlinAsserter) { 109 397 398 399 } } passRunner.run(new KotlinMetadataVerifier(configuration), appView); 400 401 402 403 404 405 406 407 408 409 /** * Optimizes usages of the Gson library. */ private void optimizeGson() throws Exception { // Perform the Gson optimization. passRunner.run(new GsonOptimizer(configuration), appView); } 410 411 412 413 414 415 416 417 /** * Performs the optimization step. */ private void optimize() throws Exception { Optimizer optimizer = new Optimizer(configuration); 418 for (int optimizationPass = 0; optimizationPass < configuration.optimizationPasses; optimizationPass++) { // Perform the actual optimization. passRunner.run(optimizer, appView); 419 420 421 422 423 424 425 426 427 428 429 430 } } // Shrink again, if we may. if (configuration.shrink) { shrink(true); } 431 432 433 434 435 436 437 /** * Disambiguates the line numbers of all program classes, after * optimizations like method inlining and class merging. */ private void linearizeLineNumbers() throws Exception 110 438 { 439 440 } passRunner.run(new LineNumberLinearizer(), appView); 441 442 443 444 445 446 447 448 /** * Performs the obfuscation step. */ private void obfuscate() throws Exception { passRunner.run(new ObfuscationPreparation(configuration), appView); 449 // Perform the actual obfuscation. passRunner.run(new Obfuscator(configuration), appView); 450 451 452 // Adapt resource file names that correspond to class names, if necessary. if (configuration.adaptResourceFileNames != null) { passRunner.run(new ResourceFileNameAdapter(configuration), appView); } 453 454 455 456 457 458 // Fix the Kotlin modules so the filename matches and the class names match. passRunner.run(new NameObfuscationReferenceFixer(configuration), appView); 459 460 461 462 463 464 465 466 467 } if (configuration.keepKotlinMetadata && configuration.enableKotlinAsserter) { passRunner.run(new KotlinMetadataVerifier(configuration), appView); } 468 469 470 471 472 473 474 /** * Adapts Kotlin Metadata annotations. */ private void adaptKotlinMetadata() throws Exception { 111 475 476 } passRunner.run(new KotlinMetadataAdapter(), appView); 477 478 479 480 481 482 483 484 485 486 /** * Expands primitive array constants back to traditional primitive array * initialization code. */ private void expandPrimitiveArrayConstants() { appView.programClassPool.classesAccept(new PrimitiveArrayConstantReplacer()); } 487 488 489 490 491 492 493 494 495 /** * Sets that target versions of the program classes. */ private void target() throws Exception { passRunner.run(new Targeter(configuration), appView); } 496 497 498 499 500 501 502 503 504 505 /** * Performs the preverification step. */ private void preverify() throws Exception { // Perform the actual preverification. passRunner.run(new Preverifier(configuration), appView); } 506 507 508 509 510 511 512 513 514 /** * Trims the line number table attributes of all program classes. */ private void trimLineNumbers() throws Exception { passRunner.run(new LineNumberTrimmer(), appView); } 112 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 /** * Sorts the elements of all program classes. */ private void sortClassElements() { appView.programClassPool.classesAccept( new ClassElementSorter( /* sortInterfaces = */ true, /* sortConstants = */ true, // Sorting members can cause problems with code such as clazz.getMethods()[1] /* sortMembers = */ false, // PGD-192: Sorting attributes can cause problems for some compilers /* sortAttributes = */ false ) ); } 533 534 535 536 537 538 539 540 541 542 /** * Writes the output class files. */ private void writeOutput() throws Exception { // Write out the program class pool. passRunner.run(new OutputWriter(configuration), appView); } 543 544 545 546 547 548 549 550 551 /** * Prints out the contents of the program classes. */ private void dump() throws Exception { passRunner.run(new Dumper(configuration), appView); } 552 553 554 555 /** * Returns the implementation version from the manifest. 113 556 557 558 559 560 561 562 563 564 565 566 567 */ public static String getVersion() { Package pack = Proguard.class.getPackage(); if (pack != null) { String version = pack.getImplementationVersion(); if (version != null) { return version; } } 568 569 570 } return "undefined"; 571 572 573 574 575 576 577 578 579 /** * The main method for Proguard. */ public static void main(String[] args) { // Getting the current working directory String currentDirectory = System.getProperty("user.dir"); 580 581 582 583 584 585 586 587 588 // Printing the current directory System.out.println("Current working directory: " + currentDirectory); if (args.length == 0) { logger.warn(VERSION); logger.warn("Usage: java proguard.Proguard [options ...]"); System.exit(1); } 589 590 591 // Create the default options. Configuration configuration = new Configuration(); 592 593 594 595 try { // Parse the options specified in the command line arguments. 114 try (ConfigurationParser parser = new ConfigurationParser(args, System.getProperties())) { parser.parse(configuration); } 596 597 598 599 600 // Execute Proguard with these options. new Proguard(configuration).execute(); 601 602 } catch (Exception ex) { logger.error("Unexpected error", ex); 603 604 605 606 607 608 } 609 System.exit(1); 610 611 612 613 } } A.5 1 System.exit(0); AppView.java package proguard; 2 3 4 5 6 import import import import proguard.classfile.*; proguard.configuration.InitialStateInfo; proguard.io.ExtraDataEntryNameMap; proguard.resources.file.ResourceFilePool; 7 8 9 10 11 12 13 public class AppView { // App model. public final ClassPool programClassPool; public final ClassPool libraryClassPool; public final ResourceFilePool resourceFilePool; 14 15 public final ExtraDataEntryNameMap extraDataEntryNameMap; 16 17 18 19 /** * Stores information about the original state of the program class pool used for configuration debugging. */ 115 public 20 InitialStateInfo initialStateInfo; 21 public AppView(ClassPool programClassPool, ClassPool libraryClassPool) { this(programClassPool, libraryClassPool, new ResourceFilePool(), new ExtraDataEntryNameMap()); } 22 23 24 25 26 public AppView() { this(new ClassPool(), new ClassPool(), new ResourceFilePool(), new ExtraDataEntryNameMap()); } 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 } public AppView(ClassPool programClassPool, ClassPool libraryClassPool, ResourceFilePool resourceFilePool, ExtraDataEntryNameMap extraDataEntryNameMap) { this.programClassPool = programClassPool; this.resourceFilePool = resourceFilePool; this.libraryClassPool = libraryClassPool; this.extraDataEntryNameMap = extraDataEntryNameMap; } A.6 1 Obfuscator.java package proguard.obfuscate; 2 3 // ... Ignoring package includes because they are too verbose 4 5 6 7 8 9 10 11 12 /** * This pass can perform obfuscation of class pools according to a given * specification. * * @author Eric Lafortune */ public class Obfuscator implements Pass { 116 13 14 private static final Logger logger = LogManager.getLogger(Obfuscator.class); private final Configuration configuration; 15 16 17 18 19 public Obfuscator(Configuration configuration) { this.configuration = configuration; } 20 21 22 23 24 25 26 27 28 /** * Performs obfuscation of the given program class pool. */ @Override public void execute(AppView appView) throws IOException { logger.info("Obfuscating..."); 29 30 31 32 // We're using the system's default character encoding for writing to // the standard output and error output. PrintWriter out = new PrintWriter(System.out, true); 33 34 35 36 // Link all non-private, non-static methods in all class hierarchies. ClassVisitor memberInfoLinker = new BottomClassFilter(new MethodLinker()); 37 38 39 appView.programClassPool.classesAccept(memberInfoLinker); appView.libraryClassPool.classesAccept(memberInfoLinker); 40 41 42 43 44 45 46 47 // If the class member names have to correspond globally, // additionally link all class members in all program classes. if (configuration.useUniqueClassMemberNames) { appView.programClassPool.classesAccept(new AllMemberVisitor( new MethodLinker())); } 48 49 50 // Create a visitor for marking the seeds. NameMarker nameMarker = new NameMarker(); 117 51 52 53 54 // All library classes and library class members keep their names. appView.libraryClassPool.classesAccept(nameMarker); appView.libraryClassPool.classesAccept(new AllMemberVisitor(nameMarker)); 55 56 57 58 59 60 61 62 63 // Mark classes that have the DONT_OBFUSCATE flag set. appView.programClassPool.classesAccept( new MultiClassVisitor( new ClassProcessingFlagFilter(ProcessingFlags.DONT_OBFUSCATE, 0, nameMarker), new AllMemberVisitor( new MemberProcessingFlagFilter(ProcessingFlags.DONT_OBFUSCATE, 0, nameMarker)))); 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 // We also keep the names of the abstract methods of functional // interfaces referenced from bootstrap method arguments (additional // interfaces with LambdaMetafactory.altMetafactory). // The functional method names have to match the names in the // dynamic method invocations with LambdaMetafactory. appView.programClassPool.classesAccept( new ClassVersionFilter(VersionConstants.CLASS_VERSION_1_7, new AllAttributeVisitor( new AttributeNameFilter(Attribute.BOOTSTRAP_METHODS, new AllBootstrapMethodInfoVisitor( new AllBootstrapMethodArgumentVisitor( new ConstantTagFilter(Constant.CLASS, new ReferencedClassVisitor( new FunctionalInterfaceFilter( new ClassHierarchyTraveler(true, false, true, false, new AllMethodVisitor( new MemberAccessFilter(AccessConstants.ABSTRACT, 0, nameMarker)))))))))))); 83 118 84 85 86 87 88 89 90 91 92 93 94 95 96 // We also keep the names of the abstract methods of functional // interfaces that are returned by dynamic method invocations. // The functional method names have to match the names in the // dynamic method invocations with LambdaMetafactory. appView.programClassPool.classesAccept( new ClassVersionFilter(VersionConstants.CLASS_VERSION_1_7, new AllConstantVisitor( new DynamicReturnedClassVisitor( new FunctionalInterfaceFilter( new ClassHierarchyTraveler(true, false, true, false, new AllMethodVisitor( new MemberAccessFilter(AccessConstants.ABSTRACT, 0, nameMarker)))))))); 97 98 99 100 101 102 103 104 if (configuration.keepKotlinMetadata) { appView.programClassPool.classesAccept( // Keep Kotlin default implementations class where the user had already kept the interface. new ClassProcessingFlagFilter(ProcessingFlags.DONT_OBFUSCATE, 0, new ReferencedKotlinMetadataVisitor(new KotlinClassToDefaultImplsClassVisitor(nameMarker)))); } 105 106 107 108 109 // Mark attributes that have to be kept. AttributeVisitor attributeUsageMarker = new NonEmptyAttributeFilter( new AttributeUsageMarker()); 110 111 112 113 114 AttributeVisitor optionalAttributeUsageMarker = configuration.keepAttributes == null ? null : new AttributeNameFilter(configuration.keepAttributes, attributeUsageMarker); 115 116 117 118 appView.programClassPool.classesAccept( new AllAttributeVisitor(true, new RequiredAttributeFilter(attributeUsageMarker, 119 119 optionalAttributeUsageMarker))); 120 121 122 123 124 125 126 127 128 129 130 131 132 // Keep parameter names and types if specified. if (configuration.keepParameterNames) { appView.programClassPool.classesAccept( // Only visits methods that have a name set in their processing info. // At this step, all methods that have to be kept have been marked // by the NameMarker with their original name, so this will visit // only methods that will not be obfuscated. new AllMethodVisitor( new NewMemberNameFilter( new AllAttributeVisitor(true, new ParameterNameMarker(attributeUsageMarker))))); 133 134 135 136 137 138 139 140 141 142 143 144 145 if (configuration.keepKotlinMetadata) { appView.programClassPool.classesAccept( new MultiClassVisitor( // javac and kotlinc don't create the required attributes on interface methods // so we conservatively mark the parameters as used. new ClassAccessFilter(AccessConstants.INTERFACE, 0, new AllMethodVisitor( new NewMemberNameFilter( new MethodToKotlinFunctionVisitor( new AllValueParameterVisitor( new KotlinValueParameterUsageMarker()))))), 146 147 148 149 // T14916: Annotation classes don't have underlying JVM constructors, // so we conservatively mark the parameters as used, if the class is kept. new ClassAccessFilter(AccessConstants.INTERFACE, 0, 120 new 150 ClassProcessingFlagFilter(ProcessingFlags.DONT_OBFUSCATE, 0, new ReferencedKotlinMetadataVisitor( new KotlinClassKindFilter(metadata -> metadata.flags.isAnnotationClass, new AllConstructorVisitor( new AllValueParameterVisitor( new KotlinValueParameterUsageMarker())))))), 151 152 153 154 155 156 157 158 159 160 161 162 } } // For all other classes, first check if we should keep // the parameter names. new ReferencedKotlinMetadataVisitor( new KotlinValueParameterUsageMarker()))); 163 164 165 166 167 168 if (configuration.keepKotlinMetadata) { appView.programClassPool.classesAccept( new ReferencedKotlinMetadataVisitor( new KotlinValueParameterNameShrinker())); 169 170 171 172 173 174 175 176 177 178 179 180 } // Keep SourceDebugExtension annotations on Kotlin synthetic classes but obfuscate them. appView.programClassPool.classesAccept( new ReferencedKotlinMetadataVisitor( new KotlinSyntheticClassKindFilter( KotlinSyntheticClassKindFilter::isLambda, new KotlinMetadataToClazzVisitor( new AllAttributeVisitor( new AttributeNameFilter(Attribute.SOURCE_DEBUG_EXTENSION, new MultiAttributeVisitor(attributeUsageMarker, new KotlinSourceDebugExtensionAttributeObfuscator())) 181 121 182 183 184 185 // Remove the attributes that can be discarded. Note that the attributes // may only be discarded after the seeds have been marked, since the // configuration may rely on annotations. appView.programClassPool.classesAccept(new AttributeShrinker()); 186 187 188 189 190 191 192 if (configuration.keepKotlinMetadata) { appView.programClassPool.classesAccept( new ReferencedKotlinMetadataVisitor( new KotlinAnnotationFlagFixer())); } 193 194 195 196 197 198 // Apply the mapping, if one has been specified. The mapping can // override the names of library classes and of library class members. if (configuration.applyMapping != null) { logger.info("Applying mapping from [{}]...", PrintWriterUtil.fileName(configuration.applyMapping)); 199 200 WarningPrinter warningPrinter = new WarningLogger(logger, configuration.warn); 201 202 MappingReader reader = new MappingReader(configuration.applyMapping); 203 204 205 206 207 208 209 MappingProcessor keeper = new MultiMappingProcessor(new MappingProcessor[] { new MappingKeeper(appView.programClassPool, warningPrinter), new MappingKeeper(appView.libraryClassPool, null), }); 210 211 reader.pump(keeper); 212 213 214 215 // Print out a summary of the warnings if necessary. int warningCount = warningPrinter.getWarningCount(); if (warningCount > 0) 122 { 216 217 218 219 logger.warn("Warning: there were {} kept classes and class members that were remapped anyway.", warningCount); logger.warn(" You should adapt your configuration or edit the mapping file."); 220 if (!configuration.ignoreWarnings) { logger.warn(" If you are sure this remapping won't hurt,"); logger.warn(" you could try your luck using the '-ignorewarnings' option."); } 221 222 223 224 225 226 logger.warn(" (https://www.guardsquare.com/proguard/manual/troubleshooting#mappingc 227 228 229 230 231 232 233 234 } } if (!configuration.ignoreWarnings) { throw new IOException("Please correct the above warnings first."); } 235 236 237 238 239 // Come up with new names for all classes. DictionaryNameFactory classNameFactory = configuration.classObfuscationDictionary != null ? new DictionaryNameFactory(configuration.classObfuscationDictionary, null) : null; 240 241 242 243 DictionaryNameFactory packageNameFactory = configuration.packageObfuscationDictionary != null ? new DictionaryNameFactory(configuration.packageObfuscationDictionary, null) : null; 244 245 246 appView.programClassPool.classesAccept( new ClassObfuscator(appView.programClassPool, 123 247 248 249 250 251 252 253 254 255 appView.libraryClassPool, classNameFactory, packageNameFactory, configuration.useMixedCaseClassNames, configuration.keepPackageNames, configuration.flattenPackageHierarchy, configuration.repackageClasses, configuration.allowAccessModification, configuration.keepKotlinMetadata)); 256 257 258 259 260 261 262 263 264 265 if (configuration.keepKotlinMetadata) { // Ensure that the companion instance field is named // the same as the companion class. appView.programClassPool.classesAccept( new ReferencedKotlinMetadataVisitor( new KotlinCompanionEqualizer()) ); } 266 267 268 269 270 271 272 273 274 // Come up with new names for all class members. NameFactory nameFactory = new SimpleNameFactory(); if (configuration.obfuscationDictionary != null) { nameFactory = new DictionaryNameFactory(configuration.obfuscationDictionary, nameFactory); } 275 276 WarningPrinter warningPrinter = new WarningLogger(logger, configuration.warn); 277 278 279 // Maintain a map of names to avoid [descriptor - new name - old name]. Map descriptorMap = new HashMap(); 280 281 282 283 284 285 286 // Do the class member names have to be globally unique? if (configuration.useUniqueClassMemberNames) { // Collect all member names in all classes. appView.programClassPool.classesAccept( new AllMemberVisitor( 124 287 288 new MemberNameCollector(configuration.overloadAggressively, descriptorMap))); 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 // Assign new names to all members in all classes. appView.programClassPool.classesAccept( new AllMemberVisitor( new MemberObfuscator(configuration.overloadAggressively, nameFactory, descriptorMap))); } else { // Come up with new names for all non-private class members. appView.programClassPool.classesAccept( new MultiClassVisitor( // Collect all private member names in this class and down // the hierarchy. new ClassHierarchyTraveler(true, false, false, true, new AllMemberVisitor( new MemberAccessFilter(AccessConstants.PRIVATE, 0, new MemberNameCollector(configuration.overloadAggressively, descriptorMap)))), 309 310 311 312 313 314 315 316 // Collect all non-private member names anywhere in the // hierarchy. new ClassHierarchyTraveler(true, true, true, true, new AllMemberVisitor( new MemberAccessFilter(0, AccessConstants.PRIVATE, new MemberNameCollector(configuration.overloadAggressively, descriptorMap)))), 317 318 319 320 // Assign new names to all non-private members in this class. new AllMemberVisitor( new MemberAccessFilter(0, AccessConstants.PRIVATE, 125 new 321 322 323 MemberObfuscator(configuration.overloadAggressively, nameFactory, descriptorMap))), 324 325 326 327 )); // Clear the collected names. new MapCleaner(descriptorMap) 328 329 330 331 332 333 334 335 // Come up with new names for all private class members. appView.programClassPool.classesAccept( new MultiClassVisitor( // Collect all member names in this class. new AllMemberVisitor( new MemberNameCollector(configuration.overloadAggressively, descriptorMap)), 336 337 338 339 340 341 342 // Collect all non-private member names higher up the hierarchy. new ClassHierarchyTraveler(false, true, true, false, new AllMemberVisitor( new MemberAccessFilter(0, AccessConstants.PRIVATE, new MemberNameCollector(configuration.overloadAggressively, descriptorMap)))), 343 344 345 346 347 348 349 350 351 352 // Collect all member names from interfaces of abstract // classes down the hierarchy. // Due to an error in the JLS/JVMS, virtual invocations // may end up at a private method otherwise (Sun/Oracle // bugs #6691741 and #6684387, Proguard bug #3471941, // and Proguard test #1180). new ClassHierarchyTraveler(false, false, false, true, new ClassAccessFilter(AccessConstants.ABSTRACT, 0, new ClassHierarchyTraveler(false, false, true, false, 126 new AllMemberVisitor( new MemberNameCollector(configuration.overloadAggressively, descriptorMap))))), 353 354 355 356 // Collect all default method names from interfaces of // any classes down the hierarchy. // This is an extended version of the above problem // (Sun/Oracle bug #802464, Proguard bug #662, and // Proguard test #2060). new ClassHierarchyTraveler(false, false, false, true, new ClassHierarchyTraveler(false, false, true, false, new AllMethodVisitor( new MemberAccessFilter(0, AccessConstants.ABSTRACT | AccessConstants.STATIC, new MemberNameCollector(configuration.overloadAggressively, descriptorMap))))), 357 358 359 360 361 362 363 364 365 366 367 368 // Assign new names to all private members in this class. new AllMemberVisitor( new MemberAccessFilter(AccessConstants.PRIVATE, 0, new MemberObfuscator(configuration.overloadAggressively, nameFactory, descriptorMap))), 369 370 371 372 373 374 375 376 377 378 379 } )); // Clear the collected names. new MapCleaner(descriptorMap) 380 381 382 383 384 // Some class members may have ended up with conflicting names. // Come up with new, globally unique names for them. NameFactory specialNameFactory = new SpecialNameFactory(new SimpleNameFactory()); 127 385 386 387 388 // Collect a map of special names to avoid // [descriptor - new name - old name]. Map specialDescriptorMap = new HashMap(); 389 390 391 392 393 394 appView.programClassPool.classesAccept( new AllMemberVisitor( new MemberSpecialNameFilter( new MemberNameCollector(configuration.overloadAggressively, specialDescriptorMap)))); 395 396 397 398 399 400 appView.libraryClassPool.classesAccept( new AllMemberVisitor( new MemberSpecialNameFilter( new MemberNameCollector(configuration.overloadAggressively, specialDescriptorMap)))); 401 402 403 404 405 406 407 408 409 410 411 // Replace conflicting non-private member names with special names. appView.programClassPool.classesAccept( new MultiClassVisitor( // Collect all private member names in this class and down // the hierarchy. new ClassHierarchyTraveler(true, false, false, true, new AllMemberVisitor( new MemberAccessFilter(AccessConstants.PRIVATE, 0, new MemberNameCollector(configuration.overloadAggressively, descriptorMap)))), 412 413 414 415 416 417 418 419 // Collect all non-private member names in this class and // higher up the hierarchy. new ClassHierarchyTraveler(true, true, true, false, new AllMemberVisitor( new MemberAccessFilter(0, AccessConstants.PRIVATE, new MemberNameCollector(configuration.overloadAggressively, descriptorMap)))), 420 128 // Assign new names to all conflicting non-private members // in this class and higher up the hierarchy. new ClassHierarchyTraveler(true, true, true, false, new AllMemberVisitor( new MemberAccessFilter(0, AccessConstants.PRIVATE, new MemberNameConflictFixer(configuration.overloadAggressively, descriptorMap, warningPrinter, new MemberObfuscator(configuration.overloadAggressively, specialNameFactory, specialDescriptorMap))))), 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 )); // Clear the collected names. new MapCleaner(descriptorMap) 436 437 438 439 440 441 442 443 444 // Replace conflicting private member names with special names. // This is only possible if those names were kept or mapped. appView.programClassPool.classesAccept( new MultiClassVisitor( // Collect all member names in this class. new AllMemberVisitor( new MemberNameCollector(configuration.overloadAggressively, descriptorMap)), 445 446 447 448 449 450 451 // Collect all non-private member names higher up the hierarchy. new ClassHierarchyTraveler(false, true, true, false, new AllMemberVisitor( new MemberAccessFilter(0, AccessConstants.PRIVATE, new MemberNameCollector(configuration.overloadAggressively, descriptorMap)))), 452 453 454 455 // Assign new names to all conflicting private members in this // class. new AllMemberVisitor( 129 new MemberAccessFilter(AccessConstants.PRIVATE, 0, new MemberNameConflictFixer(configuration.overloadAggressively, descriptorMap, warningPrinter, new MemberObfuscator(configuration.overloadAggressively, specialNameFactory, specialDescriptorMap)))), 456 457 458 459 460 461 462 463 464 465 )); 466 // Clear the collected names. new MapCleaner(descriptorMap) 467 468 469 470 471 472 473 // Print out any warnings about member name conflicts. int warningCount = warningPrinter.getWarningCount(); if (warningCount > 0) { logger.warn("Warning: there were {} conflicting class member name mappings.", warningCount); logger.warn(" Your configuration may be inconsistent."); 474 if (!configuration.ignoreWarnings) { logger.warn(" If you are sure the conflicts are harmless,"); logger.warn(" you could try your luck using the '-ignorewarnings' option."); } 475 476 477 478 479 480 logger.warn(" (https://www.guardsquare.com/proguard/manual/troubleshooting#mappingconfl 481 482 483 484 485 486 487 } if (!configuration.ignoreWarnings) { throw new IOException("Please correct the above warnings first."); } 488 489 490 // Obfuscate the Intrinsics.check* method calls. appView.programClassPool.classesAccept( 130 491 492 493 ); new InstructionSequenceObfuscator( new KotlinIntrinsicsReplacementSequences(appView.programClassPool, appView.libraryClassPool)) 494 495 496 497 498 499 500 501 502 503 504 if (configuration.keepKotlinMetadata) { appView.programClassPool.classesAccept( new MultiClassVisitor( new ReferencedKotlinMetadataVisitor( new MultiKotlinMetadataVisitor( // Come up with new names for Kotlin Properties. new KotlinPropertyNameObfuscator(nameFactory), // Obfuscate alias names. new KotlinAliasNameObfuscator(nameFactory), 505 506 507 508 509 // Equalise/fix $DefaultImpls and $WhenMappings classes. new KotlinSyntheticClassFixer(), // Ensure object classes have the INSTANCE field. new KotlinObjectFixer(), 510 511 512 513 514 515 516 517 518 519 520 521 new AllFunctionVisitor( // Ensure that all default interface implementations of methods have the same names. new KotlinDefaultImplsMethodNameEqualizer(), // Ensure all $default methods match their counterpart but with a $default suffix. new KotlinDefaultMethodNameEqualizer(), // Obfuscate the throw new UnsupportedOperationExceptions in $default methods // because they contain the original function name in the string. new KotlinFunctionToDefaultMethodVisitor( new InstructionSequenceObfuscator( new KotlinUnsupportedExceptionReplacementSequences(appView.prog appView.libraryClassPool))) ), 522 131 523 524 525 526 527 )); 528 // Obfuscate toString & toString-impl methods in data classes and inline/value classes. new KotlinClassKindFilter( kc -> (kc.flags.isValue || kc.flags.isData), new KotlinSyntheticToStringObfuscator())) ) 529 530 531 532 533 } appView.resourceFilePool.resourceFilesAccept( new ResourceFileProcessingFlagFilter(0, ProcessingFlags.DONT_OBFUSCATE, new KotlinModuleNameObfuscator(nameFactory 534 535 536 537 538 // Print out the mapping, if requested. if (configuration.printMapping != null) { logger.info("Printing mapping to [{}]...", PrintWriterUtil.fileName(configuration.printMapping)); 539 PrintWriter mappingWriter = PrintWriterUtil.createPrintWriter(configuration.printMapping, out); 540 541 542 try { 543 544 545 546 547 548 549 550 551 552 553 554 } // Print out items that will be renamed. appView.programClassPool.classesAcceptAlphabetically( new MappingPrinter(mappingWriter)); } finally { PrintWriterUtil.closePrintWriter(configuration.printMapping, mappingWriter); } 555 556 557 558 559 if (configuration.addConfigurationDebugging) { appView.programClassPool.classesAccept(new RenamedFlagSetter()); } 132 560 561 562 563 564 565 // Collect some statistics about the number of obfuscated // classes and members. ClassCounter obfuscatedClassCounter = new ClassCounter(); MemberCounter obfuscatedFieldCounter = new MemberCounter(); MemberCounter obfuscatedMethodCounter = new MemberCounter(); 566 567 568 569 570 ClassVisitor classRenamer = new proguard.obfuscate.ClassRenamer( new ProgramClassFilter( obfuscatedClassCounter), 571 572 573 574 575 576 ); new ProgramMemberFilter( new MethodFilter( obfuscatedMethodCounter, obfuscatedFieldCounter)) 577 578 579 580 581 582 583 584 if (configuration.keepKotlinMetadata) { // Ensure multi-file parts and facades are in the same package. appView.programClassPool.classesAccept( new ReferencedKotlinMetadataVisitor( new KotlinMultiFileFacadeFixer())); } 585 586 587 588 // Actually apply the new names. appView.programClassPool.classesAccept(classRenamer); appView.libraryClassPool.classesAccept(classRenamer); 589 590 591 592 593 594 595 596 597 if (configuration.keepKotlinMetadata) { // Apply new names to Kotlin properties. appView.programClassPool.classesAccept( new ReferencedKotlinMetadataVisitor( new AllPropertyVisitor( new KotlinPropertyRenamer()))); } 598 599 600 // Update all references to these new names. appView.programClassPool.classesAccept(new ClassReferenceFixer(false)); 133 601 602 appView.libraryClassPool.classesAccept(new ClassReferenceFixer(false)); appView.programClassPool.classesAccept(new MemberReferenceFixer(configuration.android)); 603 604 605 606 607 608 609 610 611 if (configuration.keepKotlinMetadata) { appView.programClassPool.classesAccept( new ReferencedKotlinMetadataVisitor( new MultiKotlinMetadataVisitor( new AllTypeVisitor( // Fix all the alias references. new KotlinAliasReferenceFixer()), 612 613 614 615 } // Fix all the CallableReference interface methods to match the new names. new KotlinCallableReferenceFixer(appView.programClassPool, appView.libraryClassPool)))); 616 617 618 619 620 621 622 623 // Make package visible elements public or protected, if obfuscated // classes are being repackaged aggressively. if (configuration.repackageClasses != null && configuration.allowAccessModification) { appView.programClassPool.classesAccept( new AccessFixer()); 624 625 626 627 628 629 630 631 632 633 } // Fix the access flags of the inner classes information. // Don't change the access flags of inner classes that // have not been renamed (Guice). [DGD-63] appView.programClassPool.classesAccept( new OriginalClassNameFilter(null, new AllAttributeVisitor( new AllInnerClassesInfoVisitor( new InnerClassesAccessFixer())))); 634 635 636 637 // Fix the bridge method flags. appView.programClassPool.classesAccept( new AllMethodVisitor( 134 new BridgeMethodFixer())); 638 639 // Rename the source file attributes, if requested. if (configuration.newSourceFileAttribute != null) { appView.programClassPool.classesAccept(new SourceFileRenamer(configuration.newSourceFileAttribute)); } 640 641 642 643 644 645 // Remove unused constants. appView.programClassPool.classesAccept( new ConstantPoolShrinker()); 646 647 648 649 650 651 652 653 654 } } A.7 logger.info(" Number of obfuscated classes: obfuscatedClassCounter.getCount()); logger.info(" Number of obfuscated fields: obfuscatedFieldCounter.getCount()); logger.info(" Number of obfuscated methods: obfuscatedMethodCounter.getCount()); {}", {}", {}", ClassObfuscator.java 1 2 package proguard.obfuscate; 3 4 // ... Ignoring the imports because it's too verbose ... 5 6 7 8 9 10 11 12 13 14 15 16 /** * This <code>ClassVisitor</code> comes up with obfuscated names for the * classes it visits, and for their class members. The actual renaming is * done afterward. * * @see proguard.obfuscate.ClassRenamer * * @author Eric Lafortune */ public class ClassObfuscator implements ClassVisitor, 135 AttributeVisitor, InnerClassesInfoVisitor, ConstantVisitor 17 18 19 20 21 22 23 24 25 26 27 28 { private private private private private private private private final final final final final final final final DictionaryNameFactory classNameFactory; DictionaryNameFactory packageNameFactory; boolean useMixedCaseClassNames; StringMatcher keepPackageNamesMatcher; String flattenPackageHierarchy; String repackageClasses; boolean allowAccessModification; boolean adaptKotlin; 29 30 private final Set classNamesToAvoid HashSet(); = new 31 32 33 // Map: [package prefix - new package prefix] private final Map packagePrefixMap HashMap(); = new 34 35 36 // Map: [package prefix - package name factory] private final Map packagePrefixPackageNameFactoryMap = new HashMap(); 37 38 39 // Map: [package prefix - numeric class name factory] private final Map packagePrefixClassNameFactoryMap = new HashMap(); 40 41 42 // Map: [package prefix - numeric class name factory] private final Map packagePrefixNumericClassNameFactoryMap = new HashMap(); 43 44 45 46 47 // Field acting as temporary variables and as return values for names // of outer classes and types of inner classes. private String newClassName; private boolean numericClassName; 48 49 50 51 52 53 /** * Creates a new ClassObfuscator. * @param programClassPool the class pool in which class names * have to be unique. 136 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 * @param libraryClassPool the class pool from which class names * have to be avoided. * @param classNameFactory the optional class obfuscation dictionary. * @param packageNameFactory the optional package obfuscation * dictionary. * @param useMixedCaseClassNames specifies whether obfuscated packages and * classes can get mixed-case names. * @param keepPackageNames the optional filter for which matching * package names are kept. * @param flattenPackageHierarchy the base package if the obfuscated package * hierarchy is to be flattened. * @param repackageClasses the base package if the obfuscated classes * are to be repackaged. * @param allowAccessModification specifies whether obfuscated classes can * be freely moved between packages. * @param adaptKotlin specifies whether Kotlin should be supported. */ public ClassObfuscator(ClassPool programClassPool, ClassPool libraryClassPool, DictionaryNameFactory classNameFactory, DictionaryNameFactory packageNameFactory, boolean useMixedCaseClassNames, List keepPackageNames, String flattenPackageHierarchy, String repackageClasses, boolean allowAccessModification, boolean adaptKotlin) { this.classNameFactory = classNameFactory; this.packageNameFactory = packageNameFactory; 84 85 86 87 88 // First append the package separator if necessary. if (flattenPackageHierarchy != null && flattenPackageHierarchy.length() > 0) { 137 89 } 90 flattenPackageHierarchy += TypeConstants.PACKAGE_SEPARATOR; 91 // First append the package separator if necessary. if (repackageClasses != null && repackageClasses.length() > 0) { repackageClasses += TypeConstants.PACKAGE_SEPARATOR; } 92 93 94 95 96 97 98 this.useMixedCaseClassNames = useMixedCaseClassNames; this.keepPackageNamesMatcher = keepPackageNames == null ? null : new ListParser(new FileNameParser()).parse(keepPackageNames); this.flattenPackageHierarchy = flattenPackageHierarchy; this.repackageClasses = repackageClasses; this.allowAccessModification = allowAccessModification; this.adaptKotlin = adaptKotlin; 99 100 101 102 103 104 105 106 // Map the root package onto the root package. packagePrefixMap.put("", ""); 107 108 109 110 111 112 113 } // Collect all names that have already been taken. programClassPool.classesAccept(new MyKeepCollector()); libraryClassPool.classesAccept(new MyKeepCollector()); 114 115 116 // Implementations for ClassVisitor. 117 118 119 120 121 122 @Override public void visitAnyClass(Clazz clazz) { throw new UnsupportedOperationException(this.getClass().getName() + " does not support " + clazz.getClass().getName()); } 123 124 125 126 @Override public void visitProgramClass(ProgramClass programClass) 138 127 { 128 129 130 131 132 133 134 135 // Does this class still need a new name? newClassName = newClassName(programClass); if (newClassName == null) { // Make sure the outer class has a name, if it exists. The name will // be stored as the new class name, as a side effect, so we'll be // able to use it as a prefix. programClass.attributesAccept(this); 136 // Figure out a package prefix. The package prefix may actually be // the an outer class prefix, if any, or it may be the fixed base // package, if classes are to be repackaged. String newPackagePrefix = newClassName != null ? newClassName + TypeConstants.INNER_CLASS_SEPARATOR : newPackagePrefix(ClassUtil.internalPackagePrefix(programClass.getName())) 137 138 139 140 141 142 143 // Come up with a new class name, numeric or ordinary. newClassName = newClassName != null && numericClassName ? generateUniqueNumericClassName(newPackagePrefix) : generateUniqueClassName(newPackagePrefix); 144 145 146 147 148 149 150 151 } } setNewClassName(programClass, newClassName); 152 153 154 155 156 157 158 159 160 @Override public void visitLibraryClass(LibraryClass libraryClass) { // This can happen for dubious input, if the outer class of a program // class is a library class, and its name is requested. newClassName = libraryClass.getName(); } 161 162 163 // Implementations for AttributeVisitor. 164 139 165 public void visitAnyAttribute(Clazz clazz, Attribute attribute) {} 166 167 168 169 170 171 172 public void visitInnerClassesAttribute(Clazz clazz, InnerClassesAttribute innerClassesAttribute) { // Make sure the outer classes have a name, if they exist. innerClassesAttribute.innerClassEntriesAccept(clazz, this); } 173 174 175 176 177 178 public void visitEnclosingMethodAttribute(Clazz clazz, EnclosingMethodAttribute enclosingMethodAttribute) { // Make sure the enclosing class has a name. enclosingMethodAttribute.referencedClassAccept(this); 179 String innerClassName = clazz.getName(); String outerClassName = clazz.getClassName(enclosingMethodAttribute.u2classIndex); 180 181 182 183 184 } numericClassName = isNumericClassName(clazz, innerClassName, outerClassName); 185 186 187 // Implementations for InnerClassesInfoVisitor. 188 189 190 191 192 193 194 195 196 197 198 199 200 public void visitInnerClassesInfo(Clazz clazz, InnerClassesInfo innerClassesInfo) { // Make sure the outer class has a name, if it exists. int innerClassIndex = innerClassesInfo.u2innerClassIndex; int outerClassIndex = innerClassesInfo.u2outerClassIndex; if (innerClassIndex != 0 && outerClassIndex != 0) { String innerClassName = clazz.getClassName(innerClassIndex); if (innerClassName.equals(clazz.getName())) { clazz.constantPoolEntryAccept(outerClassIndex, this); 140 201 String outerClassName = clazz.getClassName(outerClassIndex); 202 203 204 205 206 207 } } } numericClassName = isNumericClassName(clazz, innerClassName, outerClassName); 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 /** * Returns whether the given class is a synthetic Kotlin lambda class. * We then know it's numeric. */ private boolean isSyntheticKotlinLambdaClass(Clazz innerClass) { // Kotlin synthetic lambda classes that were named based on the // location that they were inlined from may be named like // OuterClass$methodName$1 where $methodName$1 is the inner class // name. We can rename this class to OuterClass$1 but the default // code below doesn't detect it as numeric. ClassCounter counter = new ClassCounter(); innerClass.accept( new ReferencedKotlinMetadataVisitor( new KotlinSyntheticClassKindFilter( KotlinSyntheticClassKindFilter::isLambda, new KotlinMetadataToClazzVisitor(counter)))); 227 228 229 } return counter.getCount() == 1; 230 231 232 233 234 235 236 237 /** * Returns whether the given inner class name is a numeric name. */ private boolean isNumericClassName(Clazz innerClass, String innerClassName, String outerClassName) 141 238 { 239 240 241 242 if (this.adaptKotlin && isSyntheticKotlinLambdaClass(innerClass)) { return true; } 243 int innerClassNameStart = outerClassName.length() + 1; int innerClassNameLength = innerClassName.length(); 244 245 246 if (innerClassNameStart >= innerClassNameLength) { return false; } 247 248 249 250 251 for (int index = innerClassNameStart; index < innerClassNameLength; index++) { if (!Character.isDigit(innerClassName.charAt(index))) { return false; } } 252 253 254 255 256 257 258 259 260 261 } return true; 262 263 264 // Implementations for ConstantVisitor. 265 266 267 268 269 270 public void visitClassConstant(Clazz clazz, ClassConstant classConstant) { // Make sure the outer class has a name. classConstant.referencedClassAccept(this); } 271 272 273 274 275 276 /** * This ClassVisitor collects package names and class names that have to * be kept. */ 142 277 278 279 280 281 private class MyKeepCollector implements ClassVisitor { @Override public void visitAnyClass(Clazz clazz) { } 282 283 284 285 286 287 288 289 290 291 292 @Override public void visitProgramClass(ProgramClass programClass) { // Does the program class already have a new name? String newClassName = newClassName(programClass); if (newClassName != null) { // Remember not to use this name. classNamesToAvoid.add(mixedCaseClassName(newClassName)); 293 // Are we not aggressively repackaging all obfuscated classes? if (repackageClasses == null || !allowAccessModification) { String className = programClass.getName(); 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 } } } // Keep the package name for all other classes in the same // package. Do this recursively if we're not doing any // repackaging. mapPackageName(className, newClassName, repackageClasses == null && flattenPackageHierarchy == null); 310 311 312 313 314 315 public void visitLibraryClass(LibraryClass libraryClass) { // Get the new name or the original name of the library class. String newClassName = newClassName(libraryClass); 143 if (newClassName == null) { newClassName = libraryClass.getName(); } 316 317 318 319 320 // Remember not to use this name. classNamesToAvoid.add(mixedCaseClassName(newClassName)); 321 322 323 // Are we not aggressively repackaging all obfuscated classes? if (repackageClasses == null || !allowAccessModification) { String className = libraryClass.getName(); 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 } } // Keep the package name for all other classes in the same // package. Do this recursively if we're not doing any // repackaging. mapPackageName(className, newClassName, repackageClasses == null && flattenPackageHierarchy == null); 339 340 341 342 343 344 345 346 347 348 349 350 /** * Makes sure the package name of the given class will always be mapped * consistently with its new name. */ private void mapPackageName(String className, String newClassName, boolean recursively) { String packagePrefix = ClassUtil.internalPackagePrefix(className); String newPackagePrefix = ClassUtil.internalPackagePrefix(newClassName); 351 352 // Put the mapping of this package prefix, and possibly of its 144 // entire hierarchy, into the package prefix map. do { packagePrefixMap.put(packagePrefix, newPackagePrefix); 353 354 355 356 357 if (!recursively) { break; } 358 359 360 361 362 packagePrefix = ClassUtil.internalPackagePrefix(packagePrefix); newPackagePrefix = ClassUtil.internalPackagePrefix(newPackagePrefix); 363 364 365 366 367 } 368 } while (packagePrefix.length() > 0 && newPackagePrefix.length() > 0); 369 370 } 371 372 373 // Small utility methods. 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 /** * Finds or creates the new package prefix for the given package. */ private String newPackagePrefix(String packagePrefix) { // Doesn't the package prefix have a new package prefix yet? String newPackagePrefix = (String)packagePrefixMap.get(packagePrefix); if (newPackagePrefix == null) { // Are we keeping the package name? if (keepPackageNamesMatcher != null && keepPackageNamesMatcher.matches(packagePrefix.length() > 0 ? packagePrefix.substring(0, packagePrefix.length()-1) : packagePrefix)) { 145 390 } 391 return packagePrefix; 392 // Are we forcing a new package prefix? if (repackageClasses != null) { return repackageClasses; } 393 394 395 396 397 398 // Are we forcing a new superpackage prefix? // Otherwise figure out the new superpackage prefix, recursively. String newSuperPackagePrefix = flattenPackageHierarchy != null ? flattenPackageHierarchy : newPackagePrefix(ClassUtil.internalPackagePrefix(packagePrefix)); 399 400 401 402 403 404 // Come up with a new package prefix. newPackagePrefix = generateUniquePackagePrefix(newSuperPackagePrefix); 405 406 407 408 409 } 410 // Remember to use this mapping in the future. packagePrefixMap.put(packagePrefix, newPackagePrefix); 411 412 413 } return newPackagePrefix; 414 415 416 417 418 419 420 421 422 423 424 425 426 427 /** * Creates a new package prefix in the given new superpackage. */ private String generateUniquePackagePrefix(String newSuperPackagePrefix) { // Find the right name factory for this package. NameFactory packageNameFactory = (NameFactory)packagePrefixPackageNameFactoryMap.get(newSuperPackagePrefix); if (packageNameFactory == null) { // We haven't seen packages in this superpackage before. Create // a new name factory for them. 146 packageNameFactory = new SimpleNameFactory(useMixedCaseClassNames); if (this.packageNameFactory != null) { packageNameFactory = new DictionaryNameFactory(this.packageNameFactory, packageNameFactory); } 428 429 430 431 432 433 434 435 436 437 } 438 packagePrefixPackageNameFactoryMap.put(newSuperPackagePrefix, packageNameFactory); 439 440 441 } return generateUniquePackagePrefix(newSuperPackagePrefix, packageNameFactory); 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 /** * Creates a new package prefix in the given new superpackage, with the * given package name factory. */ private String generateUniquePackagePrefix(String newSuperPackagePrefix, NameFactory packageNameFactory) { // Come up with package names until we get an original one. String newPackagePrefix; do { // Let the factory produce a package name. newPackagePrefix = newSuperPackagePrefix + packageNameFactory.nextName() + TypeConstants.PACKAGE_SEPARATOR; } while (packagePrefixMap.containsValue(newPackagePrefix)); 461 462 463 } return newPackagePrefix; 464 465 147 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 /** * Creates a new class name in the given new package. */ private String generateUniqueClassName(String newPackagePrefix) { // Find the right name factory for this package. NameFactory classNameFactory = (NameFactory)packagePrefixClassNameFactoryMap.get(newPackagePrefix); if (classNameFactory == null) { // We haven't seen classes in this package before. // Create a new name factory for them. classNameFactory = new SimpleNameFactory(useMixedCaseClassNames); if (this.classNameFactory != null) { classNameFactory = new DictionaryNameFactory(this.classNameFactory, classNameFactory); } 485 486 487 } 488 packagePrefixClassNameFactoryMap.put(newPackagePrefix, classNameFactory); 489 490 491 } return generateUniqueClassName(newPackagePrefix, classNameFactory); 492 493 494 495 496 497 498 499 500 501 502 503 504 505 /** * Creates a new class name in the given new package. */ private String generateUniqueNumericClassName(String newPackagePrefix) { // Find the right name factory for this package. NameFactory classNameFactory = (NameFactory)packagePrefixNumericClassNameFactoryMap.get(newPackagePrefix); if (classNameFactory == null) { // We haven't seen classes in this package before. // Create a new name factory for them. 148 classNameFactory = new NumericNameFactory(); 506 507 508 509 } 510 packagePrefixNumericClassNameFactoryMap.put(newPackagePrefix, classNameFactory); 511 512 513 } return generateUniqueClassName(newPackagePrefix, classNameFactory); 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 /** * Creates a new class name in the given new package, with the given * class name factory. */ private String generateUniqueClassName(String newPackagePrefix, NameFactory classNameFactory) { // Come up with class names until we get an original one. String newClassName; String newMixedCaseClassName; do { // Let the factory produce a class name. newClassName = newPackagePrefix + classNameFactory.nextName(); 531 532 533 534 newMixedCaseClassName = mixedCaseClassName(newClassName); } while (classNamesToAvoid.contains(newMixedCaseClassName)); 535 536 537 538 539 540 541 542 543 // Explicitly make sure the name isn't used again if we have a // user-specified dictionary and we're not allowed to have mixed case // class names -- just to protect against problematic dictionaries. if (this.classNameFactory != null && !useMixedCaseClassNames) { classNamesToAvoid.add(newMixedCaseClassName); } 149 544 545 546 } return newClassName; 547 548 549 550 551 552 553 554 555 556 557 558 /** * Returns the given class name, unchanged if mixed-case class names are * allowed, or the lower-case version otherwise. */ private String mixedCaseClassName(String className) { return useMixedCaseClassNames ? className : className.toLowerCase(); } 559 560 561 562 563 564 565 566 567 568 569 /** * Assigns a new name to the given class. * @param clazz the given class. * @param name the new name. */ public static void setNewClassName(Clazz clazz, String name) { clazz.setProcessingInfo(name); } 570 571 572 573 574 575 576 577 578 579 580 581 /** * Returns whether the class name of the given class has changed. * * @param clazz the given class. * @return true if the class name is unchanged, false otherwise. */ public static boolean hasOriginalClassName(Clazz clazz) { return clazz.getName().equals(newClassName(clazz)); } 582 583 584 /** 150 * Retrieves the new name of the given class. * @param clazz the given class. * @return the class's new name, or <code>null</code> if it doesn't * have one yet. */ public static String newClassName(Clazz clazz) { Object processingInfo = clazz.getProcessingInfo(); 585 586 587 588 589 590 591 592 593 594 595 596 597 598 } } A.8 1 return processingInfo instanceof String ? (String)processingInfo : null; NameFactory.java package proguard.obfuscate; 2 3 4 5 6 7 8 9 10 11 /** * This interfaces provides methods to generate unique sequences of names. * The names must be valid Java identifiers. * * @author Eric Lafortune */ public interface NameFactory { public void reset(); 12 13 14 } public String nextName(); A.9 1 2 3 4 SimpleNameFactory.java /* * Proguard -- shrinking, optimization, obfuscation, and preverification * of Java bytecode. * 151 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 * Copyright (c) 2002-2022 Guardsquare NV * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package proguard.obfuscate; 22 23 import java.util.Arrays; 24 25 26 27 28 29 30 31 32 33 /** * This <code>NameFactory</code> generates unique short names, using mixed-case * characters or lower-case characters only. * * @author Eric Lafortune */ public class SimpleNameFactory implements NameFactory { private static final int CHARACTER_COUNT = 26; 34 35 36 37 /** + + * Array of windows reserved names. * This array does not include COM{digit} or LPT{digit} as {@link SimpleNameFactory} does not generate digits. 152 38 39 40 + * This array must be sorted in ascending order as we're using {@link Arrays#binarySearch(Object[], Object)} on it. + */ private static final String[] reservedNames = new String[] {"AUX", "CON", "NUL", "PRN"}; 41 42 43 private final boolean generateMixedCaseNames; private int index = 0; 44 45 46 47 48 49 50 51 /** * Creates a new <code>SimpleNameFactory</code> that generates mixed-case names. */ public SimpleNameFactory() { this(true); } 52 53 54 55 56 57 58 59 60 61 62 /** * Creates a new <code>SimpleNameFactory</code>. * @param generateMixedCaseNames a flag to indicate whether the generated * names will be mixed-case, or lower-case only. */ public SimpleNameFactory(boolean generateMixedCaseNames) { this.generateMixedCaseNames = generateMixedCaseNames; } 63 64 65 // Implementations for NameFactory. 66 67 68 69 70 public void reset() { index = 0; } 71 72 73 74 75 public String nextName() { return name(index++); 153 76 } 77 78 79 80 81 82 83 84 85 86 /** * Returns the name at the given index. */ private String name(int index) { // Create a new name for this index return newName(index); } 87 88 89 90 91 92 93 94 95 96 97 98 /** * Creates and returns the name at the given index. */ private String newName(int index) { // If we're allowed to generate mixed-case names, we can use twice as // many characters. int totalCharacterCount = generateMixedCaseNames ? 2 * CHARACTER_COUNT : CHARACTER_COUNT; 99 int baseIndex = index / totalCharacterCount; int offset = index % totalCharacterCount; 100 101 102 char newChar = charAt(offset); 103 104 String newName = baseIndex == 0 ? new String(new char[] { newChar }) : (name(baseIndex-1) + newChar); 105 106 107 108 109 110 111 112 113 114 } if (Arrays.binarySearch(reservedNames, newName.toUpperCase()) >= 0) { newName += newChar; } return newName; 115 116 154 /** * Returns the character with the given index, between 0 and the number of * acceptable characters. */ private char charAt(int index) { return (char)((index < CHARACTER_COUNT ? 'a' - 0 : 'A' - CHARACTER_COUNT) + index); } 117 118 119 120 121 122 123 124 125 126 127 public static void main(String[] args) { System.out.println("Some mixed-case names:"); printNameSamples(new SimpleNameFactory(true), 60); System.out.println("Some lower-case names:"); printNameSamples(new SimpleNameFactory(false), 60); System.out.println("Some more mixed-case names:"); printNameSamples(new SimpleNameFactory(true), 80); System.out.println("Some more lower-case names:"); printNameSamples(new SimpleNameFactory(false), 80); } 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 } private static void printNameSamples(SimpleNameFactory factory, int count) { for (int counter = 0; counter < count; counter++) { System.out.println(" ["+factory.nextName()+"]"); } } A.10 1 ComplexNameFactory.java package proguard.obfuscate; 2 3 4 import java.util.Arrays; import java.util.Random; 155 5 6 7 8 9 10 11 12 13 14 15 /** * This <code>NameFactory</code> generates unique more complex names, using a combination of * characters, numbers, and some special characters. */ public class ComplexNameFactory implements NameFactory { private static final int CHARACTER_COUNT = 52; // a-z, A-Z private static final int DIGIT_COUNT = 10; // 0-9 private static final char[] SPECIAL_CHARACTERS = {'_', '-', '$', '@', '#'}; // 255 is used for MAX_LENGTH because even if the max value for a utf8 char is 2^16-1, if we generate package names bigger than that, when extracting a jar file with any utility it will fail since maximum filename and directory sizes are 255 in unix and windows private static final int MAX_LENGTH = 50; // Maximum length of the generated name 16 17 18 19 20 21 22 23 /** + + * Array of windows reserved names. * This array does not include COM{digit} or LPT{digit} as {@link SimpleNameFactory} does not generate digits. + * This array must be sorted in ascending order as we're using {@link Arrays#binarySearch(Object[], Object)} on it. + */ private static final String[] reservedNames = new String[] {"AUX", "CON", "NUL", "PRN"}; private final Random random = new Random(); 24 25 26 27 28 /** * Resets the name generation index. */ public void reset() {} 29 30 31 32 33 34 35 36 /** * Generates the next complex name. */ public String nextName() { int length = random.nextInt(MAX_LENGTH) + 1; // Ensure at least 1 character StringBuilder nameBuilder = new StringBuilder(length); for (int i = 0; i < length; i++) { 156 nameBuilder.append(randomCharacter()); } String newName = nameBuilder.toString(); 37 38 39 40 // Avoid reserved names if (Arrays.binarySearch(ComplexNameFactory.reservedNames, newName.toUpperCase()) >= 0) { return nextName(); // Recursively generate a new name if reserved } 41 42 43 44 45 46 } 47 return newName; 48 /** * Generates a random character (letter, digit, or special character). */ private char randomCharacter() { int choice = random.nextInt(CHARACTER_COUNT + DIGIT_COUNT + SPECIAL_CHARACTERS.length); if (choice < CHARACTER_COUNT) { return (char) ((choice % 26) + (choice < 26 ? 'a' : 'A')); } else if (choice < CHARACTER_COUNT + DIGIT_COUNT) { return (char) ('0' + (choice - CHARACTER_COUNT)); } else { return SPECIAL_CHARACTERS[choice - CHARACTER_COUNT DIGIT_COUNT]; } } 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 } // Main method for testing public static void main(String[] args) { System.out.println("Complex names:"); ComplexNameFactory factory = new ComplexNameFactory(); for (int i = 0; i < 100; i++) { System.out.println(" [" + factory.nextName() + "]"); } } 72 73 \subsection{StringObfuscationPass.java} 157 74 75 76 \label{subsec:stringobfuscationpass_dot_java} \begin{lstlisting} package proguard.obfuscate; 77 78 79 80 81 82 83 84 import import import import import import import org.apache.logging.log4j.LogManager; org.apache.logging.log4j.Logger; proguard.AppView; proguard.Configuration; proguard.classfile.visitor.AllClassVisitor; proguard.classfile.visitor.ClassVisitor; proguard.pass.Pass; 85 86 87 88 89 90 91 public class StringObfuscationPass implements Pass { Configuration configuration; private static final Logger logger = LogManager.getLogger(StringObfuscationPass.class); public StringObfuscationPass(Configuration configuration) { this.configuration = configuration; } 92 93 @Override public void execute(AppView appView) throws Exception { appView.programClassPool.accept( new AllClassVisitor( new StringObfuscatorVisitor() ) ); 94 95 96 97 98 99 100 101 102 103 } } A.11 1 StringObfusatorVisitor.java package proguard.obfuscate; 2 3 4 5 6 7 8 import import import import import import org.apache.logging.log4j.LogManager; org.apache.logging.log4j.Logger; proguard.classfile.*; proguard.classfile.attribute.CodeAttribute; proguard.classfile.attribute.visitor.AllAttributeVisitor; proguard.classfile.attribute.visitor.AttributeVisitor; 158 9 10 11 12 13 14 15 16 17 18 19 20 import import import import import import import import import import import import proguard.classfile.constant.Constant; proguard.classfile.constant.StringConstant; proguard.classfile.constant.Utf8Constant; proguard.classfile.constant.visitor.ConstantVisitor; proguard.classfile.editor.CodeAttributeEditor; proguard.classfile.editor.ConstantPoolEditor; proguard.classfile.editor.InstructionSequenceBuilder; proguard.classfile.instruction.ConstantInstruction; proguard.classfile.instruction.Instruction; proguard.classfile.instruction.visitor.InstructionVisitor; proguard.classfile.visitor.ClassPrinter; proguard.classfile.visitor.ClassVisitor; 21 22 import java.util.Base64; 23 24 25 26 27 28 29 30 31 32 33 public class StringObfuscatorVisitor implements ClassVisitor, AttributeVisitor, InstructionVisitor, ConstantVisitor { enum STATE_OF_STRING { OLDIE, NEW_OBFUSCATED, IGNORE } private static final Logger logger = LogManager.getLogger(StringObfuscatorVisitor.class); private final CodeAttributeEditor codeAttributeEditor; 34 35 36 37 38 public StringObfuscatorVisitor() { codeAttributeEditor = new CodeAttributeEditor(true, false); codeAttributeEditor.reset(10000); } 39 40 41 42 43 44 45 46 private Instruction[] generateDecodingInstructions(int indexOfObfuscatedString, ProgramClass programClass) { InstructionSequenceBuilder instructionBuilder = new InstructionSequenceBuilder(programClass); return instructionBuilder .new_("java/lang/String") .dup() .invokestatic("java/util/Base64", "getDecoder", "()Ljava/util/Base64$Decoder;") .ldc_(indexOfObfuscatedString) 159 47 48 49 50 } .invokevirtual("java/util/Base64$Decoder", "decode", "(Ljava/lang/String;)[B") .invokespecial("java/lang/String", "<init>", "([B)V") .instructions(); 51 52 53 54 @Override public void visitAnyClass(Clazz clazz) { } 55 56 57 58 59 @Override public void visitProgramClass(ProgramClass programClass) { programClass.methodsAccept(new AllAttributeVisitor(this)); } 60 61 62 63 64 65 @Override public void visitCodeAttribute(Clazz clazz, Method method, CodeAttribute codeAttribute) { logger.info("Processing code attribute in class: " + clazz.getName() + ", method: " + method.getName(clazz)); codeAttribute.instructionsAccept(clazz, method, new ClassPrinter()); codeAttribute.instructionsAccept(clazz, method, this); 66 67 68 69 70 71 72 73 74 } // Do the actual change in the code attribute codeAttributeEditor.visitCodeAttribute(clazz, method, codeAttribute); // At the end, let's reset the codeAttributeEditor so it does not mess up other methods processing codeAttributeEditor.reset(10000); logger.info("Final result: " + clazz.getName() + ", method: " + method.getName(clazz)); codeAttribute.instructionsAccept(clazz, method, new ClassPrinter()); 75 76 77 78 @Override public void visitAnyInstruction(Clazz clazz, Method method, CodeAttribute codeAttribute, int offset, Instruction instruction) { } 79 160 80 81 82 83 84 85 86 87 @Override public void visitConstantInstruction(Clazz clazz, Method method, CodeAttribute codeAttribute, int offset, ConstantInstruction constantInstruction) { if (constantInstruction.opcode == Instruction.OP_LDC) { logger.info("Found LDC instruction at offset " + offset + " in method " + method.getName(clazz) + " of class " + clazz.getName()); int constIndex = constantInstruction.constantIndex; clazz.constantPoolEntryAccept(constIndex, this); Constant constantToReplace = ((ProgramClass) clazz).getConstant(constIndex); STATE_OF_STRING stateOfConstant = (STATE_OF_STRING) constantToReplace.getProcessingInfo(); 88 89 90 91 92 93 94 } } if (stateOfConstant == STATE_OF_STRING.OLDIE){ Instruction[] newInstructions = generateDecodingInstructions(constantToReplace.getProcessingFlags(), (ProgramClass) clazz); codeAttributeEditor.replaceInstruction(offset, newInstructions); } 95 96 97 98 99 @Override public void visitAnyConstant(Clazz clazz, Constant constant) { constant.setProcessingInfo(STATE_OF_STRING.IGNORE); } 100 101 102 103 104 105 106 107 @Override public void visitStringConstant(Clazz clazz, StringConstant stringConstant) { logger.info("Visiting String Constant: " + stringConstant.getString(clazz) + " in class " + clazz.getName()); ConstantPoolEditor constantPoolEditor = new ConstantPoolEditor((ProgramClass) clazz); String originalString = stringConstant.getString(clazz); String obfuscatedString = Base64.getEncoder().encodeToString(originalString.getBytes()); stringConstant.setProcessingInfo(STATE_OF_STRING.OLDIE); 108 161 logger.info("Obfuscated String: Original: " + originalString + " -> Obfuscated: " + obfuscatedString); int indexOfAddedString = constantPoolEditor.addStringConstant(obfuscatedString); Constant addedConstant = ((ProgramClass) clazz).getConstant(indexOfAddedString); addedConstant.setProcessingInfo(STATE_OF_STRING.NEW_OBFUSCATED); 109 110 111 112 113 114 115 } 116 stringConstant.setProcessingFlags(indexOfAddedString); addedConstant.setProcessingFlags(stringConstant.u2stringIndex); 117 118 119 120 121 } @Override public void visitUtf8Constant(Clazz clazz, Utf8Constant utf8Constant) { } B B.1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Program outputs javap -c -v -private output of Main java code size 1244 bytes SHA-256 checksum c70af82a1d333dd0f7812a4e27b6683333a1df0f33d72ecaaf45a949ea2b149c Compiled from "Main.java" public class org.example.Main minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #31 // org/example/Main super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 3, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V 162 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream; #8 = Class #10 // java/lang/System #9 = NameAndType #11:#12 // out:Ljava/io/PrintStream; #10 = Utf8 java/lang/System #11 = Utf8 out #12 = Utf8 Ljava/io/PrintStream; #13 = String #14 // Hello world! #14 = Utf8 Hello world! #15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V #16 = Class #18 // java/io/PrintStream #17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V #18 = Utf8 java/io/PrintStream #19 = Utf8 println #20 = Utf8 (Ljava/lang/String;)V #21 = Class #22 // org/example/Alumno #22 = Utf8 org/example/Alumno #23 = String #24 // Fran #24 = Utf8 Fran #25 = Methodref #21.#26 // org/example/Alumno."<init>":(Ljava/lang/String;I)V #26 = NameAndType #5:#27 // "<init>":(Ljava/lang/String;I)V #27 = Utf8 (Ljava/lang/String;I)V #28 = String #29 // Nerea #29 = Utf8 Nerea #30 = Methodref #31.#32 // org/example/Main.printAlumno:(Lorg/example/Alumno;)V #31 = Class #33 // org/example/Main #32 = NameAndType #34:#35 // printAlumno:(Lorg/example/Alumno;)V #33 = Utf8 org/example/Main #34 = Utf8 printAlumno #35 = Utf8 (Lorg/example/Alumno;)V #36 = Methodref #21.#37 // org/example/Alumno.cumple:()I #37 = NameAndType #38:#39 // cumple:()I #38 = Utf8 cumple #39 = Utf8 ()I #40 = String #41 // Nombre del alumno: %s\tedad del alumno: %s\tid del alumno: %s%n 163 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 #41 = Utf8 Nombre del alumno: %s\tedad del alumno: %s\tid del alumno: %s%n #42 = Methodref #21.#43 // org/example/Alumno.getNombre:()Ljava/lang/String; #43 = NameAndType #44:#45 // getNombre:()Ljava/lang/String; #44 = Utf8 getNombre #45 = Utf8 ()Ljava/lang/String; #46 = Methodref #21.#47 // org/example/Alumno.getEdad:()I #47 = NameAndType #48:#39 // getEdad:()I #48 = Utf8 getEdad #49 = Methodref #50.#51 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #50 = Class #52 // java/lang/Integer #51 = NameAndType #53:#54 // valueOf:(I)Ljava/lang/Integer; #52 = Utf8 java/lang/Integer #53 = Utf8 valueOf #54 = Utf8 (I)Ljava/lang/Integer; #55 = Methodref #21.#56 // org/example/Alumno.getId:()Ljava/util/UUID; #56 = NameAndType #57:#58 // getId:()Ljava/util/UUID; #57 = Utf8 getId #58 = Utf8 ()Ljava/util/UUID; #59 = Methodref #16.#60 // java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintS #60 = NameAndType #61:#62 // printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; #61 = Utf8 printf #62 = Utf8 (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; #63 = Utf8 Code #64 = Utf8 LineNumberTable #65 = Utf8 LocalVariableTable #66 = Utf8 this #67 = Utf8 Lorg/example/Main; #68 = Utf8 main #69 = Utf8 ([Ljava/lang/String;)V #70 = Utf8 args #71 = Utf8 [Ljava/lang/String; #72 = Utf8 a1 #73 = Utf8 Lorg/example/Alumno; 164 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 { #74 #75 #76 #77 = = = = Utf8 Utf8 Utf8 Utf8 a2 a SourceFile Main.java public org.example.Main(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lorg/example/Main; 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=3, args_size=1 0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #13 // String Hello world! 5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: new #21 // class org/example/Alumno 11: dup 12: ldc #23 // String Fran 14: bipush 23 16: invokespecial #25 // Method org/example/Alumno."<init>":(Ljava/lang/String;I)V 19: astore_1 20: new #21 // class org/example/Alumno 23: dup 24: ldc #28 // String Nerea 26: bipush 7 28: invokespecial #25 // Method org/example/Alumno."<init>":(Ljava/lang/String;I)V 165 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 31: astore_2 32: aload_1 33: invokestatic #30 // Method printAlumno:(Lorg/example/Alumno;)V 36: aload_1 37: invokevirtual #36 // Method org/example/Alumno.cumple:()I 40: pop 41: aload_1 42: invokestatic #30 // Method printAlumno:(Lorg/example/Alumno;)V 45: return LineNumberTable: line 6: 0 line 7: 8 line 8: 20 line 10: 32 line 12: 36 line 14: 41 line 15: 45 LocalVariableTable: Start Length Slot Name Signature 0 46 0 args [Ljava/lang/String; 20 26 1 a1 Lorg/example/Alumno; 32 14 2 a2 Lorg/example/Alumno; 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 private static void printAlumno(org.example.Alumno); descriptor: (Lorg/example/Alumno;)V flags: (0x000a) ACC_PRIVATE, ACC_STATIC Code: stack=6, locals=1, args_size=1 0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #40 // String Nombre del alumno: %s\tedad del alumno: %s\tid del alumno: %s%n 5: iconst_3 6: anewarray #2 // class java/lang/Object 9: dup 10: iconst_0 11: aload_0 12: invokevirtual #42 // Method org/example/Alumno.getNombre:()Ljava/lang/String; 15: aastore 166 16: 17: 18: 19: dup iconst_1 aload_0 invokevirtual #46 // Method org/example/Alumno.getEdad:()I 22: invokestatic #49 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 25: aastore 26: dup 27: iconst_2 28: aload_0 29: invokevirtual #55 // Method org/example/Alumno.getId:()Ljava/util/UUID; 32: aastore 33: invokevirtual #59 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/P 36: pop 37: return LineNumberTable: line 18: 0 line 19: 12 line 20: 19 line 21: 29 line 18: 33 line 22: 37 LocalVariableTable: Start Length Slot Name Signature 0 38 0 a Lorg/example/Alumno; 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 } SourceFile: "Main.java" B.2 1 2 3 4 5 6 7 javap -c -v -private output of Main java code + Proguard size 875 bytes SHA-256 checksum 0ad01e321771735a34fa60350b5a253886a8f81935d8eec24995493f8b69df9c public class org.example.Main minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #9 // org/example/Main 167 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 super_class: #7 // java/lang/Object interfaces: 0, fields: 0, methods: 3, attributes: 0 Constant pool: #1 = String #45 // Fran #2 = String #46 // Hello world! #3 = String #48 // Nerea #4 = String #49 // Nombre del alumno: %s\tedad del alumno: %s\tid del alumno: %s%n #5 = Class #54 // java/io/PrintStream #6 = Class #55 // java/lang/Integer #7 = Class #56 // java/lang/Object #8 = Class #57 // java/lang/System #9 = Class #59 // org/example/Main #10 = Class #60 // org/example/a #11 = Fieldref #8.#29 // java/lang/System.out:Ljava/io/PrintStream; #12 = Methodref #5.#30 // java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintS #13 = Methodref #5.#31 // java/io/PrintStream.println:(Ljava/lang/String;)V #14 = Methodref #6.#32 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #15 = Methodref #7.#22 // java/lang/Object."<init>":()V #16 = Methodref #9.#25 // org/example/Main.a:(Lorg/example/a;)V #17 = Methodref #10.#23 // org/example/a."<init>":(Ljava/lang/String;I)V #18 = Methodref #10.#24 // org/example/a.a:()Ljava/util/UUID; #19 = Methodref #10.#26 // org/example/a.b:()I #20 = Methodref #10.#27 // org/example/a.c:()Ljava/lang/String; #21 = Methodref #10.#28 // org/example/a.d:()I #22 = NameAndType #43:#36 // "<init>":()V #23 = NameAndType #43:#39 // "<init>":(Ljava/lang/String;I)V #24 = NameAndType #50:#35 // a:()Ljava/util/UUID; #25 = NameAndType #50:#41 // a:(Lorg/example/a;)V #26 = NameAndType #51:#33 // b:()I #27 = NameAndType #52:#34 // c:()Ljava/lang/String; #28 = NameAndType #53:#33 // d:()I #29 = NameAndType #61:#47 // out:Ljava/io/PrintStream; 168 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 { #30 = NameAndType #62:#40 // printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; #31 = NameAndType #63:#38 // println:(Ljava/lang/String;)V #32 = NameAndType #64:#37 // valueOf:(I)Ljava/lang/Integer; #33 = Utf8 ()I #34 = Utf8 ()Ljava/lang/String; #35 = Utf8 ()Ljava/util/UUID; #36 = Utf8 ()V #37 = Utf8 (I)Ljava/lang/Integer; #38 = Utf8 (Ljava/lang/String;)V #39 = Utf8 (Ljava/lang/String;I)V #40 = Utf8 (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; #41 = Utf8 (Lorg/example/a;)V #42 = Utf8 ([Ljava/lang/String;)V #43 = Utf8 <init> #44 = Utf8 Code #45 = Utf8 Fran #46 = Utf8 Hello world! #47 = Utf8 Ljava/io/PrintStream; #48 = Utf8 Nerea #49 = Utf8 Nombre del alumno: %s\tedad del alumno: %s\tid del alumno: %s%n #50 = Utf8 a #51 = Utf8 b #52 = Utf8 c #53 = Utf8 d #54 = Utf8 java/io/PrintStream #55 = Utf8 java/lang/Integer #56 = Utf8 java/lang/Object #57 = Utf8 java/lang/System #58 = Utf8 main #59 = Utf8 org/example/Main #60 = Utf8 org/example/a #61 = Utf8 out #62 = Utf8 printf #63 = Utf8 println #64 = Utf8 valueOf public org.example.Main(); descriptor: ()V 169 78 79 80 81 82 83 flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #15 // Method java/lang/Object."<init>":()V 4: return 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=1, args_size=1 0: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #2 // String Hello world! 5: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: new #10 // class org/example/a 11: dup 12: ldc #1 // String Fran 14: bipush 23 16: invokespecial #17 // Method org/example/a."<init>":(Ljava/lang/String;I)V 19: astore_0 20: new #10 // class org/example/a 23: ldc #3 // String Nerea 25: bipush 7 27: invokespecial #17 // Method org/example/a."<init>":(Ljava/lang/String;I)V 30: aload_0 31: invokestatic #16 // Method a:(Lorg/example/a;)V 34: aload_0 35: invokevirtual #21 // Method org/example/a.d:()I 38: pop 39: aload_0 40: invokestatic #16 // Method a:(Lorg/example/a;)V 43: return 111 112 private static void a(org.example.a); 170 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 } descriptor: (Lorg/example/a;)V flags: (0x000a) ACC_PRIVATE, ACC_STATIC Code: stack=6, locals=1, args_size=1 0: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #4 // String Nombre del alumno: %s\tedad del alumno: %s\tid del alumno: %s%n 5: iconst_3 6: anewarray #7 // class java/lang/Object 9: dup 10: iconst_0 11: aload_0 12: invokevirtual #20 // Method org/example/a.c:()Ljava/lang/String; 15: aastore 16: dup 17: iconst_1 18: aload_0 19: invokevirtual #19 // Method org/example/a.b:()I 22: invokestatic #14 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 25: aastore 26: dup 27: iconst_2 28: aload_0 29: invokevirtual #18 // Method org/example/a.a:()Ljava/util/UUID; 32: aastore 33: invokevirtual #12 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/P 36: pop 37: return B.3 1 2 javap -c -v -private output of Alumno java code size 1512 bytes SHA-256 checksum be00c998158032c5c64a32ec150541d1393e74b41d6cb55a499ea41e76281bb3 171 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 Compiled from "Alumno.java" public class org.example.Alumno minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #12 // org/example/Alumno super_class: #2 // java/lang/Object interfaces: 0, fields: 3, methods: 7, attributes: 3 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = InvokeDynamic #0:#8 // #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String; #8 = NameAndType #9:#10 // makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String; #9 = Utf8 makeConcatWithConstants #10 = Utf8 (Ljava/lang/String;)Ljava/lang/String; #11 = Fieldref #12.#13 // org/example/Alumno.nombre:Ljava/lang/String; #12 = Class #14 // org/example/Alumno #13 = NameAndType #15:#16 // nombre:Ljava/lang/String; #14 = Utf8 org/example/Alumno #15 = Utf8 nombre #16 = Utf8 Ljava/lang/String; #17 = Fieldref #12.#18 // org/example/Alumno.edad:I #18 = NameAndType #19:#20 // edad:I #19 = Utf8 edad #20 = Utf8 I #21 = Methodref #22.#23 // java/util/UUID.randomUUID:()Ljava/util/UUID; #22 = Class #24 // java/util/UUID #23 = NameAndType #25:#26 // randomUUID:()Ljava/util/UUID; #24 = Utf8 java/util/UUID #25 = Utf8 randomUUID #26 = Utf8 ()Ljava/util/UUID; #27 = Fieldref #12.#28 // org/example/Alumno.id:Ljava/util/UUID; 172 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #28 #29 #30 #31 #32 #33 #34 #35 #36 #37 #38 #39 #40 #41 #42 #43 #44 #45 #46 #47 #48 #49 #50 = NameAndType #29:#30 // id:Ljava/util/UUID; = Utf8 id = Utf8 Ljava/util/UUID; = Utf8 (Ljava/lang/String;I)V = Utf8 Code = Utf8 LineNumberTable = Utf8 LocalVariableTable = Utf8 this = Utf8 Lorg/example/Alumno; = Utf8 getId = Utf8 getEdad = Utf8 ()I = Utf8 getNombre = Utf8 ()Ljava/lang/String; = Utf8 setNombre = Utf8 (Ljava/lang/String;)V = Utf8 setEdad = Utf8 (I)V = Utf8 cumple = Utf8 SourceFile = Utf8 Alumno.java = Utf8 BootstrapMethods = MethodHandle 6:#51 // REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/M #51 = Methodref #52.#53 // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/M #52 = Class #54 // java/lang/invoke/StringConcatFactory #53 = NameAndType #9:#55 // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String #54 = Utf8 java/lang/invoke/StringConcatFactory #55 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/Metho #56 = String #57 // \u0001. #57 = Utf8 \u0001. #58 = Utf8 InnerClasses #59 = Class #60 // java/lang/invoke/MethodHandles$Lookup #60 = Utf8 java/lang/invoke/MethodHandles$Lookup #61 = Class #62 // java/lang/invoke/MethodHandles #62 = Utf8 java/lang/invoke/MethodHandles #63 = Utf8 Lookup 173 75 76 77 78 { private java.lang.String nombre; descriptor: Ljava/lang/String; flags: (0x0002) ACC_PRIVATE 79 80 81 82 private int edad; descriptor: I flags: (0x0002) ACC_PRIVATE 83 84 85 86 private java.util.UUID id; descriptor: Ljava/util/UUID; flags: (0x0002) ACC_PRIVATE 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 public org.example.Alumno(java.lang.String, int); descriptor: (Ljava/lang/String;I)V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=3, args_size=3 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: aload_1 6: invokedynamic #7, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String; 11: putfield #11 // Field nombre:Ljava/lang/String; 14: aload_0 15: iload_2 16: putfield #17 // Field edad:I 19: aload_0 20: invokestatic #21 // Method java/util/UUID.randomUUID:()Ljava/util/UUID; 23: putfield #27 // Field id:Ljava/util/UUID; 26: return LineNumberTable: line 9: 0 line 10: 4 line 11: 14 line 12: 19 line 13: 26 LocalVariableTable: Start Length Slot Name Signature 174 114 115 116 0 0 0 27 27 27 0 this Lorg/example/Alumno; 1 nombre Ljava/lang/String; 2 edad I 117 118 119 120 121 122 123 124 125 126 127 128 129 130 public java.util.UUID getId(); descriptor: ()Ljava/util/UUID; flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #27 // Field id:Ljava/util/UUID; 4: areturn LineNumberTable: line 16: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lorg/example/Alumno; 131 132 133 134 135 136 137 138 139 140 141 142 143 144 public int getEdad(); descriptor: ()I flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #17 // Field edad:I 4: ireturn LineNumberTable: line 20: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lorg/example/Alumno; 145 146 147 148 149 150 151 152 153 154 155 public java.lang.String getNombre(); descriptor: ()Ljava/lang/String; flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #11 // Field nombre:Ljava/lang/String; 4: areturn LineNumberTable: line 24: 0 175 156 157 158 LocalVariableTable: Start Length Slot Name 0 5 0 this Signature Lorg/example/Alumno; 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 public void setNombre(java.lang.String); descriptor: (Ljava/lang/String;)V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 2: putfield #11 // Field nombre:Ljava/lang/String; 5: return LineNumberTable: line 28: 0 line 29: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lorg/example/Alumno; 0 6 1 nombre Ljava/lang/String; 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 public void setEdad(int); descriptor: (I)V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #17 // Field edad:I 5: return LineNumberTable: line 32: 0 line 33: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lorg/example/Alumno; 0 6 1 edad I 193 194 195 196 197 public int cumple(); descriptor: ()I flags: (0x0001) ACC_PUBLIC Code: 176 stack=3, locals=1, args_size=1 0: aload_0 1: aload_0 2: getfield #17 // Field edad:I 5: iconst_1 6: iadd 7: putfield #17 // Field edad:I 10: aload_0 11: getfield #17 // Field edad:I 14: ireturn LineNumberTable: line 36: 0 line 37: 10 LocalVariableTable: Start Length Slot Name Signature 0 15 0 this Lorg/example/Alumno; 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 1 2 3 4 5 6 7 } SourceFile: "Alumno.java" BootstrapMethods: 0: #50 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/M Method arguments: #56 \u0001. InnerClasses: public static final #63= #59 of #61; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles B.4 javap -c -v -private output of Alumno java code + Proguard B.5 javap -c -v -private output of Main java code + Proguard Enhanced size 1264 bytes SHA-256 checksum d8b140b3cac8e175297c1c390e72dfe08cc5462728b185375e657d9827e83c1e public class org.example.Main minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #12 // org/example/Main 177 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 super_class: #7 // java/lang/Object interfaces: 0, fields: 0, methods: 3, attributes: 0 Constant pool: #1 = String #61 // RnJhbg== #2 = String #62 // SGVsbG8gd29ybGQh #3 = String #63 // Tm9tYnJlIGRlbCBhbHVtbm86ICVzCWVkYWQgZGVsIGFsdW1ubzogJXMJaWQgZGVsIGFsdW1ubzogJXMl #4 = String #64 // TmVyZWE= #5 = Class #68 // java/io/PrintStream #6 = Class #69 // java/lang/Integer #7 = Class #70 // java/lang/Object #8 = Class #71 // java/lang/String #9 = Class #72 // java/lang/System #10 = Class #73 // java/util/Base64 #11 = Class #74 // java/util/Base64$Decoder #12 = Class #76 // org/example/Main #13 = Class #77 // org/example/hq2U$V95XxR #14 = Fieldref #9.#37 // java/lang/System.out:Ljava/io/PrintStream; #15 = Methodref #5.#38 // java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintS #16 = Methodref #5.#39 // java/io/PrintStream.println:(Ljava/lang/String;)V #17 = Methodref #6.#40 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #18 = Methodref #7.#29 // java/lang/Object."<init>":()V #19 = Methodref #8.#31 // java/lang/String."<init>":([B)V #20 = Methodref #10.#35 // java/util/Base64.getDecoder:()Ljava/util/Base64$Decoder; #21 = Methodref #11.#34 // java/util/Base64$Decoder.decode:(Ljava/lang/String;)[B #22 = Methodref #12.#36 // org/example/Main."iSPn70HD2q6w1d0n4w-bixLy#lFOyGFhhBJgLmJdI0Cxlvp":(Lorg/example/ #23 = Methodref #13.#28 // org/example/hq2U$V95XxR."1feP":()I #24 = Methodref #13.#30 // org/example/hq2U$V95XxR."<init>":(Ljava/lang/String;I)V #25 = Methodref #13.#32 // org/example/hq2U$V95XxR."@F@00QJYK6p9":()Ljava/lang/String; #26 = Methodref #13.#33 // org/example/hq2U$V95XxR."AyR1t9wmEUrtz#Jn9Z":()Ljava/util/UUID; 178 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #27 = Methodref #13.#41 // org/example/hq2U$V95XxR.wL_5GTSxcjv6c:()I #28 = NameAndType #55:#42 // "1feP":()I #29 = NameAndType #56:#46 // "<init>":()V #30 = NameAndType #56:#50 // "<init>":(Ljava/lang/String;I)V #31 = NameAndType #56:#53 // "<init>":([B)V #32 = NameAndType #57:#43 // "@F@00QJYK6p9":()Ljava/lang/String; #33 = NameAndType #58:#45 // "AyR1t9wmEUrtz#Jn9Z":()Ljava/util/UUID; #34 = NameAndType #65:#49 // decode:(Ljava/lang/String;)[B #35 = NameAndType #66:#44 // getDecoder:()Ljava/util/Base64$Decoder; #36 = NameAndType #67:#52 // "iSPn70HD2q6w1d0n4w-bixLy#lFOyGFhhBJgLmJdI0Cxlvp":(Lorg/example/hq2U$V95XxR;)V #37 = NameAndType #78:#60 // out:Ljava/io/PrintStream; #38 = NameAndType #79:#51 // printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; #39 = NameAndType #80:#48 // println:(Ljava/lang/String;)V #40 = NameAndType #81:#47 // valueOf:(I)Ljava/lang/Integer; #41 = NameAndType #82:#42 // wL_5GTSxcjv6c:()I #42 = Utf8 ()I #43 = Utf8 ()Ljava/lang/String; #44 = Utf8 ()Ljava/util/Base64$Decoder; #45 = Utf8 ()Ljava/util/UUID; #46 = Utf8 ()V #47 = Utf8 (I)Ljava/lang/Integer; #48 = Utf8 (Ljava/lang/String;)V #49 = Utf8 (Ljava/lang/String;)[B #50 = Utf8 (Ljava/lang/String;I)V #51 = Utf8 (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; #52 = Utf8 (Lorg/example/hq2U$V95XxR;)V #53 = Utf8 ([B)V #54 = Utf8 ([Ljava/lang/String;)V #55 = Utf8 1feP #56 = Utf8 <init> #57 = Utf8 @F@00QJYK6p9 #58 = Utf8 AyR1t9wmEUrtz#Jn9Z 179 #59 #60 #61 #62 #63 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 { = Utf8 Code = Utf8 Ljava/io/PrintStream; = Utf8 RnJhbg== = Utf8 SGVsbG8gd29ybGQh = Utf8 Tm9tYnJlIGRlbCBhbHVtbm86ICVzCWVkYWQgZGVsIGFsdW1ubzogJXMJaWQgZGVsIGFsdW1ubzogJXMlb #64 = Utf8 TmVyZWE= #65 = Utf8 decode #66 = Utf8 getDecoder #67 = Utf8 iSPn70HD2q6w1d0n4w-bixLy#lFOyGFhhBJgLmJdI0Cxlvp #68 = Utf8 java/io/PrintStream #69 = Utf8 java/lang/Integer #70 = Utf8 java/lang/Object #71 = Utf8 java/lang/String #72 = Utf8 java/lang/System #73 = Utf8 java/util/Base64 #74 = Utf8 java/util/Base64$Decoder #75 = Utf8 main #76 = Utf8 org/example/Main #77 = Utf8 org/example/hq2U$V95XxR #78 = Utf8 out #79 = Utf8 printf #80 = Utf8 println #81 = Utf8 valueOf #82 = Utf8 wL_5GTSxcjv6c public org.example.Main(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #18 // Method java/lang/Object."<init>":()V 4: return 102 103 104 105 106 107 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=6, locals=1, args_size=1 180 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 0: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream; 3: new #8 // class java/lang/String 6: dup 7: invokestatic #20 // Method java/util/Base64.getDecoder:()Ljava/util/Base64$Decoder; 10: ldc #2 // String SGVsbG8gd29ybGQh 12: invokevirtual #21 // Method java/util/Base64$Decoder.decode:(Ljava/lang/String;)[B 15: invokespecial #19 // Method java/lang/String."<init>":([B)V 18: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 21: new #13 // class org/example/hq2U$V95XxR 24: dup 25: new #8 // class java/lang/String 28: dup 29: invokestatic #20 // Method java/util/Base64.getDecoder:()Ljava/util/Base64$Decoder; 32: ldc #1 // String RnJhbg== 34: invokevirtual #21 // Method java/util/Base64$Decoder.decode:(Ljava/lang/String;)[B 37: invokespecial #19 // Method java/lang/String."<init>":([B)V 40: bipush 23 42: invokespecial #24 // Method org/example/hq2U$V95XxR."<init>":(Ljava/lang/String;I)V 45: astore_0 46: new #13 // class org/example/hq2U$V95XxR 49: new #8 // class java/lang/String 52: dup 53: invokestatic #20 // Method java/util/Base64.getDecoder:()Ljava/util/Base64$Decoder; 56: ldc #4 // String TmVyZWE= 58: invokevirtual #21 // Method java/util/Base64$Decoder.decode:(Ljava/lang/String;)[B 61: invokespecial #19 // Method java/lang/String."<init>":([B)V 64: bipush 7 66: invokespecial #24 // Method org/example/hq2U$V95XxR."<init>":(Ljava/lang/String;I)V 181 136 137 138 139 140 141 142 143 69: aload_0 70: invokestatic #22 // Method "iSPn70HD2q6w1d0n4w-bixLy#lFOyGFhhBJgLmJdI0Cxlvp":(Lorg/example/hq2U$V95XxR; 73: aload_0 74: invokevirtual #27 // Method org/example/hq2U$V95XxR.wL_5GTSxcjv6c:()I 77: pop 78: aload_0 79: invokestatic #22 // Method "iSPn70HD2q6w1d0n4w-bixLy#lFOyGFhhBJgLmJdI0Cxlvp":(Lorg/example/hq2U$V95XxR; 82: return 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 private static void iSPn70HD2q6w1d0n4w-bixLy#lFOyGFhhBJgLmJdI0Cxlvp(org.example.hq2U$V95XxR); descriptor: (Lorg/example/hq2U$V95XxR;)V flags: (0x000a) ACC_PRIVATE, ACC_STATIC Code: stack=6, locals=1, args_size=1 0: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream; 3: new #8 // class java/lang/String 6: dup 7: invokestatic #20 // Method java/util/Base64.getDecoder:()Ljava/util/Base64$Decoder; 10: ldc #3 // String Tm9tYnJlIGRlbCBhbHVtbm86ICVzCWVkYWQgZGVsIGFsdW1ubzogJXMJaWQgZGVsIGFsdW1ubzog 12: invokevirtual #21 // Method java/util/Base64$Decoder.decode:(Ljava/lang/String;)[B 15: invokespecial #19 // Method java/lang/String."<init>":([B)V 18: iconst_3 19: anewarray #7 // class java/lang/Object 22: dup 23: iconst_0 24: aload_0 25: invokevirtual #25 // Method org/example/hq2U$V95XxR."@F@00QJYK6p9":()Ljava/lang/String; 28: aastore 29: dup 30: iconst_1 31: aload_0 32: invokevirtual #23 // Method org/example/hq2U$V95XxR."1feP":()I 182 168 169 170 171 172 173 174 175 176 177 178 } B.6 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 35: invokestatic #17 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 38: aastore 39: dup 40: iconst_2 41: aload_0 42: invokevirtual #26 // Method org/example/hq2U$V95XxR."AyR1t9wmEUrtz#Jn9Z":()Ljava/util/UUID; 45: aastore 46: invokevirtual #15 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/P 49: pop 50: return javap -c -v -private output of Alumno java code + Proguard Enhanced size 965 bytes SHA-256 checksum a3ac4af2a2ad8ba637a2bba0c360fe15a5169a20a11b7b130dd609cea550ec56 public final class org.example.hq2U$V95XxR minor version: 0 major version: 61 flags: (0x0031) ACC_PUBLIC, ACC_FINAL, ACC_SUPER this_class: #5 // org/example/hq2U$V95XxR super_class: #2 // java/lang/Object interfaces: 0, fields: 3, methods: 5, attributes: 1 Constant pool: #1 = String #21 // \u0001. #2 = Class #41 // java/lang/Object #3 = Class #42 // java/lang/invoke/StringConcatFactory #4 = Class #43 // java/util/UUID #5 = Class #45 // org/example/hq2U$V95XxR #6 = Fieldref #5.#14 // org/example/hq2U$V95XxR."0@Gfy2":I #7 = Fieldref #5.#16 // org/example/hq2U$V95XxR."U5CWUw0wp69FszeP#ep#j@bS6":Ljava/lang/String; #8 = Fieldref #5.#17 // org/example/hq2U$V95XxR."Zp9EaOz8gTYnlrcg9T_B-Rj8YBp":Ljava/util/UUID; 183 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #9 = Methodref #2.#15 // java/lang/Object."<init>":()V #10 = Methodref #3.#19 // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/M #11 = Methodref #4.#20 // java/util/UUID.randomUUID:()Ljava/util/UUID; #12 = InvokeDynamic #0:#18 // #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String; #13 = MethodHandle 6:#10 // REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/M #14 = NameAndType #29:#36 // "0@Gfy2":I #15 = NameAndType #31:#25 // "<init>":()V #16 = NameAndType #39:#37 // "U5CWUw0wp69FszeP#ep#j@bS6":Ljava/lang/String; #17 = NameAndType #40:#38 // "Zp9EaOz8gTYnlrcg9T_B-Rj8YBp":Ljava/util/UUID; #18 = NameAndType #44:#26 // makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String; #19 = NameAndType #44:#28 // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String #20 = NameAndType #46:#24 // randomUUID:()Ljava/util/UUID; #21 = Utf8 \u0001. #22 = Utf8 ()I #23 = Utf8 ()Ljava/lang/String; #24 = Utf8 ()Ljava/util/UUID; #25 = Utf8 ()V #26 = Utf8 (Ljava/lang/String;)Ljava/lang/String; #27 = Utf8 (Ljava/lang/String;I)V #28 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/Metho #29 = Utf8 0@Gfy2 #30 = Utf8 1feP #31 = Utf8 <init> #32 = Utf8 @F@00QJYK6p9 #33 = Utf8 AyR1t9wmEUrtz#Jn9Z #34 = Utf8 BootstrapMethods #35 = Utf8 Code #36 = Utf8 I #37 = Utf8 Ljava/lang/String; #38 = Utf8 Ljava/util/UUID; #39 = Utf8 U5CWUw0wp69FszeP#ep#j@bS6 #40 = Utf8 Zp9EaOz8gTYnlrcg9T_B-Rj8YBp 184 51 52 53 54 55 56 57 58 59 60 61 { #41 #42 #43 #44 #45 #46 #47 = = = = = = = Utf8 Utf8 Utf8 Utf8 Utf8 Utf8 Utf8 java/lang/Object java/lang/invoke/StringConcatFactory java/util/UUID makeConcatWithConstants org/example/hq2U$V95XxR randomUUID wL_5GTSxcjv6c private java.lang.String U5CWUw0wp69FszeP#ep#j@bS6; descriptor: Ljava/lang/String; flags: (0x0002) ACC_PRIVATE 62 63 64 65 private int 0@Gfy2; descriptor: I flags: (0x0002) ACC_PRIVATE 66 67 68 69 private java.util.UUID Zp9EaOz8gTYnlrcg9T_B-Rj8YBp; descriptor: Ljava/util/UUID; flags: (0x0002) ACC_PRIVATE 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 public org.example.hq2U$V95XxR(java.lang.String, int); descriptor: (Ljava/lang/String;I)V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=3, args_size=3 0: aload_0 1: invokespecial #9 // Method java/lang/Object."<init>":()V 4: aload_0 5: aload_1 6: invokedynamic #12, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String; 11: putfield #7 // Field "U5CWUw0wp69FszeP#ep#j@bS6":Ljava/lang/String; 14: aload_0 15: iload_2 16: putfield #6 // Field "0@Gfy2":I 19: aload_0 20: invokestatic #11 // Method java/util/UUID.randomUUID:()Ljava/util/UUID; 23: putfield #8 // Field "Zp9EaOz8gTYnlrcg9T_B-Rj8YBp":Ljava/util/UUID; 26: return 185 89 90 91 92 93 94 95 96 97 public final java.util.UUID AyR1t9wmEUrtz#Jn9Z(); descriptor: ()Ljava/util/UUID; flags: (0x0011) ACC_PUBLIC, ACC_FINAL Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #8 // Field "Zp9EaOz8gTYnlrcg9T_B-Rj8YBp":Ljava/util/UUID; 4: areturn 98 99 100 101 102 103 104 105 106 public final int 1feP(); descriptor: ()I flags: (0x0011) ACC_PUBLIC, ACC_FINAL Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #6 // Field "0@Gfy2":I 4: ireturn 107 108 109 110 111 112 113 114 115 public final java.lang.String @F@00QJYK6p9(); descriptor: ()Ljava/lang/String; flags: (0x0011) ACC_PUBLIC, ACC_FINAL Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #7 // Field "U5CWUw0wp69FszeP#ep#j@bS6":Ljava/lang/String; 4: areturn 116 117 118 119 120 121 122 123 124 125 126 127 128 129 public final int wL_5GTSxcjv6c(); descriptor: ()I flags: (0x0011) ACC_PUBLIC, ACC_FINAL Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #6 // Field "0@Gfy2":I 5: iconst_1 6: iadd 7: putfield #6 // Field "0@Gfy2":I 10: aload_0 11: getfield #6 // Field "0@Gfy2":I 186 130 131 132 133 134 135 14: ireturn } BootstrapMethods: 0: #13 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/M Method arguments: #1 \u0001. B.7 jadx-gui screenshots of the Main java code without Proguard Enhanced Figure 7: Screenshot of decompiled jadx-gui code of the Main class 187 B.8 jadx-gui screenshots of the Alumno java code without Proguard Enhanced Figure 8: Screenshot of decompiled jadx-gui code of the Alumno class 188 B.9 jadx-gui screenshots of the Main java code + Proguard Enhanced Figure 9: Screenshot of decompiled jadx-gui code of the Main class 189 B.10 jadx-gui screenshots of the Alumno java code + Proguard Enhanced Figure 10: Screenshot of decompiled jadx-gui code of the Alumno class 190 References [1] “Overview - owasp mobile application security.” [Online]. Available: https://mas.owasp.org/MASVS/ [2] “Masvs-resilience-3 owasp mobile application security.” [Online]. Available: https://mas.owasp.org/MASVS/controls/ MASVS-RESILIENCE-3/ [3] “Shrink, obfuscate, and optimize your app | android studio | android developers.” [Online]. Available: https://developer.android.com/ build/shrink-code [4] C. Collberg, C. Thomborson, and D. Low, “A taxonomy of obfuscating transformations.” [5] “Salario para programador java en españa - salario medio.” [Online]. Available: https://es.talent.com/salary?job=programador+java [6] “Salario para project manager en españa - salario medio.” [Online]. Available: https://es.talent.com/salary?job=project+manager [7] A. Inc., “Product environmental report 13-inch macbook pro,” 2022. [8] “Proguard vs. dexguard: An overview | guardsquare.” [Online]. Available: https://www.guardsquare.com/blog/dexguard-vs.-proguard [9] “Openjdk.” [Online]. Available: https://openjdk.org/ [10] “Hotspot group.” [Online]. Available: hotspot/ https://openjdk.org/groups/ [11] “Avian.” [Online]. Available: https://readytalk.github.io/avian/ [12] “Chapter 6. the java virtual machine instruction set.” [Online]. Available: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6. html#jvms-6.5.iadd [13] “Chapter 2. the structure of the java virtual machine.” [Online]. Available: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2. html#jvms-2.4 [14] “Chapter 2. the structure of the java virtual machine.” [Online]. Available: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2. html#jvms-2.5.2 191 [15] “Chapter 2. the structure of the java virtual machine.” [Online]. Available: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2. html#jvms-2.6 [16] “Chapter 2. the structure of the java virtual machine.” [Online]. Available: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2. html#jvms-2.5.1 [17] “Chapter 4. the class file format.” [Online]. Available: //docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html https: [18] “Chapter 4. the class file format.” [Online]. Available: https: //docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4 [19] “Chapter 5. loading, linking, and initializing.” [Online]. Available: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5. html#jvms-5.4.3.5 [20] amosshi, “Github - amosshi/binaryinternals: Free tools to view internals of binary file, including .class, .dex, .elf, .zip, etc.” [Online]. Available: https://github.com/amosshi/binaryinternals [21] “Chapter 2. the structure of the java virtual machine.” [Online]. Available: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2. html#jvms-2.9 [22] “Chapter 4. the class file format.” [Online]. Available: https: //docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.3 [23] “Chapter 6. the java virtual machine instruction set.” [Online]. Available: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html [24] “Jvm bytecode for dummies (and the rest of us too) - youtube.” [Online]. Available: https://www.youtube.com/watch?v=rPyqB1l4gko [25] “Chapter 4. the class file format.” [Online]. Available: https://docs. oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.13 [26] “Chapter 4. the class file format.” [Online]. Available: https://docs. oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.14 [27] “The proguard story: 20 years of innovation in java optimization | guardsquare.” [Online]. Available: https://www.guardsquare.com/blog/ the-proguard-story-20-years-of-innovation-in-java-optimization-guardsquare 192 [28] “Github - guardsquare/proguard: Proguard, java optimizer and obfuscator.” [Online]. Available: https://github.com/Guardsquare/ proguard [29] “Proguard manual: Home | guardsquare.” [Online]. Available: https://www.guardsquare.com/manual/home#how-it-works [30] R. Russon and Y. Fledel, NTFS Documentation. [Online]. Available: http://dubeyko.com/development/FileSystems/NTFS/ntfsdoc.pdf [31] A. Mathur, M. Cao, S. Bhattacharya, A. Dilger, A. Z. (Tomas), and L. Vivier, “The new ext4 filesystem: current status and future plans,” Proceedings of the Linux Symposium, 2007. [Online]. Available: http://www.linuxsymposium.org/archives/OLS/Reprints-2007/ mathur-Reprint.pdf [32] “Php: base64 encode - manual.” [Online]. Available: //www.php.net/manual/en/function.base64-encode.php 193 https: