Uploaded by loviya7841

innerworkings of jjvm

advertisement
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:
Download